@gadmin2n/schematics 0.0.96 → 0.0.98
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/dist/lib/application/files/gadmin2-game-angle-demo/DESIGN.md +348 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/PRODUCT.md +75 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +24 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +220 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +129 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.dto.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.service.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +46 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +27 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +43 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +109 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +205 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +55 -56
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/layout.tsx +7 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/logo.tsx +7 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/sider.tsx +179 -160
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/title.tsx +34 -31
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/constants/layout.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.tsx +66 -51
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +7 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.tsx +66 -26
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/FlowRenderer.tsx +7 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +9 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useWorkflowAgent.ts +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +9 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.ts +27 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package-lock.json +0 -15579
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package-lock.json +0 -17555
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateDslHandles,
|
|
3
|
+
type NodeOutputsMap,
|
|
4
|
+
type WorkflowDsl,
|
|
5
|
+
} from './workflow-dsl-validate';
|
|
6
|
+
|
|
7
|
+
// Test fixture: a small NodeOutputsMap mirroring the live DB seed for the
|
|
8
|
+
// node types we exercise here. Mirrors NODE_TYPE_META used in the worker test.
|
|
9
|
+
const OUTPUTS_MAP: NodeOutputsMap = {
|
|
10
|
+
if_else: ['true', 'false'],
|
|
11
|
+
switch: null,
|
|
12
|
+
cron_trigger: [],
|
|
13
|
+
manual_trigger: [],
|
|
14
|
+
webhook_trigger: [],
|
|
15
|
+
http_request: [],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function makeNode(
|
|
21
|
+
id: string,
|
|
22
|
+
type: string,
|
|
23
|
+
config: Record<string, any> = {},
|
|
24
|
+
): WorkflowDsl['nodes'][number] {
|
|
25
|
+
return { id, type, config };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeEdge(
|
|
29
|
+
id: string,
|
|
30
|
+
source: string,
|
|
31
|
+
target: string,
|
|
32
|
+
sourceHandle?: string,
|
|
33
|
+
): WorkflowDsl['edges'][number] {
|
|
34
|
+
return sourceHandle === undefined
|
|
35
|
+
? { id, source, target }
|
|
36
|
+
: { id, source, target, sourceHandle };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── if_else (enumerated outputs) ────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe('validateDslHandles — if_else', () => {
|
|
42
|
+
it('accepts sourceHandle "true"', () => {
|
|
43
|
+
const dsl: WorkflowDsl = {
|
|
44
|
+
nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
|
|
45
|
+
edges: [makeEdge('e1', 'a', 'b', 'true')],
|
|
46
|
+
};
|
|
47
|
+
expect(validateDslHandles(dsl, OUTPUTS_MAP)).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('accepts sourceHandle "false"', () => {
|
|
51
|
+
const dsl: WorkflowDsl = {
|
|
52
|
+
nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
|
|
53
|
+
edges: [makeEdge('e1', 'a', 'b', 'false')],
|
|
54
|
+
};
|
|
55
|
+
expect(validateDslHandles(dsl, OUTPUTS_MAP)).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rejects unknown sourceHandle "maybe"', () => {
|
|
59
|
+
const dsl: WorkflowDsl = {
|
|
60
|
+
nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
|
|
61
|
+
edges: [makeEdge('e1', 'a', 'b', 'maybe')],
|
|
62
|
+
};
|
|
63
|
+
const errors = validateDslHandles(dsl, OUTPUTS_MAP);
|
|
64
|
+
expect(errors).toHaveLength(1);
|
|
65
|
+
expect(errors[0].edgeId).toBe('e1');
|
|
66
|
+
expect(errors[0].code).toBe('UNKNOWN_HANDLE');
|
|
67
|
+
expect(errors[0].reason).toMatch(/not in declared outputs/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('rejects missing sourceHandle', () => {
|
|
71
|
+
const dsl: WorkflowDsl = {
|
|
72
|
+
nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
|
|
73
|
+
edges: [makeEdge('e1', 'a', 'b')],
|
|
74
|
+
};
|
|
75
|
+
const errors = validateDslHandles(dsl, OUTPUTS_MAP);
|
|
76
|
+
expect(errors).toHaveLength(1);
|
|
77
|
+
expect(errors[0].edgeId).toBe('e1');
|
|
78
|
+
expect(errors[0].code).toBe('MISSING_HANDLE');
|
|
79
|
+
expect(errors[0].reason).toMatch(/requires a sourceHandle/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects empty-string sourceHandle as missing', () => {
|
|
83
|
+
const dsl: WorkflowDsl = {
|
|
84
|
+
nodes: [makeNode('a', 'if_else'), makeNode('b', 'http_request')],
|
|
85
|
+
edges: [makeEdge('e1', 'a', 'b', '')],
|
|
86
|
+
};
|
|
87
|
+
const errors = validateDslHandles(dsl, OUTPUTS_MAP);
|
|
88
|
+
expect(errors).toHaveLength(1);
|
|
89
|
+
expect(errors[0].edgeId).toBe('e1');
|
|
90
|
+
expect(errors[0].code).toBe('MISSING_HANDLE');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ─── cron_trigger (no named outputs) ─────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe('validateDslHandles — cron_trigger (no named outputs)', () => {
|
|
97
|
+
it('rejects sourceHandle "foo" on a node with no named handles', () => {
|
|
98
|
+
const dsl: WorkflowDsl = {
|
|
99
|
+
nodes: [makeNode('t', 'cron_trigger'), makeNode('a', 'http_request')],
|
|
100
|
+
edges: [makeEdge('e1', 't', 'a', 'foo')],
|
|
101
|
+
};
|
|
102
|
+
const errors = validateDslHandles(dsl, OUTPUTS_MAP);
|
|
103
|
+
expect(errors).toHaveLength(1);
|
|
104
|
+
expect(errors[0].edgeId).toBe('e1');
|
|
105
|
+
expect(errors[0].code).toBe('EXTRA_HANDLE');
|
|
106
|
+
expect(errors[0].reason).toMatch(/no named output handles/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('accepts no sourceHandle', () => {
|
|
110
|
+
const dsl: WorkflowDsl = {
|
|
111
|
+
nodes: [makeNode('t', 'cron_trigger'), makeNode('a', 'http_request')],
|
|
112
|
+
edges: [makeEdge('e1', 't', 'a')],
|
|
113
|
+
};
|
|
114
|
+
expect(validateDslHandles(dsl, OUTPUTS_MAP)).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('accepts empty-string sourceHandle (treated as absent for [] nodes)', () => {
|
|
118
|
+
const dsl: WorkflowDsl = {
|
|
119
|
+
nodes: [makeNode('t', 'cron_trigger'), makeNode('a', 'http_request')],
|
|
120
|
+
edges: [makeEdge('e1', 't', 'a', '')],
|
|
121
|
+
};
|
|
122
|
+
expect(validateDslHandles(dsl, OUTPUTS_MAP)).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ─── switch (dynamic outputs) ────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
describe('validateDslHandles — switch (dynamic outputs)', () => {
|
|
129
|
+
it('accepts sourceHandle matching a case value', () => {
|
|
130
|
+
const dsl: WorkflowDsl = {
|
|
131
|
+
nodes: [
|
|
132
|
+
makeNode('s', 'switch', { cases: [{ value: 'a' }, { value: 'b' }] }),
|
|
133
|
+
makeNode('x', 'http_request'),
|
|
134
|
+
],
|
|
135
|
+
edges: [makeEdge('e1', 's', 'x', 'a')],
|
|
136
|
+
};
|
|
137
|
+
expect(validateDslHandles(dsl, OUTPUTS_MAP)).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('rejects sourceHandle not in case values', () => {
|
|
141
|
+
const dsl: WorkflowDsl = {
|
|
142
|
+
nodes: [
|
|
143
|
+
makeNode('s', 'switch', { cases: [{ value: 'a' }] }),
|
|
144
|
+
makeNode('x', 'http_request'),
|
|
145
|
+
],
|
|
146
|
+
edges: [makeEdge('e1', 's', 'x', 'c')],
|
|
147
|
+
};
|
|
148
|
+
const errors = validateDslHandles(dsl, OUTPUTS_MAP);
|
|
149
|
+
expect(errors).toHaveLength(1);
|
|
150
|
+
expect(errors[0].edgeId).toBe('e1');
|
|
151
|
+
expect(errors[0].code).toBe('UNKNOWN_HANDLE');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('skips validation when switch config has no cases (in-progress editor)', () => {
|
|
155
|
+
const dsl: WorkflowDsl = {
|
|
156
|
+
nodes: [makeNode('s', 'switch'), makeNode('x', 'http_request')],
|
|
157
|
+
edges: [makeEdge('e1', 's', 'x', 'anything')],
|
|
158
|
+
};
|
|
159
|
+
expect(validateDslHandles(dsl, OUTPUTS_MAP)).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ─── out-of-scope (orphan / unknown) ────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
describe('validateDslHandles — out-of-scope', () => {
|
|
166
|
+
it('ignores edge referencing missing source node', () => {
|
|
167
|
+
const dsl: WorkflowDsl = {
|
|
168
|
+
nodes: [makeNode('b', 'http_request')],
|
|
169
|
+
edges: [makeEdge('e1', 'ghost', 'b', 'true')],
|
|
170
|
+
};
|
|
171
|
+
expect(validateDslHandles(dsl, OUTPUTS_MAP)).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('ignores edges from unknown node types', () => {
|
|
175
|
+
const dsl: WorkflowDsl = {
|
|
176
|
+
nodes: [
|
|
177
|
+
makeNode('u', 'wholly_made_up_type'),
|
|
178
|
+
makeNode('b', 'http_request'),
|
|
179
|
+
],
|
|
180
|
+
edges: [makeEdge('e1', 'u', 'b', 'whatever')],
|
|
181
|
+
};
|
|
182
|
+
expect(validateDslHandles(dsl, OUTPUTS_MAP)).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ─── multiple errors collected ──────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
describe('validateDslHandles — multiple errors', () => {
|
|
189
|
+
it('collects every error in one pass', () => {
|
|
190
|
+
const dsl: WorkflowDsl = {
|
|
191
|
+
nodes: [
|
|
192
|
+
makeNode('if', 'if_else'),
|
|
193
|
+
makeNode('cron', 'cron_trigger'),
|
|
194
|
+
makeNode('sw', 'switch', { cases: [{ value: 'a' }] }),
|
|
195
|
+
makeNode('x', 'http_request'),
|
|
196
|
+
],
|
|
197
|
+
edges: [
|
|
198
|
+
makeEdge('e1', 'if', 'x', 'maybe'), // bad enumerated
|
|
199
|
+
makeEdge('e2', 'if', 'x'), // missing required
|
|
200
|
+
makeEdge('e3', 'cron', 'x', 'foo'), // forbidden handle
|
|
201
|
+
makeEdge('e4', 'sw', 'x', 'c'), // bad dynamic
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
const errors = validateDslHandles(dsl, OUTPUTS_MAP);
|
|
205
|
+
expect(errors).toHaveLength(4);
|
|
206
|
+
expect(errors.map((e) => e.edgeId).sort()).toEqual([
|
|
207
|
+
'e1',
|
|
208
|
+
'e2',
|
|
209
|
+
'e3',
|
|
210
|
+
'e4',
|
|
211
|
+
]);
|
|
212
|
+
const byEdge = Object.fromEntries(errors.map((e) => [e.edgeId, e.code]));
|
|
213
|
+
expect(byEdge).toEqual({
|
|
214
|
+
e1: 'UNKNOWN_HANDLE',
|
|
215
|
+
e2: 'MISSING_HANDLE',
|
|
216
|
+
e3: 'EXTRA_HANDLE',
|
|
217
|
+
e4: 'UNKNOWN_HANDLE',
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL handle validator (server-side).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors temporal/worker/src/dsl/validate.ts but is independent:
|
|
5
|
+
* - reads node-type outputs from a passed-in map (caller fetches from DB)
|
|
6
|
+
* - returns the same DslValidationCode discriminator for FE compatibility
|
|
7
|
+
*
|
|
8
|
+
* Why duplicated rather than imported from worker: server and worker are
|
|
9
|
+
* separate projects without a shared package; cross-project imports would
|
|
10
|
+
* break tsc paths and Docker build.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type DslValidationCode =
|
|
14
|
+
| 'EXTRA_HANDLE' // node has [] outputs, but edge set sourceHandle
|
|
15
|
+
| 'MISSING_HANDLE' // node has named outputs, but edge has no sourceHandle (or empty string)
|
|
16
|
+
| 'UNKNOWN_HANDLE'; // edge sourceHandle is set but ∉ declared outputs
|
|
17
|
+
|
|
18
|
+
export interface DslValidationError {
|
|
19
|
+
edgeId: string;
|
|
20
|
+
source: string;
|
|
21
|
+
target: string;
|
|
22
|
+
code: DslValidationCode;
|
|
23
|
+
reason: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DslNode {
|
|
27
|
+
id: string;
|
|
28
|
+
type: string;
|
|
29
|
+
config?: Record<string, any>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DslEdge {
|
|
33
|
+
id: string;
|
|
34
|
+
source: string;
|
|
35
|
+
target: string;
|
|
36
|
+
sourceHandle?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WorkflowDsl {
|
|
40
|
+
nodes: DslNode[];
|
|
41
|
+
edges: DslEdge[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Map from node type name -> outputs declaration.
|
|
46
|
+
*
|
|
47
|
+
* - null: dynamic (e.g. switch — derived from node.config)
|
|
48
|
+
* - []: single anonymous output, no sourceHandle allowed
|
|
49
|
+
* - ['a','b',…]: enumerated named outputs; sourceHandle MUST be one of them
|
|
50
|
+
* - undefined: unknown type; validator skips edges from such nodes
|
|
51
|
+
*/
|
|
52
|
+
export type NodeOutputsMap = Record<string, string[] | null>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate every edge's sourceHandle against the source node's outputs metadata.
|
|
56
|
+
* Returns an array of errors (empty if DSL is valid).
|
|
57
|
+
*/
|
|
58
|
+
export function validateDslHandles(
|
|
59
|
+
dsl: WorkflowDsl,
|
|
60
|
+
outputsMap: NodeOutputsMap,
|
|
61
|
+
): DslValidationError[] {
|
|
62
|
+
const errors: DslValidationError[] = [];
|
|
63
|
+
const nodeMap = new Map<string, DslNode>(dsl.nodes.map((n) => [n.id, n]));
|
|
64
|
+
|
|
65
|
+
for (const edge of dsl.edges) {
|
|
66
|
+
const src = nodeMap.get(edge.source);
|
|
67
|
+
if (!src) continue; // separate concern: orphan edge — not this validator's job
|
|
68
|
+
if (!(src.type in outputsMap)) continue; // unknown node type — out of scope
|
|
69
|
+
|
|
70
|
+
const declared = outputsMap[src.type];
|
|
71
|
+
const handles =
|
|
72
|
+
declared === null ? deriveDynamicOutputs(src) : declared;
|
|
73
|
+
|
|
74
|
+
// Empty named-handle list AND declared-non-null => no sourceHandle allowed
|
|
75
|
+
if (declared !== null && handles.length === 0) {
|
|
76
|
+
if (edge.sourceHandle != null && edge.sourceHandle !== '') {
|
|
77
|
+
errors.push({
|
|
78
|
+
edgeId: edge.id,
|
|
79
|
+
source: edge.source,
|
|
80
|
+
target: edge.target,
|
|
81
|
+
code: 'EXTRA_HANDLE',
|
|
82
|
+
reason: `Node "${src.type}" has no named output handles; edge must not set sourceHandle (got "${edge.sourceHandle}").`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Dynamic + still-empty (e.g. switch with no cases): in-progress, skip
|
|
89
|
+
if (handles.length === 0) continue;
|
|
90
|
+
|
|
91
|
+
if (edge.sourceHandle == null || edge.sourceHandle === '') {
|
|
92
|
+
errors.push({
|
|
93
|
+
edgeId: edge.id,
|
|
94
|
+
source: edge.source,
|
|
95
|
+
target: edge.target,
|
|
96
|
+
code: 'MISSING_HANDLE',
|
|
97
|
+
reason: `Node "${src.type}" requires a sourceHandle; choose one of: ${handles.join(', ')}.`,
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!handles.includes(edge.sourceHandle)) {
|
|
103
|
+
errors.push({
|
|
104
|
+
edgeId: edge.id,
|
|
105
|
+
source: edge.source,
|
|
106
|
+
target: edge.target,
|
|
107
|
+
code: 'UNKNOWN_HANDLE',
|
|
108
|
+
reason: `Node "${src.type}" sourceHandle "${edge.sourceHandle}" is not in declared outputs: ${handles.join(', ')}.`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return errors;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* For dynamic-output nodes (currently just `switch`), derive the list of
|
|
118
|
+
* handle ids from the node's config.
|
|
119
|
+
*/
|
|
120
|
+
function deriveDynamicOutputs(node: DslNode): string[] {
|
|
121
|
+
if (node.type === 'switch') {
|
|
122
|
+
const cases = node.config?.cases as Array<{ value?: string }> | undefined;
|
|
123
|
+
if (!Array.isArray(cases)) return [];
|
|
124
|
+
return cases
|
|
125
|
+
.map((c) => c?.value)
|
|
126
|
+
.filter((v): v is string => typeof v === 'string' && v.length > 0);
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
@@ -129,6 +129,7 @@ export class WorkflowExportService {
|
|
|
129
129
|
configSchema: nt.configSchema as any,
|
|
130
130
|
inputSchema: nt.inputSchema as any,
|
|
131
131
|
outputSchema: nt.outputSchema as any,
|
|
132
|
+
outputs: nt.outputs as any,
|
|
132
133
|
isBuiltin: nt.isBuiltin,
|
|
133
134
|
})),
|
|
134
135
|
};
|
|
@@ -193,6 +194,7 @@ export class WorkflowExportService {
|
|
|
193
194
|
configSchema: nt.configSchema as any,
|
|
194
195
|
inputSchema: nt.inputSchema as any,
|
|
195
196
|
outputSchema: nt.outputSchema as any,
|
|
197
|
+
outputs: nt.outputs as any,
|
|
196
198
|
isBuiltin: nt.isBuiltin,
|
|
197
199
|
})),
|
|
198
200
|
}
|
|
@@ -224,6 +226,7 @@ export class WorkflowExportService {
|
|
|
224
226
|
configSchema: nt.configSchema,
|
|
225
227
|
inputSchema: nt.inputSchema,
|
|
226
228
|
outputSchema: nt.outputSchema,
|
|
229
|
+
outputs: nt.outputs,
|
|
227
230
|
isBuiltin: nt.isBuiltin,
|
|
228
231
|
creator,
|
|
229
232
|
},
|
|
@@ -235,6 +238,7 @@ export class WorkflowExportService {
|
|
|
235
238
|
configSchema: nt.configSchema,
|
|
236
239
|
inputSchema: nt.inputSchema,
|
|
237
240
|
outputSchema: nt.outputSchema,
|
|
241
|
+
outputs: nt.outputs,
|
|
238
242
|
},
|
|
239
243
|
});
|
|
240
244
|
}
|
|
@@ -22,6 +22,16 @@ describe('WorkflowService — cron lifecycle hooks', () => {
|
|
|
22
22
|
findFirst: jest.fn(),
|
|
23
23
|
create: jest.fn(),
|
|
24
24
|
},
|
|
25
|
+
workflowNodeType: {
|
|
26
|
+
findMany: jest.fn().mockResolvedValue([
|
|
27
|
+
{ type: 'cron_trigger', outputs: [] },
|
|
28
|
+
{ type: 'manual_trigger', outputs: [] },
|
|
29
|
+
{ type: 'webhook_trigger', outputs: [] },
|
|
30
|
+
{ type: 'http_request', outputs: [] },
|
|
31
|
+
{ type: 'if_else', outputs: ['true', 'false'] },
|
|
32
|
+
{ type: 'switch', outputs: null },
|
|
33
|
+
]),
|
|
34
|
+
},
|
|
25
35
|
$transaction: jest.fn(async (cb: any) =>
|
|
26
36
|
cb({
|
|
27
37
|
workflowVersion: {
|
|
@@ -197,6 +207,42 @@ describe('WorkflowService — cron lifecycle hooks', () => {
|
|
|
197
207
|
service.publish(ID, { changeSummary: 'init' }, 'tester'),
|
|
198
208
|
).resolves.toBeDefined();
|
|
199
209
|
});
|
|
210
|
+
|
|
211
|
+
it('rejects with 400 BadRequestException when DSL has invalid sourceHandle', async () => {
|
|
212
|
+
// if_else node has enumerated outputs ['true','false']; sending 'maybe'
|
|
213
|
+
// must trigger the new sourceHandle validator and produce a 400 with a
|
|
214
|
+
// structured error payload the frontend can use to highlight bad edges.
|
|
215
|
+
const dsl = {
|
|
216
|
+
nodes: [
|
|
217
|
+
{ id: 't1', type: 'manual_trigger', config: {} },
|
|
218
|
+
{ id: 'if1', type: 'if_else', config: {} },
|
|
219
|
+
{ id: 'h1', type: 'http_request', config: {} },
|
|
220
|
+
],
|
|
221
|
+
edges: [
|
|
222
|
+
{ id: 'e1', source: 't1', target: 'if1' },
|
|
223
|
+
{ id: 'e2', source: 'if1', target: 'h1', sourceHandle: 'maybe' },
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
prisma.workflow.findUnique.mockResolvedValueOnce(mkWorkflow(dsl));
|
|
227
|
+
|
|
228
|
+
await expect(
|
|
229
|
+
service.publish(ID, { changeSummary: 'init' }, 'tester'),
|
|
230
|
+
).rejects.toMatchObject({
|
|
231
|
+
status: 400,
|
|
232
|
+
response: expect.objectContaining({
|
|
233
|
+
code: 'DSL_HANDLE_VALIDATION',
|
|
234
|
+
errors: expect.arrayContaining([
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
edgeId: 'e2',
|
|
237
|
+
code: 'UNKNOWN_HANDLE',
|
|
238
|
+
}),
|
|
239
|
+
]),
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Status flip must NOT have happened
|
|
244
|
+
expect(prisma.workflow.update).not.toHaveBeenCalled();
|
|
245
|
+
});
|
|
200
246
|
});
|
|
201
247
|
|
|
202
248
|
describe('toggleEnabled', () => {
|
|
@@ -10,6 +10,10 @@ import { PrismaService } from 'nestjs-prisma';
|
|
|
10
10
|
import { validateDslGraph } from './dsl-validate.util';
|
|
11
11
|
import { TemporalService } from './temporal.service';
|
|
12
12
|
import { verifyWebhookSignature } from './webhook-signature.util';
|
|
13
|
+
import {
|
|
14
|
+
validateDslHandles,
|
|
15
|
+
type NodeOutputsMap,
|
|
16
|
+
} from './workflow-dsl-validate';
|
|
13
17
|
|
|
14
18
|
@Injectable()
|
|
15
19
|
export class WorkflowService {
|
|
@@ -285,6 +289,29 @@ export class WorkflowService {
|
|
|
285
289
|
throw new BadRequestException(`Invalid DSL: ${err?.message ?? err}`);
|
|
286
290
|
}
|
|
287
291
|
|
|
292
|
+
// Per-edge sourceHandle validation against node-type outputs declared in the
|
|
293
|
+
// database (t_workflow_node_type.outputs). Hard-fail with a structured error
|
|
294
|
+
// list so the frontend can highlight invalid edges.
|
|
295
|
+
const nodeTypes = await this.prisma.workflowNodeType.findMany({
|
|
296
|
+
select: { type: true, outputs: true },
|
|
297
|
+
});
|
|
298
|
+
const outputsMap: NodeOutputsMap = {};
|
|
299
|
+
for (const nt of nodeTypes) {
|
|
300
|
+
// Pass through: null = dynamic, [] = single anonymous, [..] = enumerated
|
|
301
|
+
outputsMap[nt.type] =
|
|
302
|
+
nt.outputs === undefined
|
|
303
|
+
? []
|
|
304
|
+
: (nt.outputs as unknown as string[] | null);
|
|
305
|
+
}
|
|
306
|
+
const handleErrors = validateDslHandles(dslToPublish as any, outputsMap);
|
|
307
|
+
if (handleErrors.length > 0) {
|
|
308
|
+
throw new BadRequestException({
|
|
309
|
+
message: 'DSL validation failed: invalid edge sourceHandle(s)',
|
|
310
|
+
code: 'DSL_HANDLE_VALIDATION',
|
|
311
|
+
errors: handleErrors,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
288
315
|
// Validate webhook path collisions across other published workflows
|
|
289
316
|
const webhookConfigs = this.extractWebhookConfigs(dslToPublish);
|
|
290
317
|
if (webhookConfigs.length > 0) {
|
|
@@ -78,6 +78,7 @@ describe('WorkflowNodeTypeService', () => {
|
|
|
78
78
|
configSchema: {},
|
|
79
79
|
inputSchema: {},
|
|
80
80
|
outputSchema: {},
|
|
81
|
+
outputs: [],
|
|
81
82
|
description: 'test_description',
|
|
82
83
|
isBuiltin: true,
|
|
83
84
|
},
|
|
@@ -103,6 +104,7 @@ describe('WorkflowNodeTypeService', () => {
|
|
|
103
104
|
configSchema: {},
|
|
104
105
|
inputSchema: {},
|
|
105
106
|
outputSchema: {},
|
|
107
|
+
outputs: [],
|
|
106
108
|
description: 'test_description',
|
|
107
109
|
isBuiltin: true,
|
|
108
110
|
},
|
|
@@ -128,6 +130,7 @@ describe('WorkflowNodeTypeService', () => {
|
|
|
128
130
|
configSchema: {},
|
|
129
131
|
inputSchema: {},
|
|
130
132
|
outputSchema: {},
|
|
133
|
+
outputs: [],
|
|
131
134
|
description: 'test_description',
|
|
132
135
|
isBuiltin: true,
|
|
133
136
|
},
|
|
@@ -139,6 +142,7 @@ describe('WorkflowNodeTypeService', () => {
|
|
|
139
142
|
configSchema: {},
|
|
140
143
|
inputSchema: {},
|
|
141
144
|
outputSchema: {},
|
|
145
|
+
outputs: [],
|
|
142
146
|
description: 'test_description',
|
|
143
147
|
isBuiltin: true,
|
|
144
148
|
},
|
|
@@ -230,6 +234,7 @@ describe('WorkflowNodeTypeService', () => {
|
|
|
230
234
|
configSchema: {},
|
|
231
235
|
inputSchema: {},
|
|
232
236
|
outputSchema: {},
|
|
237
|
+
outputs: [],
|
|
233
238
|
description: 'test_description',
|
|
234
239
|
isBuiltin: true,
|
|
235
240
|
} as any;
|
|
@@ -259,6 +264,7 @@ describe('WorkflowNodeTypeService', () => {
|
|
|
259
264
|
configSchema: {},
|
|
260
265
|
inputSchema: {},
|
|
261
266
|
outputSchema: {},
|
|
267
|
+
outputs: [],
|
|
262
268
|
description: 'test_description',
|
|
263
269
|
isBuiltin: true,
|
|
264
270
|
},
|
package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Node type metadata. Source of truth for which node types exist and
|
|
3
|
+
* what handle ids each one declares.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* outputs semantics:
|
|
6
|
+
* null = dynamic (handles derived from config at render/validate time, e.g. switch)
|
|
7
|
+
* [] = single anonymous source (no named sourceHandle on outgoing edges)
|
|
8
|
+
* ['a','b'] = enumerated named handles (each outgoing edge MUST have sourceHandle ∈ outputs)
|
|
9
|
+
*
|
|
10
|
+
* MUST stay in sync with server/seed/workflow-node-types.ts and the
|
|
11
|
+
* t_workflow_node_type.outputs column. Validators read this map.
|
|
12
|
+
*/
|
|
13
|
+
export interface NodeTypeMeta {
|
|
14
|
+
outputs: string[] | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Canonical tuple of all workflow node types.
|
|
19
|
+
*
|
|
20
|
+
* Preserved as an `as const` tuple (in addition to NODE_TYPE_META below)
|
|
21
|
+
* so that consumers like DslNode.type and the executeNode switch keep
|
|
22
|
+
* compile-time literal-union types and exhaustiveness checks.
|
|
23
|
+
*
|
|
24
|
+
* The order and contents MUST match the keys of NODE_TYPE_META.
|
|
7
25
|
*/
|
|
8
26
|
export const ALL_NODE_TYPES = [
|
|
9
27
|
// TRIGGER
|
|
@@ -36,6 +54,27 @@ export const ALL_NODE_TYPES = [
|
|
|
36
54
|
/** Union of all supported node type strings. */
|
|
37
55
|
export type NodeType = (typeof ALL_NODE_TYPES)[number];
|
|
38
56
|
|
|
57
|
+
export const NODE_TYPE_META: Record<NodeType, NodeTypeMeta> = {
|
|
58
|
+
cron_trigger: { outputs: [] },
|
|
59
|
+
webhook_trigger: { outputs: [] },
|
|
60
|
+
event_trigger: { outputs: [] },
|
|
61
|
+
manual_trigger: { outputs: [] },
|
|
62
|
+
http_request: { outputs: [] },
|
|
63
|
+
db_query: { outputs: [] },
|
|
64
|
+
db_execute: { outputs: [] },
|
|
65
|
+
send_notification: { outputs: [] },
|
|
66
|
+
code: { outputs: [] },
|
|
67
|
+
set_variable: { outputs: [] },
|
|
68
|
+
if_else: { outputs: ['true', 'false'] },
|
|
69
|
+
switch: { outputs: null },
|
|
70
|
+
for_each: { outputs: [] },
|
|
71
|
+
delay: { outputs: [] },
|
|
72
|
+
parallel: { outputs: [] },
|
|
73
|
+
error_handler: { outputs: [] },
|
|
74
|
+
approval: { outputs: [] },
|
|
75
|
+
sub_workflow: { outputs: [] },
|
|
76
|
+
};
|
|
77
|
+
|
|
39
78
|
/** Node types that require a Temporal activity call. */
|
|
40
79
|
export const ACTIVITY_NODE_TYPES = [
|
|
41
80
|
'http_request',
|