@gadmin2n/schematics 0.0.135 → 0.0.137
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/Dockerfile.server +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/canvas.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +14 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +13 -92
- package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +105 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/ensure-database.js +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +7 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/gen-jwt.ts +85 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +9 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +43 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.dto.ts +27 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.spec.ts +430 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.ts +203 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +36 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.module.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +54 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +245 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.spec.ts +366 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.ts +110 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +41 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +37 -573
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/ecosystem.dev.config.cjs +12 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +39 -26
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +3 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +23 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +66 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +16 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +21 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +84 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +166 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasHistory.ts +52 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +6 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +18 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +225 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts +31 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +0 -220
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +0 -132
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +0 -152
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +0 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +0 -25
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +0 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +0 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +0 -79
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +0 -270
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +0 -122
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +0 -4285
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +0 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +0 -51
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +0 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +0 -54
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +0 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +0 -47
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +0 -58
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +0 -184
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +0 -133
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +0 -96
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +0 -77
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +0 -109
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +0 -382
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/code-execute.test.ts +0 -44
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/http-request.test.ts +0 -87
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +0 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/helpers.test.ts +0 -225
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/node-type-consistency.test.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +0 -205
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +0 -499
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +0 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +0 -8
- 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
|
+
}
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts
ADDED
|
@@ -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,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
|
-
}
|