@gadmin2n/schematics 0.0.147 → 0.0.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +67 -15
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +83 -53
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.spec.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/lib/structured-all-exceptions.filter.ts +15 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +31 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/codeRuntime/code-runtime.controller.spec.ts +2 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.spec.ts +28 -29
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/approval-subject-source.util.ts +16 -34
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.dto.ts +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/human-approval.service.spec.ts +61 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/humanApproval/runtime/human-approval-runtime.controller.spec.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.controller.ts +11 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.spec.ts +126 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/integrationConnection/integration-connection.service.ts +136 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.controller.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.dto.ts +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.spec.ts +62 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.service.ts +94 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staff-directory.types.ts +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/staffDirectory/staffDirectory.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user.provider.ts +6 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +20 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +21 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +11 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +97 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.ts +150 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.controller.ts +13 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.dto.ts +13 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.errors.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.spec.ts +125 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/userGroup/user-group.service.ts +118 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +20 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +6 -9
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +166 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +82 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-runtime.facade.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +5 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +7 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +44 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +106 -37
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +59 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.spec.ts +33 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/subject-config-validator.service.ts +10 -23
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.test.tsx +69 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/StaffSelect.tsx +145 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/index.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/StaffSelect/staff-directory.api.ts +16 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/index.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +20 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +60 -14
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +60 -14
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +41 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/PeopleGroupModal.tsx +181 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +203 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/people-groups.api.ts +145 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/PersonModal.tsx +112 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +214 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/person-directory.api.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/components/form-modal.tsx +7 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +7 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.test.tsx +46 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/RegisterBusinessEventModal.tsx +14 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.test.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.test.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeOutputSummary.test.tsx +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +97 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +87 -119
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.test.tsx +22 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/WorkflowCodeEditor.tsx +8 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connections.css +8 -46
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/connectionsApi.ts +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.test.tsx +32 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +163 -106
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +8 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +26 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-config.css +13 -16
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +185 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +224 -105
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/CodeConfigEditor.test.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +21 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.test.tsx +140 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +124 -62
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.test.tsx +160 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/HumanApprovalConfigEditor.tsx +125 -55
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +78 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.test.tsx +33 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SubjectPicker.tsx +28 -46
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/event-catalog.api.ts +4 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/human-approval-config.css +16 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/subject-provider.api.ts +0 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.test.tsx +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +14 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CreateNodeInstanceDrawer.tsx +0 -162
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/UserGroupCreateModal.tsx +0 -72
|
@@ -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
|
});
|
|
@@ -50,6 +50,7 @@ import { TemporalTenantConfigService } from './temporalTenantConfig.service';
|
|
|
50
50
|
|
|
51
51
|
import { ConfigService } from '@nestjs/config';
|
|
52
52
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
53
|
+
import { randomBytes } from 'crypto';
|
|
53
54
|
|
|
54
55
|
@UseGuards(ACGuard)
|
|
55
56
|
@Controller('temporalTenantConfig')
|
|
@@ -69,7 +70,6 @@ export class TemporalTenantConfigController {
|
|
|
69
70
|
possession: 'any',
|
|
70
71
|
})
|
|
71
72
|
async testConnection(@Body() body: CreateTemporalTenantConfigDto) {
|
|
72
|
-
assertRuntimeToken(body.runtimeToken);
|
|
73
73
|
let response: Response;
|
|
74
74
|
const adminUrl = body.adminUrl.replace(/\/+$/, '');
|
|
75
75
|
|
|
@@ -117,8 +117,12 @@ export class TemporalTenantConfigController {
|
|
|
117
117
|
@ACAttrFilter() acAttrFilter,
|
|
118
118
|
@Req() request: Request & { user: UserType },
|
|
119
119
|
) {
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
const runtimeToken = randomBytes(32).toString('hex');
|
|
121
|
+
const registration = {
|
|
122
|
+
...body,
|
|
123
|
+
creator: request.user.username,
|
|
124
|
+
runtimeToken,
|
|
125
|
+
};
|
|
122
126
|
|
|
123
127
|
// 先调 shared temporal admin API(原子:失败则不写 DB)
|
|
124
128
|
const res = await fetch(`${body.adminUrl}/admin/tenants`, {
|
|
@@ -132,7 +136,7 @@ export class TemporalTenantConfigController {
|
|
|
132
136
|
displayName: body.displayName,
|
|
133
137
|
dbUrl: body.dbUrl,
|
|
134
138
|
runtimeBaseUrl: body.runtimeBaseUrl,
|
|
135
|
-
runtimeToken
|
|
139
|
+
runtimeToken,
|
|
136
140
|
}),
|
|
137
141
|
});
|
|
138
142
|
if (!res.ok) {
|
|
@@ -144,7 +148,7 @@ export class TemporalTenantConfigController {
|
|
|
144
148
|
|
|
145
149
|
return new TemporalTenantConfigEntity(
|
|
146
150
|
await this.temporalTenantConfigService.createOne({
|
|
147
|
-
data: acAttrFilter(
|
|
151
|
+
data: acAttrFilter(registration),
|
|
148
152
|
}),
|
|
149
153
|
);
|
|
150
154
|
}
|
|
@@ -210,7 +214,8 @@ export class TemporalTenantConfigController {
|
|
|
210
214
|
@Body() body: UpdateTemporalTenantConfigDto,
|
|
211
215
|
@ACAttrFilter() acAttrFilter,
|
|
212
216
|
) {
|
|
213
|
-
|
|
217
|
+
const editableBody = { ...body };
|
|
218
|
+
delete editableBody.runtimeToken;
|
|
214
219
|
const existing = await this.temporalTenantConfigService.findUnique(id, {
|
|
215
220
|
adminUrl: true,
|
|
216
221
|
adminToken: true,
|
|
@@ -218,15 +223,17 @@ export class TemporalTenantConfigController {
|
|
|
218
223
|
});
|
|
219
224
|
if (!existing) throw new NotFoundException();
|
|
220
225
|
|
|
221
|
-
const adminUrl = (
|
|
222
|
-
const adminToken = (
|
|
223
|
-
|
|
226
|
+
const adminUrl = (editableBody.adminUrl ?? existing.adminUrl) as string;
|
|
227
|
+
const adminToken = (editableBody.adminToken ??
|
|
228
|
+
existing.adminToken) as string;
|
|
229
|
+
const namespace = (editableBody.namespace ?? existing.namespace) as string;
|
|
224
230
|
|
|
225
231
|
const patchBody: Record<string, string> = {};
|
|
226
|
-
if (
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
if (
|
|
232
|
+
if (editableBody.dbUrl) patchBody.dbUrl = editableBody.dbUrl;
|
|
233
|
+
if (editableBody.displayName)
|
|
234
|
+
patchBody.displayName = editableBody.displayName;
|
|
235
|
+
if (editableBody.runtimeBaseUrl)
|
|
236
|
+
patchBody.runtimeBaseUrl = editableBody.runtimeBaseUrl;
|
|
230
237
|
|
|
231
238
|
if (Object.keys(patchBody).length > 0) {
|
|
232
239
|
const res = await fetch(
|
|
@@ -250,7 +257,7 @@ export class TemporalTenantConfigController {
|
|
|
250
257
|
|
|
251
258
|
const item = await this.temporalTenantConfigService.updateUnique(
|
|
252
259
|
id,
|
|
253
|
-
acAttrFilter(
|
|
260
|
+
acAttrFilter(editableBody),
|
|
254
261
|
);
|
|
255
262
|
return new TemporalTenantConfigEntity(acAttrFilter(item));
|
|
256
263
|
}
|
|
@@ -295,8 +302,3 @@ export class TemporalTenantConfigController {
|
|
|
295
302
|
};
|
|
296
303
|
}
|
|
297
304
|
}
|
|
298
|
-
|
|
299
|
-
function assertRuntimeToken(value: string | undefined): void {
|
|
300
|
-
if (!value || value.length < 32)
|
|
301
|
-
throw new BadRequestException('Runtime Token 至少需要 32 个字符');
|
|
302
|
-
}
|
|
@@ -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
|
}
|