From 33e66bc414e2c6c005a14f8fb340d045b0d230dd Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Fri, 29 May 2026 12:26:43 +0000 Subject: [PATCH] Add reference graph type Modules (CallGraph, SecretGraph) --- src/graphs/mod.ts | 6 + src/graphs/modules/call-graph.ts | 75 +++++++++++++ src/graphs/modules/index.ts | 3 + src/graphs/modules/secret-graph.ts | 44 ++++++++ test/reference-modules.test.ts | 173 +++++++++++++++++++++++++++++ 5 files changed, 301 insertions(+) create mode 100644 src/graphs/modules/call-graph.ts create mode 100644 src/graphs/modules/index.ts create mode 100644 src/graphs/modules/secret-graph.ts create mode 100644 test/reference-modules.test.ts diff --git a/src/graphs/mod.ts b/src/graphs/mod.ts index edb10cb..78ebbd0 100644 --- a/src/graphs/mod.ts +++ b/src/graphs/mod.ts @@ -1,3 +1,6 @@ +export { + CallGraph, +} from "./modules/call-graph.ts"; export { BaseEdgeAttributes, BaseNodeAttributes, @@ -6,6 +9,9 @@ export { GraphStatus, Metagraph, } from "./modules/metagraph.ts"; +export { + SecretGraph, +} from "./modules/secret-graph.ts"; export { decrypt, encrypt, diff --git a/src/graphs/modules/call-graph.ts b/src/graphs/modules/call-graph.ts new file mode 100644 index 0000000..c1013f0 --- /dev/null +++ b/src/graphs/modules/call-graph.ts @@ -0,0 +1,75 @@ +import { Type } from "@alkdev/typebox"; +import { Metagraph } from "./metagraph.ts"; + +export const CallGraph = Type.Module({ + Config: Type.Object({ + type: Type.Literal("directed"), + multi: Type.Literal(false), + allowSelfLoops: Type.Literal(false), + }), + + CallNode: Type.Composite([ + Metagraph.Import("BaseNode"), + Type.Object({ + requestId: Type.String(), + operationId: Type.String(), + status: Type.Ref("CallStatus"), + parentRequestId: Type.Optional(Type.String()), + input: Type.Unknown(), + output: Type.Optional(Type.Unknown()), + identity: Type.Optional(Type.Ref("Identity")), + startedAt: Type.Optional(Type.String({ format: "date-time" })), + completedAt: Type.Optional(Type.String({ format: "date-time" })), + }), + ]), + + SubcallNode: Type.Composite([ + Metagraph.Import("BaseNode"), + Type.Object({ + requestId: Type.String(), + parentRequestId: Type.String(), + operationId: Type.String(), + status: Type.Ref("CallStatus"), + }), + ]), + + TriggeredEdge: Type.Composite([ + Metagraph.Import("BaseEdge"), + Type.Object({ + type: Type.Literal("triggered"), + }), + ]), + + DependsOnEdge: Type.Composite([ + Metagraph.Import("BaseEdge"), + Type.Object({ + type: Type.Literal("depends_on"), + }), + ]), + + TriggeredEdgeConstraints: Type.Object({ + edgeType: Type.Literal("triggered"), + allowedSourceTypes: Type.Array(Type.String(), { default: ["Call"] }), + allowedTargetTypes: Type.Array(Type.String(), { default: ["Call", "Subcall"] }), + }), + + DependsOnEdgeConstraints: Type.Object({ + edgeType: Type.Literal("depends_on"), + allowedSourceTypes: Type.Array(Type.String(), { default: ["Call", "Subcall"] }), + allowedTargetTypes: Type.Array(Type.String(), { default: ["Call", "Subcall"] }), + }), + + CallStatus: Type.Union([ + Type.Literal("pending"), + Type.Literal("running"), + Type.Literal("completed"), + Type.Literal("failed"), + Type.Literal("aborted"), + ]), + + Identity: Type.Object({ + id: Type.String(), + scopes: Type.Array(Type.String()), + resources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()))), + }), +}); \ No newline at end of file diff --git a/src/graphs/modules/index.ts b/src/graphs/modules/index.ts new file mode 100644 index 0000000..fe8e8aa --- /dev/null +++ b/src/graphs/modules/index.ts @@ -0,0 +1,3 @@ +export { CallGraph } from "./call-graph.ts"; +export { Metagraph } from "./metagraph.ts"; +export { SecretGraph } from "./secret-graph.ts"; \ No newline at end of file diff --git a/src/graphs/modules/secret-graph.ts b/src/graphs/modules/secret-graph.ts new file mode 100644 index 0000000..4a6314e --- /dev/null +++ b/src/graphs/modules/secret-graph.ts @@ -0,0 +1,44 @@ +import { Type } from "@alkdev/typebox"; +import { EncryptedDataSchema } from "../crypto.ts"; +import { Metagraph } from "./metagraph.ts"; + +export const SecretGraph = Type.Module({ + Config: Type.Object({ + type: Type.Literal("undirected"), + multi: Type.Literal(false), + allowSelfLoops: Type.Literal(false), + }), + + SecretNode: Type.Composite([ + Metagraph.Import("BaseNode"), + Type.Object({ + key: Type.String({ minLength: 1, maxLength: 255 }), + encryptedData: EncryptedDataSchema, + expiresAt: Type.Optional(Type.String({ format: "date-time" })), + }), + ]), + + ClientNode: Type.Composite([ + Metagraph.Import("BaseNode"), + Type.Object({ + name: Type.String(), + type: Type.String(), + config: Type.Record(Type.String(), Type.Unknown()), + enabled: Type.Boolean({ default: true }), + }), + ]), + + HasSecretEdge: Type.Composite([ + Metagraph.Import("BaseEdge"), + Type.Object({ + type: Type.Literal("has_secret"), + secretKey: Type.String(), + }), + ]), + + HasSecretEdgeConstraints: Type.Object({ + edgeType: Type.Literal("has_secret"), + allowedSourceTypes: Type.Array(Type.String(), { default: ["Client"] }), + allowedTargetTypes: Type.Array(Type.String(), { default: ["Secret"] }), + }), +}); \ No newline at end of file diff --git a/test/reference-modules.test.ts b/test/reference-modules.test.ts new file mode 100644 index 0000000..b1c543e --- /dev/null +++ b/test/reference-modules.test.ts @@ -0,0 +1,173 @@ +import { assertEquals } from "@std/assert"; +import { + CallGraph, + moduleToDbSchema, + SecretGraph, + validateEdge, + validateNode, +} from "../mod.ts"; +import type { TModule, TProperties } from "@alkdev/typebox"; + +Deno.test("CallGraph has required entries", () => { + const entries = Object.keys( + (CallGraph.Import("Config" as never).$defs as Record), + ); + const required = [ + "Config", + "CallNode", + "SubcallNode", + "TriggeredEdge", + "DependsOnEdge", + "TriggeredEdgeConstraints", + "DependsOnEdgeConstraints", + "CallStatus", + "Identity", + ]; + for (const name of required) { + assertEquals(entries.includes(name), true, `Missing entry: ${name}`); + } +}); + +Deno.test("CallGraph.Config uses literal values", () => { + const configImport = CallGraph.Import("Config" as never); + const configSchema = configImport.$defs.Config as Record; + const properties = configSchema.properties as Record; + + const typeProp = properties.type as Record; + assertEquals(typeProp.const, "directed"); + + const multiProp = properties.multi as Record; + assertEquals(multiProp.const, false); + + const allowSelfLoopsProp = properties.allowSelfLoops as Record; + assertEquals(allowSelfLoopsProp.const, false); +}); + +Deno.test("validateNode with CallGraph CallNode returns true for valid data", () => { + const result = validateNode( + CallGraph as unknown as TModule, + "CallNode", + { + requestId: "req-001", + operationId: "op-001", + status: "pending", + input: {}, + }, + ); + assertEquals(result, true); +}); + +Deno.test("validateNode with CallGraph CallNode returns false for invalid data", () => { + const result = validateNode( + CallGraph as unknown as TModule, + "CallNode", + { + requestId: "req-001", + }, + ); + assertEquals(result, false); +}); + +Deno.test("moduleToDbSchema with CallGraph produces valid DbSchema", () => { + const schema = moduleToDbSchema( + CallGraph as unknown as TModule, + "call-graph", + ); + + assertEquals(schema.graphType.name, "call-graph"); + assertEquals(schema.graphType.config.properties !== undefined, true); + + assertEquals(schema.nodeTypes.length, 2); + assertEquals(schema.nodeTypes[0].name, "Call"); + assertEquals(schema.nodeTypes[1].name, "Subcall"); + + assertEquals(schema.edgeTypes.length, 2); + assertEquals(schema.edgeTypes[0].name, "Triggered"); + assertEquals(schema.edgeTypes[1].name, "DependsOn"); +}); + +Deno.test("SecretGraph has required entries", () => { + const entries = Object.keys( + (SecretGraph.Import("Config" as never).$defs as Record), + ); + const required = [ + "Config", + "SecretNode", + "ClientNode", + "HasSecretEdge", + "HasSecretEdgeConstraints", + ]; + for (const name of required) { + assertEquals(entries.includes(name), true, `Missing entry: ${name}`); + } +}); + +Deno.test("SecretGraph.Config uses literal values", () => { + const configImport = SecretGraph.Import("Config" as never); + const configSchema = configImport.$defs.Config as Record; + const properties = configSchema.properties as Record; + + const typeProp = properties.type as Record; + assertEquals(typeProp.const, "undirected"); + + const multiProp = properties.multi as Record; + assertEquals(multiProp.const, false); + + const allowSelfLoopsProp = properties.allowSelfLoops as Record; + assertEquals(allowSelfLoopsProp.const, false); +}); + +Deno.test("moduleToDbSchema with SecretGraph produces valid DbSchema", () => { + const schema = moduleToDbSchema( + SecretGraph as unknown as TModule, + "secret-graph", + ); + + assertEquals(schema.graphType.name, "secret-graph"); + + assertEquals(schema.nodeTypes.length, 2); + assertEquals(schema.nodeTypes[0].name, "Secret"); + assertEquals(schema.nodeTypes[1].name, "Client"); + + assertEquals(schema.edgeTypes.length, 1); + assertEquals(schema.edgeTypes[0].name, "HasSecret"); +}); + +Deno.test("validateNode with SecretGraph SecretNode validates encrypted data", () => { + const result = validateNode( + SecretGraph as unknown as TModule, + "SecretNode", + { + key: "api_key", + encryptedData: { + keyVersion: 1, + salt: "dGVzdA==", + iv: "dGVzdA==dGVzdA==", + data: "dGVzdA==", + }, + }, + ); + assertEquals(result, true); +}); + +Deno.test("validateEdge with CallGraph TriggeredEdge returns true for valid data", () => { + const result = validateEdge( + CallGraph as unknown as TModule, + "TriggeredEdge", + { + type: "triggered", + }, + ); + assertEquals(result, true); +}); + +Deno.test("validateEdge with CallGraph TriggeredEdge returns false for wrong type", () => { + const result = validateEdge( + CallGraph as unknown as TModule, + "TriggeredEdge", + { + type: "depends_on", + }, + ); + assertEquals(result, false); +}); \ No newline at end of file