@gadmin2n/schematics 0.0.136 → 0.0.138

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.
Files changed (94) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +2 -2
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/canvas.prisma +1 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +14 -3
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +4 -4
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +2 -2
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +13 -92
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +105 -7
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/ensure-database.js +1 -1
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +5 -5
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +9 -1
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +9 -8
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +5 -5
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +43 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +9 -1
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +24 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.dto.ts +27 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.spec.ts +430 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.ts +203 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +36 -3
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.module.ts +2 -1
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +54 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +245 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +10 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.spec.ts +366 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.ts +110 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -1
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +79 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +41 -19
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +5 -5
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +37 -573
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/web/ecosystem.dev.config.cjs +12 -1
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +2 -1
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +3 -2
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +39 -26
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +3 -1
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -0
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +23 -5
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +66 -10
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +16 -7
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +21 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +30 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +84 -0
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +166 -0
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasHistory.ts +52 -13
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +6 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +6 -1
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +17 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +18 -6
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +225 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts +31 -0
  54. package/package.json +1 -1
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +0 -220
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +0 -132
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +0 -152
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +0 -5
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +0 -25
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +0 -13
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +0 -12
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +0 -7
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +0 -79
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +0 -270
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +0 -122
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +0 -4285
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +0 -28
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +0 -51
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +0 -85
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +0 -35
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +0 -54
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +0 -6
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +0 -85
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +0 -47
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +0 -13
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +0 -101
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +0 -58
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +0 -184
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +0 -133
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +0 -96
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +0 -77
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +0 -109
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +0 -101
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +0 -382
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/code-execute.test.ts +0 -44
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/http-request.test.ts +0 -87
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +0 -20
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/helpers.test.ts +0 -225
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/node-type-consistency.test.ts +0 -101
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +0 -205
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +0 -499
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +0 -19
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +0 -8
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/yarn.lock +0 -1905
@@ -0,0 +1,225 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ Alert,
4
+ Button,
5
+ Card,
6
+ Form,
7
+ Input,
8
+ Spin,
9
+ Typography,
10
+ message,
11
+ } from 'antd';
12
+ import { ArrowLeftOutlined } from '@ant-design/icons';
13
+ import { useNavigate } from 'react-router-dom';
14
+ import { useGetIdentity } from '@refinedev/core';
15
+ import { customRequest } from 'helpers/http';
16
+
17
+ const { Title, Text } = Typography;
18
+
19
+ interface TenantConfig {
20
+ id: number;
21
+ adminUrl: string;
22
+ adminToken: string;
23
+ namespace: string;
24
+ dbUrl: string;
25
+ displayName: string;
26
+ }
27
+
28
+ export default function TemporalTenantPage() {
29
+ const navigate = useNavigate();
30
+ const [form] = Form.useForm();
31
+ const { data: identity } = useGetIdentity<any>();
32
+ const isAdmin = (identity?.configPerms?.roles || []).includes('SYSTEM_ADMIN');
33
+
34
+ const [loading, setLoading] = useState(true);
35
+ const [saving, setSaving] = useState(false);
36
+ const [existing, setExisting] = useState<TenantConfig | null>(null);
37
+
38
+ useEffect(() => {
39
+ customRequest('temporalTenantConfig/findMany', 'POST', {
40
+ take: 1,
41
+ skip: 0,
42
+ select: {
43
+ id: true,
44
+ adminUrl: true,
45
+ adminToken: true,
46
+ namespace: true,
47
+ dbUrl: true,
48
+ displayName: true,
49
+ },
50
+ })
51
+ .then((res: any) => {
52
+ const item = res?.data?.data?.[0] ?? null;
53
+ setExisting(item);
54
+ if (item) form.setFieldsValue(item);
55
+ })
56
+ .catch(() => message.error('加载配置失败'))
57
+ .finally(() => setLoading(false));
58
+ }, [form]);
59
+
60
+ const handleSave = async (values: Omit<TenantConfig, 'id'>) => {
61
+ setSaving(true);
62
+ try {
63
+ if (existing) {
64
+ await customRequest(
65
+ `temporalTenantConfig/updateUnique/${existing.id}`,
66
+ 'PATCH',
67
+ values,
68
+ );
69
+ message.success('更新成功,Temporal worker 已热重载连接');
70
+ } else {
71
+ await customRequest('temporalTenantConfig/createOne', 'POST', values);
72
+ message.success('注册成功');
73
+ }
74
+ const res: any = await customRequest(
75
+ 'temporalTenantConfig/findMany',
76
+ 'POST',
77
+ {
78
+ take: 1,
79
+ skip: 0,
80
+ select: {
81
+ id: true,
82
+ adminUrl: true,
83
+ adminToken: true,
84
+ namespace: true,
85
+ dbUrl: true,
86
+ displayName: true,
87
+ },
88
+ },
89
+ );
90
+ setExisting(res?.data?.data?.[0] ?? null);
91
+ } catch (e: any) {
92
+ message.error(e?.response?.data?.message || '操作失败');
93
+ } finally {
94
+ setSaving(false);
95
+ }
96
+ };
97
+
98
+ if (!isAdmin) {
99
+ return (
100
+ <div style={{ padding: 24 }}>
101
+ <Alert type="error" message="无权限访问此页面" />
102
+ </div>
103
+ );
104
+ }
105
+
106
+ if (loading) {
107
+ return (
108
+ <div
109
+ style={{ display: 'flex', justifyContent: 'center', paddingTop: 80 }}
110
+ >
111
+ <Spin size="large" />
112
+ </div>
113
+ );
114
+ }
115
+
116
+ return (
117
+ <div
118
+ style={{
119
+ background: '#f0f2f5',
120
+ minHeight: 'calc(100vh - 64px)',
121
+ }}
122
+ >
123
+ <Card
124
+ style={{ marginBottom: 16 }}
125
+ styles={{ body: { padding: '16px 24px' } }}
126
+ >
127
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
128
+ <Button
129
+ type="text"
130
+ icon={<ArrowLeftOutlined />}
131
+ onClick={() => navigate(-1)}
132
+ />
133
+ <div>
134
+ <Title level={4} style={{ margin: 0 }}>
135
+ 服务注册
136
+ </Title>
137
+ </div>
138
+ </div>
139
+ </Card>
140
+
141
+ <Card styles={{ body: { padding: '24px 32px' } }}>
142
+ {existing && (
143
+ <Alert
144
+ type="info"
145
+ showIcon
146
+ message="修改 dbUrl 后,Temporal worker 会立即重载数据库连接"
147
+ style={{ marginBottom: 24 }}
148
+ />
149
+ )}
150
+ <Form
151
+ form={form}
152
+ layout="vertical"
153
+ onFinish={handleSave}
154
+ style={{ maxWidth: 600 }}
155
+ >
156
+ <Title level={5} style={{ color: '#595959' }}>
157
+ Admin API 连接
158
+ </Title>
159
+ <div
160
+ style={{
161
+ display: 'grid',
162
+ gridTemplateColumns: '1fr 1fr',
163
+ gap: '0 16px',
164
+ }}
165
+ >
166
+ <Form.Item
167
+ label="Admin URL"
168
+ name="adminUrl"
169
+ rules={[{ required: true, message: '请输入 Admin URL' }]}
170
+ >
171
+ <Input placeholder="http://localhost:8081" />
172
+ </Form.Item>
173
+ <Form.Item
174
+ label="Admin Token"
175
+ name="adminToken"
176
+ rules={[{ required: true, message: '请输入 Admin Token' }]}
177
+ >
178
+ <Input.Password placeholder="32 位 hex token" />
179
+ </Form.Item>
180
+ </div>
181
+
182
+ <Title level={5} style={{ color: '#595959', marginTop: 8 }}>
183
+ 租户信息
184
+ </Title>
185
+ <div
186
+ style={{
187
+ display: 'grid',
188
+ gridTemplateColumns: '1fr 1fr',
189
+ gap: '0 16px',
190
+ }}
191
+ >
192
+ <Form.Item
193
+ label="Namespace"
194
+ name="namespace"
195
+ rules={[{ required: true, message: '请输入 namespace' }]}
196
+ >
197
+ <Input placeholder="gadmin-test" />
198
+ </Form.Item>
199
+ <Form.Item
200
+ label="Display Name"
201
+ name="displayName"
202
+ rules={[{ required: true, message: '请输入展示名' }]}
203
+ >
204
+ <Input placeholder="Gadmin Test" />
205
+ </Form.Item>
206
+ </div>
207
+
208
+ <Form.Item
209
+ label="DB URL(Temporal worker 视角)"
210
+ name="dbUrl"
211
+ rules={[{ required: true, message: '请输入 DB URL' }]}
212
+ >
213
+ <Input placeholder="postgresql://user:pw@host.containers.internal:5432/gadmin_demo" />
214
+ </Form.Item>
215
+
216
+ <Form.Item style={{ marginTop: 8 }}>
217
+ <Button type="primary" htmlType="submit" loading={saving}>
218
+ {existing ? '更新并同步到 Temporal' : '注册到 Temporal'}
219
+ </Button>
220
+ </Form.Item>
221
+ </Form>
222
+ </Card>
223
+ </div>
224
+ );
225
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+
3
+ function isChunkLoadError(error: unknown): boolean {
4
+ const msg = String((error as Error)?.message ?? '');
5
+ return (
6
+ msg.includes('Failed to fetch dynamically imported module') ||
7
+ msg.includes('Importing a module script failed') ||
8
+ msg.includes('error loading dynamically imported module') ||
9
+ msg.includes('Unable to preload CSS for')
10
+ );
11
+ }
12
+
13
+ // Detects stale-chunk errors (old hash URLs 404 after a new deploy) and
14
+ // auto-reloads once, so users don't see the "Application Error" screen.
15
+ export function lazyWithReload<T extends React.ComponentType<any>>(
16
+ factory: () => Promise<{ default: T }>,
17
+ ): React.LazyExoticComponent<T> {
18
+ return React.lazy(() =>
19
+ factory().catch((error: unknown) => {
20
+ if (isChunkLoadError(error) && !import.meta.env.DEV) {
21
+ const key = `chunk_reload_${window.location.pathname}`;
22
+ if (!sessionStorage.getItem(key)) {
23
+ sessionStorage.setItem(key, '1');
24
+ window.location.reload();
25
+ return new Promise<{ default: T }>(() => {});
26
+ }
27
+ }
28
+ throw error;
29
+ }),
30
+ );
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.136",
3
+ "version": "0.0.138",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -1,220 +0,0 @@
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
- });
@@ -1,132 +0,0 @@
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 = declared === null ? deriveDynamicOutputs(src) : declared;
72
-
73
- // Empty named-handle list AND declared-non-null => no sourceHandle allowed
74
- if (declared !== null && handles.length === 0) {
75
- if (edge.sourceHandle != null && edge.sourceHandle !== '') {
76
- errors.push({
77
- edgeId: edge.id,
78
- source: edge.source,
79
- target: edge.target,
80
- code: 'EXTRA_HANDLE',
81
- reason: `Node "${src.type}" has no named output handles; edge must not set sourceHandle (got "${edge.sourceHandle}").`,
82
- });
83
- }
84
- continue;
85
- }
86
-
87
- // Dynamic + still-empty (e.g. switch with no cases): in-progress, skip
88
- if (handles.length === 0) continue;
89
-
90
- if (edge.sourceHandle == null || edge.sourceHandle === '') {
91
- errors.push({
92
- edgeId: edge.id,
93
- source: edge.source,
94
- target: edge.target,
95
- code: 'MISSING_HANDLE',
96
- reason: `Node "${
97
- src.type
98
- }" requires a sourceHandle; choose one of: ${handles.join(', ')}.`,
99
- });
100
- continue;
101
- }
102
-
103
- if (!handles.includes(edge.sourceHandle)) {
104
- errors.push({
105
- edgeId: edge.id,
106
- source: edge.source,
107
- target: edge.target,
108
- code: 'UNKNOWN_HANDLE',
109
- reason: `Node "${src.type}" sourceHandle "${
110
- edge.sourceHandle
111
- }" is not in declared outputs: ${handles.join(', ')}.`,
112
- });
113
- }
114
- }
115
-
116
- return errors;
117
- }
118
-
119
- /**
120
- * For dynamic-output nodes (currently just `switch`), derive the list of
121
- * handle ids from the node's config.
122
- */
123
- function deriveDynamicOutputs(node: DslNode): string[] {
124
- if (node.type === 'switch') {
125
- const cases = node.config?.cases as Array<{ value?: string }> | undefined;
126
- if (!Array.isArray(cases)) return [];
127
- return cases
128
- .map((c) => c?.value)
129
- .filter((v): v is string => typeof v === 'string' && v.length > 0);
130
- }
131
- return [];
132
- }