|
|
|
|
@@ -13,6 +13,12 @@ import {
|
|
|
|
|
reachableFrom as reachableFromFn,
|
|
|
|
|
} from "./queries.js";
|
|
|
|
|
import { validate as _validate } from "./validation.js";
|
|
|
|
|
import {
|
|
|
|
|
OperationNodeAttrs as OperationNodeAttrsSchema,
|
|
|
|
|
OperationEdgeAttrs as OperationEdgeAttrsSchema,
|
|
|
|
|
} from "../schema/index.js";
|
|
|
|
|
import type { OperationNodeAttrs } from "../schema/index.js";
|
|
|
|
|
import { typeCompat, type TypeCompatResult } from "../analysis/type-compat.js";
|
|
|
|
|
|
|
|
|
|
export interface FlowGraphOptions {
|
|
|
|
|
type?: "directed";
|
|
|
|
|
@@ -20,6 +26,26 @@ export interface FlowGraphOptions {
|
|
|
|
|
allowSelfLoops?: false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface OperationSpec {
|
|
|
|
|
name: string;
|
|
|
|
|
namespace: string;
|
|
|
|
|
version: string;
|
|
|
|
|
type: "query" | "mutation" | "subscription";
|
|
|
|
|
inputSchema: TSchema;
|
|
|
|
|
outputSchema: TSchema;
|
|
|
|
|
description?: string;
|
|
|
|
|
tags?: string[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type OperationGraph = FlowGraph<typeof OperationNodeAttrsSchema, typeof OperationEdgeAttrsSchema>;
|
|
|
|
|
|
|
|
|
|
type TypedEdgeAttrs = {
|
|
|
|
|
edgeType: "typed";
|
|
|
|
|
compatible: boolean;
|
|
|
|
|
detail?: string;
|
|
|
|
|
mismatches?: TypeCompatResult["mismatches"];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type Attrs = Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
export class FlowGraph<
|
|
|
|
|
@@ -292,10 +318,47 @@ export class FlowGraph<
|
|
|
|
|
return _validate(this, schema as NodeAttrs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static fromSpecs(
|
|
|
|
|
_specs: unknown[],
|
|
|
|
|
): FlowGraph<TSchema, TSchema> {
|
|
|
|
|
throw new Error("not implemented");
|
|
|
|
|
addOperation(spec: OperationSpec): void {
|
|
|
|
|
const key = `${spec.namespace}.${spec.name}`;
|
|
|
|
|
this.addNode(key, {
|
|
|
|
|
name: spec.name,
|
|
|
|
|
namespace: spec.namespace,
|
|
|
|
|
version: spec.version,
|
|
|
|
|
type: spec.type,
|
|
|
|
|
inputSchema: spec.inputSchema,
|
|
|
|
|
outputSchema: spec.outputSchema,
|
|
|
|
|
...(spec.description !== undefined ? { description: spec.description } : {}),
|
|
|
|
|
...(spec.tags !== undefined ? { tags: spec.tags } : {}),
|
|
|
|
|
} as Static<NodeAttrs>);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addTypedEdge(
|
|
|
|
|
source: string,
|
|
|
|
|
target: string,
|
|
|
|
|
attrs: { compatible: boolean; detail?: string; mismatches?: TypeCompatResult["mismatches"] },
|
|
|
|
|
): void {
|
|
|
|
|
if (!this._graph.hasNode(source)) {
|
|
|
|
|
throw new NodeNotFoundError(source);
|
|
|
|
|
}
|
|
|
|
|
if (!this._graph.hasNode(target)) {
|
|
|
|
|
throw new NodeNotFoundError(target);
|
|
|
|
|
}
|
|
|
|
|
const edgeAttrs: TypedEdgeAttrs = {
|
|
|
|
|
edgeType: "typed",
|
|
|
|
|
compatible: attrs.compatible,
|
|
|
|
|
...(attrs.detail !== undefined ? { detail: attrs.detail } : {}),
|
|
|
|
|
...(attrs.mismatches !== undefined ? { mismatches: attrs.mismatches } : {}),
|
|
|
|
|
};
|
|
|
|
|
this.addEdge(source, target, edgeAttrs as Static<EdgeAttrs>);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static fromSpecs(specs: OperationSpec[]): OperationGraph {
|
|
|
|
|
const graph = new FlowGraph<typeof OperationNodeAttrsSchema, typeof OperationEdgeAttrsSchema>();
|
|
|
|
|
for (const spec of specs) {
|
|
|
|
|
graph.addOperation(spec);
|
|
|
|
|
}
|
|
|
|
|
buildTypeEdges(graph);
|
|
|
|
|
return graph;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static fromCallEvents(
|
|
|
|
|
@@ -334,4 +397,25 @@ export class FlowGraph<
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildTypeEdges(graph: OperationGraph): void {
|
|
|
|
|
const nodeKeys = graph.nodes();
|
|
|
|
|
for (const source of nodeKeys) {
|
|
|
|
|
for (const target of nodeKeys) {
|
|
|
|
|
if (source === target) continue;
|
|
|
|
|
const sourceAttrs = graph.getNodeAttributes(source as never) as unknown as OperationNodeAttrs;
|
|
|
|
|
const targetAttrs = graph.getNodeAttributes(target as never) as unknown as OperationNodeAttrs;
|
|
|
|
|
const result = typeCompat(sourceAttrs.outputSchema as TSchema, targetAttrs.inputSchema as TSchema);
|
|
|
|
|
if (result === undefined) continue;
|
|
|
|
|
if (graph.hasEdge(source, target)) continue;
|
|
|
|
|
if (willCreateCycle(graph.graph, source, target)) continue;
|
|
|
|
|
const detail = result.detail ?? `${sourceAttrs.namespace}.${sourceAttrs.name}.output → ${targetAttrs.namespace}.${targetAttrs.name}.input`;
|
|
|
|
|
graph.addTypedEdge(source, target, {
|
|
|
|
|
compatible: result.compatible,
|
|
|
|
|
detail,
|
|
|
|
|
...(result.mismatches !== undefined ? { mismatches: result.mismatches } : {}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|