@dypai-ai/flow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ import { flow } from "../src/builder.ts";
2
+ import { db, email } from "../src/nodes.ts";
3
+ import { ref } from "../src/ref.ts";
4
+ import { s } from "../src/schema.ts";
5
+
6
+ export default flow("create-booking")
7
+ .sourceFile("dypai/flows/create-booking.flow.ts")
8
+ .http({ method: "POST", auth: "jwt", roles: ["authenticated"] })
9
+ .input({
10
+ name: s.string(),
11
+ email: s.email(),
12
+ date: s.date(),
13
+ })
14
+ .output({
15
+ booking: s.object({
16
+ id: s.string(),
17
+ name: s.string(),
18
+ email: s.email(),
19
+ date: s.date(),
20
+ }),
21
+ })
22
+ .step("booking", db.insert({
23
+ table: "public.bookings",
24
+ values: {
25
+ name: ref.input("name"),
26
+ email: ref.input("email"),
27
+ date: ref.input("date"),
28
+ user_id: ref.currentUserId(),
29
+ },
30
+ returning: ["id", "name", "email", "date"],
31
+ }))
32
+ .step("confirmation", email.send({
33
+ to: ref.input("email"),
34
+ template: "booking_confirmation",
35
+ variables: {
36
+ booking: ref.step("booking"),
37
+ },
38
+ }))
39
+ .return({
40
+ booking: ref.step("booking"),
41
+ });
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@dypai-ai/flow",
3
+ "version": "0.1.0",
4
+ "description": "Typed Flow DSL for authoring DYPAI backend endpoints in TypeScript",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/DYPAI-SOLUTIONS/dypai.git"
13
+ },
14
+ "homepage": "https://dypai.ai",
15
+ "keywords": [
16
+ "dypai",
17
+ "flow",
18
+ "workflow",
19
+ "backend",
20
+ "endpoints"
21
+ ],
22
+ "exports": {
23
+ ".": "./src/index.ts"
24
+ },
25
+ "files": [
26
+ "src/",
27
+ "flows/",
28
+ "tsconfig.json"
29
+ ],
30
+ "scripts": {
31
+ "test": "bun test",
32
+ "typecheck": "bunx tsc --noEmit"
33
+ },
34
+ "dependencies": {
35
+ "@dypai/workflow-core": "npm:@dypai-ai/workflow-core@^0.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@dypai/workflow-core": "file:../dypai-workflow-core",
39
+ "@types/bun": "^1.1.14",
40
+ "typescript": "^5.7.2"
41
+ }
42
+ }
package/src/builder.ts ADDED
@@ -0,0 +1,144 @@
1
+ import type {
2
+ AuthIR,
3
+ FlowDefinition,
4
+ JsonSchema,
5
+ RefIR,
6
+ ResponseIR,
7
+ TriggerIR,
8
+ } from "@dypai/workflow-core";
9
+ import { inferOutputSchemaFromReturns, objectSchemaFromFields, type SchemaField } from "./schema";
10
+ import type { FlowStepCall } from "./types";
11
+
12
+ type HttpOptions = {
13
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
14
+ auth: "public" | "jwt";
15
+ roles?: string[];
16
+ };
17
+
18
+ export class FlowBuilder {
19
+ private triggerValue?: TriggerIR;
20
+ private authValue?: AuthIR;
21
+ private inputValue?: JsonSchema;
22
+ private outputValue?: JsonSchema;
23
+ private responseValue: ResponseIR = { cardinality: "single" };
24
+ private stepsValue: FlowDefinition["steps"] = [];
25
+ private sourcePath?: string;
26
+ private endpointMeta: FlowDefinition["endpoint"];
27
+
28
+ constructor(private readonly flowName: string) {}
29
+
30
+ sourceFile(path: string): this {
31
+ this.sourcePath = path;
32
+ return this;
33
+ }
34
+
35
+ http(options: HttpOptions): this {
36
+ this.triggerValue = { type: "http", method: options.method };
37
+ this.authValue = options.auth === "public"
38
+ ? { mode: "public" }
39
+ : {
40
+ mode: "jwt",
41
+ allowedRoles: options.roles !== undefined ? options.roles : [],
42
+ };
43
+ return this;
44
+ }
45
+
46
+ description(text: string): this {
47
+ this.endpointMeta = { ...(this.endpointMeta || {}), description: text };
48
+ return this;
49
+ }
50
+
51
+ tool(options?: { description?: string }): this {
52
+ this.endpointMeta = {
53
+ ...(this.endpointMeta || {}),
54
+ isTool: true,
55
+ ...(options?.description ? { toolDescription: options.description } : {}),
56
+ };
57
+ return this;
58
+ }
59
+
60
+ input(fields: Record<string, SchemaField>): this {
61
+ this.inputValue = objectSchemaFromFields(fields);
62
+ return this;
63
+ }
64
+
65
+ output(fields: Record<string, SchemaField>): this {
66
+ this.outputValue = objectSchemaFromFields(fields);
67
+ return this;
68
+ }
69
+
70
+ response(cardinality: ResponseIR["cardinality"] | "array"): this {
71
+ this.responseValue = { cardinality: cardinality === "array" ? "many" : cardinality };
72
+ return this;
73
+ }
74
+
75
+ step(id: string, stepCall: FlowStepCall): this {
76
+ this.stepsValue.push({
77
+ id,
78
+ call: stepCall.call,
79
+ config: stepCall.config,
80
+ ...(this.sourcePath ? {
81
+ source: {
82
+ file: this.sourcePath,
83
+ stepId: id,
84
+ },
85
+ } : {}),
86
+ });
87
+ return this;
88
+ }
89
+
90
+ return(returns: Record<string, RefIR>): FlowDefinition {
91
+ return this.build(returns);
92
+ }
93
+
94
+ build(returns?: Record<string, RefIR>): FlowDefinition {
95
+ if (!this.triggerValue || !this.authValue) {
96
+ throw new Error(`Flow '${this.flowName}' must declare .http(...) before building.`);
97
+ }
98
+ if (!this.inputValue) {
99
+ throw new Error(`Flow '${this.flowName}' must declare .input(...) before building.`);
100
+ }
101
+ if (!returns || !Object.keys(returns).length) {
102
+ throw new Error(`Flow '${this.flowName}' must declare .return({...}).`);
103
+ }
104
+ if (!this.stepsValue.length) {
105
+ throw new Error(`Flow '${this.flowName}' must declare at least one .step(...).`);
106
+ }
107
+
108
+ const outputSchema = this.outputValue ?? inferOutputSchemaFromReturns(returns);
109
+ const edges: Array<{ from: string; to: string }> = [];
110
+ if (this.stepsValue.length) {
111
+ edges.push({ from: "$trigger", to: this.stepsValue[0].id });
112
+ for (let i = 0; i < this.stepsValue.length - 1; i += 1) {
113
+ edges.push({ from: this.stepsValue[i].id, to: this.stepsValue[i + 1].id });
114
+ }
115
+ edges.push({ from: this.stepsValue[this.stepsValue.length - 1].id, to: "return" });
116
+ }
117
+
118
+ return {
119
+ name: this.flowName,
120
+ ...(this.sourcePath ? {
121
+ source: {
122
+ kind: "flow-ts",
123
+ file: this.sourcePath,
124
+ },
125
+ } : {}),
126
+ ...(this.endpointMeta ? { endpoint: this.endpointMeta } : {}),
127
+ trigger: this.triggerValue,
128
+ auth: this.authValue,
129
+ input: this.inputValue,
130
+ output: outputSchema,
131
+ response: this.responseValue,
132
+ steps: this.stepsValue,
133
+ returns,
134
+ edges,
135
+ };
136
+ }
137
+ }
138
+
139
+ export function flow(name: string): FlowBuilder {
140
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(name)) {
141
+ throw new Error(`Flow name '${name}' must be a kebab-case slug.`);
142
+ }
143
+ return new FlowBuilder(name);
144
+ }
@@ -0,0 +1,69 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, test } from "bun:test";
5
+ import type { CapabilityCatalog } from "@dypai/workflow-core";
6
+ import {
7
+ capability,
8
+ compileFlowDefinition,
9
+ dypai,
10
+ flow,
11
+ generateCapabilityWrappersSource,
12
+ ref,
13
+ s,
14
+ } from "./index";
15
+
16
+ const fixturePath = join(
17
+ dirname(fileURLToPath(import.meta.url)),
18
+ "..",
19
+ "..",
20
+ "dypai-workflow-core",
21
+ "fixtures",
22
+ "capability-catalog.json",
23
+ );
24
+ const fixtureCatalog = JSON.parse(readFileSync(fixturePath, "utf-8")) as CapabilityCatalog;
25
+
26
+ describe("@dypai/flow dynamic capabilities", () => {
27
+ test("capability() emits grouped calls", () => {
28
+ expect(capability("stripe.checkout", "createSession", { price_id: "price_123" })).toEqual({
29
+ call: "stripe.checkout.createSession",
30
+ config: { price_id: "price_123" },
31
+ });
32
+ });
33
+
34
+ test("generates wrapper source from catalog fixture", () => {
35
+ const source = generateCapabilityWrappersSource(fixtureCatalog);
36
+ expect(source).toContain('capability("stripe.checkout", "createSession", config)');
37
+ expect(source).toContain('capability("google.sheets", "readRows", config)');
38
+ });
39
+
40
+ test("compiles grouped flow definition with catalog fixture", () => {
41
+ const definition = flow("checkout-session")
42
+ .http({ method: "POST", auth: "public" })
43
+ .input({ priceId: s.string() })
44
+ .step("checkout", capability("stripe.checkout", "createSession", {
45
+ mode: "payment",
46
+ price_id: ref.input("priceId"),
47
+ success_url: "https://example.com/success",
48
+ cancel_url: "https://example.com/cancel",
49
+ }))
50
+ .step("sheet", capability("google.sheets", "readRows", {
51
+ spreadsheet_id: "sheet_123",
52
+ }))
53
+ .return({
54
+ checkout: ref.step("checkout"),
55
+ rows: ref.step("sheet", "rows"),
56
+ });
57
+
58
+ const compiled = compileFlowDefinition(definition, { capabilityCatalog: fixtureCatalog });
59
+ expect(compiled.ok).toBe(true);
60
+ expect(compiled.workflowCode?.nodes.some((node) =>
61
+ node.node_type === "stripe"
62
+ && node.parameters.operation === "stripe_create_checkout_session",
63
+ )).toBe(true);
64
+ });
65
+
66
+ test("legacy db helper still works", () => {
67
+ expect(dypai.db.query({ sql: "SELECT 1" }).call).toBe("dypai.db.query");
68
+ });
69
+ });
@@ -0,0 +1,147 @@
1
+ import {
2
+ flowDefinitionToIr,
3
+ irToWorkflowCodeV2,
4
+ type CapabilityCatalog,
5
+ type FlowDefinition,
6
+ type IRDiagnostic,
7
+ type WorkflowIR,
8
+ validateWorkflowIR,
9
+ } from "@dypai/workflow-core";
10
+ import { createHash } from "node:crypto";
11
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
+ import type { CompileFlowResult, FlowCompileDiagnostic } from "./types";
15
+
16
+ const DEFAULT_COMPILE_TIMEOUT_MS = 5_000;
17
+ const FLOW_PACKAGE_SRC = fileURLToPath(new URL(".", import.meta.url));
18
+ const FLOW_COMPILE_CACHE_DIR = fileURLToPath(new URL("../.flow-compile-cache", import.meta.url));
19
+
20
+ async function materializeFlowFileForImport(sourcePath: string): Promise<string> {
21
+ const raw = await readFile(sourcePath, "utf-8");
22
+ if (!raw.includes("@dypai/flow")) {
23
+ return sourcePath;
24
+ }
25
+
26
+ const flowEntry = join(FLOW_PACKAGE_SRC, "index.ts").replace(/\\/g, "/");
27
+ const rewritten = raw.replace(/from\s+["']@dypai\/flow["']/g, `from "${flowEntry}"`);
28
+ const cacheKey = createHash("sha256").update(`${sourcePath}\0${rewritten}`).digest("hex");
29
+ await mkdir(FLOW_COMPILE_CACHE_DIR, { recursive: true });
30
+ const cachePath = join(FLOW_COMPILE_CACHE_DIR, `${cacheKey}.flow.ts`);
31
+ await writeFile(cachePath, rewritten);
32
+ return cachePath;
33
+ }
34
+
35
+ function mapDiagnostic(item: IRDiagnostic): FlowCompileDiagnostic {
36
+ return {
37
+ severity: item.severity,
38
+ rule: item.rule,
39
+ message: item.message,
40
+ fixHint: item.fixHint,
41
+ file: item.location?.file,
42
+ };
43
+ }
44
+
45
+ function isFlowDefinition(value: unknown): value is FlowDefinition {
46
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
47
+ const candidate = value as FlowDefinition;
48
+ return typeof candidate.name === "string"
49
+ && Array.isArray(candidate.steps)
50
+ && candidate.input !== undefined
51
+ && candidate.output !== undefined
52
+ && candidate.returns !== undefined;
53
+ }
54
+
55
+ export function compileFlowDefinition(
56
+ definition: FlowDefinition,
57
+ options: { adapt?: boolean; capabilityCatalog?: CapabilityCatalog } = {},
58
+ ): CompileFlowResult {
59
+ const diagnostics: FlowCompileDiagnostic[] = [];
60
+ const compileContext = options.capabilityCatalog
61
+ ? { capabilityCatalog: options.capabilityCatalog }
62
+ : {};
63
+ const workflow = flowDefinitionToIr(definition, compileContext);
64
+ const validation = validateWorkflowIR(workflow, {
65
+ capabilityCatalog: options.capabilityCatalog,
66
+ });
67
+ diagnostics.push(...validation.diagnostics.map(mapDiagnostic));
68
+
69
+ if (!validation.ok) {
70
+ return { ok: false, definition, workflow, diagnostics };
71
+ }
72
+
73
+ if (options.adapt === false) {
74
+ return { ok: true, definition, workflow, diagnostics };
75
+ }
76
+
77
+ const adapted = irToWorkflowCodeV2(workflow, {
78
+ capabilityCatalog: options.capabilityCatalog,
79
+ });
80
+ diagnostics.push(...adapted.diagnostics.map(mapDiagnostic));
81
+ if (!adapted.ok || !adapted.value) {
82
+ return { ok: false, definition, workflow, diagnostics };
83
+ }
84
+
85
+ return {
86
+ ok: true,
87
+ definition,
88
+ workflow,
89
+ workflowCode: adapted.value,
90
+ diagnostics,
91
+ };
92
+ }
93
+
94
+ async function importFlowModule(filePath: string, timeoutMs: number): Promise<unknown> {
95
+ const url = `${pathToFileURL(filePath).href}?flow-compile=${Date.now()}`;
96
+ const imported = Promise.resolve(import(url));
97
+ const timeout = new Promise<never>((_, reject) => {
98
+ setTimeout(() => reject(new Error(`Flow compile timed out after ${timeoutMs}ms.`)), timeoutMs);
99
+ });
100
+ return Promise.race([imported, timeout]);
101
+ }
102
+
103
+ export async function compileFlowFile(
104
+ filePath: string,
105
+ options: { timeoutMs?: number; adapt?: boolean; capabilityCatalog?: CapabilityCatalog } = {},
106
+ ): Promise<CompileFlowResult> {
107
+ const diagnostics: FlowCompileDiagnostic[] = [];
108
+ const timeoutMs = options.timeoutMs ?? DEFAULT_COMPILE_TIMEOUT_MS;
109
+
110
+ try {
111
+ const resolvedPath = await materializeFlowFileForImport(filePath);
112
+ const mod = await importFlowModule(resolvedPath, timeoutMs) as { default?: unknown };
113
+ const definition = mod.default;
114
+ if (!isFlowDefinition(definition)) {
115
+ diagnostics.push({
116
+ severity: "error",
117
+ rule: "flow_invalid_default_export",
118
+ message: `Flow file '${filePath}' must default-export a FlowDefinition from flow(...).return(...).`,
119
+ file: filePath,
120
+ fixHint: "Use export default flow(\"my-endpoint\").http(...).input(...).step(...).return(...).",
121
+ });
122
+ return { ok: false, diagnostics };
123
+ }
124
+
125
+ const withSource: FlowDefinition = {
126
+ ...definition,
127
+ source: definition.source ?? { kind: "flow-ts", file: filePath },
128
+ };
129
+
130
+ return compileFlowDefinition(withSource, {
131
+ adapt: options.adapt,
132
+ capabilityCatalog: options.capabilityCatalog,
133
+ });
134
+ } catch (error) {
135
+ diagnostics.push({
136
+ severity: "error",
137
+ rule: "flow_compile_failed",
138
+ message: error instanceof Error ? error.message : "Failed to compile flow file.",
139
+ file: filePath,
140
+ });
141
+ return { ok: false, diagnostics };
142
+ }
143
+ }
144
+
145
+ export function compileFlowDefinitionToIr(definition: FlowDefinition): WorkflowIR {
146
+ return flowDefinitionToIr(definition);
147
+ }
@@ -0,0 +1,134 @@
1
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { fileURLToPath } from "node:url";
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ compileFlowDefinition,
8
+ compileFlowFile,
9
+ db,
10
+ email,
11
+ flow,
12
+ ref,
13
+ s,
14
+ } from "./index";
15
+
16
+ const demoFlowPath = join(
17
+ dirname(fileURLToPath(import.meta.url)),
18
+ "..",
19
+ "flows",
20
+ "create-booking.flow.ts",
21
+ );
22
+
23
+ describe("@dypai/flow schema helpers", () => {
24
+ test("s helpers produce JSON Schema fragments", () => {
25
+ expect(s.string()).toEqual({ type: "string" });
26
+ expect(s.email()).toEqual({ type: "string", format: "email" });
27
+ expect(s.date()).toEqual({ type: "string", format: "date" });
28
+ expect(s.optional(s.string())).toEqual({ type: "string", optional: true });
29
+ });
30
+ });
31
+
32
+ describe("@dypai/flow ref helpers", () => {
33
+ test("ref helpers produce canonical RefIR", () => {
34
+ expect(ref.input("email")).toEqual({ kind: "input", path: ["email"] });
35
+ expect(ref.currentUserId()).toEqual({ kind: "currentUserId" });
36
+ expect(ref.step("booking", "id")).toEqual({ kind: "step", stepId: "booking", path: ["id"] });
37
+ expect(ref.literal(true)).toEqual({ kind: "literal", value: true });
38
+ });
39
+ });
40
+
41
+ describe("flow() builder", () => {
42
+ test("produces FlowDefinition with mapped node calls", () => {
43
+ const definition = flow("create-booking")
44
+ .http({ method: "POST", auth: "jwt", roles: ["authenticated"] })
45
+ .input({
46
+ name: s.string(),
47
+ email: s.email(),
48
+ date: s.date(),
49
+ })
50
+ .step("booking", db.insert({
51
+ table: "public.bookings",
52
+ values: {
53
+ name: ref.input("name"),
54
+ email: ref.input("email"),
55
+ date: ref.input("date"),
56
+ user_id: ref.currentUserId(),
57
+ },
58
+ }))
59
+ .step("confirmation", email.send({
60
+ to: ref.input("email"),
61
+ template: "booking_confirmation",
62
+ body: "fallback",
63
+ }))
64
+ .return({
65
+ booking: ref.step("booking"),
66
+ });
67
+
68
+ expect(definition.name).toBe("create-booking");
69
+ expect(definition.steps[0]?.call).toBe("dypai.db.insert");
70
+ expect(definition.steps[1]?.call).toBe("dypai.email.send");
71
+ expect(definition.auth).toEqual({ mode: "jwt", allowedRoles: ["authenticated"] });
72
+ });
73
+
74
+ test("jwt without explicit roles defaults to empty allowed_roles", () => {
75
+ const definition = flow("open-dashboard")
76
+ .http({ method: "GET", auth: "jwt" })
77
+ .input({ _unused: s.optional(s.string()) })
78
+ .step("main", db.query({ sql: "SELECT 1" }))
79
+ .return({ data: ref.step("main") });
80
+
81
+ expect(definition.auth).toEqual({ mode: "jwt", allowedRoles: [] });
82
+ });
83
+
84
+ test("response alias array maps to many", () => {
85
+ const definition = flow("weekly-stats")
86
+ .http({ method: "GET", auth: "jwt" })
87
+ .response("array")
88
+ .input({ _unused: s.optional(s.string()) })
89
+ .step("main", db.query({ sql: "SELECT 1" }))
90
+ .return({ data: ref.step("main") });
91
+
92
+ expect(definition.response).toEqual({ cardinality: "many" });
93
+ });
94
+ });
95
+
96
+ describe("flow compiler", () => {
97
+ test("compiles create-booking.flow.ts to valid WorkflowIR and workflow_code", async () => {
98
+ const compiled = await compileFlowFile(demoFlowPath);
99
+ expect(compiled.ok).toBe(true);
100
+ expect(compiled.definition?.name).toBe("create-booking");
101
+ expect(compiled.workflow?.steps.some((step) => step.node === "dypai.db" && step.operation === "insert")).toBe(true);
102
+ expect(compiled.workflow?.steps.some((step) => step.node === "dypai.email" && step.operation === "send")).toBe(true);
103
+ expect(compiled.workflowCode?.nodes.some((node) => node.node_type === "dypai_database")).toBe(true);
104
+ expect(compiled.workflowCode?.nodes.some((node) => node.node_type === "resend")).toBe(true);
105
+ expect(compiled.workflowCode?.nodes.some((node) => node.node_type === "set_fields" && node.is_return)).toBe(true);
106
+ });
107
+
108
+ test("returns actionable diagnostics for invalid flow files", async () => {
109
+ const root = await mkdtemp(join(tmpdir(), "dypai-flow-invalid-"));
110
+ const badPath = join(root, "bad.flow.ts");
111
+ await writeFile(badPath, "export default { name: 'nope' };\n");
112
+
113
+ try {
114
+ const compiled = await compileFlowFile(badPath);
115
+ expect(compiled.ok).toBe(false);
116
+ expect(compiled.diagnostics.some((item) => item.rule === "flow_invalid_default_export")).toBe(true);
117
+ } finally {
118
+ await rm(root, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ test("compileFlowDefinition adapts IR without YAML", () => {
123
+ const definition = flow("ping")
124
+ .http({ method: "GET", auth: "public" })
125
+ .input({ ok: s.optional(s.boolean()) })
126
+ .step("noop", db.query({ sql: "SELECT 1 AS ok" }))
127
+ .return({ ok: ref.literal(true) });
128
+
129
+ const compiled = compileFlowDefinition(definition);
130
+ expect(compiled.ok).toBe(true);
131
+ expect(compiled.workflow?.version).toBe("workflow-ir.v1");
132
+ expect(compiled.workflowCode?.schema_version).toBe("2.0");
133
+ });
134
+ });
@@ -0,0 +1,38 @@
1
+ import type { CapabilityCatalog } from "@dypai/workflow-core";
2
+
3
+ export function generateCapabilityWrappersSource(catalog: CapabilityCatalog): string {
4
+ const lines = [
5
+ "// Auto-generated from dypai/capability-catalog.json. Do not edit by hand.",
6
+ 'import { capability } from "@dypai/flow";',
7
+ "",
8
+ ];
9
+
10
+ const namespaces = new Map<string, Map<string, Array<{ operation: string; groupId: string }>>>();
11
+
12
+ for (const group of catalog.groups) {
13
+ const resourceMap = namespaces.get(group.namespace) ?? new Map();
14
+ namespaces.set(group.namespace, resourceMap);
15
+ const operations = resourceMap.get(group.resource) ?? [];
16
+ resourceMap.set(group.resource, operations);
17
+ for (const operation of group.operations) {
18
+ operations.push({ operation: operation.operation, groupId: group.id });
19
+ }
20
+ }
21
+
22
+ for (const [namespace, resources] of namespaces) {
23
+ lines.push(`export const ${namespace} = {`);
24
+ for (const [resource, operations] of resources) {
25
+ lines.push(` ${resource}: {`);
26
+ for (const item of operations) {
27
+ lines.push(` ${item.operation}(config: Record<string, unknown>) {`);
28
+ lines.push(` return capability("${item.groupId}", "${item.operation}", config);`);
29
+ lines.push(" },");
30
+ }
31
+ lines.push(" },");
32
+ }
33
+ lines.push("};");
34
+ lines.push("");
35
+ }
36
+
37
+ return `${lines.join("\n").trimEnd()}\n`;
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { flow, FlowBuilder } from "./builder";
2
+ export { compileFlowDefinition, compileFlowDefinitionToIr, compileFlowFile } from "./compiler";
3
+ export { capability, db, email, dypai } from "./nodes";
4
+ export { generateCapabilityWrappersSource } from "./generateCapabilityWrappers";
5
+ export { ref } from "./ref";
6
+ export { inferOutputSchemaFromReturns, objectSchemaFromFields, s, type SchemaField } from "./schema";
7
+ export type { CompileFlowResult, FlowCompileDiagnostic, FlowStepCall } from "./types";
8
+
9
+ export type {
10
+ CapabilityCatalog,
11
+ FlowDefinition,
12
+ FlowStepDefinition,
13
+ WorkflowIR,
14
+ } from "@dypai/workflow-core";
package/src/nodes.ts ADDED
@@ -0,0 +1,62 @@
1
+ export type { FlowStepCall } from "./types";
2
+
3
+ export function capability(
4
+ groupId: string,
5
+ operation: string,
6
+ config: Record<string, unknown>,
7
+ ): import("./types").FlowStepCall {
8
+ return {
9
+ call: `${groupId}.${operation}`,
10
+ config,
11
+ };
12
+ }
13
+
14
+ export const db = {
15
+ query(config: { sql: string; params?: Record<string, unknown> }) {
16
+ return capability("dypai.db", "query", config);
17
+ },
18
+ insert(config: { table: string; values: Record<string, unknown>; returning?: string[] }) {
19
+ return capability("dypai.db", "insert", config);
20
+ },
21
+ update(config: {
22
+ table: string;
23
+ where: Record<string, unknown>;
24
+ set: Record<string, unknown>;
25
+ returning?: string[];
26
+ }) {
27
+ return capability("dypai.db", "update", config);
28
+ },
29
+ list(config: {
30
+ table: string;
31
+ where?: Record<string, unknown>;
32
+ orderBy?: string;
33
+ limit?: unknown;
34
+ offset?: unknown;
35
+ }) {
36
+ return capability("dypai.db", "list", config);
37
+ },
38
+ };
39
+
40
+ export const email = {
41
+ send(config: {
42
+ to: unknown;
43
+ subject?: string;
44
+ body?: string;
45
+ template?: string;
46
+ variables?: Record<string, unknown>;
47
+ data?: Record<string, unknown>;
48
+ }) {
49
+ const { data, variables, ...rest } = config;
50
+ return capability("dypai.email", "send", {
51
+ ...rest,
52
+ ...(variables ? { variables } : {}),
53
+ ...(data ? { variables: data } : {}),
54
+ });
55
+ },
56
+ };
57
+
58
+ export const dypai = {
59
+ db,
60
+ email,
61
+ capability,
62
+ };
package/src/ref.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { RefIR } from "@dypai/workflow-core";
2
+
3
+ export const ref = {
4
+ input(key: string, ...path: string[]): RefIR {
5
+ return { kind: "input", path: [key, ...path] };
6
+ },
7
+
8
+ currentUserId(): RefIR {
9
+ return { kind: "currentUserId" };
10
+ },
11
+
12
+ step(stepId: string, ...path: string[]): RefIR {
13
+ return path.length
14
+ ? { kind: "step", stepId, path }
15
+ : { kind: "step", stepId };
16
+ },
17
+
18
+ literal(value: unknown): RefIR {
19
+ return { kind: "literal", value };
20
+ },
21
+
22
+ env(name: string): RefIR {
23
+ return { kind: "env", name };
24
+ },
25
+ };
26
+
27
+ export type { RefIR };
package/src/schema.ts ADDED
@@ -0,0 +1,100 @@
1
+ import type { JsonSchema } from "@dypai/workflow-core";
2
+
3
+ export type SchemaField = JsonSchema & {
4
+ optional?: boolean;
5
+ };
6
+
7
+ function isSchemaField(value: unknown): value is SchemaField {
8
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+
11
+ export function objectSchemaFromFields(fields: Record<string, SchemaField>): JsonSchema {
12
+ const properties: Record<string, JsonSchema> = {};
13
+ const required: string[] = [];
14
+
15
+ for (const [key, field] of Object.entries(fields)) {
16
+ const { optional, ...schema } = field;
17
+ properties[key] = schema;
18
+ if (!optional) required.push(key);
19
+ }
20
+
21
+ return {
22
+ type: "object",
23
+ additionalProperties: false,
24
+ ...(required.length ? { required } : {}),
25
+ properties,
26
+ };
27
+ }
28
+
29
+ export const s = {
30
+ string(options?: { format?: string; minLength?: number }): SchemaField {
31
+ return {
32
+ type: "string",
33
+ ...(options?.format ? { format: options.format } : {}),
34
+ ...(options?.minLength ? { minLength: options.minLength } : {}),
35
+ };
36
+ },
37
+
38
+ email(): SchemaField {
39
+ return { type: "string", format: "email" };
40
+ },
41
+
42
+ number(): SchemaField {
43
+ return { type: "number" };
44
+ },
45
+
46
+ integer(): SchemaField {
47
+ return { type: "integer" };
48
+ },
49
+
50
+ boolean(): SchemaField {
51
+ return { type: "boolean" };
52
+ },
53
+
54
+ date(): SchemaField {
55
+ return { type: "string", format: "date" };
56
+ },
57
+
58
+ optional(field: SchemaField): SchemaField {
59
+ return { ...field, optional: true };
60
+ },
61
+
62
+ array(items: SchemaField): SchemaField {
63
+ const { optional, ...itemSchema } = items;
64
+ return {
65
+ type: "array",
66
+ items: itemSchema,
67
+ ...(optional ? { optional: true } : {}),
68
+ };
69
+ },
70
+
71
+ object(properties: Record<string, SchemaField>, options?: { required?: string[] }): SchemaField {
72
+ const schema = objectSchemaFromFields(properties);
73
+ if (options?.required) {
74
+ schema.required = options.required;
75
+ }
76
+ return schema;
77
+ },
78
+
79
+ uuid(): SchemaField {
80
+ return { type: "string", format: "uuid" };
81
+ },
82
+ };
83
+
84
+ export function inferOutputSchemaFromReturns(
85
+ returns: Record<string, unknown>,
86
+ ): JsonSchema {
87
+ const properties: Record<string, JsonSchema> = {};
88
+ for (const key of Object.keys(returns)) {
89
+ properties[key] = { type: "object" };
90
+ }
91
+ return {
92
+ type: "object",
93
+ additionalProperties: false,
94
+ properties,
95
+ };
96
+ }
97
+
98
+ export function isSchemaFieldValue(value: unknown): value is SchemaField {
99
+ return isSchemaField(value);
100
+ }
package/src/types.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type FlowStepCall = {
2
+ call: string;
3
+ config: Record<string, unknown>;
4
+ };
5
+
6
+ export type FlowCompileDiagnostic = {
7
+ severity: "error" | "warn";
8
+ rule: string;
9
+ message: string;
10
+ file?: string;
11
+ fixHint?: string;
12
+ };
13
+
14
+ export type CompileFlowResult = {
15
+ ok: boolean;
16
+ definition?: import("@dypai/workflow-core").FlowDefinition;
17
+ workflow?: import("@dypai/workflow-core").WorkflowIR;
18
+ workflowCode?: import("@dypai/workflow-core").WorkflowCodeV2;
19
+ diagnostics: FlowCompileDiagnostic[];
20
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "noEmit": true,
9
+ "allowImportingTsExtensions": true,
10
+ "types": ["bun"]
11
+ },
12
+ "include": ["src/**/*.ts", "flows/**/*.ts"]
13
+ }