@gadmin2n/schematics 0.0.147 → 0.0.149
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/config/prisma/workflow.prisma +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +83 -53
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +31 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +166 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +82 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +7 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +106 -37
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +59 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +60 -14
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +60 -14
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +41 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.test.tsx +46 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.tsx +14 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.test.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.test.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.test.tsx +140 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +124 -62
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +78 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/event-catalog.api.ts +4 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
|
@@ -61,7 +61,7 @@ export interface BusinessEventCatalogItem {
|
|
|
61
61
|
label: string;
|
|
62
62
|
schemaName: string;
|
|
63
63
|
tableName: string;
|
|
64
|
-
|
|
64
|
+
operations: Array<'INSERT' | 'UPDATE' | 'DELETE'>;
|
|
65
65
|
watchedFields: string[];
|
|
66
66
|
primaryKeyFields: string[];
|
|
67
67
|
isEnabled: boolean;
|
|
@@ -128,7 +128,7 @@ export function buildBusinessEventCatalog(
|
|
|
128
128
|
code: string;
|
|
129
129
|
schemaName: string;
|
|
130
130
|
tableName: string;
|
|
131
|
-
|
|
131
|
+
operations: string[];
|
|
132
132
|
watchedFields: Prisma.JsonValue;
|
|
133
133
|
primaryKeyFields: Prisma.JsonValue;
|
|
134
134
|
isEnabled: boolean;
|
|
@@ -143,12 +143,14 @@ export function buildBusinessEventCatalog(
|
|
|
143
143
|
column.schemaName === row.schemaName &&
|
|
144
144
|
column.tableName === row.tableName,
|
|
145
145
|
);
|
|
146
|
-
const prefixes =
|
|
147
|
-
row.operation
|
|
148
|
-
? ['after']
|
|
149
|
-
: row.operation === 'DELETE'
|
|
146
|
+
const prefixes = [
|
|
147
|
+
...(row.operations.some((operation) => operation !== 'INSERT')
|
|
150
148
|
? ['before']
|
|
151
|
-
: [
|
|
149
|
+
: []),
|
|
150
|
+
...(row.operations.some((operation) => operation !== 'DELETE')
|
|
151
|
+
? ['after']
|
|
152
|
+
: []),
|
|
153
|
+
];
|
|
152
154
|
const fields = [
|
|
153
155
|
...primaryKeyFields.flatMap((columnName) => {
|
|
154
156
|
const column = sourceColumns.find(
|
|
@@ -164,17 +166,22 @@ export function buildBusinessEventCatalog(
|
|
|
164
166
|
`${prefix}.${column.columnName}`,
|
|
165
167
|
column,
|
|
166
168
|
false,
|
|
167
|
-
column.nullable
|
|
169
|
+
column.nullable ||
|
|
170
|
+
row.operations.includes(
|
|
171
|
+
prefix === 'before' ? 'INSERT' : 'DELETE',
|
|
172
|
+
),
|
|
168
173
|
),
|
|
169
174
|
),
|
|
170
175
|
),
|
|
171
176
|
];
|
|
172
177
|
return {
|
|
173
178
|
registrationCode: row.code,
|
|
174
|
-
label: `${row.schemaName}.${row.tableName} · ${row.
|
|
179
|
+
label: `${row.schemaName}.${row.tableName} · ${row.operations.join(
|
|
180
|
+
' / ',
|
|
181
|
+
)}`,
|
|
175
182
|
schemaName: row.schemaName,
|
|
176
183
|
tableName: row.tableName,
|
|
177
|
-
|
|
184
|
+
operations: row.operations as BusinessEventCatalogItem['operations'],
|
|
178
185
|
watchedFields,
|
|
179
186
|
primaryKeyFields,
|
|
180
187
|
isEnabled: row.isEnabled,
|
|
@@ -226,7 +233,28 @@ export function validateBusinessEventTriggers(
|
|
|
226
233
|
const event = eventByCode.get(registrationCode);
|
|
227
234
|
if (!event) continue;
|
|
228
235
|
|
|
229
|
-
|
|
236
|
+
const rules = config.operationRules;
|
|
237
|
+
if (!Array.isArray(rules) || rules.length === 0)
|
|
238
|
+
throw invalidBusinessEventMatch(event, '请至少启用一种操作的启动规则');
|
|
239
|
+
const seen = new Set<string>();
|
|
240
|
+
for (const value of rules) {
|
|
241
|
+
const rule = asRecord(value);
|
|
242
|
+
const operation =
|
|
243
|
+
rule.operation as BusinessEventCatalogItem['operations'][number];
|
|
244
|
+
if (!event.operations.includes(operation) || seen.has(operation))
|
|
245
|
+
throw invalidBusinessEventMatch(event, '操作未登记或重复配置');
|
|
246
|
+
seen.add(operation);
|
|
247
|
+
// before is unavailable for INSERT; after is unavailable for DELETE.
|
|
248
|
+
const availableEvent = {
|
|
249
|
+
...event,
|
|
250
|
+
fields: event.fields.filter(
|
|
251
|
+
(field) =>
|
|
252
|
+
!(operation === 'INSERT' && field.path.startsWith('before.')) &&
|
|
253
|
+
!(operation === 'DELETE' && field.path.startsWith('after.')),
|
|
254
|
+
),
|
|
255
|
+
};
|
|
256
|
+
validateBusinessEventMatch(rule.match, availableEvent);
|
|
257
|
+
}
|
|
230
258
|
|
|
231
259
|
if (config.processingMode !== 'SERIAL_BY_KEY') continue;
|
|
232
260
|
|
|
@@ -246,7 +274,13 @@ export function validateBusinessEventTriggers(
|
|
|
246
274
|
}
|
|
247
275
|
}
|
|
248
276
|
|
|
249
|
-
const EVENT_MATCH_OPERATORS = new Set([
|
|
277
|
+
const EVENT_MATCH_OPERATORS = new Set([
|
|
278
|
+
'EQ',
|
|
279
|
+
'NEQ',
|
|
280
|
+
'IN',
|
|
281
|
+
'EMPTY',
|
|
282
|
+
'NOT_EMPTY',
|
|
283
|
+
]);
|
|
250
284
|
const EVENT_MATCH_UNARY_OPERATORS = new Set(['EMPTY', 'NOT_EMPTY']);
|
|
251
285
|
|
|
252
286
|
function validateBusinessEventMatch(
|
|
@@ -292,14 +326,27 @@ function validateBusinessEventMatch(
|
|
|
292
326
|
: field.type === 'boolean'
|
|
293
327
|
? 'BOOLEAN'
|
|
294
328
|
: 'STRING';
|
|
329
|
+
let values: unknown[] = [right.value];
|
|
330
|
+
if (operator === 'IN') {
|
|
331
|
+
try {
|
|
332
|
+
values = JSON.parse(String(right.value));
|
|
333
|
+
} catch {
|
|
334
|
+
throw invalidBusinessEventMatch(event, '比较值列表无效');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
295
337
|
if (
|
|
296
338
|
right.source !== 'FIXED' ||
|
|
297
339
|
right.valueType !== expectedValueType ||
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
340
|
+
!Array.isArray(values) ||
|
|
341
|
+
values.length === 0 ||
|
|
342
|
+
values.some(
|
|
343
|
+
(value) =>
|
|
344
|
+
typeof value !== 'string' ||
|
|
345
|
+
(expectedValueType === 'NUMBER' &&
|
|
346
|
+
(!value.trim() || !Number.isFinite(Number(value)))) ||
|
|
347
|
+
(expectedValueType === 'BOOLEAN' &&
|
|
348
|
+
!['true', 'false'].includes(value)),
|
|
349
|
+
)
|
|
303
350
|
) {
|
|
304
351
|
throw invalidBusinessEventMatch(event, `字段“${path}”的比较值类型不正确`);
|
|
305
352
|
}
|
|
@@ -592,7 +639,13 @@ export class WorkflowService {
|
|
|
592
639
|
|
|
593
640
|
const runsMap = new Map<
|
|
594
641
|
bigint,
|
|
595
|
-
{
|
|
642
|
+
{
|
|
643
|
+
pending: number;
|
|
644
|
+
running: number;
|
|
645
|
+
completed: number;
|
|
646
|
+
failed: number;
|
|
647
|
+
cancelled: number;
|
|
648
|
+
}
|
|
596
649
|
>();
|
|
597
650
|
for (const row of instanceCounts) {
|
|
598
651
|
if (!runsMap.has(row.workflowId)) {
|
|
@@ -601,6 +654,7 @@ export class WorkflowService {
|
|
|
601
654
|
running: 0,
|
|
602
655
|
completed: 0,
|
|
603
656
|
failed: 0,
|
|
657
|
+
cancelled: 0,
|
|
604
658
|
});
|
|
605
659
|
}
|
|
606
660
|
const entry = runsMap.get(row.workflowId)!;
|
|
@@ -627,6 +681,7 @@ export class WorkflowService {
|
|
|
627
681
|
running: 0,
|
|
628
682
|
completed: 0,
|
|
629
683
|
failed: 0,
|
|
684
|
+
cancelled: 0,
|
|
630
685
|
},
|
|
631
686
|
lastRun: lastRunMap.get(item.id)?.createdAt || null,
|
|
632
687
|
}));
|
|
@@ -713,11 +768,7 @@ export class WorkflowService {
|
|
|
713
768
|
const [events, columns, schemaName] = await Promise.all([
|
|
714
769
|
this.prisma.workflowBusinessEventRegistration.findMany({
|
|
715
770
|
where: { isEnabled: true },
|
|
716
|
-
orderBy: [
|
|
717
|
-
{ schemaName: 'asc' },
|
|
718
|
-
{ tableName: 'asc' },
|
|
719
|
-
{ operation: 'asc' },
|
|
720
|
-
],
|
|
771
|
+
orderBy: [{ schemaName: 'asc' }, { tableName: 'asc' }, { code: 'asc' }],
|
|
721
772
|
}),
|
|
722
773
|
this.findBusinessEventColumns(),
|
|
723
774
|
this.findBusinessEventSchema(),
|
|
@@ -802,7 +853,7 @@ export class WorkflowService {
|
|
|
802
853
|
data: {
|
|
803
854
|
schemaName: string;
|
|
804
855
|
tableName: string;
|
|
805
|
-
|
|
856
|
+
operations: Array<'INSERT' | 'UPDATE' | 'DELETE'>;
|
|
806
857
|
watchedFields: string[];
|
|
807
858
|
},
|
|
808
859
|
creator: string,
|
|
@@ -823,17 +874,24 @@ export class WorkflowService {
|
|
|
823
874
|
if (sourceColumns.length === 0)
|
|
824
875
|
throw new BadRequestException('请选择可监听的数据库表');
|
|
825
876
|
|
|
877
|
+
if (
|
|
878
|
+
!Array.isArray(data.operations) ||
|
|
879
|
+
data.operations.length === 0 ||
|
|
880
|
+
data.operations.some(
|
|
881
|
+
(operation) => !['INSERT', 'UPDATE', 'DELETE'].includes(operation),
|
|
882
|
+
)
|
|
883
|
+
) {
|
|
884
|
+
throw new BadRequestException('请至少选择一种有效的数据操作');
|
|
885
|
+
}
|
|
886
|
+
const operations = [...new Set(data.operations)].sort();
|
|
826
887
|
const requestedFields = [
|
|
827
888
|
...new Set(
|
|
828
889
|
data.watchedFields.map((field) => field.trim()).filter(Boolean),
|
|
829
890
|
),
|
|
830
891
|
].sort();
|
|
831
|
-
if (
|
|
892
|
+
if (operations.includes('UPDATE') && requestedFields.length === 0)
|
|
832
893
|
throw new BadRequestException('请至少选择一个监听字段');
|
|
833
|
-
const watchedFields =
|
|
834
|
-
data.operation === 'UPDATE'
|
|
835
|
-
? requestedFields
|
|
836
|
-
: sourceColumns.map((column) => column.columnName).sort();
|
|
894
|
+
const watchedFields = operations.includes('UPDATE') ? requestedFields : [];
|
|
837
895
|
const availableFields = new Set(
|
|
838
896
|
sourceColumns.map((column) => column.columnName),
|
|
839
897
|
);
|
|
@@ -853,7 +911,7 @@ export class WorkflowService {
|
|
|
853
911
|
JSON.stringify({
|
|
854
912
|
schemaName: data.schemaName,
|
|
855
913
|
tableName: data.tableName,
|
|
856
|
-
|
|
914
|
+
operations,
|
|
857
915
|
watchedFields,
|
|
858
916
|
}),
|
|
859
917
|
)
|
|
@@ -877,7 +935,7 @@ export class WorkflowService {
|
|
|
877
935
|
registrationKey,
|
|
878
936
|
schemaName: data.schemaName,
|
|
879
937
|
tableName: data.tableName,
|
|
880
|
-
|
|
938
|
+
operations,
|
|
881
939
|
watchedFields,
|
|
882
940
|
primaryKeyFields,
|
|
883
941
|
creator,
|
|
@@ -887,7 +945,9 @@ export class WorkflowService {
|
|
|
887
945
|
return registration;
|
|
888
946
|
});
|
|
889
947
|
this.logger.log(
|
|
890
|
-
`Registered ${event.
|
|
948
|
+
`Registered ${event.operations.join('/')} listener ${event.schemaName}.${
|
|
949
|
+
event.tableName
|
|
950
|
+
} (${event.code})`,
|
|
891
951
|
);
|
|
892
952
|
return buildBusinessEventCatalog([event], columns)[0];
|
|
893
953
|
}
|
|
@@ -898,7 +958,7 @@ export class WorkflowService {
|
|
|
898
958
|
id: bigint;
|
|
899
959
|
schemaName: string;
|
|
900
960
|
tableName: string;
|
|
901
|
-
|
|
961
|
+
operations: string[];
|
|
902
962
|
watchedFields: Prisma.JsonValue;
|
|
903
963
|
primaryKeyFields: Prisma.JsonValue;
|
|
904
964
|
},
|
|
@@ -926,10 +986,13 @@ export class WorkflowService {
|
|
|
926
986
|
const workflowSchema = quoteIdentifier(
|
|
927
987
|
workflowSchemaRows[0]?.schemaName || 'public',
|
|
928
988
|
);
|
|
929
|
-
const triggerEvent =
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
989
|
+
const triggerEvent = event.operations
|
|
990
|
+
.map((operation) =>
|
|
991
|
+
operation === 'UPDATE'
|
|
992
|
+
? `UPDATE OF ${watchedFields.map(quoteIdentifier).join(', ')}`
|
|
993
|
+
: operation,
|
|
994
|
+
)
|
|
995
|
+
.join(' OR ');
|
|
933
996
|
|
|
934
997
|
await tx.$executeRawUnsafe(`
|
|
935
998
|
CREATE OR REPLACE FUNCTION ${schema}.${functionName}()
|
|
@@ -2260,8 +2323,13 @@ export class WorkflowService {
|
|
|
2260
2323
|
async cancelExecutionInTransaction(
|
|
2261
2324
|
tx: Prisma.TransactionClient,
|
|
2262
2325
|
instanceId: bigint,
|
|
2326
|
+
requestedBy: string,
|
|
2263
2327
|
reason?: string,
|
|
2264
2328
|
) {
|
|
2329
|
+
const normalizedRequestedBy = requestedBy.trim().slice(0, 128);
|
|
2330
|
+
if (!normalizedRequestedBy)
|
|
2331
|
+
throw new BadRequestException('Cancellation requester is required');
|
|
2332
|
+
|
|
2265
2333
|
const rows = await tx.$queryRaw<
|
|
2266
2334
|
Array<{
|
|
2267
2335
|
id: bigint;
|
|
@@ -2301,6 +2369,7 @@ export class WorkflowService {
|
|
|
2301
2369
|
status: 'CANCELLED',
|
|
2302
2370
|
finishedAt: now,
|
|
2303
2371
|
cancellationRequestedAt: now,
|
|
2372
|
+
cancellationRequestedBy: normalizedRequestedBy,
|
|
2304
2373
|
cancellationReason: reason?.trim().slice(0, 512) || null,
|
|
2305
2374
|
cancellationStatus: instance.temporalRunId ? 'PENDING' : 'COMPLETED',
|
|
2306
2375
|
cancellationLastError: null,
|
|
@@ -112,7 +112,7 @@ describe('NodeConfigValidatorService', () => {
|
|
|
112
112
|
},
|
|
113
113
|
details: [],
|
|
114
114
|
},
|
|
115
|
-
subjects: [{ source: 'STATIC',
|
|
115
|
+
subjects: [{ source: 'STATIC', refs: ['reviewer'] }],
|
|
116
116
|
rejectionRule: { mode: 'ANY' },
|
|
117
117
|
timeout: { durationSeconds: 0, action: 'REJECTED' },
|
|
118
118
|
};
|
|
@@ -148,7 +148,7 @@ describe('NodeConfigValidatorService', () => {
|
|
|
148
148
|
},
|
|
149
149
|
details: [],
|
|
150
150
|
},
|
|
151
|
-
subjects: [{ source: 'STATIC',
|
|
151
|
+
subjects: [{ source: 'STATIC', refs: ['reviewer'] }],
|
|
152
152
|
approvalRule: { mode: 'ANY' },
|
|
153
153
|
rejectionRule: { mode: 'ANY' },
|
|
154
154
|
timeout: { durationSeconds: 0, action: 'REJECTED' },
|
|
@@ -210,7 +210,6 @@ describe('NodeConfigValidatorService', () => {
|
|
|
210
210
|
subjects: [
|
|
211
211
|
{
|
|
212
212
|
source: 'DYNAMIC',
|
|
213
|
-
provider: 'USER',
|
|
214
213
|
inputKey: 'direct_manager',
|
|
215
214
|
label: '查询直属领导',
|
|
216
215
|
},
|
|
@@ -354,3 +353,60 @@ describe('NodeConfigValidatorService', () => {
|
|
|
354
353
|
).not.toThrow();
|
|
355
354
|
});
|
|
356
355
|
});
|
|
356
|
+
|
|
357
|
+
describe('event trigger operation rules schema', () => {
|
|
358
|
+
const schema = NODE_TYPES.find((node) => node.type === 'event_trigger')!
|
|
359
|
+
.configSchema as JsonSchema;
|
|
360
|
+
const service = new NodeConfigValidatorService();
|
|
361
|
+
const validate = (operationRules: unknown) =>
|
|
362
|
+
service.validateOrThrow('event:5', schema, {
|
|
363
|
+
registrationCode: 'request-changes',
|
|
364
|
+
processingMode: 'PARALLEL',
|
|
365
|
+
operationRules,
|
|
366
|
+
});
|
|
367
|
+
it('accepts multiple operations with list conditions', () => {
|
|
368
|
+
expect(() =>
|
|
369
|
+
validate([
|
|
370
|
+
{ operation: 'INSERT' },
|
|
371
|
+
{
|
|
372
|
+
operation: 'UPDATE',
|
|
373
|
+
match: {
|
|
374
|
+
combineOperation: 'AND',
|
|
375
|
+
conditions: [
|
|
376
|
+
{
|
|
377
|
+
id: 'status',
|
|
378
|
+
left: { source: 'WORKFLOW', path: 'after.status' },
|
|
379
|
+
operator: 'IN',
|
|
380
|
+
right: {
|
|
381
|
+
source: 'FIXED',
|
|
382
|
+
valueType: 'STRING',
|
|
383
|
+
value: '["PENDING","PENDING_SUPERIOR"]',
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
]),
|
|
390
|
+
).not.toThrow();
|
|
391
|
+
});
|
|
392
|
+
it('rejects empty rules and incomplete conditions', () => {
|
|
393
|
+
expect(() => validate([])).toThrow(BadRequestException);
|
|
394
|
+
expect(() =>
|
|
395
|
+
validate([
|
|
396
|
+
{
|
|
397
|
+
operation: 'UPDATE',
|
|
398
|
+
match: {
|
|
399
|
+
combineOperation: 'AND',
|
|
400
|
+
conditions: [
|
|
401
|
+
{
|
|
402
|
+
id: 'status',
|
|
403
|
+
left: { source: 'WORKFLOW', path: 'after.status' },
|
|
404
|
+
operator: 'IN',
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
]),
|
|
410
|
+
).toThrow(BadRequestException);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BadRequestException } from '@nestjs/common';
|
|
2
|
-
import { SubjectProviderError } from '../subjectProvider/subject-provider.errors';
|
|
3
2
|
import { SubjectProviderRegistry } from '../subjectProvider/subject-provider.registry';
|
|
4
3
|
import {
|
|
5
4
|
ActorContext,
|
|
@@ -28,9 +27,9 @@ const schema = {
|
|
|
28
27
|
},
|
|
29
28
|
};
|
|
30
29
|
|
|
31
|
-
function provider(): jest.Mocked<SubjectProvider> {
|
|
30
|
+
function provider(key = 'USER'): jest.Mocked<SubjectProvider> {
|
|
32
31
|
return {
|
|
33
|
-
key
|
|
32
|
+
key,
|
|
34
33
|
descriptor: jest.fn(),
|
|
35
34
|
search: jest.fn(),
|
|
36
35
|
create: jest.fn(),
|
|
@@ -58,8 +57,8 @@ describe('SubjectConfigValidatorService', () => {
|
|
|
58
57
|
{
|
|
59
58
|
approval: {
|
|
60
59
|
subjects: [
|
|
61
|
-
{
|
|
62
|
-
{
|
|
60
|
+
{ source: 'STATIC', refs: ['alice'] },
|
|
61
|
+
{ source: 'STATIC', refs: ['bob'] },
|
|
63
62
|
],
|
|
64
63
|
},
|
|
65
64
|
},
|
|
@@ -67,15 +66,39 @@ describe('SubjectConfigValidatorService', () => {
|
|
|
67
66
|
);
|
|
68
67
|
|
|
69
68
|
expect(subjectProvider.validate).toHaveBeenCalledWith(
|
|
70
|
-
['
|
|
69
|
+
['alice', 'bob'],
|
|
71
70
|
actor,
|
|
72
71
|
);
|
|
73
72
|
});
|
|
74
73
|
|
|
74
|
+
it('validates fixed user groups with the user-group provider', async () => {
|
|
75
|
+
const userGroupProvider = provider('USER_GROUP');
|
|
76
|
+
registry.register(userGroupProvider);
|
|
77
|
+
|
|
78
|
+
await service.validateSubjects(
|
|
79
|
+
schema,
|
|
80
|
+
{
|
|
81
|
+
approval: {
|
|
82
|
+
subjects: [
|
|
83
|
+
{
|
|
84
|
+
source: 'STATIC',
|
|
85
|
+
provider: 'USER_GROUP',
|
|
86
|
+
refs: ['finance'],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
actor,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(userGroupProvider.validate).toHaveBeenCalledWith(['finance'], actor);
|
|
95
|
+
expect(subjectProvider.validate).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
75
98
|
it('rejects malformed, duplicate, and provider-invalid references', async () => {
|
|
76
99
|
subjectProvider.validate.mockResolvedValue({
|
|
77
100
|
valid: false,
|
|
78
|
-
issues: [{ ref: '
|
|
101
|
+
issues: [{ ref: 'missing', code: 'NOT_FOUND', message: 'Missing' }],
|
|
79
102
|
});
|
|
80
103
|
|
|
81
104
|
await expect(
|
|
@@ -83,7 +106,7 @@ describe('SubjectConfigValidatorService', () => {
|
|
|
83
106
|
schema,
|
|
84
107
|
{
|
|
85
108
|
approval: {
|
|
86
|
-
subjects: [{
|
|
109
|
+
subjects: [{ source: 'STATIC', refs: ['missing'] }],
|
|
87
110
|
},
|
|
88
111
|
},
|
|
89
112
|
actor,
|
|
@@ -96,19 +119,7 @@ describe('SubjectConfigValidatorService', () => {
|
|
|
96
119
|
});
|
|
97
120
|
});
|
|
98
121
|
|
|
99
|
-
it('
|
|
100
|
-
await expect(
|
|
101
|
-
service.validateSubjects(
|
|
102
|
-
schema,
|
|
103
|
-
{
|
|
104
|
-
approval: {
|
|
105
|
-
subjects: [{ provider: 'MISSING', refs: ['value'] }],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
actor,
|
|
109
|
-
),
|
|
110
|
-
).rejects.toBeInstanceOf(SubjectProviderError);
|
|
111
|
-
|
|
122
|
+
it('sanitizes unexpected provider failures', async () => {
|
|
112
123
|
subjectProvider.validate.mockRejectedValueOnce(
|
|
113
124
|
new BadRequestException({ code: 'PROVIDER_LEAK', secret: 'secret DSN' }),
|
|
114
125
|
);
|
|
@@ -117,7 +128,7 @@ describe('SubjectConfigValidatorService', () => {
|
|
|
117
128
|
schema,
|
|
118
129
|
{
|
|
119
130
|
approval: {
|
|
120
|
-
subjects: [{
|
|
131
|
+
subjects: [{ source: 'STATIC', refs: ['alice'] }],
|
|
121
132
|
},
|
|
122
133
|
},
|
|
123
134
|
actor,
|
|
@@ -137,16 +137,10 @@ export class SubjectConfigValidatorService {
|
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
const provider = validNonBlankString(entry.provider);
|
|
141
140
|
const source = entry.source ?? 'STATIC';
|
|
141
|
+
const provider =
|
|
142
|
+
source === 'DYNAMIC' ? 'USER' : validProvider(entry.provider) ?? 'USER';
|
|
142
143
|
const rawRefs = entry.refs;
|
|
143
|
-
if (!provider) {
|
|
144
|
-
issues.push({
|
|
145
|
-
path: `${entryPath}/provider`,
|
|
146
|
-
code: 'INVALID_PROVIDER',
|
|
147
|
-
message: 'provider must be a nonblank string',
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
144
|
if (source === 'DYNAMIC') {
|
|
151
145
|
const inputKey = validNonBlankString(entry.inputKey);
|
|
152
146
|
if (!inputKey) {
|
|
@@ -164,11 +158,9 @@ export class SubjectConfigValidatorService {
|
|
|
164
158
|
} else {
|
|
165
159
|
seenDynamicInputs.add(inputKey);
|
|
166
160
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
providerPaths.set(provider, entryPath);
|
|
171
|
-
}
|
|
161
|
+
grouped.set(provider, grouped.get(provider) ?? []);
|
|
162
|
+
if (!providerPaths.has(provider))
|
|
163
|
+
providerPaths.set(provider, entryPath);
|
|
172
164
|
return;
|
|
173
165
|
}
|
|
174
166
|
if (!Array.isArray(rawRefs) || rawRefs.length === 0) {
|
|
@@ -190,17 +182,8 @@ export class SubjectConfigValidatorService {
|
|
|
190
182
|
});
|
|
191
183
|
return;
|
|
192
184
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const duplicateKey = `${provider}\0${ref}`;
|
|
185
|
+
const duplicateKey = `${provider}\u0000${ref}`;
|
|
196
186
|
if (seen.has(duplicateKey)) {
|
|
197
|
-
issues.push({
|
|
198
|
-
path: `${entryPath}/refs/${refIndex}`,
|
|
199
|
-
provider,
|
|
200
|
-
ref,
|
|
201
|
-
code: 'DUPLICATE_REF',
|
|
202
|
-
message: 'duplicate ref for provider',
|
|
203
|
-
});
|
|
204
187
|
return;
|
|
205
188
|
}
|
|
206
189
|
|
|
@@ -232,6 +215,10 @@ function validNonBlankString(value: unknown): string | undefined {
|
|
|
232
215
|
: undefined;
|
|
233
216
|
}
|
|
234
217
|
|
|
218
|
+
function validProvider(value: unknown): 'USER' | 'USER_GROUP' | undefined {
|
|
219
|
+
return value === 'USER' || value === 'USER_GROUP' ? value : undefined;
|
|
220
|
+
}
|
|
221
|
+
|
|
235
222
|
function toJsonPointer(path: readonly string[]): string {
|
|
236
223
|
if (path.length === 0) return '/';
|
|
237
224
|
return `/${path
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
+
import { act } from 'react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { searchStaffDirectory } from './staff-directory.api';
|
|
5
|
+
import { StaffSelect } from './StaffSelect';
|
|
6
|
+
|
|
7
|
+
vi.mock('./staff-directory.api', () => ({
|
|
8
|
+
searchStaffDirectory: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('antd', () => ({
|
|
12
|
+
Select: ({ onSearch, onChange, options = [] }: any) => (
|
|
13
|
+
<div>
|
|
14
|
+
<input
|
|
15
|
+
aria-label="staff search"
|
|
16
|
+
onChange={(event) => onSearch(event.target.value)}
|
|
17
|
+
/>
|
|
18
|
+
{options.map((option: any) => (
|
|
19
|
+
<button key={option.value} onClick={() => onChange(option.value)}>
|
|
20
|
+
{option.label}
|
|
21
|
+
</button>
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
),
|
|
25
|
+
Empty: Object.assign(() => <div>empty</div>, {
|
|
26
|
+
PRESENTED_IMAGE_SIMPLE: 'simple',
|
|
27
|
+
}),
|
|
28
|
+
Space: ({ children }: any) => <div>{children}</div>,
|
|
29
|
+
Spin: () => <div>loading</div>,
|
|
30
|
+
Typography: {
|
|
31
|
+
Text: ({ children }: any) => <span>{children}</span>,
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
describe('StaffSelect', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.useFakeTimers();
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => vi.useRealTimers());
|
|
42
|
+
|
|
43
|
+
it('searches through the shared backend and returns the selected person', async () => {
|
|
44
|
+
vi.mocked(searchStaffDirectory).mockResolvedValue([
|
|
45
|
+
{
|
|
46
|
+
userId: 'junrujrwang',
|
|
47
|
+
displayName: 'Junru Wang',
|
|
48
|
+
businessGroup: 'OIT',
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
const onChange = vi.fn();
|
|
52
|
+
const onPersonChange = vi.fn();
|
|
53
|
+
render(<StaffSelect onChange={onChange} onPersonChange={onPersonChange} />);
|
|
54
|
+
|
|
55
|
+
fireEvent.change(screen.getByRole('textbox', { name: 'staff search' }), {
|
|
56
|
+
target: { value: 'junru' },
|
|
57
|
+
});
|
|
58
|
+
await act(() => vi.advanceTimersByTimeAsync(300));
|
|
59
|
+
|
|
60
|
+
expect(searchStaffDirectory).toHaveBeenCalledWith('junru');
|
|
61
|
+
fireEvent.click(
|
|
62
|
+
screen.getByRole('button', { name: 'Junru Wang (junrujrwang)' }),
|
|
63
|
+
);
|
|
64
|
+
expect(onChange).toHaveBeenCalledWith('junrujrwang');
|
|
65
|
+
expect(onPersonChange).toHaveBeenCalledWith(
|
|
66
|
+
expect.objectContaining({ userId: 'junrujrwang' }),
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|