@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
|
@@ -175,6 +175,17 @@ export class UserController {
|
|
|
175
175
|
return res;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
@Delete('deletePerson/:id')
|
|
179
|
+
@UseRoles({
|
|
180
|
+
resource: 'user',
|
|
181
|
+
action: 'delete',
|
|
182
|
+
possession: 'any',
|
|
183
|
+
})
|
|
184
|
+
@ApiOkResponse({ type: EffectedCount })
|
|
185
|
+
async deletePerson(@Param('id', ParseIntPipe) id: number) {
|
|
186
|
+
return this.userService.deletePerson(id);
|
|
187
|
+
}
|
|
188
|
+
|
|
178
189
|
@Post('upload')
|
|
179
190
|
@UseRoles({
|
|
180
191
|
resource: 'user',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/// <reference types="jest" />
|
|
2
|
+
import { ConflictException } from '@nestjs/common';
|
|
2
3
|
import { ConfigService } from '@nestjs/config';
|
|
3
4
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
5
|
+
import { Prisma } from '@prisma/client';
|
|
4
6
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
|
5
7
|
|
|
6
8
|
// Mock nestjs-prisma 模块
|
|
@@ -26,6 +28,12 @@ const mockPrismaService = {
|
|
|
26
28
|
groupBy: jest.fn(),
|
|
27
29
|
aggregate: jest.fn(),
|
|
28
30
|
},
|
|
31
|
+
workflow: {
|
|
32
|
+
findMany: jest.fn(),
|
|
33
|
+
},
|
|
34
|
+
approvalCandidate: {
|
|
35
|
+
count: jest.fn(),
|
|
36
|
+
},
|
|
29
37
|
};
|
|
30
38
|
|
|
31
39
|
// Mock Logger
|
|
@@ -103,6 +111,28 @@ describe('UserService', () => {
|
|
|
103
111
|
'Database connection failed',
|
|
104
112
|
);
|
|
105
113
|
});
|
|
114
|
+
|
|
115
|
+
it('returns a useful conflict when userid already exists', async () => {
|
|
116
|
+
prisma.user.create.mockRejectedValue(
|
|
117
|
+
new Prisma.PrismaClientKnownRequestError('duplicate userid', {
|
|
118
|
+
code: 'P2002',
|
|
119
|
+
clientVersion: '6.19.3',
|
|
120
|
+
meta: { target: ['userid'] },
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await expect(
|
|
125
|
+
service.createOne({
|
|
126
|
+
data: { userid: 'existing-user', roles: '[]' },
|
|
127
|
+
} as any),
|
|
128
|
+
).rejects.toMatchObject({
|
|
129
|
+
constructor: ConflictException,
|
|
130
|
+
response: {
|
|
131
|
+
code: 'USER_ALREADY_EXISTS',
|
|
132
|
+
message: '该人员已存在,无需重复添加',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
});
|
|
106
136
|
});
|
|
107
137
|
|
|
108
138
|
describe('createMany', () => {
|
|
@@ -133,6 +163,73 @@ describe('UserService', () => {
|
|
|
133
163
|
});
|
|
134
164
|
});
|
|
135
165
|
|
|
166
|
+
describe('deletePerson', () => {
|
|
167
|
+
it('deletes an unreferenced person without roles', async () => {
|
|
168
|
+
prisma.user.findUnique.mockResolvedValue({
|
|
169
|
+
userid: 'person-a',
|
|
170
|
+
roles: '[]',
|
|
171
|
+
groupMemberships: [],
|
|
172
|
+
});
|
|
173
|
+
prisma.workflow.findMany.mockResolvedValue([]);
|
|
174
|
+
prisma.approvalCandidate.count.mockResolvedValue(0);
|
|
175
|
+
prisma.user.delete.mockResolvedValue({ id: 10 });
|
|
176
|
+
|
|
177
|
+
await expect(service.deletePerson(10)).resolves.toEqual({ count: 1 });
|
|
178
|
+
expect(prisma.user.delete).toHaveBeenCalledWith({ where: { id: 10 } });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('rejects deleting a person that still has a role', async () => {
|
|
182
|
+
prisma.user.findUnique.mockResolvedValue({
|
|
183
|
+
userid: 'admin-a',
|
|
184
|
+
roles: '["SYSTEM_ADMIN"]',
|
|
185
|
+
groupMemberships: [],
|
|
186
|
+
});
|
|
187
|
+
prisma.workflow.findMany.mockResolvedValue([]);
|
|
188
|
+
prisma.approvalCandidate.count.mockResolvedValue(0);
|
|
189
|
+
|
|
190
|
+
await expect(service.deletePerson(10)).rejects.toMatchObject({
|
|
191
|
+
response: {
|
|
192
|
+
code: 'PERSON_IN_USE',
|
|
193
|
+
roles: ['SYSTEM_ADMIN'],
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
expect(prisma.user.delete).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('rejects deleting a person referenced by a Workflow', async () => {
|
|
200
|
+
prisma.user.findUnique.mockResolvedValue({
|
|
201
|
+
userid: 'approver-a',
|
|
202
|
+
roles: '[]',
|
|
203
|
+
groupMemberships: [],
|
|
204
|
+
});
|
|
205
|
+
prisma.workflow.findMany.mockResolvedValue([
|
|
206
|
+
{
|
|
207
|
+
id: BigInt(7),
|
|
208
|
+
name: '资产审批',
|
|
209
|
+
dsl: {
|
|
210
|
+
nodes: [
|
|
211
|
+
{
|
|
212
|
+
config: {
|
|
213
|
+
subjects: [{ source: 'STATIC', refs: ['approver-a'] }],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
versions: [],
|
|
219
|
+
},
|
|
220
|
+
]);
|
|
221
|
+
prisma.approvalCandidate.count.mockResolvedValue(0);
|
|
222
|
+
|
|
223
|
+
await expect(service.deletePerson(10)).rejects.toMatchObject({
|
|
224
|
+
response: {
|
|
225
|
+
code: 'PERSON_IN_USE',
|
|
226
|
+
workflows: [{ id: '7', name: '资产审批' }],
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
expect(prisma.user.delete).not.toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
136
233
|
describe('findMany', () => {
|
|
137
234
|
it('should return entities with itemCount', async () => {
|
|
138
235
|
const findArgs = {
|
package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ConflictException,
|
|
3
|
+
Inject,
|
|
4
|
+
Injectable,
|
|
5
|
+
NotFoundException,
|
|
6
|
+
} from '@nestjs/common';
|
|
2
7
|
import { Prisma, User } from '@prisma/client';
|
|
3
8
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
|
4
9
|
import { PrismaService } from 'nestjs-prisma';
|
|
@@ -20,7 +25,21 @@ export class UserService {
|
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
async createOne(userCreateArgs: Prisma.UserCreateArgs) {
|
|
23
|
-
|
|
28
|
+
try {
|
|
29
|
+
return await this.prisma.user.create(userCreateArgs);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (
|
|
32
|
+
error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
33
|
+
error.code === 'P2002' &&
|
|
34
|
+
prismaErrorTargets(error).some((target) => target.includes('userid'))
|
|
35
|
+
) {
|
|
36
|
+
throw new ConflictException({
|
|
37
|
+
code: 'USER_ALREADY_EXISTS',
|
|
38
|
+
message: '该人员已存在,无需重复添加',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
24
43
|
}
|
|
25
44
|
|
|
26
45
|
async createMany(data: Prisma.UserCreateManyArgs['data']) {
|
|
@@ -69,6 +88,94 @@ export class UserService {
|
|
|
69
88
|
return { count: 1 };
|
|
70
89
|
}
|
|
71
90
|
|
|
91
|
+
async deletePerson(id: number) {
|
|
92
|
+
const person = await this.prisma.user.findUnique({
|
|
93
|
+
where: { id },
|
|
94
|
+
select: {
|
|
95
|
+
userid: true,
|
|
96
|
+
roles: true,
|
|
97
|
+
groupMemberships: {
|
|
98
|
+
select: { group: { select: { code: true, name: true } } },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
if (!person) throw new NotFoundException('人员不存在');
|
|
103
|
+
|
|
104
|
+
const [workflows, activeApprovalCount] = await Promise.all([
|
|
105
|
+
this.prisma.workflow.findMany({
|
|
106
|
+
select: {
|
|
107
|
+
id: true,
|
|
108
|
+
name: true,
|
|
109
|
+
dsl: true,
|
|
110
|
+
versions: { select: { dsl: true } },
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
this.prisma.approvalCandidate.count({
|
|
114
|
+
where: {
|
|
115
|
+
principalType: 'USER',
|
|
116
|
+
principalRef: person.userid,
|
|
117
|
+
invalidatedAt: null,
|
|
118
|
+
request: { status: { in: ['PENDING', 'BLOCKED'] } },
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
]);
|
|
122
|
+
const roles = assignedRoles(person.roles);
|
|
123
|
+
const referencedWorkflows = workflows
|
|
124
|
+
.filter(
|
|
125
|
+
(workflow) =>
|
|
126
|
+
workflowDslReferencesUser(workflow.dsl, person.userid) ||
|
|
127
|
+
workflow.versions.some((version) =>
|
|
128
|
+
workflowDslReferencesUser(version.dsl, person.userid),
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
.map((workflow) => ({ id: String(workflow.id), name: workflow.name }));
|
|
132
|
+
const groups = person.groupMemberships.map(({ group }) => group);
|
|
133
|
+
const reasons = [
|
|
134
|
+
...(roles.length ? [`已授予角色:${roles.join('、')}`] : []),
|
|
135
|
+
...(groups.length
|
|
136
|
+
? [`已加入人员组:${groups.map((group) => group.name).join('、')}`]
|
|
137
|
+
: []),
|
|
138
|
+
...(referencedWorkflows.length
|
|
139
|
+
? [
|
|
140
|
+
`被 Workflow 引用:${referencedWorkflows
|
|
141
|
+
.map((workflow) => workflow.name)
|
|
142
|
+
.join('、')}`,
|
|
143
|
+
]
|
|
144
|
+
: []),
|
|
145
|
+
...(activeApprovalCount
|
|
146
|
+
? [`仍有 ${activeApprovalCount} 个待处理审批`]
|
|
147
|
+
: []),
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
if (reasons.length) {
|
|
151
|
+
throw new ConflictException({
|
|
152
|
+
code: 'PERSON_IN_USE',
|
|
153
|
+
message: `无法删除该人员:${reasons.join(';')}`,
|
|
154
|
+
reasons,
|
|
155
|
+
roles,
|
|
156
|
+
groups,
|
|
157
|
+
workflows: referencedWorkflows,
|
|
158
|
+
activeApprovalCount,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await this.prisma.user.delete({ where: { id } });
|
|
164
|
+
return { count: 1 };
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (
|
|
167
|
+
error instanceof Prisma.PrismaClientKnownRequestError &&
|
|
168
|
+
error.code === 'P2003'
|
|
169
|
+
) {
|
|
170
|
+
throw new ConflictException({
|
|
171
|
+
code: 'PERSON_IN_USE',
|
|
172
|
+
message: '该人员刚被其他数据引用,请刷新后重试',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
72
179
|
async count(userCountArgs?: Prisma.UserCountArgs) {
|
|
73
180
|
return await this.prisma.user.count(userCountArgs);
|
|
74
181
|
}
|
|
@@ -102,3 +209,44 @@ export class UserService {
|
|
|
102
209
|
return key.substring(0, 8) + '****';
|
|
103
210
|
}
|
|
104
211
|
}
|
|
212
|
+
|
|
213
|
+
function prismaErrorTargets(
|
|
214
|
+
error: Prisma.PrismaClientKnownRequestError,
|
|
215
|
+
): string[] {
|
|
216
|
+
const target = error.meta?.target;
|
|
217
|
+
if (Array.isArray(target)) return target.map(String);
|
|
218
|
+
return target == null ? [] : [String(target)];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function assignedRoles(value: string): string[] {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = JSON.parse(value);
|
|
224
|
+
return Array.isArray(parsed)
|
|
225
|
+
? parsed.map(String).filter((role) => role.trim())
|
|
226
|
+
: value.trim()
|
|
227
|
+
? [value]
|
|
228
|
+
: [];
|
|
229
|
+
} catch {
|
|
230
|
+
return value.trim() ? [value] : [];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function workflowDslReferencesUser(
|
|
235
|
+
value: Prisma.JsonValue | null,
|
|
236
|
+
userid: string,
|
|
237
|
+
): boolean {
|
|
238
|
+
if (Array.isArray(value))
|
|
239
|
+
return value.some((item) => workflowDslReferencesUser(item, userid));
|
|
240
|
+
if (!value || typeof value !== 'object') return false;
|
|
241
|
+
|
|
242
|
+
const record = value as Record<string, Prisma.JsonValue>;
|
|
243
|
+
const refs = Array.isArray(record.refs) ? record.refs.map(String) : [];
|
|
244
|
+
if (
|
|
245
|
+
refs.includes(userid) &&
|
|
246
|
+
(record.source === 'STATIC' || record.provider === 'USER')
|
|
247
|
+
)
|
|
248
|
+
return true;
|
|
249
|
+
return Object.values(record).some((item) =>
|
|
250
|
+
workflowDslReferencesUser(item, userid),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -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
|
Param,
|
|
7
8
|
Patch,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from '../subjectProvider/actor-context.util';
|
|
20
21
|
import {
|
|
21
22
|
CreateUserGroupDto,
|
|
23
|
+
DeleteUserGroupDto,
|
|
22
24
|
ReplaceUserGroupMembersDto,
|
|
23
25
|
SetUserGroupActiveDto,
|
|
24
26
|
UpdateUserGroupDto,
|
|
@@ -35,7 +37,7 @@ export class UserGroupController {
|
|
|
35
37
|
@Get()
|
|
36
38
|
@UseRoles({ resource: 'userGroup:read', action: 'read', possession: 'any' })
|
|
37
39
|
search(@Query() query: UserGroupSearchDto) {
|
|
38
|
-
return this.service.search(query.q ?? '');
|
|
40
|
+
return this.service.search(query.q ?? '', query.includeInactive ?? false);
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
@Get(':code/preview')
|
|
@@ -116,4 +118,14 @@ export class UserGroupController {
|
|
|
116
118
|
actorContextFromRequest(request),
|
|
117
119
|
);
|
|
118
120
|
}
|
|
121
|
+
|
|
122
|
+
@Delete(':code')
|
|
123
|
+
@UseRoles({
|
|
124
|
+
resource: 'userGroup:create',
|
|
125
|
+
action: 'delete',
|
|
126
|
+
possession: 'any',
|
|
127
|
+
})
|
|
128
|
+
remove(@Param('code') code: string, @Query() query: DeleteUserGroupDto) {
|
|
129
|
+
return this.service.deleteGroup(code, query.expectedRevision);
|
|
130
|
+
}
|
|
119
131
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Type } from 'class-transformer';
|
|
1
|
+
import { Transform, Type } from 'class-transformer';
|
|
2
2
|
import {
|
|
3
3
|
ArrayUnique,
|
|
4
4
|
IsArray,
|
|
@@ -84,8 +84,20 @@ export class SetUserGroupActiveDto {
|
|
|
84
84
|
isActive!: boolean;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
export class DeleteUserGroupDto {
|
|
88
|
+
@Type(() => Number)
|
|
89
|
+
@IsInt()
|
|
90
|
+
@Min(1)
|
|
91
|
+
expectedRevision!: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
export class UserGroupSearchDto {
|
|
88
95
|
@IsOptional()
|
|
89
96
|
@IsString()
|
|
90
97
|
q?: string;
|
|
98
|
+
|
|
99
|
+
@IsOptional()
|
|
100
|
+
@Transform(({ value }) => value === true || value === 'true')
|
|
101
|
+
@IsBoolean()
|
|
102
|
+
includeInactive?: boolean;
|
|
91
103
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Prisma } from '@prisma/client';
|
|
1
2
|
import { UserGroupService } from './user-group.service';
|
|
2
3
|
|
|
3
4
|
const group = {
|
|
@@ -20,6 +21,7 @@ function createHarness() {
|
|
|
20
21
|
findMany: jest.fn(),
|
|
21
22
|
create: jest.fn(),
|
|
22
23
|
updateMany: jest.fn().mockResolvedValue({ count: 1 }),
|
|
24
|
+
delete: jest.fn().mockResolvedValue(group),
|
|
23
25
|
},
|
|
24
26
|
user: { findMany: jest.fn() },
|
|
25
27
|
userGroupUser: {
|
|
@@ -32,6 +34,8 @@ function createHarness() {
|
|
|
32
34
|
deleteMany: jest.fn().mockResolvedValue({ count: 0 }),
|
|
33
35
|
createMany: jest.fn().mockResolvedValue({ count: 1 }),
|
|
34
36
|
},
|
|
37
|
+
workflow: { findMany: jest.fn().mockResolvedValue([]) },
|
|
38
|
+
approvalRequestSubject: { count: jest.fn().mockResolvedValue(0) },
|
|
35
39
|
};
|
|
36
40
|
const prisma = {
|
|
37
41
|
...tx,
|
|
@@ -44,6 +48,19 @@ function createHarness() {
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
describe('UserGroupService', () => {
|
|
51
|
+
it('can include inactive groups for the management page', async () => {
|
|
52
|
+
const { service, tx } = createHarness();
|
|
53
|
+
tx.userGroup.findMany.mockResolvedValue([]);
|
|
54
|
+
|
|
55
|
+
await service.search('finance', true);
|
|
56
|
+
|
|
57
|
+
expect(tx.userGroup.findMany).toHaveBeenCalledWith(
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
where: expect.not.objectContaining({ isActive: true }),
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
47
64
|
it('returns direct users and child groups for editing', async () => {
|
|
48
65
|
const { service, tx } = createHarness();
|
|
49
66
|
tx.userGroup.findUnique
|
|
@@ -148,6 +165,31 @@ describe('UserGroupService', () => {
|
|
|
148
165
|
});
|
|
149
166
|
});
|
|
150
167
|
|
|
168
|
+
it('only accepts active non-API users as direct members', async () => {
|
|
169
|
+
const { service, tx } = createHarness();
|
|
170
|
+
tx.user.findMany.mockResolvedValue([{ userid: 'alice' }]);
|
|
171
|
+
tx.userGroup.findMany.mockResolvedValueOnce([]).mockResolvedValueOnce([]);
|
|
172
|
+
|
|
173
|
+
await service.replaceMembers(
|
|
174
|
+
'finance',
|
|
175
|
+
{
|
|
176
|
+
expectedRevision: 4,
|
|
177
|
+
userIds: ['alice'],
|
|
178
|
+
childGroupCodes: [],
|
|
179
|
+
},
|
|
180
|
+
actor,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(tx.user.findMany).toHaveBeenCalledWith({
|
|
184
|
+
where: {
|
|
185
|
+
userid: { in: ['alice'] },
|
|
186
|
+
isActive: true,
|
|
187
|
+
isApiKey: false,
|
|
188
|
+
},
|
|
189
|
+
select: { userid: true },
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
151
193
|
it('returns 409 when expectedRevision is stale', async () => {
|
|
152
194
|
const { service } = createHarness();
|
|
153
195
|
await expect(
|
|
@@ -158,4 +200,87 @@ describe('UserGroupService', () => {
|
|
|
158
200
|
),
|
|
159
201
|
).rejects.toMatchObject({ status: 409 });
|
|
160
202
|
});
|
|
203
|
+
|
|
204
|
+
it('deletes an unreferenced group and its owned memberships', async () => {
|
|
205
|
+
const { service, tx } = createHarness();
|
|
206
|
+
tx.userGroupChild.findMany.mockResolvedValue([]);
|
|
207
|
+
|
|
208
|
+
await expect(service.deleteGroup('finance', 4)).resolves.toEqual({
|
|
209
|
+
count: 1,
|
|
210
|
+
});
|
|
211
|
+
expect(tx.userGroupUser.deleteMany).toHaveBeenCalledWith({
|
|
212
|
+
where: { groupId: group.id },
|
|
213
|
+
});
|
|
214
|
+
expect(tx.userGroupChild.deleteMany).toHaveBeenCalledWith({
|
|
215
|
+
where: { parentGroupId: group.id },
|
|
216
|
+
});
|
|
217
|
+
expect(tx.userGroup.delete).toHaveBeenCalledWith({
|
|
218
|
+
where: { id: group.id },
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('rejects deleting a group referenced by another group', async () => {
|
|
223
|
+
const { service, tx } = createHarness();
|
|
224
|
+
tx.userGroupChild.findMany.mockResolvedValue([
|
|
225
|
+
{ parentGroup: { code: 'all-finance', name: 'All Finance' } },
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
await expect(service.deleteGroup('finance', 4)).rejects.toMatchObject({
|
|
229
|
+
status: 409,
|
|
230
|
+
response: {
|
|
231
|
+
code: 'USER_GROUP_IN_USE',
|
|
232
|
+
message: expect.stringContaining('All Finance'),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
expect(tx.userGroup.delete).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('rejects deleting a group referenced by a Workflow', async () => {
|
|
239
|
+
const { service, tx } = createHarness();
|
|
240
|
+
tx.userGroupChild.findMany.mockResolvedValue([]);
|
|
241
|
+
tx.workflow.findMany.mockResolvedValue([
|
|
242
|
+
{
|
|
243
|
+
id: BigInt(9),
|
|
244
|
+
name: 'Finance Approval',
|
|
245
|
+
dsl: {
|
|
246
|
+
nodes: [
|
|
247
|
+
{
|
|
248
|
+
config: {
|
|
249
|
+
subjects: [{ provider: 'USER_GROUP', refs: ['finance'] }],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
versions: [],
|
|
255
|
+
},
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
await expect(service.deleteGroup('finance', 4)).rejects.toMatchObject({
|
|
259
|
+
status: 409,
|
|
260
|
+
response: {
|
|
261
|
+
code: 'USER_GROUP_IN_USE',
|
|
262
|
+
message: expect.stringContaining('Finance Approval'),
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
expect(tx.userGroup.delete).not.toHaveBeenCalled();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('returns 409 when a concurrent reference makes the database reject deletion', async () => {
|
|
269
|
+
const { service, tx } = createHarness();
|
|
270
|
+
tx.userGroupChild.findMany.mockResolvedValue([]);
|
|
271
|
+
tx.userGroup.delete.mockRejectedValue(
|
|
272
|
+
new Prisma.PrismaClientKnownRequestError('foreign key conflict', {
|
|
273
|
+
code: 'P2003',
|
|
274
|
+
clientVersion: '6.19.3',
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
await expect(service.deleteGroup('finance', 4)).rejects.toMatchObject({
|
|
279
|
+
status: 409,
|
|
280
|
+
response: {
|
|
281
|
+
code: 'USER_GROUP_IN_USE',
|
|
282
|
+
message: expect.stringContaining('刷新后重试'),
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
});
|
|
161
286
|
});
|