@gadmin2n/schematics 0.0.87 → 0.0.89
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/.dockerignore +16 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.codegen +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +76 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.web +53 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/Jenkinsfile +219 -33
- package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +250 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/dev/postgres/init.sql +12 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +170 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +254 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +8 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/lib/page-helpers.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/prismaModels.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/agenda.seed.ts +39 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/audit.seed.ts +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/bootstrap.ts +56 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/canvas.seed.ts +39 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/{scripts/sync-data-mngt-pages.ts → seed/data-mngt.seed.ts} +36 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/game.seed.ts +44 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +30 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +130 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-event-trigger.ts +60 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +11 -25
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +108 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/main.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agendaJob/agendaJob.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.service.spec.ts +41 -57
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.service.spec.ts +309 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.service.spec.ts +315 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.service.spec.ts +312 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.service.spec.ts +317 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.service.spec.ts +309 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.service.spec.ts +299 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.service.spec.ts +307 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +309 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.spec.ts +205 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.ts +116 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.spec.ts +158 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +110 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/webhook-signature.util.spec.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/webhook-signature.util.ts +54 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +34 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +457 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +241 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.controller.spec.ts +34 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.service.spec.ts +24 -30
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.controller.spec.ts +34 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.service.spec.ts +36 -36
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.controller.spec.ts +34 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +48 -24
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +312 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +152 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +12 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +270 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +23 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +70 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +246 -90
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +96 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/nginx.conf +74 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/ElementInspector.tsx +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/promptGenerator.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/form.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +9 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +156 -139
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +14 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +62 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/PublishModal.tsx +4 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +18 -27
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasDefaults.ts +32 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/demos.ts +48 -61
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas-page/index.tsx +3 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +16 -16
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +28 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/antd.css +6 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile +0 -63
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/sync-resources.ts +0 -100
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permissions.ts +0 -302
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.spec.ts +0 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/sql/create-event-trigger.sql +0 -87
- /package/dist/lib/application/files/gadmin2-game-angle-demo/{GRACEFUL-DEPLOYMENT.md → server/GRACEFUL-DEPLOYMENT.md} +0 -0
|
@@ -1,18 +1,326 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
1
3
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
4
|
+
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
|
5
|
+
|
|
6
|
+
// Mock nestjs-prisma 模块
|
|
7
|
+
jest.mock('nestjs-prisma', () => ({
|
|
8
|
+
PrismaService: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { PrismaService } from 'nestjs-prisma';
|
|
2
12
|
import { UserService } from './user.service';
|
|
3
13
|
|
|
14
|
+
// Mock PrismaService
|
|
15
|
+
const mockPrismaService = {
|
|
16
|
+
user: {
|
|
17
|
+
create: jest.fn(),
|
|
18
|
+
createMany: jest.fn(),
|
|
19
|
+
findMany: jest.fn(),
|
|
20
|
+
findUnique: jest.fn(),
|
|
21
|
+
update: jest.fn(),
|
|
22
|
+
updateMany: jest.fn(),
|
|
23
|
+
delete: jest.fn(),
|
|
24
|
+
deleteMany: jest.fn(),
|
|
25
|
+
count: jest.fn(),
|
|
26
|
+
groupBy: jest.fn(),
|
|
27
|
+
aggregate: jest.fn(),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Mock Logger
|
|
32
|
+
const mockLogger = {
|
|
33
|
+
child: jest.fn().mockReturnThis(),
|
|
34
|
+
info: jest.fn(),
|
|
35
|
+
error: jest.fn(),
|
|
36
|
+
warn: jest.fn(),
|
|
37
|
+
debug: jest.fn(),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Mock ConfigService
|
|
41
|
+
const mockConfigService = {
|
|
42
|
+
get: jest.fn(),
|
|
43
|
+
};
|
|
44
|
+
|
|
4
45
|
describe('UserService', () => {
|
|
5
46
|
let service: UserService;
|
|
47
|
+
let prisma: typeof mockPrismaService;
|
|
6
48
|
|
|
7
49
|
beforeEach(async () => {
|
|
50
|
+
// 每个测试前重置所有 mock
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
|
|
8
53
|
const module: TestingModule = await Test.createTestingModule({
|
|
9
|
-
providers: [
|
|
54
|
+
providers: [
|
|
55
|
+
UserService,
|
|
56
|
+
{ provide: PrismaService, useValue: mockPrismaService },
|
|
57
|
+
{ provide: ConfigService, useValue: mockConfigService },
|
|
58
|
+
{ provide: WINSTON_MODULE_PROVIDER, useValue: mockLogger },
|
|
59
|
+
],
|
|
10
60
|
}).compile();
|
|
11
61
|
|
|
12
62
|
service = module.get<UserService>(UserService);
|
|
63
|
+
prisma = mockPrismaService;
|
|
13
64
|
});
|
|
14
65
|
|
|
15
66
|
it('should be defined', () => {
|
|
16
67
|
expect(service).toBeDefined();
|
|
17
68
|
});
|
|
69
|
+
|
|
70
|
+
describe('createOne', () => {
|
|
71
|
+
it('should create a single user record', async () => {
|
|
72
|
+
const createArgs = {
|
|
73
|
+
data: {
|
|
74
|
+
userid: 'test_userid',
|
|
75
|
+
username: 'test_username',
|
|
76
|
+
roles: '[]',
|
|
77
|
+
},
|
|
78
|
+
} as any;
|
|
79
|
+
const expectedResult = { id: 1, ...createArgs.data };
|
|
80
|
+
|
|
81
|
+
prisma.user.create.mockResolvedValue(expectedResult);
|
|
82
|
+
|
|
83
|
+
const result = await service.createOne(createArgs);
|
|
84
|
+
|
|
85
|
+
expect(prisma.user.create).toHaveBeenCalledWith(createArgs);
|
|
86
|
+
expect(prisma.user.create).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(result).toEqual(expectedResult);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should throw error when create fails', async () => {
|
|
91
|
+
const createArgs = {
|
|
92
|
+
data: {
|
|
93
|
+
userid: 'test_userid',
|
|
94
|
+
username: 'test_username',
|
|
95
|
+
roles: '[]',
|
|
96
|
+
},
|
|
97
|
+
} as any;
|
|
98
|
+
const error = new Error('Database connection failed');
|
|
99
|
+
|
|
100
|
+
prisma.user.create.mockRejectedValue(error);
|
|
101
|
+
|
|
102
|
+
await expect(service.createOne(createArgs)).rejects.toThrow(
|
|
103
|
+
'Database connection failed',
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('createMany', () => {
|
|
109
|
+
it('should create multiple user records with skipDuplicates', async () => {
|
|
110
|
+
const data = [
|
|
111
|
+
{
|
|
112
|
+
userid: 'test_userid',
|
|
113
|
+
username: 'test_username',
|
|
114
|
+
roles: '[]',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
userid: 'test_userid',
|
|
118
|
+
username: 'test_username',
|
|
119
|
+
roles: '[]',
|
|
120
|
+
},
|
|
121
|
+
] as any;
|
|
122
|
+
const expectedResult = { count: 2 };
|
|
123
|
+
|
|
124
|
+
prisma.user.createMany.mockResolvedValue(expectedResult);
|
|
125
|
+
|
|
126
|
+
const result = await service.createMany(data);
|
|
127
|
+
|
|
128
|
+
expect(prisma.user.createMany).toHaveBeenCalledWith({
|
|
129
|
+
data,
|
|
130
|
+
skipDuplicates: true,
|
|
131
|
+
});
|
|
132
|
+
expect(result).toEqual(expectedResult);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('findMany', () => {
|
|
137
|
+
it('should return entities with itemCount', async () => {
|
|
138
|
+
const findArgs = {
|
|
139
|
+
where: {},
|
|
140
|
+
take: 10,
|
|
141
|
+
skip: 0,
|
|
142
|
+
} as any;
|
|
143
|
+
const mockEntities = [{ id: 1 }, { id: 2 }];
|
|
144
|
+
|
|
145
|
+
prisma.user.count.mockResolvedValue(2);
|
|
146
|
+
prisma.user.findMany.mockResolvedValue(mockEntities);
|
|
147
|
+
|
|
148
|
+
const result = await service.findMany(findArgs);
|
|
149
|
+
|
|
150
|
+
expect(prisma.user.count).toHaveBeenCalledWith({ where: findArgs.where });
|
|
151
|
+
expect(prisma.user.findMany).toHaveBeenCalledWith(findArgs);
|
|
152
|
+
expect(result).toEqual({
|
|
153
|
+
itemCount: 2,
|
|
154
|
+
entities: mockEntities,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should return empty result when no records found', async () => {
|
|
159
|
+
const findArgs = { where: {} } as any;
|
|
160
|
+
|
|
161
|
+
prisma.user.count.mockResolvedValue(0);
|
|
162
|
+
prisma.user.findMany.mockResolvedValue([]);
|
|
163
|
+
|
|
164
|
+
const result = await service.findMany(findArgs);
|
|
165
|
+
|
|
166
|
+
expect(result).toEqual({ itemCount: 0, entities: [] });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('findUnique', () => {
|
|
171
|
+
it('should find a single user record by id', async () => {
|
|
172
|
+
const id = 1;
|
|
173
|
+
const select = { id: true };
|
|
174
|
+
const expectedResult = { id: 1 };
|
|
175
|
+
|
|
176
|
+
prisma.user.findUnique.mockResolvedValue(expectedResult);
|
|
177
|
+
|
|
178
|
+
const result = await service.findUnique(id, select);
|
|
179
|
+
|
|
180
|
+
expect(prisma.user.findUnique).toHaveBeenCalledWith({
|
|
181
|
+
where: { id },
|
|
182
|
+
select,
|
|
183
|
+
});
|
|
184
|
+
expect(result).toEqual(expectedResult);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should return null when record not found', async () => {
|
|
188
|
+
prisma.user.findUnique.mockResolvedValue(null);
|
|
189
|
+
|
|
190
|
+
const result = await service.findUnique(1, { id: true });
|
|
191
|
+
|
|
192
|
+
expect(result).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('updateUnique', () => {
|
|
197
|
+
it('should update a single user record', async () => {
|
|
198
|
+
const id = 1;
|
|
199
|
+
const data = {
|
|
200
|
+
userid: 'test_userid',
|
|
201
|
+
username: 'test_username',
|
|
202
|
+
roles: '[]',
|
|
203
|
+
} as any;
|
|
204
|
+
const expectedResult = { id: 1, ...data };
|
|
205
|
+
|
|
206
|
+
prisma.user.update.mockResolvedValue(expectedResult);
|
|
207
|
+
|
|
208
|
+
const result = await service.updateUnique(id, data);
|
|
209
|
+
|
|
210
|
+
expect(prisma.user.update).toHaveBeenCalledWith({
|
|
211
|
+
where: { id },
|
|
212
|
+
data,
|
|
213
|
+
});
|
|
214
|
+
expect(result).toEqual(expectedResult);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('updateMany', () => {
|
|
219
|
+
it('should update multiple user records', async () => {
|
|
220
|
+
const updateArgs = {
|
|
221
|
+
where: {},
|
|
222
|
+
data: {
|
|
223
|
+
userid: 'test_userid',
|
|
224
|
+
username: 'test_username',
|
|
225
|
+
roles: '[]',
|
|
226
|
+
},
|
|
227
|
+
} as any;
|
|
228
|
+
const expectedResult = { count: 5 };
|
|
229
|
+
|
|
230
|
+
prisma.user.updateMany.mockResolvedValue(expectedResult);
|
|
231
|
+
|
|
232
|
+
const result = await service.updateMany(updateArgs);
|
|
233
|
+
|
|
234
|
+
expect(prisma.user.updateMany).toHaveBeenCalledWith(updateArgs);
|
|
235
|
+
expect(result).toEqual(expectedResult);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('deleteUnique', () => {
|
|
240
|
+
it('should delete a single user record and return count', async () => {
|
|
241
|
+
const id = 1;
|
|
242
|
+
|
|
243
|
+
prisma.user.delete.mockResolvedValue({ id: 1 });
|
|
244
|
+
|
|
245
|
+
const result = await service.deleteUnique(id);
|
|
246
|
+
|
|
247
|
+
expect(prisma.user.delete).toHaveBeenCalledWith({ where: { id } });
|
|
248
|
+
expect(result).toEqual({ count: 1 });
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('deleteMany', () => {
|
|
253
|
+
it('should delete multiple user records', async () => {
|
|
254
|
+
const deleteArgs = { where: {} } as any;
|
|
255
|
+
const expectedResult = { count: 3 };
|
|
256
|
+
|
|
257
|
+
prisma.user.deleteMany.mockResolvedValue(expectedResult);
|
|
258
|
+
|
|
259
|
+
const result = await service.deleteMany(deleteArgs);
|
|
260
|
+
|
|
261
|
+
expect(prisma.user.deleteMany).toHaveBeenCalledWith(deleteArgs);
|
|
262
|
+
expect(result).toEqual(expectedResult);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('count', () => {
|
|
267
|
+
it('should count user records with filter', async () => {
|
|
268
|
+
const countArgs = { where: {} } as any;
|
|
269
|
+
|
|
270
|
+
prisma.user.count.mockResolvedValue(10);
|
|
271
|
+
|
|
272
|
+
const result = await service.count(countArgs);
|
|
273
|
+
|
|
274
|
+
expect(prisma.user.count).toHaveBeenCalledWith(countArgs);
|
|
275
|
+
expect(result).toBe(10);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should count all records when no filter provided', async () => {
|
|
279
|
+
prisma.user.count.mockResolvedValue(100);
|
|
280
|
+
|
|
281
|
+
const result = await service.count();
|
|
282
|
+
|
|
283
|
+
expect(prisma.user.count).toHaveBeenCalledWith(undefined);
|
|
284
|
+
expect(result).toBe(100);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('groupBy', () => {
|
|
289
|
+
it('should group user records by specified field', async () => {
|
|
290
|
+
const groupByArgs = {
|
|
291
|
+
by: ['id'],
|
|
292
|
+
_count: { id: true },
|
|
293
|
+
} as any;
|
|
294
|
+
const expectedResult = [{ id: 1, _count: { id: 50 } }];
|
|
295
|
+
|
|
296
|
+
prisma.user.groupBy.mockResolvedValue(expectedResult);
|
|
297
|
+
|
|
298
|
+
const result = await service.groupBy(groupByArgs);
|
|
299
|
+
|
|
300
|
+
expect(prisma.user.groupBy).toHaveBeenCalledWith(groupByArgs);
|
|
301
|
+
expect(result).toEqual(expectedResult);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('aggregate', () => {
|
|
306
|
+
it('should aggregate user records', async () => {
|
|
307
|
+
const aggregateArgs = {
|
|
308
|
+
_count: true,
|
|
309
|
+
_max: { id: true },
|
|
310
|
+
_min: { id: true },
|
|
311
|
+
} as any;
|
|
312
|
+
const expectedResult = {
|
|
313
|
+
_count: 100,
|
|
314
|
+
_max: { id: 1 },
|
|
315
|
+
_min: { id: 1 },
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
prisma.user.aggregate.mockResolvedValue(expectedResult);
|
|
319
|
+
|
|
320
|
+
const result = await service.aggregate(aggregateArgs);
|
|
321
|
+
|
|
322
|
+
expect(prisma.user.aggregate).toHaveBeenCalledWith(aggregateArgs);
|
|
323
|
+
expect(result).toEqual(expectedResult);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
18
326
|
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { validateDslGraph } from './dsl-validate.util';
|
|
2
|
+
|
|
3
|
+
describe('validateDslGraph', () => {
|
|
4
|
+
const validDsl = {
|
|
5
|
+
nodes: [
|
|
6
|
+
{ id: 't1', type: 'manual_trigger' },
|
|
7
|
+
{ id: 'n1', type: 'http_request' },
|
|
8
|
+
{ id: 'n2', type: 'send_notification' },
|
|
9
|
+
],
|
|
10
|
+
edges: [
|
|
11
|
+
{ source: 't1', target: 'n1' },
|
|
12
|
+
{ source: 'n1', target: 'n2' },
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('valid input', () => {
|
|
17
|
+
it('accepts a simple linear DAG', () => {
|
|
18
|
+
expect(() => validateDslGraph(validDsl)).not.toThrow();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('accepts a diamond DAG', () => {
|
|
22
|
+
expect(() =>
|
|
23
|
+
validateDslGraph({
|
|
24
|
+
nodes: [
|
|
25
|
+
{ id: 't1', type: 'manual_trigger' },
|
|
26
|
+
{ id: 'a', type: 'http_request' },
|
|
27
|
+
{ id: 'b', type: 'http_request' },
|
|
28
|
+
{ id: 'c', type: 'send_notification' },
|
|
29
|
+
],
|
|
30
|
+
edges: [
|
|
31
|
+
{ source: 't1', target: 'a' },
|
|
32
|
+
{ source: 't1', target: 'b' },
|
|
33
|
+
{ source: 'a', target: 'c' },
|
|
34
|
+
{ source: 'b', target: 'c' },
|
|
35
|
+
],
|
|
36
|
+
}),
|
|
37
|
+
).not.toThrow();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('accepts a trigger with no successors', () => {
|
|
41
|
+
expect(() =>
|
|
42
|
+
validateDslGraph({
|
|
43
|
+
nodes: [{ id: 't1', type: 'cron_trigger' }],
|
|
44
|
+
edges: [],
|
|
45
|
+
}),
|
|
46
|
+
).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('shape errors', () => {
|
|
51
|
+
it('rejects null', () => {
|
|
52
|
+
expect(() => validateDslGraph(null)).toThrow('DSL must be an object');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('rejects missing nodes array', () => {
|
|
56
|
+
expect(() => validateDslGraph({ edges: [] })).toThrow(
|
|
57
|
+
'DSL.nodes must be an array',
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects missing edges array', () => {
|
|
62
|
+
expect(() =>
|
|
63
|
+
validateDslGraph({ nodes: [{ id: 't1', type: 'manual_trigger' }] }),
|
|
64
|
+
).toThrow('DSL.edges must be an array');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('rejects empty nodes', () => {
|
|
68
|
+
expect(() => validateDslGraph({ nodes: [], edges: [] })).toThrow(
|
|
69
|
+
'at least one node',
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('rejects node without id', () => {
|
|
74
|
+
expect(() =>
|
|
75
|
+
validateDslGraph({
|
|
76
|
+
nodes: [{ type: 'manual_trigger' }],
|
|
77
|
+
edges: [],
|
|
78
|
+
}),
|
|
79
|
+
).toThrow('non-empty string id');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects duplicate node ids', () => {
|
|
83
|
+
expect(() =>
|
|
84
|
+
validateDslGraph({
|
|
85
|
+
nodes: [
|
|
86
|
+
{ id: 't1', type: 'manual_trigger' },
|
|
87
|
+
{ id: 't1', type: 'http_request' },
|
|
88
|
+
],
|
|
89
|
+
edges: [],
|
|
90
|
+
}),
|
|
91
|
+
).toThrow('Duplicate node id');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('rejects node without type', () => {
|
|
95
|
+
expect(() =>
|
|
96
|
+
validateDslGraph({
|
|
97
|
+
nodes: [{ id: 't1' }],
|
|
98
|
+
edges: [],
|
|
99
|
+
}),
|
|
100
|
+
).toThrow('non-empty string type');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects DSL with no trigger node', () => {
|
|
104
|
+
expect(() =>
|
|
105
|
+
validateDslGraph({
|
|
106
|
+
nodes: [{ id: 'n1', type: 'http_request' }],
|
|
107
|
+
edges: [],
|
|
108
|
+
}),
|
|
109
|
+
).toThrow('at least one *_trigger node');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('edge errors', () => {
|
|
114
|
+
it('rejects edge with unknown source', () => {
|
|
115
|
+
expect(() =>
|
|
116
|
+
validateDslGraph({
|
|
117
|
+
nodes: [{ id: 't1', type: 'manual_trigger' }],
|
|
118
|
+
edges: [{ source: 'ghost', target: 't1' }],
|
|
119
|
+
}),
|
|
120
|
+
).toThrow('unknown source');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects edge with unknown target', () => {
|
|
124
|
+
expect(() =>
|
|
125
|
+
validateDslGraph({
|
|
126
|
+
nodes: [{ id: 't1', type: 'manual_trigger' }],
|
|
127
|
+
edges: [{ source: 't1', target: 'ghost' }],
|
|
128
|
+
}),
|
|
129
|
+
).toThrow('unknown target');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('rejects self-loop', () => {
|
|
133
|
+
expect(() =>
|
|
134
|
+
validateDslGraph({
|
|
135
|
+
nodes: [
|
|
136
|
+
{ id: 't1', type: 'manual_trigger' },
|
|
137
|
+
{ id: 'a', type: 'http_request' },
|
|
138
|
+
],
|
|
139
|
+
edges: [
|
|
140
|
+
{ source: 't1', target: 'a' },
|
|
141
|
+
{ source: 'a', target: 'a' },
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
).toThrow('Self-loop');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('cycle detection', () => {
|
|
149
|
+
it('rejects a 2-node cycle', () => {
|
|
150
|
+
expect(() =>
|
|
151
|
+
validateDslGraph({
|
|
152
|
+
nodes: [
|
|
153
|
+
{ id: 't1', type: 'manual_trigger' },
|
|
154
|
+
{ id: 'a', type: 'http_request' },
|
|
155
|
+
{ id: 'b', type: 'http_request' },
|
|
156
|
+
],
|
|
157
|
+
edges: [
|
|
158
|
+
{ source: 't1', target: 'a' },
|
|
159
|
+
{ source: 'a', target: 'b' },
|
|
160
|
+
{ source: 'b', target: 'a' },
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
).toThrow('cycle');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects a 3-node cycle', () => {
|
|
167
|
+
expect(() =>
|
|
168
|
+
validateDslGraph({
|
|
169
|
+
nodes: [
|
|
170
|
+
{ id: 't1', type: 'manual_trigger' },
|
|
171
|
+
{ id: 'a', type: 'http_request' },
|
|
172
|
+
{ id: 'b', type: 'http_request' },
|
|
173
|
+
{ id: 'c', type: 'http_request' },
|
|
174
|
+
],
|
|
175
|
+
edges: [
|
|
176
|
+
{ source: 't1', target: 'a' },
|
|
177
|
+
{ source: 'a', target: 'b' },
|
|
178
|
+
{ source: 'b', target: 'c' },
|
|
179
|
+
{ source: 'c', target: 'a' },
|
|
180
|
+
],
|
|
181
|
+
}),
|
|
182
|
+
).toThrow('cycle');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('error message lists nodes still in the cycle', () => {
|
|
186
|
+
try {
|
|
187
|
+
validateDslGraph({
|
|
188
|
+
nodes: [
|
|
189
|
+
{ id: 't1', type: 'manual_trigger' },
|
|
190
|
+
{ id: 'a', type: 'http_request' },
|
|
191
|
+
{ id: 'b', type: 'http_request' },
|
|
192
|
+
],
|
|
193
|
+
edges: [
|
|
194
|
+
{ source: 't1', target: 'a' },
|
|
195
|
+
{ source: 'a', target: 'b' },
|
|
196
|
+
{ source: 'b', target: 'a' },
|
|
197
|
+
],
|
|
198
|
+
});
|
|
199
|
+
fail('expected validation to throw');
|
|
200
|
+
} catch (err: any) {
|
|
201
|
+
expect(err.message).toMatch(/a|b/);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL graph structural validation.
|
|
3
|
+
*
|
|
4
|
+
* Used by WorkflowService.publish() to reject malformed or cyclic DSLs before
|
|
5
|
+
* they become a published version. The worker's executeGraph still has a runtime
|
|
6
|
+
* iteration cap as defense-in-depth, but with this validation in place the cap
|
|
7
|
+
* should never trip in practice.
|
|
8
|
+
*
|
|
9
|
+
* Validates:
|
|
10
|
+
* 1. nodes / edges are present and well-typed
|
|
11
|
+
* 2. every edge references existing nodes
|
|
12
|
+
* 3. node ids are unique
|
|
13
|
+
* 4. there is at least one trigger node
|
|
14
|
+
* 5. graph is a DAG (Kahn's algorithm — no cycles)
|
|
15
|
+
*
|
|
16
|
+
* Throws Error with a human-readable message on the first failure;
|
|
17
|
+
* caller should map to BadRequestException.
|
|
18
|
+
*/
|
|
19
|
+
export interface DslGraphLike {
|
|
20
|
+
nodes?: Array<{ id?: string; type?: string }>;
|
|
21
|
+
edges?: Array<{ source?: string; target?: string }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function validateDslGraph(dsl: unknown): void {
|
|
25
|
+
if (!dsl || typeof dsl !== 'object') {
|
|
26
|
+
throw new Error('DSL must be an object');
|
|
27
|
+
}
|
|
28
|
+
const g = dsl as DslGraphLike;
|
|
29
|
+
|
|
30
|
+
if (!Array.isArray(g.nodes)) {
|
|
31
|
+
throw new Error('DSL.nodes must be an array');
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(g.edges)) {
|
|
34
|
+
throw new Error('DSL.edges must be an array');
|
|
35
|
+
}
|
|
36
|
+
if (g.nodes.length === 0) {
|
|
37
|
+
throw new Error('DSL must contain at least one node');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 1. node ids unique + non-empty
|
|
41
|
+
const idSet = new Set<string>();
|
|
42
|
+
for (const n of g.nodes) {
|
|
43
|
+
if (!n || typeof n.id !== 'string' || n.id.length === 0) {
|
|
44
|
+
throw new Error('Every node must have a non-empty string id');
|
|
45
|
+
}
|
|
46
|
+
if (idSet.has(n.id)) {
|
|
47
|
+
throw new Error(`Duplicate node id: "${n.id}"`);
|
|
48
|
+
}
|
|
49
|
+
idSet.add(n.id);
|
|
50
|
+
if (typeof n.type !== 'string' || n.type.length === 0) {
|
|
51
|
+
throw new Error(`Node "${n.id}" must have a non-empty string type`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. at least one trigger node
|
|
56
|
+
const hasTrigger = g.nodes.some((n) => n.type!.endsWith('_trigger'));
|
|
57
|
+
if (!hasTrigger) {
|
|
58
|
+
throw new Error('DSL must contain at least one *_trigger node');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. edges reference existing nodes; no self-loops
|
|
62
|
+
for (const e of g.edges) {
|
|
63
|
+
if (!e || typeof e.source !== 'string' || typeof e.target !== 'string') {
|
|
64
|
+
throw new Error('Every edge must have string source and target');
|
|
65
|
+
}
|
|
66
|
+
if (!idSet.has(e.source)) {
|
|
67
|
+
throw new Error(`Edge references unknown source node "${e.source}"`);
|
|
68
|
+
}
|
|
69
|
+
if (!idSet.has(e.target)) {
|
|
70
|
+
throw new Error(`Edge references unknown target node "${e.target}"`);
|
|
71
|
+
}
|
|
72
|
+
if (e.source === e.target) {
|
|
73
|
+
throw new Error(`Self-loop detected on node "${e.source}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. cycle detection — Kahn's algorithm.
|
|
78
|
+
// 注意:parallel/for_each 节点的"分支"在 worker 侧也是普通的 edge,因此用普通 DAG 校验即可。
|
|
79
|
+
const indegree = new Map<string, number>();
|
|
80
|
+
const adj = new Map<string, string[]>();
|
|
81
|
+
for (const id of idSet) {
|
|
82
|
+
indegree.set(id, 0);
|
|
83
|
+
adj.set(id, []);
|
|
84
|
+
}
|
|
85
|
+
for (const e of g.edges) {
|
|
86
|
+
indegree.set(e.target!, indegree.get(e.target!)! + 1);
|
|
87
|
+
adj.get(e.source!)!.push(e.target!);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const queue: string[] = [];
|
|
91
|
+
for (const [id, deg] of indegree) {
|
|
92
|
+
if (deg === 0) queue.push(id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let visited = 0;
|
|
96
|
+
while (queue.length > 0) {
|
|
97
|
+
const id = queue.shift()!;
|
|
98
|
+
visited++;
|
|
99
|
+
for (const next of adj.get(id) || []) {
|
|
100
|
+
const d = indegree.get(next)! - 1;
|
|
101
|
+
indegree.set(next, d);
|
|
102
|
+
if (d === 0) queue.push(next);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (visited !== idSet.size) {
|
|
107
|
+
const stuck = [...indegree.entries()]
|
|
108
|
+
.filter(([, d]) => d > 0)
|
|
109
|
+
.map(([id]) => id);
|
|
110
|
+
throw new Error(
|
|
111
|
+
`DSL contains a cycle. Nodes still in cycle or downstream of one: [${stuck.join(
|
|
112
|
+
', ',
|
|
113
|
+
)}]`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|