@gadmin2n/schematics 0.0.136 → 0.0.137
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/Dockerfile.server +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/canvas.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +14 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +4 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +13 -92
- package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +105 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/ensure-database.js +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +9 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +43 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.dto.ts +27 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.spec.ts +430 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.ts +203 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +36 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.module.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +54 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +245 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +10 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.spec.ts +366 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.ts +110 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +41 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +5 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +37 -573
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/ecosystem.dev.config.cjs +12 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +39 -26
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +3 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +23 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +66 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +16 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +21 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +30 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +84 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +166 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasHistory.ts +52 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +6 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +6 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +18 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +225 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts +31 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +0 -220
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +0 -132
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +0 -152
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +0 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +0 -25
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +0 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +0 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +0 -79
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +0 -270
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +0 -122
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +0 -4285
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +0 -28
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +0 -51
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +0 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +0 -54
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +0 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +0 -85
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +0 -47
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +0 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +0 -58
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +0 -184
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +0 -133
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +0 -96
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +0 -77
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +0 -109
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +0 -382
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/code-execute.test.ts +0 -44
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/http-request.test.ts +0 -87
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +0 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/helpers.test.ts +0 -225
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/node-type-consistency.test.ts +0 -101
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +0 -205
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +0 -499
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +0 -19
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +0 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/yarn.lock +0 -1905
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
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';
|
|
12
|
+
import { TemporalTenantConfigService } from './temporalTenantConfig.service';
|
|
13
|
+
|
|
14
|
+
// Mock PrismaService
|
|
15
|
+
const mockPrismaService = {
|
|
16
|
+
temporalTenantConfig: {
|
|
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
|
+
|
|
45
|
+
describe('TemporalTenantConfigService', () => {
|
|
46
|
+
let service: TemporalTenantConfigService;
|
|
47
|
+
let prisma: typeof mockPrismaService;
|
|
48
|
+
|
|
49
|
+
beforeEach(async () => {
|
|
50
|
+
// 每个测试前重置所有 mock
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
|
|
53
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
54
|
+
providers: [
|
|
55
|
+
TemporalTenantConfigService,
|
|
56
|
+
{ provide: PrismaService, useValue: mockPrismaService },
|
|
57
|
+
{ provide: ConfigService, useValue: mockConfigService },
|
|
58
|
+
{ provide: WINSTON_MODULE_PROVIDER, useValue: mockLogger },
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
// 兜底:自动 mock 任何未显式提供的依赖
|
|
62
|
+
// (例如手工给 service 增加的额外依赖:RoleService、其他 Service 等)
|
|
63
|
+
.useMocker(() => ({}))
|
|
64
|
+
.compile();
|
|
65
|
+
|
|
66
|
+
service = module.get<TemporalTenantConfigService>(
|
|
67
|
+
TemporalTenantConfigService,
|
|
68
|
+
);
|
|
69
|
+
prisma = mockPrismaService;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should be defined', () => {
|
|
73
|
+
expect(service).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('createOne', () => {
|
|
77
|
+
it('should create a single temporalTenantConfig record', async () => {
|
|
78
|
+
const createArgs = {
|
|
79
|
+
data: {
|
|
80
|
+
adminUrl: 'test_adminUrl',
|
|
81
|
+
adminToken: 'test_adminToken',
|
|
82
|
+
namespace: 'test_namespace',
|
|
83
|
+
dbUrl: 'test_dbUrl',
|
|
84
|
+
displayName: 'test_displayName',
|
|
85
|
+
creator: 'test_creator',
|
|
86
|
+
},
|
|
87
|
+
} as any;
|
|
88
|
+
const expectedResult = { id: 1, ...createArgs.data };
|
|
89
|
+
|
|
90
|
+
prisma.temporalTenantConfig.create.mockResolvedValue(expectedResult);
|
|
91
|
+
|
|
92
|
+
const result = await service.createOne(createArgs);
|
|
93
|
+
|
|
94
|
+
expect(prisma.temporalTenantConfig.create).toHaveBeenCalledWith(
|
|
95
|
+
createArgs,
|
|
96
|
+
);
|
|
97
|
+
expect(prisma.temporalTenantConfig.create).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(result).toEqual(expectedResult);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should throw error when create fails', async () => {
|
|
102
|
+
const createArgs = {
|
|
103
|
+
data: {
|
|
104
|
+
adminUrl: 'test_adminUrl',
|
|
105
|
+
adminToken: 'test_adminToken',
|
|
106
|
+
namespace: 'test_namespace',
|
|
107
|
+
dbUrl: 'test_dbUrl',
|
|
108
|
+
displayName: 'test_displayName',
|
|
109
|
+
creator: 'test_creator',
|
|
110
|
+
},
|
|
111
|
+
} as any;
|
|
112
|
+
const error = new Error('Database connection failed');
|
|
113
|
+
|
|
114
|
+
prisma.temporalTenantConfig.create.mockRejectedValue(error);
|
|
115
|
+
|
|
116
|
+
await expect(service.createOne(createArgs)).rejects.toThrow(
|
|
117
|
+
'Database connection failed',
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('createMany', () => {
|
|
123
|
+
it('should create multiple temporalTenantConfig records with skipDuplicates', async () => {
|
|
124
|
+
const data = [
|
|
125
|
+
{
|
|
126
|
+
adminUrl: 'test_adminUrl',
|
|
127
|
+
adminToken: 'test_adminToken',
|
|
128
|
+
namespace: 'test_namespace',
|
|
129
|
+
dbUrl: 'test_dbUrl',
|
|
130
|
+
displayName: 'test_displayName',
|
|
131
|
+
creator: 'test_creator',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
adminUrl: 'test_adminUrl',
|
|
135
|
+
adminToken: 'test_adminToken',
|
|
136
|
+
namespace: 'test_namespace',
|
|
137
|
+
dbUrl: 'test_dbUrl',
|
|
138
|
+
displayName: 'test_displayName',
|
|
139
|
+
creator: 'test_creator',
|
|
140
|
+
},
|
|
141
|
+
] as any;
|
|
142
|
+
const expectedResult = { count: 2 };
|
|
143
|
+
|
|
144
|
+
prisma.temporalTenantConfig.createMany.mockResolvedValue(expectedResult);
|
|
145
|
+
|
|
146
|
+
const result = await service.createMany(data);
|
|
147
|
+
|
|
148
|
+
expect(prisma.temporalTenantConfig.createMany).toHaveBeenCalledWith({
|
|
149
|
+
data,
|
|
150
|
+
skipDuplicates: true,
|
|
151
|
+
});
|
|
152
|
+
expect(result).toEqual(expectedResult);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('findMany', () => {
|
|
157
|
+
it('should return entities with itemCount', async () => {
|
|
158
|
+
const findArgs = {
|
|
159
|
+
where: {},
|
|
160
|
+
take: 10,
|
|
161
|
+
skip: 0,
|
|
162
|
+
} as any;
|
|
163
|
+
const mockEntities = [{ id: 1 }, { id: 2 }];
|
|
164
|
+
|
|
165
|
+
prisma.temporalTenantConfig.count.mockResolvedValue(2);
|
|
166
|
+
prisma.temporalTenantConfig.findMany.mockResolvedValue(mockEntities);
|
|
167
|
+
|
|
168
|
+
const result = await service.findMany(findArgs);
|
|
169
|
+
|
|
170
|
+
expect(prisma.temporalTenantConfig.count).toHaveBeenCalledWith({
|
|
171
|
+
where: findArgs.where,
|
|
172
|
+
});
|
|
173
|
+
expect(prisma.temporalTenantConfig.findMany).toHaveBeenCalledWith(
|
|
174
|
+
findArgs,
|
|
175
|
+
);
|
|
176
|
+
expect(result).toEqual({
|
|
177
|
+
itemCount: 2,
|
|
178
|
+
entities: mockEntities,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should return empty result when no records found', async () => {
|
|
183
|
+
const findArgs = { where: {} } as any;
|
|
184
|
+
|
|
185
|
+
prisma.temporalTenantConfig.count.mockResolvedValue(0);
|
|
186
|
+
prisma.temporalTenantConfig.findMany.mockResolvedValue([]);
|
|
187
|
+
|
|
188
|
+
const result = await service.findMany(findArgs);
|
|
189
|
+
|
|
190
|
+
expect(result).toEqual({ itemCount: 0, entities: [] });
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('findUnique', () => {
|
|
195
|
+
it('should find a single temporalTenantConfig record by id', async () => {
|
|
196
|
+
const id = 1;
|
|
197
|
+
const select = { id: true };
|
|
198
|
+
const expectedResult = { id: 1 };
|
|
199
|
+
|
|
200
|
+
prisma.temporalTenantConfig.findUnique.mockResolvedValue(expectedResult);
|
|
201
|
+
|
|
202
|
+
const result = await service.findUnique(id, select);
|
|
203
|
+
|
|
204
|
+
expect(prisma.temporalTenantConfig.findUnique).toHaveBeenCalledWith({
|
|
205
|
+
where: { id },
|
|
206
|
+
select,
|
|
207
|
+
});
|
|
208
|
+
expect(result).toEqual(expectedResult);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should return null when record not found', async () => {
|
|
212
|
+
prisma.temporalTenantConfig.findUnique.mockResolvedValue(null);
|
|
213
|
+
|
|
214
|
+
const result = await service.findUnique(1, { id: true });
|
|
215
|
+
|
|
216
|
+
expect(result).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('updateUnique', () => {
|
|
221
|
+
it('should update a single temporalTenantConfig record', async () => {
|
|
222
|
+
const id = 1;
|
|
223
|
+
const data = {
|
|
224
|
+
adminUrl: 'test_adminUrl',
|
|
225
|
+
adminToken: 'test_adminToken',
|
|
226
|
+
namespace: 'test_namespace',
|
|
227
|
+
dbUrl: 'test_dbUrl',
|
|
228
|
+
displayName: 'test_displayName',
|
|
229
|
+
creator: 'test_creator',
|
|
230
|
+
} as any;
|
|
231
|
+
const expectedResult = { id: 1, ...data };
|
|
232
|
+
|
|
233
|
+
prisma.temporalTenantConfig.update.mockResolvedValue(expectedResult);
|
|
234
|
+
|
|
235
|
+
const result = await service.updateUnique(id, data);
|
|
236
|
+
|
|
237
|
+
expect(prisma.temporalTenantConfig.update).toHaveBeenCalledWith({
|
|
238
|
+
where: { id },
|
|
239
|
+
data,
|
|
240
|
+
});
|
|
241
|
+
expect(result).toEqual(expectedResult);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('updateMany', () => {
|
|
246
|
+
it('should update multiple temporalTenantConfig records', async () => {
|
|
247
|
+
const updateArgs = {
|
|
248
|
+
where: {},
|
|
249
|
+
data: {
|
|
250
|
+
adminUrl: 'test_adminUrl',
|
|
251
|
+
adminToken: 'test_adminToken',
|
|
252
|
+
namespace: 'test_namespace',
|
|
253
|
+
dbUrl: 'test_dbUrl',
|
|
254
|
+
displayName: 'test_displayName',
|
|
255
|
+
creator: 'test_creator',
|
|
256
|
+
},
|
|
257
|
+
} as any;
|
|
258
|
+
const expectedResult = { count: 5 };
|
|
259
|
+
|
|
260
|
+
prisma.temporalTenantConfig.updateMany.mockResolvedValue(expectedResult);
|
|
261
|
+
|
|
262
|
+
const result = await service.updateMany(updateArgs);
|
|
263
|
+
|
|
264
|
+
expect(prisma.temporalTenantConfig.updateMany).toHaveBeenCalledWith(
|
|
265
|
+
updateArgs,
|
|
266
|
+
);
|
|
267
|
+
expect(result).toEqual(expectedResult);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('deleteUnique', () => {
|
|
272
|
+
it('should delete a single temporalTenantConfig record and return count', async () => {
|
|
273
|
+
const id = 1;
|
|
274
|
+
|
|
275
|
+
prisma.temporalTenantConfig.delete.mockResolvedValue({ id: 1 });
|
|
276
|
+
|
|
277
|
+
const result = await service.deleteUnique(id);
|
|
278
|
+
|
|
279
|
+
expect(prisma.temporalTenantConfig.delete).toHaveBeenCalledWith({
|
|
280
|
+
where: { id },
|
|
281
|
+
});
|
|
282
|
+
expect(result).toEqual({ count: 1 });
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('deleteMany', () => {
|
|
287
|
+
it('should delete multiple temporalTenantConfig records', async () => {
|
|
288
|
+
const deleteArgs = { where: {} } as any;
|
|
289
|
+
const expectedResult = { count: 3 };
|
|
290
|
+
|
|
291
|
+
prisma.temporalTenantConfig.deleteMany.mockResolvedValue(expectedResult);
|
|
292
|
+
|
|
293
|
+
const result = await service.deleteMany(deleteArgs);
|
|
294
|
+
|
|
295
|
+
expect(prisma.temporalTenantConfig.deleteMany).toHaveBeenCalledWith(
|
|
296
|
+
deleteArgs,
|
|
297
|
+
);
|
|
298
|
+
expect(result).toEqual(expectedResult);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('count', () => {
|
|
303
|
+
it('should count temporalTenantConfig records with filter', async () => {
|
|
304
|
+
const countArgs = { where: {} } as any;
|
|
305
|
+
|
|
306
|
+
prisma.temporalTenantConfig.count.mockResolvedValue(10);
|
|
307
|
+
|
|
308
|
+
const result = await service.count(countArgs);
|
|
309
|
+
|
|
310
|
+
expect(prisma.temporalTenantConfig.count).toHaveBeenCalledWith(countArgs);
|
|
311
|
+
expect(result).toBe(10);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should count all records when no filter provided', async () => {
|
|
315
|
+
prisma.temporalTenantConfig.count.mockResolvedValue(100);
|
|
316
|
+
|
|
317
|
+
const result = await service.count();
|
|
318
|
+
|
|
319
|
+
expect(prisma.temporalTenantConfig.count).toHaveBeenCalledWith(undefined);
|
|
320
|
+
expect(result).toBe(100);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('groupBy', () => {
|
|
325
|
+
it('should group temporalTenantConfig records by specified field', async () => {
|
|
326
|
+
const groupByArgs = {
|
|
327
|
+
by: ['id'],
|
|
328
|
+
_count: { id: true },
|
|
329
|
+
} as any;
|
|
330
|
+
const expectedResult = [{ id: 1, _count: { id: 50 } }];
|
|
331
|
+
|
|
332
|
+
prisma.temporalTenantConfig.groupBy.mockResolvedValue(expectedResult);
|
|
333
|
+
|
|
334
|
+
const result = await service.groupBy(groupByArgs);
|
|
335
|
+
|
|
336
|
+
expect(prisma.temporalTenantConfig.groupBy).toHaveBeenCalledWith(
|
|
337
|
+
groupByArgs,
|
|
338
|
+
);
|
|
339
|
+
expect(result).toEqual(expectedResult);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('aggregate', () => {
|
|
344
|
+
it('should aggregate temporalTenantConfig records', async () => {
|
|
345
|
+
const aggregateArgs = {
|
|
346
|
+
_count: true,
|
|
347
|
+
_max: { id: true },
|
|
348
|
+
_min: { id: true },
|
|
349
|
+
} as any;
|
|
350
|
+
const expectedResult = {
|
|
351
|
+
_count: 100,
|
|
352
|
+
_max: { id: 1 },
|
|
353
|
+
_min: { id: 1 },
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
prisma.temporalTenantConfig.aggregate.mockResolvedValue(expectedResult);
|
|
357
|
+
|
|
358
|
+
const result = await service.aggregate(aggregateArgs);
|
|
359
|
+
|
|
360
|
+
expect(prisma.temporalTenantConfig.aggregate).toHaveBeenCalledWith(
|
|
361
|
+
aggregateArgs,
|
|
362
|
+
);
|
|
363
|
+
expect(result).toEqual(expectedResult);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Inject, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Prisma, TemporalTenantConfig } from '@prisma/client';
|
|
3
|
+
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
|
4
|
+
import { PrismaService } from 'nestjs-prisma';
|
|
5
|
+
import { Logger } from 'winston';
|
|
6
|
+
|
|
7
|
+
import { ConfigService } from '@nestjs/config';
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class TemporalTenantConfigService {
|
|
11
|
+
constructor(
|
|
12
|
+
private prisma: PrismaService,
|
|
13
|
+
private readonly config: ConfigService,
|
|
14
|
+
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
async createOne(
|
|
18
|
+
temporalTenantConfigCreateArgs: Prisma.TemporalTenantConfigCreateArgs,
|
|
19
|
+
) {
|
|
20
|
+
return await this.prisma.temporalTenantConfig.create(
|
|
21
|
+
temporalTenantConfigCreateArgs,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async createMany(data: Prisma.TemporalTenantConfigCreateManyArgs['data']) {
|
|
26
|
+
return await this.prisma.temporalTenantConfig.createMany({
|
|
27
|
+
data,
|
|
28
|
+
skipDuplicates: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async findMany(
|
|
33
|
+
temporalTenantConfigFindManyArgs: Prisma.TemporalTenantConfigFindManyArgs,
|
|
34
|
+
): Promise<{ itemCount: number; entities: TemporalTenantConfig[] }> {
|
|
35
|
+
const itemCount = await this.prisma.temporalTenantConfig.count({
|
|
36
|
+
where: temporalTenantConfigFindManyArgs.where,
|
|
37
|
+
});
|
|
38
|
+
const entities = await this.prisma.temporalTenantConfig.findMany(
|
|
39
|
+
temporalTenantConfigFindManyArgs,
|
|
40
|
+
);
|
|
41
|
+
return { itemCount, entities };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async findUnique(
|
|
45
|
+
id: number,
|
|
46
|
+
select: Prisma.TemporalTenantConfigFindUniqueArgs['select'],
|
|
47
|
+
) {
|
|
48
|
+
return await this.prisma.temporalTenantConfig.findUnique({
|
|
49
|
+
where: { id: id },
|
|
50
|
+
select,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async updateMany(
|
|
55
|
+
temporalTenantConfigUpdateManyArgs: Prisma.TemporalTenantConfigUpdateManyArgs,
|
|
56
|
+
) {
|
|
57
|
+
return await this.prisma.temporalTenantConfig.updateMany(
|
|
58
|
+
temporalTenantConfigUpdateManyArgs,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async updateUnique(
|
|
63
|
+
id: number,
|
|
64
|
+
data: Prisma.TemporalTenantConfigUpdateManyArgs['data'],
|
|
65
|
+
) {
|
|
66
|
+
return await this.prisma.temporalTenantConfig.update({
|
|
67
|
+
where: { id: id },
|
|
68
|
+
data,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async deleteMany(
|
|
73
|
+
temporalTenantConfigDeleteManyArgs: Prisma.TemporalTenantConfigDeleteManyArgs,
|
|
74
|
+
) {
|
|
75
|
+
return await this.prisma.temporalTenantConfig.deleteMany(
|
|
76
|
+
temporalTenantConfigDeleteManyArgs,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async deleteUnique(id: number) {
|
|
81
|
+
await this.prisma.temporalTenantConfig.delete({
|
|
82
|
+
where: { id: id },
|
|
83
|
+
});
|
|
84
|
+
return { count: 1 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async count(
|
|
88
|
+
temporalTenantConfigCountArgs?: Prisma.TemporalTenantConfigCountArgs,
|
|
89
|
+
) {
|
|
90
|
+
return await this.prisma.temporalTenantConfig.count(
|
|
91
|
+
temporalTenantConfigCountArgs,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async groupBy(
|
|
96
|
+
temporalTenantConfigGroupByArgs: Prisma.TemporalTenantConfigGroupByArgs,
|
|
97
|
+
) {
|
|
98
|
+
return await this.prisma.temporalTenantConfig.groupBy(
|
|
99
|
+
temporalTenantConfigGroupByArgs as any,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async aggregate(
|
|
104
|
+
temporalTenantConfigAggregateArgs: Prisma.TemporalTenantConfigAggregateArgs,
|
|
105
|
+
) {
|
|
106
|
+
return await this.prisma.temporalTenantConfig.aggregate(
|
|
107
|
+
temporalTenantConfigAggregateArgs,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
IsBoolean,
|
|
3
|
+
IsInt,
|
|
4
|
+
IsObject,
|
|
5
|
+
IsOptional,
|
|
6
|
+
IsString,
|
|
7
|
+
} from 'class-validator';
|
|
2
8
|
|
|
3
9
|
export class ExecuteWorkflowDto {
|
|
4
10
|
@IsOptional()
|
|
@@ -17,3 +23,15 @@ export class SignalWorkflowDto {
|
|
|
17
23
|
@IsObject()
|
|
18
24
|
payload: Record<string, any>;
|
|
19
25
|
}
|
|
26
|
+
|
|
27
|
+
export class ApproveInstanceDto {
|
|
28
|
+
@IsString()
|
|
29
|
+
nodeId: string;
|
|
30
|
+
|
|
31
|
+
@IsBoolean()
|
|
32
|
+
approved: boolean;
|
|
33
|
+
|
|
34
|
+
@IsOptional()
|
|
35
|
+
@IsString()
|
|
36
|
+
comment?: string;
|
|
37
|
+
}
|
|
@@ -20,6 +20,7 @@ import { Request } from 'express';
|
|
|
20
20
|
import AuthGuard, { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
21
21
|
import { TemporalService } from './temporal.service';
|
|
22
22
|
import {
|
|
23
|
+
ApproveInstanceDto,
|
|
23
24
|
ExecuteWorkflowDto,
|
|
24
25
|
SignalWorkflowDto,
|
|
25
26
|
} from './workflow-execution.dto';
|
|
@@ -222,6 +223,20 @@ export class WorkflowController {
|
|
|
222
223
|
);
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
@Post('instance/:instanceId/approve')
|
|
227
|
+
async approveInstance(
|
|
228
|
+
@Param('instanceId', ParseIntPipe) instanceId: number,
|
|
229
|
+
@Body() dto: ApproveInstanceDto,
|
|
230
|
+
@Req() req,
|
|
231
|
+
) {
|
|
232
|
+
return this.workflowService.approveInstance(
|
|
233
|
+
BigInt(instanceId),
|
|
234
|
+
dto,
|
|
235
|
+
req.user.userid,
|
|
236
|
+
this.temporalService,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
225
240
|
/**
|
|
226
241
|
* Public webhook entry point.
|
|
227
242
|
* Triggers any published workflow with a webhook_trigger node matching (path, method).
|
|
@@ -501,3 +501,82 @@ describe('WorkflowService — receiveWebhook', () => {
|
|
|
501
501
|
).rejects.toBeInstanceOf(UnauthorizedException);
|
|
502
502
|
});
|
|
503
503
|
});
|
|
504
|
+
|
|
505
|
+
describe('WorkflowService — approveInstance', () => {
|
|
506
|
+
let service: WorkflowService;
|
|
507
|
+
let prisma: any;
|
|
508
|
+
let temporal: any;
|
|
509
|
+
|
|
510
|
+
beforeEach(async () => {
|
|
511
|
+
prisma = {
|
|
512
|
+
workflowInstance: { findUnique: jest.fn() },
|
|
513
|
+
workflowNodeExecution: { findUnique: jest.fn() },
|
|
514
|
+
};
|
|
515
|
+
temporal = { signalWorkflow: jest.fn() };
|
|
516
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
517
|
+
providers: [
|
|
518
|
+
WorkflowService,
|
|
519
|
+
{ provide: PrismaService, useValue: prisma },
|
|
520
|
+
{ provide: TemporalService, useValue: temporal },
|
|
521
|
+
],
|
|
522
|
+
}).compile();
|
|
523
|
+
service = module.get(WorkflowService);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it('rejects when caller is not in the resolved approver list', async () => {
|
|
527
|
+
prisma.workflowInstance.findUnique.mockResolvedValue({
|
|
528
|
+
id: BigInt(1),
|
|
529
|
+
temporalRunId: 'run-1',
|
|
530
|
+
});
|
|
531
|
+
prisma.workflowNodeExecution.findUnique.mockResolvedValue({
|
|
532
|
+
input: { approvers: ['alice'] },
|
|
533
|
+
});
|
|
534
|
+
await expect(
|
|
535
|
+
service.approveInstance(
|
|
536
|
+
BigInt(1),
|
|
537
|
+
{ nodeId: 'n1', approved: true },
|
|
538
|
+
'bob',
|
|
539
|
+
temporal,
|
|
540
|
+
),
|
|
541
|
+
).rejects.toThrow('not an approver');
|
|
542
|
+
expect(temporal.signalWorkflow).not.toHaveBeenCalled();
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('forwards signal with real approverId when caller is authorized', async () => {
|
|
546
|
+
prisma.workflowInstance.findUnique.mockResolvedValue({
|
|
547
|
+
id: BigInt(1),
|
|
548
|
+
temporalRunId: 'run-1',
|
|
549
|
+
});
|
|
550
|
+
prisma.workflowNodeExecution.findUnique.mockResolvedValue({
|
|
551
|
+
input: { approvers: ['alice'] },
|
|
552
|
+
});
|
|
553
|
+
await service.approveInstance(
|
|
554
|
+
BigInt(1),
|
|
555
|
+
{ nodeId: 'n1', approved: true, comment: 'ok' },
|
|
556
|
+
'alice',
|
|
557
|
+
temporal,
|
|
558
|
+
);
|
|
559
|
+
expect(temporal.signalWorkflow).toHaveBeenCalledWith('run-1', 'approval', {
|
|
560
|
+
nodeId: 'n1',
|
|
561
|
+
approved: true,
|
|
562
|
+
approvedBy: 'alice',
|
|
563
|
+
comment: 'ok',
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('throws NotFoundException when no node execution exists for nodeId', async () => {
|
|
568
|
+
prisma.workflowInstance.findUnique.mockResolvedValue({
|
|
569
|
+
id: BigInt(1),
|
|
570
|
+
temporalRunId: 'run-1',
|
|
571
|
+
});
|
|
572
|
+
prisma.workflowNodeExecution.findUnique.mockResolvedValue(null);
|
|
573
|
+
await expect(
|
|
574
|
+
service.approveInstance(
|
|
575
|
+
BigInt(1),
|
|
576
|
+
{ nodeId: 'missing', approved: true },
|
|
577
|
+
'alice',
|
|
578
|
+
temporal,
|
|
579
|
+
),
|
|
580
|
+
).rejects.toThrow('Node execution not found');
|
|
581
|
+
});
|
|
582
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateDslHandles } from '@gadmin2n/nest-common/workflow-dsl';
|
|
1
2
|
import {
|
|
2
3
|
BadRequestException,
|
|
3
4
|
Injectable,
|
|
@@ -10,10 +11,6 @@ import { PrismaService } from 'nestjs-prisma';
|
|
|
10
11
|
import { validateDslGraph } from './dsl-validate.util';
|
|
11
12
|
import { TemporalService } from './temporal.service';
|
|
12
13
|
import { verifyWebhookSignature } from './webhook-signature.util';
|
|
13
|
-
import {
|
|
14
|
-
validateDslHandles,
|
|
15
|
-
type NodeOutputsMap,
|
|
16
|
-
} from './workflow-dsl-validate';
|
|
17
14
|
|
|
18
15
|
@Injectable()
|
|
19
16
|
export class WorkflowService {
|
|
@@ -289,21 +286,11 @@ export class WorkflowService {
|
|
|
289
286
|
throw new BadRequestException(`Invalid DSL: ${err?.message ?? err}`);
|
|
290
287
|
}
|
|
291
288
|
|
|
292
|
-
// Per-edge sourceHandle validation against node-type outputs declared in
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
});
|
|
298
|
-
const outputsMap: NodeOutputsMap = {};
|
|
299
|
-
for (const nt of nodeTypes) {
|
|
300
|
-
// Pass through: null = dynamic, [] = single anonymous, [..] = enumerated
|
|
301
|
-
outputsMap[nt.type] =
|
|
302
|
-
nt.outputs === undefined
|
|
303
|
-
? []
|
|
304
|
-
: (nt.outputs as unknown as string[] | null);
|
|
305
|
-
}
|
|
306
|
-
const handleErrors = validateDslHandles(dslToPublish as any, outputsMap);
|
|
289
|
+
// Per-edge sourceHandle validation against node-type outputs declared in
|
|
290
|
+
// @gadmin2n/nest-common (NODE_TYPE_META). nest-common is the single source
|
|
291
|
+
// of truth for handle metadata; the seeded t_workflow_node_type.outputs
|
|
292
|
+
// column mirrors it (verified at seed time in workflow-node-types.ts).
|
|
293
|
+
const handleErrors = validateDslHandles(dslToPublish as any);
|
|
307
294
|
if (handleErrors.length > 0) {
|
|
308
295
|
throw new BadRequestException({
|
|
309
296
|
message: 'DSL validation failed: invalid edge sourceHandle(s)',
|
|
@@ -786,4 +773,39 @@ export class WorkflowService {
|
|
|
786
773
|
);
|
|
787
774
|
return { success: true };
|
|
788
775
|
}
|
|
776
|
+
|
|
777
|
+
async approveInstance(
|
|
778
|
+
instanceId: bigint,
|
|
779
|
+
dto: { nodeId: string; approved: boolean; comment?: string },
|
|
780
|
+
approverId: string,
|
|
781
|
+
temporalService: any,
|
|
782
|
+
) {
|
|
783
|
+
const instance = await this.prisma.workflowInstance.findUnique({
|
|
784
|
+
where: { id: instanceId },
|
|
785
|
+
});
|
|
786
|
+
if (!instance) throw new NotFoundException('Instance not found');
|
|
787
|
+
if (!instance.temporalRunId) {
|
|
788
|
+
throw new Error('Instance has no Temporal run ID');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const nodeExecution = await this.prisma.workflowNodeExecution.findUnique({
|
|
792
|
+
where: { instanceId_nodeId: { instanceId, nodeId: dto.nodeId } },
|
|
793
|
+
});
|
|
794
|
+
if (!nodeExecution) throw new NotFoundException('Node execution not found');
|
|
795
|
+
|
|
796
|
+
const approvers: string[] = (nodeExecution.input as any)?.approvers || [];
|
|
797
|
+
if (!approvers.includes(approverId)) {
|
|
798
|
+
throw new UnauthorizedException(
|
|
799
|
+
`${approverId} is not an approver for this node`,
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
await temporalService.signalWorkflow(instance.temporalRunId, 'approval', {
|
|
804
|
+
nodeId: dto.nodeId,
|
|
805
|
+
approved: dto.approved,
|
|
806
|
+
approvedBy: approverId,
|
|
807
|
+
comment: dto.comment,
|
|
808
|
+
});
|
|
809
|
+
return { success: true };
|
|
810
|
+
}
|
|
789
811
|
}
|