@gadmin2n/schematics 0.0.147 → 0.0.148
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 +1 -0
- 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 +17 -4
- 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/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-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.service.spec.ts +44 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +15 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +2 -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 +49 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +49 -11
- 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/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.tsx +1 -2
- 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 +47 -3
- 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/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
|
@@ -138,11 +138,102 @@ export class UserGroupService {
|
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
async deleteGroup(code: string, expectedRevision: number) {
|
|
142
|
+
try {
|
|
143
|
+
return await this.prisma.$transaction(async (tx) => {
|
|
144
|
+
const group = await this.findGroup(tx, code);
|
|
145
|
+
this.assertRevision(group.revision, expectedRevision);
|
|
146
|
+
|
|
147
|
+
const [parents, workflows, activeApprovalCount] = await Promise.all([
|
|
148
|
+
tx.userGroupChild.findMany({
|
|
149
|
+
where: { childGroupId: group.id },
|
|
150
|
+
select: {
|
|
151
|
+
parentGroup: { select: { code: true, name: true } },
|
|
152
|
+
},
|
|
153
|
+
}),
|
|
154
|
+
tx.workflow.findMany({
|
|
155
|
+
select: {
|
|
156
|
+
id: true,
|
|
157
|
+
name: true,
|
|
158
|
+
dsl: true,
|
|
159
|
+
versions: { select: { dsl: true } },
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
tx.approvalRequestSubject.count({
|
|
163
|
+
where: {
|
|
164
|
+
provider: 'USER_GROUP',
|
|
165
|
+
ref: group.code,
|
|
166
|
+
request: { status: { in: ['PENDING', 'BLOCKED'] } },
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
]);
|
|
170
|
+
const parentGroups = parents.map(({ parentGroup }) => parentGroup);
|
|
171
|
+
const referencedWorkflows = workflows
|
|
172
|
+
.filter(
|
|
173
|
+
(workflow) =>
|
|
174
|
+
workflowDslReferencesGroup(workflow.dsl, group.code) ||
|
|
175
|
+
workflow.versions.some((version) =>
|
|
176
|
+
workflowDslReferencesGroup(version.dsl, group.code),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
.map((workflow) => ({
|
|
180
|
+
id: String(workflow.id),
|
|
181
|
+
name: workflow.name,
|
|
182
|
+
}));
|
|
183
|
+
const reasons = [
|
|
184
|
+
...(parentGroups.length
|
|
185
|
+
? [
|
|
186
|
+
`被人员组引用:${parentGroups
|
|
187
|
+
.map((parent) => parent.name)
|
|
188
|
+
.join('、')}`,
|
|
189
|
+
]
|
|
190
|
+
: []),
|
|
191
|
+
...(referencedWorkflows.length
|
|
192
|
+
? [
|
|
193
|
+
`被 Workflow 引用:${referencedWorkflows
|
|
194
|
+
.map((workflow) => workflow.name)
|
|
195
|
+
.join('、')}`,
|
|
196
|
+
]
|
|
197
|
+
: []),
|
|
198
|
+
...(activeApprovalCount
|
|
199
|
+
? [`仍有 ${activeApprovalCount} 个进行中的审批引用`]
|
|
200
|
+
: []),
|
|
201
|
+
];
|
|
202
|
+
if (reasons.length) {
|
|
203
|
+
throw new UserGroupDomainError(
|
|
204
|
+
'USER_GROUP_IN_USE',
|
|
205
|
+
`无法删除该人员组:${reasons.join(';')}`,
|
|
206
|
+
409,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await Promise.all([
|
|
211
|
+
tx.userGroupUser.deleteMany({ where: { groupId: group.id } }),
|
|
212
|
+
tx.userGroupChild.deleteMany({ where: { parentGroupId: group.id } }),
|
|
213
|
+
]);
|
|
214
|
+
await tx.userGroup.delete({ where: { id: group.id } });
|
|
215
|
+
return { count: 1 };
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (
|
|
219
|
+
error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
220
|
+
error.code === 'P2003'
|
|
221
|
+
) {
|
|
222
|
+
throw new UserGroupDomainError(
|
|
223
|
+
'USER_GROUP_IN_USE',
|
|
224
|
+
'该人员组刚被其他数据引用,请刷新后重试',
|
|
225
|
+
409,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
search(query: string, includeInactive = false) {
|
|
142
233
|
const q = query.trim();
|
|
143
234
|
return this.prisma.userGroup.findMany({
|
|
144
235
|
where: {
|
|
145
|
-
isActive: true,
|
|
236
|
+
...(!includeInactive && { isActive: true }),
|
|
146
237
|
...(q && {
|
|
147
238
|
OR: [
|
|
148
239
|
{ code: { contains: q, mode: 'insensitive' } },
|
|
@@ -156,6 +247,8 @@ export class UserGroupService {
|
|
|
156
247
|
name: true,
|
|
157
248
|
description: true,
|
|
158
249
|
revision: true,
|
|
250
|
+
isActive: true,
|
|
251
|
+
updatedAt: true,
|
|
159
252
|
},
|
|
160
253
|
take: 50,
|
|
161
254
|
});
|
|
@@ -220,7 +313,7 @@ export class UserGroupService {
|
|
|
220
313
|
where: { id: { in: ids }, isActive: true },
|
|
221
314
|
select: {
|
|
222
315
|
users: {
|
|
223
|
-
where: { user: { isActive: true } },
|
|
316
|
+
where: { user: { isActive: true, isApiKey: false } },
|
|
224
317
|
select: {
|
|
225
318
|
user: { select: { userid: true, username: true } },
|
|
226
319
|
},
|
|
@@ -267,13 +360,13 @@ export class UserGroupService {
|
|
|
267
360
|
private async requireActiveUsers(tx: Transaction, userIds: string[]) {
|
|
268
361
|
if (userIds.length === 0) return [];
|
|
269
362
|
const users = await tx.user.findMany({
|
|
270
|
-
where: { userid: { in: userIds }, isActive: true },
|
|
363
|
+
where: { userid: { in: userIds }, isActive: true, isApiKey: false },
|
|
271
364
|
select: { userid: true },
|
|
272
365
|
});
|
|
273
366
|
if (users.length !== userIds.length) {
|
|
274
367
|
throw new UserGroupDomainError(
|
|
275
368
|
'USER_GROUP_MEMBER_INVALID',
|
|
276
|
-
'Every direct user must exist and be
|
|
369
|
+
'Every direct user must exist, be active, and not be an API key user',
|
|
277
370
|
);
|
|
278
371
|
}
|
|
279
372
|
return users;
|
|
@@ -388,3 +481,23 @@ export class UserGroupService {
|
|
|
388
481
|
);
|
|
389
482
|
}
|
|
390
483
|
}
|
|
484
|
+
|
|
485
|
+
function workflowDslReferencesGroup(
|
|
486
|
+
value: Prisma.JsonValue | null,
|
|
487
|
+
groupCode: string,
|
|
488
|
+
): boolean {
|
|
489
|
+
if (Array.isArray(value))
|
|
490
|
+
return value.some((item) => workflowDslReferencesGroup(item, groupCode));
|
|
491
|
+
if (!value || typeof value !== 'object') return false;
|
|
492
|
+
|
|
493
|
+
const record = value as Record<string, Prisma.JsonValue>;
|
|
494
|
+
const refs = Array.isArray(record.refs) ? record.refs.map(String) : [];
|
|
495
|
+
if (
|
|
496
|
+
refs.includes(`group:${groupCode}`) ||
|
|
497
|
+
(record.provider === 'USER_GROUP' && refs.includes(groupCode))
|
|
498
|
+
)
|
|
499
|
+
return true;
|
|
500
|
+
return Object.values(record).some((item) =>
|
|
501
|
+
workflowDslReferencesGroup(item, groupCode),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
@@ -69,6 +69,7 @@ describe('Workflow cancellation', () => {
|
|
|
69
69
|
runtime.cancelExecution({
|
|
70
70
|
tx: tx as any,
|
|
71
71
|
instanceId: BigInt(11),
|
|
72
|
+
requestedBy: 'business-user',
|
|
72
73
|
reason: 'business cancellation',
|
|
73
74
|
}),
|
|
74
75
|
).resolves.toEqual({ instanceId: '11', status: 'CANCELLED' });
|
|
@@ -79,6 +80,8 @@ describe('Workflow cancellation', () => {
|
|
|
79
80
|
data: expect.objectContaining({
|
|
80
81
|
status: 'CANCELLED',
|
|
81
82
|
cancellationStatus: 'PENDING',
|
|
83
|
+
cancellationRequestedAt: expect.any(Date),
|
|
84
|
+
cancellationRequestedBy: 'business-user',
|
|
82
85
|
cancellationReason: 'business cancellation',
|
|
83
86
|
}),
|
|
84
87
|
});
|
|
@@ -112,6 +115,7 @@ describe('Workflow cancellation', () => {
|
|
|
112
115
|
await runtime.cancelExecution({
|
|
113
116
|
tx: tx as any,
|
|
114
117
|
instanceId: execution!.instanceId,
|
|
118
|
+
requestedBy: 'applicant-user',
|
|
115
119
|
reason: 'applicant cancellation',
|
|
116
120
|
});
|
|
117
121
|
|
|
@@ -150,7 +154,11 @@ describe('Workflow cancellation', () => {
|
|
|
150
154
|
const { runtime, temporal, tx } = setup({ status: 'COMPLETED' });
|
|
151
155
|
|
|
152
156
|
await expect(
|
|
153
|
-
runtime.cancelExecution({
|
|
157
|
+
runtime.cancelExecution({
|
|
158
|
+
tx: tx as any,
|
|
159
|
+
instanceId: BigInt(11),
|
|
160
|
+
requestedBy: 'admin-user',
|
|
161
|
+
}),
|
|
154
162
|
).rejects.toBeInstanceOf(ConflictException);
|
|
155
163
|
expect(tx.workflowInstance.update).not.toHaveBeenCalled();
|
|
156
164
|
expect(tx.approvalRequest.updateMany).not.toHaveBeenCalled();
|
|
@@ -164,7 +172,11 @@ describe('Workflow cancellation', () => {
|
|
|
164
172
|
});
|
|
165
173
|
|
|
166
174
|
await expect(
|
|
167
|
-
runtime.cancelExecution({
|
|
175
|
+
runtime.cancelExecution({
|
|
176
|
+
tx: tx as any,
|
|
177
|
+
instanceId: BigInt(11),
|
|
178
|
+
requestedBy: 'admin-user',
|
|
179
|
+
}),
|
|
168
180
|
).rejects.toMatchObject({
|
|
169
181
|
response: {
|
|
170
182
|
code: 'WORKFLOW_CANCELLATION_BLOCKED',
|
|
@@ -180,7 +192,11 @@ describe('Workflow cancellation', () => {
|
|
|
180
192
|
});
|
|
181
193
|
|
|
182
194
|
await expect(
|
|
183
|
-
runtime.cancelExecution({
|
|
195
|
+
runtime.cancelExecution({
|
|
196
|
+
tx: tx as any,
|
|
197
|
+
instanceId: BigInt(11),
|
|
198
|
+
requestedBy: 'second-user',
|
|
199
|
+
}),
|
|
184
200
|
).resolves.toEqual({ instanceId: '11', status: 'CANCELLED' });
|
|
185
201
|
expect(tx.workflowInstance.update).not.toHaveBeenCalled();
|
|
186
202
|
expect(temporal.cancelWorkflow).not.toHaveBeenCalled();
|
|
@@ -193,6 +209,7 @@ describe('Workflow cancellation', () => {
|
|
|
193
209
|
runtime.cancelExecution({
|
|
194
210
|
tx: transactionClient,
|
|
195
211
|
instanceId: BigInt(11),
|
|
212
|
+
requestedBy: `${reason} user`,
|
|
196
213
|
reason,
|
|
197
214
|
}),
|
|
198
215
|
);
|
|
@@ -279,7 +279,7 @@ describe('WorkflowCompilerService', () => {
|
|
|
279
279
|
type: 'approval',
|
|
280
280
|
label: '部门负责人审批',
|
|
281
281
|
config: {
|
|
282
|
-
subjects: [{ source: 'STATIC',
|
|
282
|
+
subjects: [{ source: 'STATIC', refs: [' '] }],
|
|
283
283
|
},
|
|
284
284
|
},
|
|
285
285
|
],
|
|
@@ -426,7 +426,7 @@ describe('WorkflowCompilerService', () => {
|
|
|
426
426
|
code: 'FINANCE_APPROVAL',
|
|
427
427
|
nodeTypeId: BigInt(7),
|
|
428
428
|
isEnabled: true,
|
|
429
|
-
config: { subjects: [{
|
|
429
|
+
config: { subjects: [{ source: 'STATIC', refs: ['alice'] }] },
|
|
430
430
|
};
|
|
431
431
|
const prisma = {
|
|
432
432
|
workflowNodeType: {
|
|
@@ -483,7 +483,7 @@ describe('WorkflowCompilerService', () => {
|
|
|
483
483
|
});
|
|
484
484
|
instance.config.subjects[0].refs[0] = 'bob';
|
|
485
485
|
expect(compiled.nodes[0].config).toEqual({
|
|
486
|
-
subjects: [{
|
|
486
|
+
subjects: [{ source: 'STATIC', refs: ['alice'] }],
|
|
487
487
|
});
|
|
488
488
|
});
|
|
489
489
|
|
|
@@ -529,9 +529,7 @@ describe('WorkflowCompilerService', () => {
|
|
|
529
529
|
config: instance.config,
|
|
530
530
|
configOverride: {
|
|
531
531
|
timeoutSeconds: 3600,
|
|
532
|
-
subjects: [
|
|
533
|
-
{ source: 'STATIC', provider: 'USER', refs: ['alice'] },
|
|
534
|
-
],
|
|
532
|
+
subjects: [{ source: 'STATIC', refs: ['alice'] }],
|
|
535
533
|
},
|
|
536
534
|
instanceRef: {
|
|
537
535
|
instanceId: '12',
|
|
@@ -546,7 +544,7 @@ describe('WorkflowCompilerService', () => {
|
|
|
546
544
|
|
|
547
545
|
const expectedOverride = {
|
|
548
546
|
timeoutSeconds: 3600,
|
|
549
|
-
subjects: [{ source: 'STATIC',
|
|
547
|
+
subjects: [{ source: 'STATIC', refs: ['alice'] }],
|
|
550
548
|
};
|
|
551
549
|
expect(compiled.nodes[0].config).toEqual(expectedOverride);
|
|
552
550
|
expect(compiled.nodes[0].source?.configHash).toBe(
|
|
@@ -571,7 +569,7 @@ describe('WorkflowCompilerService', () => {
|
|
|
571
569
|
},
|
|
572
570
|
details: [],
|
|
573
571
|
},
|
|
574
|
-
subjects: [{ source: 'STATIC',
|
|
572
|
+
subjects: [{ source: 'STATIC', refs: ['alice'] }],
|
|
575
573
|
approvalRule: { mode: 'ANY' },
|
|
576
574
|
rejectionRule: { mode: 'ANY' },
|
|
577
575
|
timeout: { durationSeconds: 0, action: 'REJECTED' },
|
|
@@ -655,7 +653,6 @@ describe('WorkflowCompilerService', () => {
|
|
|
655
653
|
subjects: [
|
|
656
654
|
{
|
|
657
655
|
source: 'DYNAMIC',
|
|
658
|
-
provider: 'USER',
|
|
659
656
|
inputKey: 'direct_manager',
|
|
660
657
|
label: '查询直属领导',
|
|
661
658
|
lookup: { request: { connectionRef: 'erp' } },
|
|
@@ -22,6 +22,7 @@ export interface WorkflowCancellationSummary {
|
|
|
22
22
|
export interface CancelWorkflowExecutionInput {
|
|
23
23
|
tx: Prisma.TransactionClient;
|
|
24
24
|
instanceId: bigint;
|
|
25
|
+
requestedBy: string;
|
|
25
26
|
reason?: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -60,6 +61,7 @@ export class WorkflowRuntimeFacade {
|
|
|
60
61
|
await this.workflows.cancelExecutionInTransaction(
|
|
61
62
|
input.tx,
|
|
62
63
|
input.instanceId,
|
|
64
|
+
input.requestedBy,
|
|
63
65
|
input.reason,
|
|
64
66
|
);
|
|
65
67
|
return { instanceId: input.instanceId.toString(), status: 'CANCELLED' };
|
|
@@ -81,7 +81,9 @@ describe('WorkflowController runtime contracts', () => {
|
|
|
81
81
|
prisma as never,
|
|
82
82
|
);
|
|
83
83
|
|
|
84
|
-
await expect(
|
|
84
|
+
await expect(
|
|
85
|
+
controller.cancelInstance(42, { user: { userid: 'admin-user' } }),
|
|
86
|
+
).resolves.toEqual({
|
|
85
87
|
instanceId: '42',
|
|
86
88
|
status: 'CANCELLED',
|
|
87
89
|
});
|
|
@@ -89,6 +91,7 @@ describe('WorkflowController runtime contracts', () => {
|
|
|
89
91
|
expect(runtime.cancelExecution).toHaveBeenCalledWith({
|
|
90
92
|
tx,
|
|
91
93
|
instanceId: BigInt(42),
|
|
94
|
+
requestedBy: 'admin-user',
|
|
92
95
|
});
|
|
93
96
|
});
|
|
94
97
|
});
|
|
@@ -272,11 +272,15 @@ export class WorkflowController {
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
@Post('instance/:instanceId/cancel')
|
|
275
|
-
async cancelInstance(
|
|
275
|
+
async cancelInstance(
|
|
276
|
+
@Param('instanceId', ParseIntPipe) instanceId: number,
|
|
277
|
+
@Req() req,
|
|
278
|
+
) {
|
|
276
279
|
return this.prisma.$transaction((tx) =>
|
|
277
280
|
this.workflowRuntime.cancelExecution({
|
|
278
281
|
tx,
|
|
279
282
|
instanceId: BigInt(instanceId),
|
|
283
|
+
requestedBy: req.user.userid,
|
|
280
284
|
}),
|
|
281
285
|
);
|
|
282
286
|
}
|
|
@@ -7,6 +7,50 @@ import { signWebhookPayload } from './webhook-signature.util';
|
|
|
7
7
|
import { WorkflowCompilerService } from './workflow-compiler.service';
|
|
8
8
|
import { WorkflowService } from './workflow.service';
|
|
9
9
|
|
|
10
|
+
describe('WorkflowService — workflow list run counts', () => {
|
|
11
|
+
it('includes cancelled instances in the run summary', async () => {
|
|
12
|
+
const prisma = {
|
|
13
|
+
workflow: {
|
|
14
|
+
findMany: jest.fn().mockResolvedValue([
|
|
15
|
+
{
|
|
16
|
+
id: BigInt(42),
|
|
17
|
+
dsl: null,
|
|
18
|
+
versions: [],
|
|
19
|
+
_count: { instances: 2 },
|
|
20
|
+
},
|
|
21
|
+
]),
|
|
22
|
+
count: jest.fn().mockResolvedValue(1),
|
|
23
|
+
},
|
|
24
|
+
workflowInstance: {
|
|
25
|
+
groupBy: jest.fn().mockResolvedValue([
|
|
26
|
+
{
|
|
27
|
+
workflowId: BigInt(42),
|
|
28
|
+
status: 'CANCELLED',
|
|
29
|
+
_count: 2,
|
|
30
|
+
},
|
|
31
|
+
]),
|
|
32
|
+
findMany: jest.fn().mockResolvedValue([]),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const service = new WorkflowService(
|
|
36
|
+
prisma as any,
|
|
37
|
+
{} as any,
|
|
38
|
+
{} as any,
|
|
39
|
+
{} as any,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const result = await service.findMany({});
|
|
43
|
+
|
|
44
|
+
expect(result.items[0].runs).toEqual({
|
|
45
|
+
pending: 0,
|
|
46
|
+
running: 0,
|
|
47
|
+
completed: 0,
|
|
48
|
+
failed: 0,
|
|
49
|
+
cancelled: 2,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
10
54
|
describe('WorkflowService — cron lifecycle hooks', () => {
|
|
11
55
|
let service: WorkflowService;
|
|
12
56
|
let prisma: any;
|
|
@@ -592,7 +592,13 @@ export class WorkflowService {
|
|
|
592
592
|
|
|
593
593
|
const runsMap = new Map<
|
|
594
594
|
bigint,
|
|
595
|
-
{
|
|
595
|
+
{
|
|
596
|
+
pending: number;
|
|
597
|
+
running: number;
|
|
598
|
+
completed: number;
|
|
599
|
+
failed: number;
|
|
600
|
+
cancelled: number;
|
|
601
|
+
}
|
|
596
602
|
>();
|
|
597
603
|
for (const row of instanceCounts) {
|
|
598
604
|
if (!runsMap.has(row.workflowId)) {
|
|
@@ -601,6 +607,7 @@ export class WorkflowService {
|
|
|
601
607
|
running: 0,
|
|
602
608
|
completed: 0,
|
|
603
609
|
failed: 0,
|
|
610
|
+
cancelled: 0,
|
|
604
611
|
});
|
|
605
612
|
}
|
|
606
613
|
const entry = runsMap.get(row.workflowId)!;
|
|
@@ -627,6 +634,7 @@ export class WorkflowService {
|
|
|
627
634
|
running: 0,
|
|
628
635
|
completed: 0,
|
|
629
636
|
failed: 0,
|
|
637
|
+
cancelled: 0,
|
|
630
638
|
},
|
|
631
639
|
lastRun: lastRunMap.get(item.id)?.createdAt || null,
|
|
632
640
|
}));
|
|
@@ -2260,8 +2268,13 @@ export class WorkflowService {
|
|
|
2260
2268
|
async cancelExecutionInTransaction(
|
|
2261
2269
|
tx: Prisma.TransactionClient,
|
|
2262
2270
|
instanceId: bigint,
|
|
2271
|
+
requestedBy: string,
|
|
2263
2272
|
reason?: string,
|
|
2264
2273
|
) {
|
|
2274
|
+
const normalizedRequestedBy = requestedBy.trim().slice(0, 128);
|
|
2275
|
+
if (!normalizedRequestedBy)
|
|
2276
|
+
throw new BadRequestException('Cancellation requester is required');
|
|
2277
|
+
|
|
2265
2278
|
const rows = await tx.$queryRaw<
|
|
2266
2279
|
Array<{
|
|
2267
2280
|
id: bigint;
|
|
@@ -2301,6 +2314,7 @@ export class WorkflowService {
|
|
|
2301
2314
|
status: 'CANCELLED',
|
|
2302
2315
|
finishedAt: now,
|
|
2303
2316
|
cancellationRequestedAt: now,
|
|
2317
|
+
cancellationRequestedBy: normalizedRequestedBy,
|
|
2304
2318
|
cancellationReason: reason?.trim().slice(0, 512) || null,
|
|
2305
2319
|
cancellationStatus: instance.temporalRunId ? 'PENDING' : 'COMPLETED',
|
|
2306
2320
|
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
|
},
|
|
@@ -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
|