Add bridge.ts with moduleToDbSchema, validateNode, validateEdge

This commit is contained in:
2026-05-29 12:23:46 +00:00
parent 6f6cabaf42
commit 14c7fa4746
2 changed files with 210 additions and 7 deletions

194
src/graphs/bridge.ts Normal file
View File

@@ -0,0 +1,194 @@
import type { TImport, TModule, TProperties } from "@alkdev/typebox";
import { Check, Create } from "@alkdev/typebox/value";
export interface DbGraphTypeRow {
name: string;
config: Record<string, unknown>;
}
export interface DbNodeTypeRow {
name: string;
schema: Record<string, unknown>;
}
export interface DbEdgeTypeRow {
name: string;
schema: Record<string, unknown>;
allowedSourceTypes: string[];
allowedTargetTypes: string[];
}
export interface DbSchema {
graphType: DbGraphTypeRow;
nodeTypes: DbNodeTypeRow[];
edgeTypes: DbEdgeTypeRow[];
}
function classifyEntry(
name: string,
): "config" | "node" | "edge" | "edgeConstraints" | "shared" | "base" {
if (name === "Config") return "config";
if (name === "BaseNode" || name === "BaseEdge") return "base";
if (name.endsWith("EdgeConstraints")) return "edgeConstraints";
if (name.endsWith("Node")) return "node";
if (name.endsWith("Edge")) return "edge";
if (name.endsWith("Enum")) return "shared";
return "shared";
}
function stripSuffix(name: string, suffix: string): string {
return name.slice(0, -suffix.length);
}
function getEntryNames(module: TModule<TProperties>): string[] {
const defs = (module as unknown as { $defs: Record<string, unknown> }).$defs;
const firstKey = Object.keys(defs)[0];
const probe = module.Import(firstKey as keyof TProperties & string);
return Object.keys((probe as TImport).$defs);
}
function getEntrySchema(
module: TModule<TProperties>,
entryName: string,
): Record<string, unknown> {
const entryImport = module.Import(
entryName as keyof TProperties & string,
) as TImport;
const defs = entryImport.$defs as Record<string, unknown>;
return defs[entryName] as Record<string, unknown>;
}
function extractEdgeConstraintValues(
constraintImport: TImport,
): { allowedSourceTypes: string[]; allowedTargetTypes: string[] } {
const instance = Create(constraintImport) as Record<string, unknown>;
const sourceArr = Array.isArray(instance.allowedSourceTypes)
? instance.allowedSourceTypes as string[]
: [];
const targetArr = Array.isArray(instance.allowedTargetTypes)
? instance.allowedTargetTypes as string[]
: [];
return { allowedSourceTypes: sourceArr, allowedTargetTypes: targetArr };
}
export function moduleToDbSchema(
module: TModule<TProperties>,
name: string,
): DbSchema {
const entryNames = getEntryNames(module);
const nodeEntries: string[] = [];
const edgeEntries: string[] = [];
const constraintEntries: string[] = [];
let hasConfig = false;
for (const entryName of entryNames) {
const role = classifyEntry(entryName);
switch (role) {
case "config":
hasConfig = true;
break;
case "node":
nodeEntries.push(entryName);
break;
case "edge":
edgeEntries.push(entryName);
break;
case "edgeConstraints":
constraintEntries.push(entryName);
break;
case "shared":
case "base":
break;
}
}
if (!hasConfig) {
throw new Error("Module must have a Config entry.");
}
const edgeShortNameMap = new Map<string, string>();
for (const edgeEntry of edgeEntries) {
edgeShortNameMap.set(stripSuffix(edgeEntry, "Edge"), edgeEntry);
}
const constraintsByEdgeType = new Map<
string,
{ allowedSourceTypes: string[]; allowedTargetTypes: string[] }
>();
for (const constraintName of constraintEntries) {
const edgeShortName = stripSuffix(constraintName, "EdgeConstraints");
const matchedEdge = edgeShortNameMap.get(edgeShortName);
if (!matchedEdge) {
throw new Error(
`EdgeConstraints entry "${constraintName}" references edge type "${edgeShortName}" not found in module.`,
);
}
const constraintImport = module.Import(
constraintName as keyof TProperties & string,
) as TImport;
const values = extractEdgeConstraintValues(constraintImport);
if (
values.allowedSourceTypes.length === 0 &&
values.allowedTargetTypes.length === 0
) {
throw new Error(
`EdgeConstraints entry "${constraintName}" has empty allowedSourceTypes and allowedTargetTypes. Omit the entry for "no restriction".`,
);
}
constraintsByEdgeType.set(matchedEdge, values);
}
const configSchema = getEntrySchema(module, "Config");
const nodeTypes: DbNodeTypeRow[] = nodeEntries.map((entryName) => {
return {
name: stripSuffix(entryName, "Node"),
schema: getEntrySchema(module, entryName),
};
});
const edgeTypes: DbEdgeTypeRow[] = edgeEntries.map((entryName) => {
const shortName = stripSuffix(entryName, "Edge");
const constraints = constraintsByEdgeType.get(entryName);
return {
name: shortName,
schema: getEntrySchema(module, entryName),
allowedSourceTypes: constraints?.allowedSourceTypes ?? [],
allowedTargetTypes: constraints?.allowedTargetTypes ?? [],
};
});
return {
graphType: { name, config: configSchema },
nodeTypes,
edgeTypes,
};
}
export function validateNode(
module: TModule<TProperties>,
entryName: string,
data: unknown,
): boolean {
if (!entryName.endsWith("Node") || entryName === "BaseNode") {
throw new Error(
`"${entryName}" is not a node type entry. Entry name must end in "Node" (excluding "BaseNode").`,
);
}
const entryImport = module.Import(entryName as keyof TProperties & string);
return Check(entryImport, data);
}
export function validateEdge(
module: TModule<TProperties>,
entryName: string,
data: unknown,
): boolean {
if (!entryName.endsWith("Edge") || entryName === "BaseEdge") {
throw new Error(
`"${entryName}" is not an edge type entry. Entry name must end in "Edge" (excluding "BaseEdge").`,
);
}
const entryImport = module.Import(entryName as keyof TProperties & string);
return Check(entryImport, data);
}

View File

@@ -1,15 +1,24 @@
export {
Metagraph,
GraphConfig,
BaseNodeAttributes,
BaseEdgeAttributes,
BaseNodeAttributes,
GRAPH_STATUS,
GraphConfig,
GraphStatus,
Metagraph,
} from "./modules/metagraph.ts";
export {
encrypt,
decrypt,
generateEncryptionKey,
EncryptedDataSchema,
encrypt,
type EncryptedData,
EncryptedDataSchema,
generateEncryptionKey,
} from "./crypto.ts";
export {
type DbEdgeTypeRow,
type DbGraphTypeRow,
type DbNodeTypeRow,
type DbSchema,
moduleToDbSchema,
validateEdge,
validateNode,
} from "./bridge.ts";