Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion entrypoint/flow-schema-extraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {Scalars} from "@code0-tech/sagittarius-graphql-types";
const data = await readSingleValidation(console);
const validationInput = mapToFlowValidation(data);

const result = getFlowSchemas(validationInput.flow, validationInput.functions, validationInput.dataTypes)
const result = getFlowSchemas(validationInput.flow, validationInput.functions, validationInput.dataTypes, validationInput.flowTypes)

if (result) {
type MappedResult = {
Expand Down
22 changes: 18 additions & 4 deletions entrypoint/mapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {SingleValidationInputData} from "./readSingle";
import {
DataType,
Flow, FlowSetting,
Flow, FlowSetting, FlowType,
FunctionDefinition,
NodeFunction,
NodeParameter,
Expand All @@ -27,14 +27,28 @@ import {toAllowedValue} from "@code0-tech/tucana/helpers";
export type TriangulumFlowValidationInput = {
flow?: Flow,
functions?: FunctionDefinition[],
dataTypes?: DataType[]
dataTypes?: DataType[],
flowTypes?: FlowType[]
}

export function mapToFlowValidation(data: SingleValidationInputData): TriangulumFlowValidationInput {
return {
flow: mapFlow(data.flow!),
functions: data.functions.map(mapFunctionDefinition),
dataTypes: data.dataTypes.map(mapDataType)
dataTypes: data.dataTypes.map(mapDataType),
flowTypes: [mapFlowType(data.flow!)]
}
}

// The validation input carries the flow's signature inline rather than a flow type
// catalogue, so the flow type the flow points at is reconstructed from it.
const FLOW_TYPE_ID = gid('FlowType', 1) as FlowType['id'];

function mapFlowType(flow: ValidationFlow): FlowType {
return {
__typename: "FlowType",
id: FLOW_TYPE_ID,
signature: flow.signature
}
}

Expand All @@ -46,7 +60,7 @@ function mapFlow(flow: ValidationFlow): Flow {
return {
__typename: "Flow",
id: gid('Flow', flow.flowId) as Flow['id'],
signature: flow.signature,
type: {__typename: "FlowType", id: FLOW_TYPE_ID},
startingNodeId: gid('NodeFunction', flow.startingNodeId) as NodeFunction['id'],
settings: {
nodes: flow.settings.map(mapFlowSetting)
Expand Down
2 changes: 1 addition & 1 deletion entrypoint/single-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import {getFlowValidation} from "@code0-tech/triangulum";
const data = await readSingleValidation(console);
const validationInput = mapToFlowValidation(data);

const result = getFlowValidation(validationInput.flow, validationInput.functions, validationInput.dataTypes)
const result = getFlowValidation(validationInput.flow, validationInput.functions, validationInput.dataTypes, validationInput.flowTypes)

console.info(JSON.stringify(result));
6 changes: 5 additions & 1 deletion src/extraction/getFlowSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ts from "typescript"
import type {
DataType,
Flow,
FlowType,
FunctionDefinition,
NodeFunction,
NodeParameter,
Expand Down Expand Up @@ -66,18 +67,21 @@ export type SchematizedFlow = Flow & SignatureSchemas
* @param flow - The flow to enrich; returned unchanged when undefined
* @param functions - Available function definitions (drive the generated code)
* @param dataTypes - Available data type definitions (drive the type aliases)
* @param flowTypes - Available flow types; the one the flow points at supplies
* the flow's own signature, and with it its input/output schemas
* @returns The same flow, enriched with input/output schemas, or `undefined`
* when no flow was supplied
*/
export const getFlowSchemas = (
flow?: Flow,
functions?: FunctionDefinition[],
dataTypes?: DataType[],
flowTypes?: FlowType[],
): SchematizedFlow | undefined => {
if (!flow) return undefined

const fileName = "index.ts"
const sourceCode = generateFlowSourceCode(flow, functions, dataTypes)
const sourceCode = generateFlowSourceCode(flow, functions, dataTypes, false, true, flowTypes)
const host = createCompilerHost(fileName, sourceCode)
const sourceFile = host.getSourceFile(fileName)!
const checker = host.languageService.getProgram()!.getTypeChecker()
Expand Down
7 changes: 5 additions & 2 deletions src/schema/getSignatureSchema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {DataType, Flow, FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types"
import {DataType, Flow, FlowType, FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types"
import {createCompilerHost, generateFlowSourceCode, sanitizeId} from "../utils"
import ts, {Type} from "typescript"
import {
Expand Down Expand Up @@ -76,6 +76,8 @@ interface ParameterDependency {
* @param dataTypes - Array of available data type definitions
* @param functions - Array of available function definitions
* @param nodeId - Optional specific node ID to analyze; if provided, only that node's schema is processed
* @param flowTypes - Array of available flow types; the one the flow points at supplies
* the flow's own signature, which drives the flow-level schemas
*
* @returns Array of NodeSchema objects, each containing a schema and optional blocked dependencies
*
Expand All @@ -90,9 +92,10 @@ export const getSignatureSchema = (
dataTypes: DataType[],
functions: FunctionDefinition[],
nodeId?: NodeFunction["id"],
flowTypes?: FlowType[],
): SignatureSchema => {
// Generate TypeScript source code from the flow definition
const sourceCode = generateFlowSourceCode(flow, functions, dataTypes)
const sourceCode = generateFlowSourceCode(flow, functions, dataTypes, false, true, flowTypes)

// Set up the TypeScript compiler environment
const fileName = "index.ts"
Expand Down
14 changes: 12 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
DataType,
Flow,
FlowType,
FunctionDefinition,
NodeFunction,
SubFlowValue,
Expand Down Expand Up @@ -324,6 +325,14 @@ const emitLiteralNode = (value: unknown, refExpr: Map<string, string>): string =
return stringify(value) ?? "undefined";
};

/**
* Resolves the signature a flow is callable by. A flow carries only the ID of its
* flow type, so the signature is looked up in the supplied flow types; a flow
* whose type is unknown falls back to the parameterless `(): void`.
*/
export const resolveFlowSignature = (flow?: Flow, flowTypes?: FlowType[]): string =>
flowTypes?.find(t => t?.id === flow?.type?.id)?.signature ?? "(): void";

/**
* Generates TypeScript source code for a flow, suitable for validation and type inference.
*/
Expand All @@ -332,7 +341,8 @@ export function generateFlowSourceCode(
functions?: FunctionDefinition[],
dataTypes?: DataType[],
isForInference: boolean = false,
assertNonNullReferences: boolean = true
assertNonNullReferences: boolean = true,
flowTypes?: FlowType[]
): string {
const nodes = flow?.nodes?.nodes || [];
const funcMap = new Map(functions?.map(f => [f.identifier, f]));
Expand Down Expand Up @@ -498,7 +508,7 @@ export function generateFlowSourceCode(
};

const typeDefs = getSharedTypeDeclarations(dataTypes);
const flowTypeDeclaration = `declare function flow${flow?.signature ?? "(): void"}`
const flowTypeDeclaration = `declare function flow${resolveFlowSignature(flow, flowTypes)}`
const funcDeclarations = functions?.map(f => `declare function fn_${f.identifier?.replace(/::/g, '_')}${f.signature}`).join('\n');

const nextNodeIds = new Set(nodes.map(n => n?.nextNodeId).filter(id => !!id));
Expand Down
11 changes: 8 additions & 3 deletions src/validation/getFlowValidation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ts, {flattenDiagnosticMessageText} from "typescript";
import {DataType, Flow, FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types";
import {DataType, Flow, FlowType, FunctionDefinition, NodeFunction} from "@code0-tech/sagittarius-graphql-types";
import {createCompilerHost, generateFlowSourceCode, ValidationResult} from "../utils";

// TypeScript diagnostic codes we may soften into warnings for union-branch references.
Expand Down Expand Up @@ -83,11 +83,16 @@ const isSoftReferenceMismatch = (

/**
* Validates a flow by generating virtual TypeScript code and running it through the TS compiler.
*
* The flow's own signature comes from its flow type: the flow carries only the
* type's ID, so `flowTypes` has to contain the type it points at for the flow's
* settings and its return to be validated against the real signature.
*/
export const getFlowValidation = (
flow?: Flow,
functions?: FunctionDefinition[],
dataTypes?: DataType[]
dataTypes?: DataType[],
flowTypes?: FlowType[]
): ValidationResult => {

if (!flow?.startingNodeId) {
Expand Down Expand Up @@ -140,7 +145,7 @@ export const getFlowValidation = (
// Validation keeps reference nullability (assertNonNullReferences = false) so that a
// possibly-null reference into a non-null parameter surfaces as a diagnostic, which is
// then downgraded to a warning below instead of being silently suppressed by a `!`.
const sourceCode = generateFlowSourceCode(flow, functions, dataTypes, false, false);
const sourceCode = generateFlowSourceCode(flow, functions, dataTypes, false, false, flowTypes);

// 3. Virtual TypeScript Compilation
const fileName = "index.ts";
Expand Down
15 changes: 14 additions & 1 deletion test/data.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import {DataType, FunctionDefinition} from "@code0-tech/sagittarius-graphql-types";
import {DataType, Flow, FlowType, FunctionDefinition} from "@code0-tech/sagittarius-graphql-types";
// The variants are declared as a `const enum` in a types-only package, so there
// is no runtime value to reference — the string literals are cast instead.
import type {DataTypeRulesVariant} from "@code0-tech/sagittarius-graphql-types";

/**
* A flow is callable by the signature of its flow type, and carries only that
* type's ID — so a fixture flow that needs a signature points its `type` at
* {@link FLOW_TYPE} and the call under test gets {@link flowTypesFor} with the
* signature itself. A flow whose type is not in the catalogue is `(): void`.
*/
export const FLOW_TYPE_ID = "gid://sagittarius/FlowType/1" as FlowType["id"];

export const FLOW_TYPE: Flow["type"] = {__typename: "FlowType", id: FLOW_TYPE_ID};

export const flowTypesFor = (signature: string): FlowType[] =>
[{__typename: "FlowType", id: FLOW_TYPE_ID, signature}];

/**
* Available data types and their TypeScript mappings.
*/
Expand Down
14 changes: 8 additions & 6 deletions test/flowCast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {generateFlowSourceCode} from "../src/utils";
import {getFlowValidation} from "../src/validation/getFlowValidation";
import {getSignatureSchema} from "../src/schema/getSignatureSchema";
// @ts-ignore
import {DATA_TYPES, FUNCTION_SIGNATURES} from "./data";
import {DATA_TYPES, FLOW_TYPE, FUNCTION_SIGNATURES, flowTypesFor} from "./data";

/**
* A parameter or flow setting may carry a `cast` — the type its value is meant to
Expand Down Expand Up @@ -178,11 +178,13 @@ describe("cast handling in the generated flow source", () => {
// The REST trigger is `<T extends TYPE>(input_schema: T, ...)`: whatever
// the `input_schema` setting resolves to is what T — and with it the
// trigger's payload — binds to.
const REST_TRIGGER_TYPES = flowTypesFor(
"<T extends TYPE>(input_schema: T, httpURL: HTTP_URL, httpMethod: HTTP_METHOD): REST_ADAPTER_INPUT<T>",
);
const restTrigger = (cast?: string): Flow => ({
id: "gid://sagittarius/Flow/1",
startingNodeId: "gid://sagittarius/NodeFunction/1",
signature:
"<T extends TYPE>(input_schema: T, httpURL: HTTP_URL, httpMethod: HTTP_METHOD): REST_ADAPTER_INPUT<T>",
type: FLOW_TYPE,
settings: {
nodes: [
{value: {name: "text"}, cast},
Expand All @@ -194,19 +196,19 @@ describe("cast handling in the generated flow source", () => {
} as Flow);

const cast = "{ name: TEXT, age: NUMBER }";
expect(generateFlowSourceCode(restTrigger(cast), FUNCTION_SIGNATURES, DATA_TYPES))
expect(generateFlowSourceCode(restTrigger(cast), FUNCTION_SIGNATURES, DATA_TYPES, false, true, REST_TRIGGER_TYPES))
.toContain(`as unknown as ${cast}`);

// Uncast, T is the shape of the setting value itself. The payload is a
// return, so it renders that shape as data — never as the `T extends
// TYPE` picker the parameter side uses.
const uncastPayload = (getSignatureSchema(restTrigger(), DATA_TYPES, FUNCTION_SIGNATURES)
const uncastPayload = (getSignatureSchema(restTrigger(), DATA_TYPES, FUNCTION_SIGNATURES, undefined, REST_TRIGGER_TYPES)
.return as { properties: Record<string, any> }).properties.payload;
expect(uncastPayload.input).toBe("data");
expect(uncastPayload.type).toBe("{ name: string; }");

// Cast, T is the cast type — the payload gains the declared `age`.
const castPayload = (getSignatureSchema(restTrigger(cast), DATA_TYPES, FUNCTION_SIGNATURES)
const castPayload = (getSignatureSchema(restTrigger(cast), DATA_TYPES, FUNCTION_SIGNATURES, undefined, REST_TRIGGER_TYPES)
.return as { properties: Record<string, any> }).properties.payload;
expect(castPayload.input).toBe("data");
expect(castPayload.properties.age.input).toBe("number");
Expand Down
27 changes: 15 additions & 12 deletions test/flowSchemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {describe, expect, it} from "vitest";
import type {Flow, FunctionDefinition, NodeFunction, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types";
import {getFlowSchemas} from "../src/server";
import type {JsonSchema, SchematizedSubFlowValue} from "../src/server";
import {DATA_TYPES, FUNCTION_SIGNATURES} from "./data";
import {DATA_TYPES, FLOW_TYPE, FUNCTION_SIGNATURES, flowTypesFor} from "./data";

type NodeId = NonNullable<NodeFunction["id"]>;

Expand Down Expand Up @@ -52,11 +52,13 @@ const node = (
parameters: {nodes: parameterValues.map((value) => ({value}))},
});

const flowWithNodes = (nodes: NodeFunction[], signature = "(): void"): Flow => ({
// The flow points at the fixture flow type; a test whose schemas depend on the
// flow's own signature passes that type's signature via `flowTypesFor`.
const flowWithNodes = (nodes: NodeFunction[]): Flow => ({
id: FLOW_ID,
startingNodeId: nodes[0]?.id,
nodes: {nodes},
signature,
type: FLOW_TYPE,
});

/**
Expand All @@ -82,9 +84,10 @@ describe("getFlowSchemas", () => {
});

it("adds input/output schemas derived from the flow signature", () => {
const flow = flowWithNodes([], "(method: HTTP_METHOD): NUMBER");
const flow = flowWithNodes([]);

const result = getFlowSchemas(flow, FUNCTION_SIGNATURES, DATA_TYPES);
const result = getFlowSchemas(flow, FUNCTION_SIGNATURES, DATA_TYPES,
flowTypesFor("(method: HTTP_METHOD): NUMBER"));

// inputSchema is an object keyed by parameter name.
expect(result?.inputSchema.type).toBe("object");
Expand All @@ -104,12 +107,11 @@ describe("getFlowSchemas", () => {
// Taken from the REST trigger scenario in schema.test.ts: input_schema is
// the non-generic TYPE data type, echoed by the payload of
// REST_ADAPTER_INPUT<TYPE>.
const flow = flowWithNodes(
[],
"<T extends TYPE>(input_schema: T, httpURL: HTTP_URL, httpMethod: HTTP_METHOD): REST_ADAPTER_INPUT<T>",
);
const flow = flowWithNodes([]);

const result = getFlowSchemas(flow, FUNCTION_SIGNATURES, DATA_TYPES);
const result = getFlowSchemas(flow, FUNCTION_SIGNATURES, DATA_TYPES, flowTypesFor(
"<T extends TYPE>(input_schema: T, httpURL: HTTP_URL, httpMethod: HTTP_METHOD): REST_ADAPTER_INPUT<T>",
));

// TYPE is `any` underneath, so it maps to the open object schema; the
// concrete parameters resolve fully.
Expand All @@ -131,9 +133,10 @@ describe("getFlowSchemas", () => {
});

it("maps the DATE data type to its underlying number type", () => {
const flow = flowWithNodes([], "(created: DATE): void");
const flow = flowWithNodes([]);

const result = getFlowSchemas(flow, FUNCTION_SIGNATURES, DATA_TYPES);
const result = getFlowSchemas(flow, FUNCTION_SIGNATURES, DATA_TYPES,
flowTypesFor("(created: DATE): void"));

expect(result?.inputSchema.properties?.created).toEqual({type: "number"});
});
Expand Down
Loading
Loading