@dypai-ai/workflow-core 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.
- package/fixtures/capability-catalog.json +126 -0
- package/fixtures/legacy-create-booking.yaml +40 -0
- package/package.json +40 -0
- package/src/adapters/adapters.test.ts +168 -0
- package/src/adapters/engineBinding.ts +35 -0
- package/src/adapters/flowDefinitionToIr.ts +141 -0
- package/src/adapters/irToWorkflowCodeV2.ts +293 -0
- package/src/adapters/legacyYamlToIr.ts +340 -0
- package/src/adapters/placeholderToRef.ts +74 -0
- package/src/adapters/refToLegacyPlaceholder.ts +33 -0
- package/src/adapters/sqlBuilders.ts +81 -0
- package/src/adapters/triggers.ts +86 -0
- package/src/adapters/types.ts +15 -0
- package/src/adapters/workflowCodeTypes.ts +45 -0
- package/src/capabilities/agentBrief.ts +42 -0
- package/src/capabilities/capabilities.test.ts +126 -0
- package/src/capabilities/capabilityRegistry.ts +112 -0
- package/src/capabilities/catalogSchema.ts +14 -0
- package/src/capabilities/fromCatalog.ts +30 -0
- package/src/capabilities/index.ts +35 -0
- package/src/capabilities/types.ts +57 -0
- package/src/fixtures/createBooking.flow.ts +64 -0
- package/src/fixtures/createBooking.ir.ts +103 -0
- package/src/fixtures/listBookings.ir.ts +61 -0
- package/src/index.ts +172 -0
- package/src/ir/refs.ts +103 -0
- package/src/ir/schema.ts +149 -0
- package/src/ir/sourceMap.ts +59 -0
- package/src/ir/types.ts +147 -0
- package/src/ir/validate.test.ts +181 -0
- package/src/ir/validate.ts +365 -0
- package/src/registry/defineNode.ts +19 -0
- package/src/registry/nodeRegistry.ts +87 -0
- package/src/registry/nodes/dypaiDb.ts +164 -0
- package/src/registry/nodes/dypaiEmail.ts +57 -0
- package/src/registry/nodes/dypaiFlow.ts +25 -0
- package/src/registry/nodes/legacyWorkflow.ts +27 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { dypaiDbNode } from "./nodes/dypaiDb";
|
|
2
|
+
import { dypaiEmailNode } from "./nodes/dypaiEmail";
|
|
3
|
+
import { dypaiFlowNode } from "./nodes/dypaiFlow";
|
|
4
|
+
import { legacyWorkflowNode } from "./nodes/legacyWorkflow";
|
|
5
|
+
import type { CapabilityCatalog } from "../capabilities/types";
|
|
6
|
+
import { mergeNodeRegistryDefinitions } from "../capabilities";
|
|
7
|
+
import type { NodeDefinition, RegistryLookup, ResourceIR, SideEffectKind } from "../ir/types";
|
|
8
|
+
import { operationKey } from "./defineNode";
|
|
9
|
+
|
|
10
|
+
export class NodeRegistry {
|
|
11
|
+
private readonly nodes = new Map<string, NodeDefinition>();
|
|
12
|
+
|
|
13
|
+
constructor(definitions: NodeDefinition[] = []) {
|
|
14
|
+
for (const definition of definitions) this.register(definition);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static withCatalog(catalog?: CapabilityCatalog | null): NodeRegistry {
|
|
18
|
+
return new NodeRegistry(mergeNodeRegistryDefinitions([
|
|
19
|
+
dypaiDbNode,
|
|
20
|
+
dypaiEmailNode,
|
|
21
|
+
dypaiFlowNode,
|
|
22
|
+
legacyWorkflowNode,
|
|
23
|
+
], catalog));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
register(definition: NodeDefinition): void {
|
|
27
|
+
this.nodes.set(definition.id, definition);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getNode(nodeId: string): NodeDefinition | undefined {
|
|
31
|
+
return this.nodes.get(nodeId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
lookup(nodeId: string, operation: string, version: number): RegistryLookup | undefined {
|
|
35
|
+
const node = this.nodes.get(nodeId);
|
|
36
|
+
if (!node || node.version !== version) return undefined;
|
|
37
|
+
const op = node.operations[operation];
|
|
38
|
+
if (!op) return undefined;
|
|
39
|
+
return { node, operation: op };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
has(nodeId: string, operation: string, version: number): boolean {
|
|
43
|
+
return Boolean(this.lookup(nodeId, operation, version));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
listNodes(): NodeDefinition[] {
|
|
47
|
+
return [...this.nodes.values()];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
listOperationKeys(): string[] {
|
|
51
|
+
const keys: string[] = [];
|
|
52
|
+
for (const node of this.nodes.values()) {
|
|
53
|
+
for (const operationName of Object.keys(node.operations)) {
|
|
54
|
+
keys.push(operationKey(node.id, operationName, node.version));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return keys.sort();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
deriveResources(
|
|
61
|
+
nodeId: string,
|
|
62
|
+
operation: string,
|
|
63
|
+
version: number,
|
|
64
|
+
config: Record<string, unknown>,
|
|
65
|
+
): ResourceIR[] {
|
|
66
|
+
const resolved = this.lookup(nodeId, operation, version);
|
|
67
|
+
if (!resolved?.operation.resources) return [];
|
|
68
|
+
return resolved.operation.resources(config);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
deriveSideEffects(
|
|
72
|
+
nodeId: string,
|
|
73
|
+
operation: string,
|
|
74
|
+
version: number,
|
|
75
|
+
): SideEffectKind[] {
|
|
76
|
+
const resolved = this.lookup(nodeId, operation, version);
|
|
77
|
+
if (!resolved) return [];
|
|
78
|
+
return [
|
|
79
|
+
...(resolved.node.sideEffects ?? []),
|
|
80
|
+
...(resolved.operation.sideEffects ?? []),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const defaultNodeRegistry = NodeRegistry.withCatalog();
|
|
86
|
+
|
|
87
|
+
export { dypaiDbNode, dypaiEmailNode, dypaiFlowNode, legacyWorkflowNode };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { defineNode, defineOperation } from "../defineNode";
|
|
2
|
+
|
|
3
|
+
const tableNamePattern = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
|
|
4
|
+
|
|
5
|
+
function tableResource(config: Record<string, unknown>, access: "read" | "write") {
|
|
6
|
+
const table = typeof config.table === "string" ? config.table.trim() : "";
|
|
7
|
+
if (!table || !tableNamePattern.test(table)) return [];
|
|
8
|
+
return [{ type: "postgres_table", name: table, access }];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const dypaiDbNode = defineNode({
|
|
12
|
+
id: "dypai.db",
|
|
13
|
+
version: 1,
|
|
14
|
+
label: "DYPAI Database",
|
|
15
|
+
category: "database",
|
|
16
|
+
docs: "Declarative and SQL database operations backed by Postgres.",
|
|
17
|
+
sideEffects: ["db.read", "db.write"],
|
|
18
|
+
operations: {
|
|
19
|
+
query: defineOperation({
|
|
20
|
+
label: "Run SQL query",
|
|
21
|
+
docs: "Execute explicit SQL against Postgres.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
required: ["sql"],
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
properties: {
|
|
27
|
+
sql: { type: "string", minLength: 1 },
|
|
28
|
+
params: { type: "object" },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
outputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
rows: { type: "array" },
|
|
35
|
+
row: { type: "object" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
sideEffects: ["db.read"],
|
|
39
|
+
resources: (config) => tableResource(config, "read"),
|
|
40
|
+
validate: (config) => {
|
|
41
|
+
const sql = typeof config.sql === "string" ? config.sql.trim() : "";
|
|
42
|
+
if (!sql) {
|
|
43
|
+
return [{
|
|
44
|
+
severity: "error",
|
|
45
|
+
rule: "db_query_missing_sql",
|
|
46
|
+
message: "dypai.db.query requires config.sql.",
|
|
47
|
+
fixHint: "Add sql with explicit SELECT/INSERT/UPDATE/DELETE SQL.",
|
|
48
|
+
}];
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
insert: defineOperation({
|
|
54
|
+
label: "Insert row",
|
|
55
|
+
docs: "Insert a row into a Postgres table.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
required: ["table", "values"],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
properties: {
|
|
61
|
+
table: { type: "string", minLength: 1 },
|
|
62
|
+
values: { type: "object", minProperties: 1 },
|
|
63
|
+
returning: {
|
|
64
|
+
type: "array",
|
|
65
|
+
items: { type: "string" },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
outputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
row: { type: "object" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
sideEffects: ["db.write"],
|
|
76
|
+
resources: (config) => tableResource(config, "write"),
|
|
77
|
+
validate: (config) => {
|
|
78
|
+
const diagnostics = [];
|
|
79
|
+
const table = typeof config.table === "string" ? config.table.trim() : "";
|
|
80
|
+
if (!table) {
|
|
81
|
+
diagnostics.push({
|
|
82
|
+
severity: "error" as const,
|
|
83
|
+
rule: "db_insert_missing_table",
|
|
84
|
+
message: "dypai.db.insert requires config.table.",
|
|
85
|
+
fixHint: 'Add table: "public.bookings" (or another schema-qualified table).',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!config.values || typeof config.values !== "object" || Array.isArray(config.values)) {
|
|
89
|
+
diagnostics.push({
|
|
90
|
+
severity: "error" as const,
|
|
91
|
+
rule: "db_insert_missing_values",
|
|
92
|
+
message: "dypai.db.insert requires config.values with at least one column.",
|
|
93
|
+
fixHint: "Map column names to literals or RefIR values.",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return diagnostics;
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
update: defineOperation({
|
|
100
|
+
label: "Update rows",
|
|
101
|
+
docs: "Update rows in a Postgres table with a required where clause.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
required: ["table", "where", "set"],
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
properties: {
|
|
107
|
+
table: { type: "string", minLength: 1 },
|
|
108
|
+
where: { type: "object", minProperties: 1 },
|
|
109
|
+
set: { type: "object", minProperties: 1 },
|
|
110
|
+
returning: {
|
|
111
|
+
type: "array",
|
|
112
|
+
items: { type: "string" },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
outputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
row: { type: "object" },
|
|
120
|
+
rows: { type: "array" },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
sideEffects: ["db.write"],
|
|
124
|
+
resources: (config) => tableResource(config, "write"),
|
|
125
|
+
validate: (config) => {
|
|
126
|
+
const where = config.where;
|
|
127
|
+
if (!where || typeof where !== "object" || Array.isArray(where) || !Object.keys(where).length) {
|
|
128
|
+
return [{
|
|
129
|
+
severity: "error" as const,
|
|
130
|
+
rule: "db_update_missing_where",
|
|
131
|
+
message: "dypai.db.update requires config.where with at least one filter.",
|
|
132
|
+
fixHint: "Use where: { id: { kind: 'input', path: ['id'] }, user_id: { kind: 'currentUserId' } }.",
|
|
133
|
+
}];
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
list: defineOperation({
|
|
139
|
+
label: "List rows",
|
|
140
|
+
docs: "List rows from a Postgres table with optional filters and pagination.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
required: ["table"],
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
properties: {
|
|
146
|
+
table: { type: "string", minLength: 1 },
|
|
147
|
+
where: { type: "object" },
|
|
148
|
+
orderBy: { type: "string" },
|
|
149
|
+
limit: {},
|
|
150
|
+
offset: {},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
outputSchema: {
|
|
154
|
+
type: "object",
|
|
155
|
+
required: ["rows"],
|
|
156
|
+
properties: {
|
|
157
|
+
rows: { type: "array" },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
sideEffects: ["db.read"],
|
|
161
|
+
resources: (config) => tableResource(config, "read"),
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineNode, defineOperation } from "../defineNode";
|
|
2
|
+
|
|
3
|
+
export const dypaiEmailNode = defineNode({
|
|
4
|
+
id: "dypai.email",
|
|
5
|
+
version: 1,
|
|
6
|
+
label: "DYPAI Email",
|
|
7
|
+
category: "communications",
|
|
8
|
+
docs: "Send transactional email through configured DYPAI email providers.",
|
|
9
|
+
operations: {
|
|
10
|
+
send: defineOperation({
|
|
11
|
+
label: "Send email",
|
|
12
|
+
docs: "Send an email using a template or explicit subject/body.",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
required: ["to"],
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
to: {},
|
|
19
|
+
subject: { type: "string" },
|
|
20
|
+
body: { type: "string" },
|
|
21
|
+
template: { type: "string" },
|
|
22
|
+
variables: { type: "object" },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
outputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
messageId: { type: "string" },
|
|
29
|
+
ok: { type: "boolean" },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
sideEffects: ["email.send"],
|
|
33
|
+
validate: (config) => {
|
|
34
|
+
const diagnostics = [];
|
|
35
|
+
if (config.to === undefined || config.to === null) {
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
severity: "error" as const,
|
|
38
|
+
rule: "email_send_missing_to",
|
|
39
|
+
message: "dypai.email.send requires config.to.",
|
|
40
|
+
fixHint: "Use to: { kind: 'input', path: ['email'] } or a literal email address.",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const hasTemplate = typeof config.template === "string" && config.template.trim().length > 0;
|
|
44
|
+
const hasBody = typeof config.body === "string" && config.body.trim().length > 0;
|
|
45
|
+
if (!hasTemplate && !hasBody) {
|
|
46
|
+
diagnostics.push({
|
|
47
|
+
severity: "error" as const,
|
|
48
|
+
rule: "email_send_missing_content",
|
|
49
|
+
message: "dypai.email.send requires config.template or config.body.",
|
|
50
|
+
fixHint: "Provide template: 'welcome' or body/subject for a direct email.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return diagnostics;
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineNode, defineOperation } from "../defineNode";
|
|
2
|
+
|
|
3
|
+
export const dypaiFlowNode = defineNode({
|
|
4
|
+
id: "dypai.flow",
|
|
5
|
+
version: 1,
|
|
6
|
+
label: "DYPAI Flow",
|
|
7
|
+
category: "flow",
|
|
8
|
+
docs: "Flow control helpers for building endpoint responses.",
|
|
9
|
+
operations: {
|
|
10
|
+
return: defineOperation({
|
|
11
|
+
label: "Return HTTP response",
|
|
12
|
+
docs: "Marks the public response mapping for the workflow.",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
properties: {},
|
|
17
|
+
},
|
|
18
|
+
outputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
additionalProperties: true,
|
|
21
|
+
},
|
|
22
|
+
sideEffects: ["http.response"],
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineNode, defineOperation } from "../defineNode";
|
|
2
|
+
|
|
3
|
+
export const legacyWorkflowNode = defineNode({
|
|
4
|
+
id: "legacy.workflow",
|
|
5
|
+
version: 1,
|
|
6
|
+
label: "Legacy Workflow Node",
|
|
7
|
+
category: "legacy",
|
|
8
|
+
docs: "Compatibility wrapper for legacy engine nodes not yet represented in Node Registry v2.",
|
|
9
|
+
operations: {
|
|
10
|
+
node: defineOperation({
|
|
11
|
+
label: "Legacy engine node",
|
|
12
|
+
docs: "Preserves the original legacy node shape for adapter compatibility.",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
required: ["legacyNode"],
|
|
16
|
+
properties: {
|
|
17
|
+
legacyNode: { type: "object" },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
outputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
additionalProperties: true,
|
|
23
|
+
},
|
|
24
|
+
sideEffects: [],
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
});
|