@gadmin2n/schematics 0.0.146 → 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/CODEBUDDY.local.md +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +70 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +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
|
@@ -5,6 +5,7 @@ import { ExternalInteractionModule } from './externalInteraction/externalInterac
|
|
|
5
5
|
import { HttpRequestModule } from './httpRequest/httpRequest.module';
|
|
6
6
|
import { HumanApprovalModule } from './humanApproval/humanApproval.module';
|
|
7
7
|
import { IntegrationConnectionModule } from './integrationConnection/integrationConnection.module';
|
|
8
|
+
import { StaffDirectoryModule } from './staffDirectory/staffDirectory.module';
|
|
8
9
|
import { SubjectProviderModule } from './subjectProvider/subjectProvider.module';
|
|
9
10
|
import { UserGroupModule } from './userGroup/userGroup.module';
|
|
10
11
|
import { WorkflowModule } from './workflow/workflow.module';
|
|
@@ -17,6 +18,7 @@ export default [
|
|
|
17
18
|
HumanApprovalModule,
|
|
18
19
|
HttpRequestModule,
|
|
19
20
|
IntegrationConnectionModule,
|
|
21
|
+
StaffDirectoryModule,
|
|
20
22
|
WorkflowModule,
|
|
21
23
|
SubjectProviderModule,
|
|
22
24
|
UserGroupModule,
|
|
@@ -2,6 +2,7 @@ import { ACGuard } from '@gadmin2n/nest-common';
|
|
|
2
2
|
import {
|
|
3
3
|
Body,
|
|
4
4
|
Controller,
|
|
5
|
+
Delete,
|
|
5
6
|
Get,
|
|
6
7
|
Header,
|
|
7
8
|
Param,
|
|
@@ -87,4 +88,14 @@ export class IntegrationConnectionController {
|
|
|
87
88
|
) {
|
|
88
89
|
return this.service.update(code, input, actorContextFromRequest(request));
|
|
89
90
|
}
|
|
91
|
+
|
|
92
|
+
@Delete(':code')
|
|
93
|
+
@UseRoles({
|
|
94
|
+
resource: 'integrationConnection',
|
|
95
|
+
action: 'delete',
|
|
96
|
+
possession: 'any',
|
|
97
|
+
})
|
|
98
|
+
remove(@Param('code') code: string) {
|
|
99
|
+
return this.service.remove(code);
|
|
100
|
+
}
|
|
90
101
|
}
|
|
@@ -41,4 +41,130 @@ describe('IntegrationConnectionService', () => {
|
|
|
41
41
|
);
|
|
42
42
|
await expect(service.getSecrets(entity.code)).resolves.toEqual(secrets);
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
it('rejects deletion when a published Workflow references the connection', async () => {
|
|
46
|
+
const deleteConnection = jest.fn();
|
|
47
|
+
const prisma = {
|
|
48
|
+
integrationConnection: {
|
|
49
|
+
findUnique: jest.fn().mockResolvedValue({ code: 'ticket-api' }),
|
|
50
|
+
delete: deleteConnection,
|
|
51
|
+
},
|
|
52
|
+
workflow: {
|
|
53
|
+
findMany: jest.fn().mockResolvedValue([
|
|
54
|
+
{
|
|
55
|
+
id: BigInt(7),
|
|
56
|
+
name: '工单审批',
|
|
57
|
+
dsl: null,
|
|
58
|
+
versions: [
|
|
59
|
+
{
|
|
60
|
+
version: 2,
|
|
61
|
+
dsl: {
|
|
62
|
+
nodes: [
|
|
63
|
+
{
|
|
64
|
+
id: 'send-ticket',
|
|
65
|
+
config: { connectionRef: 'ticket-api' },
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
]),
|
|
73
|
+
},
|
|
74
|
+
workflowNodeInstance: { findMany: jest.fn() },
|
|
75
|
+
};
|
|
76
|
+
const service = new IntegrationConnectionService(prisma as any, {} as any);
|
|
77
|
+
|
|
78
|
+
await expect(service.remove('ticket-api')).rejects.toMatchObject({
|
|
79
|
+
response: {
|
|
80
|
+
code: 'INTEGRATION_CONNECTION_IN_USE',
|
|
81
|
+
callers: [
|
|
82
|
+
{
|
|
83
|
+
workflowId: '7',
|
|
84
|
+
workflowName: '工单审批',
|
|
85
|
+
source: 'PUBLISHED',
|
|
86
|
+
version: 2,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
expect(deleteConnection).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('rejects deletion when a draft uses a referenced Node Instance connection', async () => {
|
|
95
|
+
const deleteConnection = jest.fn();
|
|
96
|
+
const prisma = {
|
|
97
|
+
integrationConnection: {
|
|
98
|
+
findUnique: jest.fn().mockResolvedValue({ code: 'ticket-api' }),
|
|
99
|
+
delete: deleteConnection,
|
|
100
|
+
},
|
|
101
|
+
workflow: {
|
|
102
|
+
findMany: jest.fn().mockResolvedValue([
|
|
103
|
+
{
|
|
104
|
+
id: BigInt(8),
|
|
105
|
+
name: '资产审批',
|
|
106
|
+
dsl: {
|
|
107
|
+
nodes: [
|
|
108
|
+
{
|
|
109
|
+
id: 'notify',
|
|
110
|
+
instanceRef: {
|
|
111
|
+
instanceId: '12',
|
|
112
|
+
instanceCode: 'NOTIFY',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
versions: [],
|
|
118
|
+
},
|
|
119
|
+
]),
|
|
120
|
+
},
|
|
121
|
+
workflowNodeInstance: {
|
|
122
|
+
findMany: jest.fn().mockResolvedValue([
|
|
123
|
+
{
|
|
124
|
+
id: BigInt(12),
|
|
125
|
+
config: {
|
|
126
|
+
external: {
|
|
127
|
+
request: { connectionRef: 'ticket-api' },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
]),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const service = new IntegrationConnectionService(prisma as any, {} as any);
|
|
135
|
+
|
|
136
|
+
await expect(service.remove('ticket-api')).rejects.toMatchObject({
|
|
137
|
+
response: {
|
|
138
|
+
code: 'INTEGRATION_CONNECTION_IN_USE',
|
|
139
|
+
callers: [
|
|
140
|
+
{
|
|
141
|
+
workflowId: '8',
|
|
142
|
+
workflowName: '资产审批',
|
|
143
|
+
source: 'DRAFT',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
expect(deleteConnection).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('deletes an unreferenced connection', async () => {
|
|
152
|
+
const deleteConnection = jest.fn().mockResolvedValue({});
|
|
153
|
+
const prisma = {
|
|
154
|
+
integrationConnection: {
|
|
155
|
+
findUnique: jest.fn().mockResolvedValue({ code: 'ticket-api' }),
|
|
156
|
+
delete: deleteConnection,
|
|
157
|
+
},
|
|
158
|
+
workflow: { findMany: jest.fn().mockResolvedValue([]) },
|
|
159
|
+
workflowNodeInstance: { findMany: jest.fn() },
|
|
160
|
+
};
|
|
161
|
+
const service = new IntegrationConnectionService(prisma as any, {} as any);
|
|
162
|
+
|
|
163
|
+
await expect(service.remove('ticket-api')).resolves.toEqual({
|
|
164
|
+
code: 'ticket-api',
|
|
165
|
+
});
|
|
166
|
+
expect(deleteConnection).toHaveBeenCalledWith({
|
|
167
|
+
where: { code: 'ticket-api' },
|
|
168
|
+
});
|
|
169
|
+
});
|
|
44
170
|
});
|
|
@@ -135,6 +135,84 @@ export class IntegrationConnectionService {
|
|
|
135
135
|
return this.get(code);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
async remove(code: string) {
|
|
139
|
+
await this.findOrThrow(code);
|
|
140
|
+
const workflows = await this.prisma.workflow.findMany({
|
|
141
|
+
select: {
|
|
142
|
+
id: true,
|
|
143
|
+
name: true,
|
|
144
|
+
dsl: true,
|
|
145
|
+
versions: { select: { version: true, dsl: true } },
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const dsls = workflows.flatMap((workflow) => [
|
|
149
|
+
workflow.dsl,
|
|
150
|
+
...workflow.versions.map((version) => version.dsl),
|
|
151
|
+
]);
|
|
152
|
+
const referencedInstanceIds = [
|
|
153
|
+
...new Set(dsls.flatMap((dsl) => referencedNodeInstanceIds(dsl))),
|
|
154
|
+
];
|
|
155
|
+
const referencedInstances = referencedInstanceIds.length
|
|
156
|
+
? await this.prisma.workflowNodeInstance.findMany({
|
|
157
|
+
where: {
|
|
158
|
+
id: { in: referencedInstanceIds.map((id) => BigInt(id)) },
|
|
159
|
+
},
|
|
160
|
+
select: { id: true, config: true },
|
|
161
|
+
})
|
|
162
|
+
: [];
|
|
163
|
+
const instanceConfigs = new Map(
|
|
164
|
+
referencedInstances.map((instance) => [
|
|
165
|
+
String(instance.id),
|
|
166
|
+
instance.config,
|
|
167
|
+
]),
|
|
168
|
+
);
|
|
169
|
+
const callers: Array<{
|
|
170
|
+
workflowId: string;
|
|
171
|
+
workflowName: string;
|
|
172
|
+
source: 'DRAFT' | 'PUBLISHED';
|
|
173
|
+
version?: number;
|
|
174
|
+
}> = [];
|
|
175
|
+
|
|
176
|
+
for (const workflow of workflows) {
|
|
177
|
+
if (dslReferencesConnection(workflow.dsl, code, instanceConfigs))
|
|
178
|
+
callers.push({
|
|
179
|
+
workflowId: String(workflow.id),
|
|
180
|
+
workflowName: workflow.name,
|
|
181
|
+
source: 'DRAFT',
|
|
182
|
+
});
|
|
183
|
+
for (const version of workflow.versions) {
|
|
184
|
+
if (!dslReferencesConnection(version.dsl, code, instanceConfigs))
|
|
185
|
+
continue;
|
|
186
|
+
callers.push({
|
|
187
|
+
workflowId: String(workflow.id),
|
|
188
|
+
workflowName: workflow.name,
|
|
189
|
+
source: 'PUBLISHED',
|
|
190
|
+
version: version.version,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (callers.length > 0) {
|
|
196
|
+
const workflowNames = [
|
|
197
|
+
...new Set(callers.map((item) => item.workflowName)),
|
|
198
|
+
];
|
|
199
|
+
const visibleNames = workflowNames.slice(0, 3).join('、');
|
|
200
|
+
const remaining = workflowNames.length - 3;
|
|
201
|
+
throw new ConflictException({
|
|
202
|
+
code: 'INTEGRATION_CONNECTION_IN_USE',
|
|
203
|
+
message:
|
|
204
|
+
'该连接仍被 Workflow ' +
|
|
205
|
+
visibleNames +
|
|
206
|
+
(remaining > 0 ? ' 等 ' + workflowNames.length + ' 个' : '') +
|
|
207
|
+
' 使用,解除引用后才能删除',
|
|
208
|
+
callers,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await this.prisma.integrationConnection.delete({ where: { code } });
|
|
213
|
+
return { code };
|
|
214
|
+
}
|
|
215
|
+
|
|
138
216
|
async resolveRuntime(code: string) {
|
|
139
217
|
const entity = await this.findOrThrow(code);
|
|
140
218
|
if (!entity.isEnabled)
|
|
@@ -281,3 +359,61 @@ function asObject(value: Prisma.JsonValue): Record<string, unknown> {
|
|
|
281
359
|
? (value as Record<string, unknown>)
|
|
282
360
|
: {};
|
|
283
361
|
}
|
|
362
|
+
|
|
363
|
+
function referencedNodeInstanceIds(dsl: unknown): string[] {
|
|
364
|
+
return workflowNodes(dsl).flatMap((node) => {
|
|
365
|
+
if (isRecord(node.source)) return [];
|
|
366
|
+
const instanceId = String(asPlainObject(node.instanceRef).instanceId ?? '');
|
|
367
|
+
return /^[1-9]\d*$/.test(instanceId) ? [instanceId] : [];
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function dslReferencesConnection(
|
|
372
|
+
dsl: unknown,
|
|
373
|
+
connectionCode: string,
|
|
374
|
+
instanceConfigs: Map<string, unknown>,
|
|
375
|
+
): boolean {
|
|
376
|
+
return workflowNodes(dsl).some((node) => {
|
|
377
|
+
let effectiveConfig: unknown = node.config;
|
|
378
|
+
if (!isRecord(node.source)) {
|
|
379
|
+
if (isRecord(node.configOverride)) {
|
|
380
|
+
effectiveConfig = node.configOverride;
|
|
381
|
+
} else {
|
|
382
|
+
const instanceId = String(
|
|
383
|
+
asPlainObject(node.instanceRef).instanceId ?? '',
|
|
384
|
+
);
|
|
385
|
+
effectiveConfig = instanceConfigs.get(instanceId) ?? node.config;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return containsConnectionReference(effectiveConfig, connectionCode);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function workflowNodes(dsl: unknown): Array<Record<string, unknown>> {
|
|
393
|
+
const nodes = asPlainObject(dsl).nodes;
|
|
394
|
+
return Array.isArray(nodes) ? nodes.filter(isRecord) : [];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function containsConnectionReference(
|
|
398
|
+
value: unknown,
|
|
399
|
+
connectionCode: string,
|
|
400
|
+
): boolean {
|
|
401
|
+
if (Array.isArray(value))
|
|
402
|
+
return value.some((item) =>
|
|
403
|
+
containsConnectionReference(item, connectionCode),
|
|
404
|
+
);
|
|
405
|
+
if (!isRecord(value)) return false;
|
|
406
|
+
return Object.entries(value).some(
|
|
407
|
+
([key, item]) =>
|
|
408
|
+
(key === 'connectionRef' && item === connectionCode) ||
|
|
409
|
+
containsConnectionReference(item, connectionCode),
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function asPlainObject(value: unknown): Record<string, unknown> {
|
|
414
|
+
return isRecord(value) ? value : {};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
418
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
419
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ACGuard } from '@gadmin2n/nest-common';
|
|
2
|
+
import { Controller, Get, Header, Query, UseGuards } from '@nestjs/common';
|
|
3
|
+
import { ApiTags } from '@nestjs/swagger';
|
|
4
|
+
import { UseRoles } from 'nest-access-control';
|
|
5
|
+
import { SearchStaffDirectoryDto } from './staff-directory.dto';
|
|
6
|
+
import { StaffDirectoryService } from './staff-directory.service';
|
|
7
|
+
|
|
8
|
+
@ApiTags('staff-directory')
|
|
9
|
+
@UseGuards(ACGuard)
|
|
10
|
+
@Controller('staff-directory')
|
|
11
|
+
export class StaffDirectoryController {
|
|
12
|
+
constructor(private readonly service: StaffDirectoryService) {}
|
|
13
|
+
|
|
14
|
+
@Get('search')
|
|
15
|
+
@Header('Cache-Control', 'no-store')
|
|
16
|
+
@UseRoles({
|
|
17
|
+
resource: 'staffDirectory',
|
|
18
|
+
action: 'read',
|
|
19
|
+
possession: 'any',
|
|
20
|
+
})
|
|
21
|
+
search(@Query() query: SearchStaffDirectoryDto) {
|
|
22
|
+
return this.service.search(query.keyword ?? '');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { StaffDirectoryService } from './staff-directory.service';
|
|
3
|
+
|
|
4
|
+
describe('StaffDirectoryService', () => {
|
|
5
|
+
afterEach(() => jest.restoreAllMocks());
|
|
6
|
+
|
|
7
|
+
it('calls ERP with the server-side API key and normalizes people', async () => {
|
|
8
|
+
const request = jest.spyOn(axios, 'request').mockResolvedValue({
|
|
9
|
+
data: [
|
|
10
|
+
{
|
|
11
|
+
staffAccountName: 'junrujrwang',
|
|
12
|
+
staffDisplayName: 'Junru Wang',
|
|
13
|
+
bgNameEn: 'OIT',
|
|
14
|
+
deptNameEn: 'Operations',
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
const service = new StaffDirectoryService({
|
|
19
|
+
get: jest.fn(
|
|
20
|
+
(key: string) =>
|
|
21
|
+
({
|
|
22
|
+
'staffDirectory.baseUrl': 'https://erp.example.com/',
|
|
23
|
+
'staffDirectory.apiKey': 'secret',
|
|
24
|
+
'staffDirectory.searchPath': '/api/staff/search',
|
|
25
|
+
}[key]),
|
|
26
|
+
),
|
|
27
|
+
} as never);
|
|
28
|
+
|
|
29
|
+
await expect(service.search(' junru ')).resolves.toEqual([
|
|
30
|
+
{
|
|
31
|
+
userId: 'junrujrwang',
|
|
32
|
+
displayName: 'Junru Wang',
|
|
33
|
+
businessGroup: 'OIT',
|
|
34
|
+
department: 'Operations',
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
expect(request).toHaveBeenCalledWith(
|
|
38
|
+
expect.objectContaining({
|
|
39
|
+
url: 'https://erp.example.com/api/staff/search',
|
|
40
|
+
params: { keyword: 'junru' },
|
|
41
|
+
headers: { 'X-API-KEY': 'secret' },
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns no results without calling ERP for an empty keyword', async () => {
|
|
47
|
+
const request = jest.spyOn(axios, 'request');
|
|
48
|
+
const service = new StaffDirectoryService({ get: jest.fn() } as never);
|
|
49
|
+
|
|
50
|
+
await expect(service.search(' ')).resolves.toEqual([]);
|
|
51
|
+
expect(request).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('reports missing server configuration without exposing secrets', async () => {
|
|
55
|
+
const service = new StaffDirectoryService({ get: jest.fn() } as never);
|
|
56
|
+
|
|
57
|
+
await expect(service.search('alice')).rejects.toMatchObject({
|
|
58
|
+
response: { code: 'STAFF_DIRECTORY_NOT_CONFIGURED' },
|
|
59
|
+
status: 503,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadGatewayException,
|
|
3
|
+
Injectable,
|
|
4
|
+
ServiceUnavailableException,
|
|
5
|
+
} from '@nestjs/common';
|
|
6
|
+
import { ConfigService } from '@nestjs/config';
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
import { StaffDirectoryPerson } from './staff-directory.types';
|
|
9
|
+
|
|
10
|
+
interface UpstreamStaff {
|
|
11
|
+
staffAccountName?: unknown;
|
|
12
|
+
staffDisplayName?: unknown;
|
|
13
|
+
bgNameEn?: unknown;
|
|
14
|
+
deptNameEn?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class StaffDirectoryService {
|
|
19
|
+
constructor(private readonly config: ConfigService) {}
|
|
20
|
+
|
|
21
|
+
async search(keyword: string): Promise<StaffDirectoryPerson[]> {
|
|
22
|
+
const query = keyword.trim();
|
|
23
|
+
if (!query) return [];
|
|
24
|
+
|
|
25
|
+
const baseUrl = this.config
|
|
26
|
+
.get<string>('staffDirectory.baseUrl')
|
|
27
|
+
?.trim()
|
|
28
|
+
.replace(/\/$/, '');
|
|
29
|
+
const apiKey = this.config.get<string>('staffDirectory.apiKey')?.trim();
|
|
30
|
+
const searchPath = normalizeSearchPath(
|
|
31
|
+
this.config.get<string>('staffDirectory.searchPath'),
|
|
32
|
+
);
|
|
33
|
+
if (!baseUrl || !apiKey) {
|
|
34
|
+
throw new ServiceUnavailableException({
|
|
35
|
+
code: 'STAFF_DIRECTORY_NOT_CONFIGURED',
|
|
36
|
+
message:
|
|
37
|
+
'Staff directory is not configured; set STAFF_DIRECTORY_BASE_URL and STAFF_DIRECTORY_API_KEY',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await axios.request<unknown>({
|
|
43
|
+
url: `${baseUrl}${searchPath}`,
|
|
44
|
+
method: 'GET',
|
|
45
|
+
params: { keyword: query },
|
|
46
|
+
headers: { 'X-API-KEY': apiKey },
|
|
47
|
+
timeout: 5000,
|
|
48
|
+
maxRedirects: 0,
|
|
49
|
+
validateStatus: (status) => status >= 200 && status < 300,
|
|
50
|
+
});
|
|
51
|
+
if (!Array.isArray(response.data)) throw new Error('invalid response');
|
|
52
|
+
return [
|
|
53
|
+
...new Map(
|
|
54
|
+
response.data
|
|
55
|
+
.flatMap(normalizePerson)
|
|
56
|
+
.map((person) => [person.userId, person]),
|
|
57
|
+
).values(),
|
|
58
|
+
];
|
|
59
|
+
} catch {
|
|
60
|
+
throw new BadGatewayException({
|
|
61
|
+
code: 'STAFF_DIRECTORY_UNAVAILABLE',
|
|
62
|
+
message: 'Staff directory search is temporarily unavailable',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeSearchPath(value?: string): string {
|
|
69
|
+
const path = value?.trim() || '/api/staff/search';
|
|
70
|
+
return path.startsWith('/') ? path : `/${path}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizePerson(value: unknown): StaffDirectoryPerson[] {
|
|
74
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return [];
|
|
75
|
+
const staff = value as UpstreamStaff;
|
|
76
|
+
const userId = stringValue(staff.staffAccountName);
|
|
77
|
+
if (!userId) return [];
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
userId,
|
|
81
|
+
displayName: stringValue(staff.staffDisplayName) || userId,
|
|
82
|
+
...(stringValue(staff.bgNameEn) && {
|
|
83
|
+
businessGroup: stringValue(staff.bgNameEn),
|
|
84
|
+
}),
|
|
85
|
+
...(stringValue(staff.deptNameEn) && {
|
|
86
|
+
department: stringValue(staff.deptNameEn),
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stringValue(value: unknown): string {
|
|
93
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
94
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { StaffDirectoryController } from './staff-directory.controller';
|
|
3
|
+
import { StaffDirectoryService } from './staff-directory.service';
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
controllers: [StaffDirectoryController],
|
|
7
|
+
providers: [StaffDirectoryService],
|
|
8
|
+
exports: [StaffDirectoryService],
|
|
9
|
+
})
|
|
10
|
+
export class StaffDirectoryModule {}
|
|
@@ -46,6 +46,7 @@ export class UserProvider implements SubjectProvider {
|
|
|
46
46
|
const { entities } = await this.users.findMany({
|
|
47
47
|
where: {
|
|
48
48
|
isActive: true,
|
|
49
|
+
isApiKey: false,
|
|
49
50
|
...(q && {
|
|
50
51
|
OR: [
|
|
51
52
|
{ userid: { contains: q, mode: 'insensitive' } },
|
|
@@ -132,8 +133,11 @@ export class UserProvider implements SubjectProvider {
|
|
|
132
133
|
private async find(refs: string[]): Promise<User[]> {
|
|
133
134
|
if (!refs.length) return [];
|
|
134
135
|
try {
|
|
135
|
-
return (
|
|
136
|
-
.
|
|
136
|
+
return (
|
|
137
|
+
await this.users.findMany({
|
|
138
|
+
where: { userid: { in: refs }, isApiKey: false },
|
|
139
|
+
})
|
|
140
|
+
).entities;
|
|
137
141
|
} catch {
|
|
138
142
|
throw new SubjectProviderUnavailableError(this.key);
|
|
139
143
|
}
|
|
@@ -85,7 +85,6 @@ describe('TemporalTenantConfigController', () => {
|
|
|
85
85
|
dbUrl: 'postgresql://user:pass@gadmin-db:5432/gadmin_demo',
|
|
86
86
|
displayName: 'Gadmin Test',
|
|
87
87
|
runtimeBaseUrl: 'http://gadmin-server:8000/api',
|
|
88
|
-
runtimeToken: '0123456789abcdef0123456789abcdef',
|
|
89
88
|
} as any);
|
|
90
89
|
|
|
91
90
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
@@ -120,34 +119,40 @@ describe('TemporalTenantConfigController', () => {
|
|
|
120
119
|
dbUrl: 'postgresql://user:pass@gadmin-db:5432/gadmin_demo',
|
|
121
120
|
displayName: 'Gadmin Test',
|
|
122
121
|
runtimeBaseUrl: 'http://gadmin-server:8000/api',
|
|
123
|
-
runtimeToken: '
|
|
122
|
+
runtimeToken: 'caller-controlled-runtime-token-value',
|
|
124
123
|
} as any,
|
|
125
124
|
(value) => value,
|
|
126
125
|
{ user: { username: 'admin' } } as any,
|
|
127
126
|
);
|
|
128
127
|
|
|
128
|
+
const registrationRequest = (global.fetch as jest.Mock).mock.calls[0][1];
|
|
129
|
+
const registrationBody = JSON.parse(registrationRequest.body);
|
|
130
|
+
|
|
129
131
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
130
132
|
'http://temporal.example:8081/admin/tenants',
|
|
131
133
|
expect.objectContaining({
|
|
132
134
|
method: 'POST',
|
|
133
|
-
body: JSON.stringify({
|
|
134
|
-
namespace: 'gadmin-test',
|
|
135
|
-
displayName: 'Gadmin Test',
|
|
136
|
-
dbUrl: 'postgresql://user:pass@gadmin-db:5432/gadmin_demo',
|
|
137
|
-
runtimeBaseUrl: 'http://gadmin-server:8000/api',
|
|
138
|
-
runtimeToken: '0123456789abcdef0123456789abcdef',
|
|
139
|
-
}),
|
|
140
135
|
}),
|
|
141
136
|
);
|
|
137
|
+
expect(registrationBody).toMatchObject({
|
|
138
|
+
namespace: 'gadmin-test',
|
|
139
|
+
displayName: 'Gadmin Test',
|
|
140
|
+
dbUrl: 'postgresql://user:pass@gadmin-db:5432/gadmin_demo',
|
|
141
|
+
runtimeBaseUrl: 'http://gadmin-server:8000/api',
|
|
142
|
+
});
|
|
143
|
+
expect(registrationBody.runtimeToken).toMatch(/^[0-9a-f]{64}$/);
|
|
144
|
+
expect(registrationBody.runtimeToken).not.toBe(
|
|
145
|
+
'caller-controlled-runtime-token-value',
|
|
146
|
+
);
|
|
142
147
|
expect(mockTemporalTenantConfigService.createOne).toHaveBeenCalledWith({
|
|
143
148
|
data: expect.objectContaining({
|
|
144
149
|
runtimeBaseUrl: 'http://gadmin-server:8000/api',
|
|
145
|
-
runtimeToken:
|
|
150
|
+
runtimeToken: registrationBody.runtimeToken,
|
|
146
151
|
}),
|
|
147
152
|
});
|
|
148
153
|
});
|
|
149
154
|
|
|
150
|
-
it('updates the tenant runtime connection
|
|
155
|
+
it('updates the tenant runtime connection without accepting a replacement token', async () => {
|
|
151
156
|
global.fetch = jest.fn().mockResolvedValue({
|
|
152
157
|
ok: true,
|
|
153
158
|
});
|
|
@@ -175,9 +180,12 @@ describe('TemporalTenantConfigController', () => {
|
|
|
175
180
|
method: 'PATCH',
|
|
176
181
|
body: JSON.stringify({
|
|
177
182
|
runtimeBaseUrl: 'http://gadmin-server:8000/api',
|
|
178
|
-
runtimeToken: '0123456789abcdef0123456789abcdef',
|
|
179
183
|
}),
|
|
180
184
|
}),
|
|
181
185
|
);
|
|
186
|
+
expect(mockTemporalTenantConfigService.updateUnique).toHaveBeenCalledWith(
|
|
187
|
+
1,
|
|
188
|
+
expect.not.objectContaining({ runtimeToken: expect.anything() }),
|
|
189
|
+
);
|
|
182
190
|
});
|
|
183
191
|
});
|