@nest-util/nest-crud 1.1.4 → 1.2.1
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/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/lib/controllers/nest-crud.controller.d.ts +11 -0
- package/dist/lib/controllers/nest-crud.controller.d.ts.map +1 -1
- package/dist/lib/controllers/nest-crud.controller.js +113 -0
- package/dist/lib/dtos/request-modification.dto.d.ts +11 -0
- package/dist/lib/dtos/request-modification.dto.d.ts.map +1 -0
- package/dist/lib/dtos/request-modification.dto.js +57 -0
- package/dist/lib/dtos/status-change.dto.d.ts +5 -0
- package/dist/lib/dtos/status-change.dto.d.ts.map +1 -0
- package/dist/lib/dtos/status-change.dto.js +16 -0
- package/dist/lib/entities/approval-status.entity.d.ts +29 -0
- package/dist/lib/entities/approval-status.entity.d.ts.map +1 -0
- package/dist/lib/entities/approval-status.entity.js +72 -0
- package/dist/lib/entities/modification-request-history.entity.d.ts +20 -0
- package/dist/lib/entities/modification-request-history.entity.d.ts.map +1 -0
- package/dist/lib/entities/modification-request-history.entity.js +48 -0
- package/dist/lib/interfaces/approval-pipeline.interface.d.ts +90 -0
- package/dist/lib/interfaces/approval-pipeline.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/approval-pipeline.interface.js +20 -0
- package/dist/lib/interfaces/crud.interface.d.ts +12 -1
- package/dist/lib/interfaces/crud.interface.d.ts.map +1 -1
- package/dist/lib/interfaces/status-pipeline.interface.d.ts +63 -0
- package/dist/lib/interfaces/status-pipeline.interface.d.ts.map +1 -0
- package/dist/lib/interfaces/status-pipeline.interface.js +2 -0
- package/dist/lib/nest-crud.module.d.ts.map +1 -1
- package/dist/lib/nest-crud.module.js +9 -1
- package/dist/lib/services/nest-crud.service.d.ts +44 -0
- package/dist/lib/services/nest-crud.service.d.ts.map +1 -1
- package/dist/lib/services/nest-crud.service.js +345 -1
- package/dist/lib/testing/crud-controller.test-suites.d.ts.map +1 -1
- package/dist/lib/testing/crud-controller.test-suites.js +15 -0
- package/dist/lib/testing/mock-repository.d.ts.map +1 -1
- package/dist/lib/testing/mock-repository.js +2 -0
- package/dist/lib/testing/testing.interface.d.ts +2 -1
- package/dist/lib/testing/testing.interface.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -7,8 +7,12 @@ const audit_log_entity_1 = require("../entities/audit-log.entity");
|
|
|
7
7
|
const filter_helper_1 = require("../helpers/filter.helper");
|
|
8
8
|
const pagination_helper_1 = require("../helpers/pagination.helper");
|
|
9
9
|
const cursor_pagination_helper_1 = require("../helpers/cursor-pagination.helper");
|
|
10
|
+
const approval_status_entity_1 = require("../entities/approval-status.entity");
|
|
11
|
+
const modification_request_history_entity_1 = require("../entities/modification-request-history.entity");
|
|
12
|
+
const approval_pipeline_interface_1 = require("../interfaces/approval-pipeline.interface");
|
|
10
13
|
let NestCrudService = class NestCrudService {
|
|
11
14
|
constructor(options) {
|
|
15
|
+
this.statusTransitions = new Map();
|
|
12
16
|
this.repo = options.repository;
|
|
13
17
|
this.allowedFilters = options.allowedFilters ?? [];
|
|
14
18
|
this.allowedSortFields = options.allowedSortFields ?? [];
|
|
@@ -28,6 +32,50 @@ let NestCrudService = class NestCrudService {
|
|
|
28
32
|
this.ownershipBypassPermissions = options.ownershipBypassPermissions ?? [];
|
|
29
33
|
this.ownershipBypass = options.ownershipBypass;
|
|
30
34
|
this.superAdminPermission = options.superAdminPermission;
|
|
35
|
+
this.statusField = options.statusPipeline?.field;
|
|
36
|
+
this.statusInitial = options.statusPipeline?.initial;
|
|
37
|
+
this.statusOnTransition = options.statusPipeline?.onTransition;
|
|
38
|
+
this.statusCreateAllow = [
|
|
39
|
+
...(options.statusPipeline?.initial !== undefined
|
|
40
|
+
? [options.statusPipeline.initial]
|
|
41
|
+
: []),
|
|
42
|
+
...(options.statusPipeline?.allowCreateStatuses ?? []),
|
|
43
|
+
];
|
|
44
|
+
if (options.statusPipeline?.transitions) {
|
|
45
|
+
this.statusTransitions = this.normalizeStatusTransitions(options.statusPipeline.transitions);
|
|
46
|
+
}
|
|
47
|
+
this.approvalPipeline = options.approvalPipeline;
|
|
48
|
+
}
|
|
49
|
+
normalizeStatusTransitions(transitions) {
|
|
50
|
+
const result = new Map();
|
|
51
|
+
const addEdge = (from, to, permission, action) => {
|
|
52
|
+
const existing = result.get(from) ?? {
|
|
53
|
+
to: new Set(),
|
|
54
|
+
permission: undefined,
|
|
55
|
+
action: undefined,
|
|
56
|
+
};
|
|
57
|
+
for (const target of to) {
|
|
58
|
+
existing.to.add(target);
|
|
59
|
+
}
|
|
60
|
+
if (permission !== undefined) {
|
|
61
|
+
existing.permission = permission;
|
|
62
|
+
}
|
|
63
|
+
if (action !== undefined) {
|
|
64
|
+
existing.action = action;
|
|
65
|
+
}
|
|
66
|
+
result.set(from, existing);
|
|
67
|
+
};
|
|
68
|
+
if (Array.isArray(transitions)) {
|
|
69
|
+
for (const edge of transitions) {
|
|
70
|
+
addEdge(edge.from, edge.to, edge.permission, edge.action);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
for (const [from, to] of Object.entries(transitions)) {
|
|
75
|
+
addEdge(from, to);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
31
79
|
}
|
|
32
80
|
async resolveRelations(payload) {
|
|
33
81
|
if (!this.relations.length)
|
|
@@ -199,9 +247,82 @@ let NestCrudService = class NestCrudService {
|
|
|
199
247
|
const rows = await qb.getMany();
|
|
200
248
|
return rows[0] ?? null;
|
|
201
249
|
}
|
|
250
|
+
isStatusPipelineConfigured() {
|
|
251
|
+
return Boolean(this.statusField);
|
|
252
|
+
}
|
|
253
|
+
applyInitialStatus(payload) {
|
|
254
|
+
if (!this.isStatusPipelineConfigured()) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const field = String(this.statusField);
|
|
258
|
+
const requested = payload[field];
|
|
259
|
+
if (requested === undefined) {
|
|
260
|
+
if (this.statusInitial !== undefined) {
|
|
261
|
+
payload[field] = this.statusInitial;
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (this.statusCreateAllow.length > 0 &&
|
|
266
|
+
!this.statusCreateAllow.includes(requested)) {
|
|
267
|
+
throw new common_1.BadRequestException(`Invalid initial status '${String(requested)}'. Allowed: ${this.statusCreateAllow
|
|
268
|
+
.map(String)
|
|
269
|
+
.join(', ') || 'none'}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
validateStatusTransition(existing, payload, user) {
|
|
273
|
+
if (!this.isStatusPipelineConfigured()) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const field = String(this.statusField);
|
|
277
|
+
const requested = payload[field];
|
|
278
|
+
if (requested === undefined) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const current = existing[field];
|
|
282
|
+
const requestedValue = requested;
|
|
283
|
+
if (current === requestedValue) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const entry = this.statusTransitions.get(current);
|
|
287
|
+
if (!entry) {
|
|
288
|
+
throw new common_1.BadRequestException(`Invalid status transition: '${String(current)}' -> '${String(requestedValue)}'. Status '${String(current)}' has no allowed transitions`);
|
|
289
|
+
}
|
|
290
|
+
if (!entry.to.has(requestedValue)) {
|
|
291
|
+
throw new common_1.BadRequestException(`Invalid status transition: '${String(current)}' -> '${String(requestedValue)}'. Allowed: ${[...entry.to].map(String).join(', ')}`);
|
|
292
|
+
}
|
|
293
|
+
if (entry.permission && !this.canBypassOwnershipForTransition(user, entry.permission)) {
|
|
294
|
+
throw new common_1.ForbiddenException(`Missing required permission '${entry.permission}' for status transition`);
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
from: current,
|
|
298
|
+
to: requestedValue,
|
|
299
|
+
action: entry.action,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async runTransitionActions(transition, id, entity, user) {
|
|
303
|
+
const context = {
|
|
304
|
+
id,
|
|
305
|
+
entity,
|
|
306
|
+
from: transition.from,
|
|
307
|
+
to: transition.to,
|
|
308
|
+
user,
|
|
309
|
+
};
|
|
310
|
+
if (transition.action) {
|
|
311
|
+
await transition.action(context);
|
|
312
|
+
}
|
|
313
|
+
if (this.statusOnTransition) {
|
|
314
|
+
await this.statusOnTransition(context);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
canBypassOwnershipForTransition(user, permission) {
|
|
318
|
+
if (!user)
|
|
319
|
+
return false;
|
|
320
|
+
return this.resolveUserPermissions(user).includes(permission);
|
|
321
|
+
}
|
|
202
322
|
async findAll(query) {
|
|
203
323
|
const qb = this.repo.createQueryBuilder('e');
|
|
204
324
|
this.applyIncludeJoins(qb);
|
|
325
|
+
this.applyApprovalVisibility(qb);
|
|
205
326
|
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters, this.include);
|
|
206
327
|
const paginationMeta = (0, pagination_helper_1.applyPagination)(qb, query);
|
|
207
328
|
this.applyOrderBy(qb, query.orderBy, query.orderDirection === 'ASC' ? 'ASC' : 'DESC');
|
|
@@ -220,6 +341,7 @@ let NestCrudService = class NestCrudService {
|
|
|
220
341
|
const qb = this.repo.createQueryBuilder('e');
|
|
221
342
|
this.applyOwnershipCondition(qb, userId);
|
|
222
343
|
this.applyIncludeJoins(qb);
|
|
344
|
+
this.applyApprovalVisibility(qb);
|
|
223
345
|
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters, this.include);
|
|
224
346
|
const paginationMeta = (0, pagination_helper_1.applyPagination)(qb, query);
|
|
225
347
|
this.applyOrderBy(qb, query.orderBy, query.orderDirection === 'ASC' ? 'ASC' : 'DESC');
|
|
@@ -238,6 +360,7 @@ let NestCrudService = class NestCrudService {
|
|
|
238
360
|
const qb = this.repo.createQueryBuilder('e');
|
|
239
361
|
// Join relations
|
|
240
362
|
this.applyIncludeJoins(qb);
|
|
363
|
+
this.applyApprovalVisibility(qb);
|
|
241
364
|
// Apply filters
|
|
242
365
|
(0, filter_helper_1.applyFilters)(qb, query.filter, this.allowedFilters, this.include);
|
|
243
366
|
// Apply cursor filter
|
|
@@ -260,6 +383,7 @@ let NestCrudService = class NestCrudService {
|
|
|
260
383
|
// Build a clean count query (reuse same filters but no cursor/order/take)
|
|
261
384
|
const countQb = this.repo.createQueryBuilder('e');
|
|
262
385
|
this.applyIncludeJoins(countQb, false);
|
|
386
|
+
this.applyApprovalVisibility(countQb);
|
|
263
387
|
(0, filter_helper_1.applyFilters)(countQb, query.filter, this.allowedFilters, this.include);
|
|
264
388
|
total = await countQb.getCount();
|
|
265
389
|
}
|
|
@@ -282,6 +406,13 @@ let NestCrudService = class NestCrudService {
|
|
|
282
406
|
if (this.enforceOwnershipFor(user)) {
|
|
283
407
|
entity = await this.findOwnedEntity(id, user);
|
|
284
408
|
}
|
|
409
|
+
else if (this.hasApprovalVisibilityFilter()) {
|
|
410
|
+
const qb = this.repo.createQueryBuilder('e');
|
|
411
|
+
this.applyIncludeJoins(qb);
|
|
412
|
+
this.applyApprovalVisibility(qb);
|
|
413
|
+
qb.andWhere('e.id = :id', { id });
|
|
414
|
+
entity = await qb.getOne();
|
|
415
|
+
}
|
|
285
416
|
else {
|
|
286
417
|
const relationsObj = this.include.length > 0
|
|
287
418
|
? this.buildRelationsObject(this.include)
|
|
@@ -328,10 +459,13 @@ let NestCrudService = class NestCrudService {
|
|
|
328
459
|
}
|
|
329
460
|
}
|
|
330
461
|
}
|
|
462
|
+
this.applyInitialStatus(payload);
|
|
331
463
|
await this.executeHook(this.hooks.beforeCreate, { payload });
|
|
332
464
|
const payloadSnapshot = { ...payload };
|
|
333
465
|
const resolved = await this.resolveRelations(payload);
|
|
334
|
-
const entity =
|
|
466
|
+
const entity = this.isApprovalPipelineConfigured()
|
|
467
|
+
? await this.createWithApproval(resolved, user)
|
|
468
|
+
: await this.repo.save(resolved);
|
|
335
469
|
const result = this.toResponseDto
|
|
336
470
|
? this.toResponseDto(entity)
|
|
337
471
|
: entity;
|
|
@@ -339,6 +473,212 @@ let NestCrudService = class NestCrudService {
|
|
|
339
473
|
return result;
|
|
340
474
|
}
|
|
341
475
|
async update(id, payload, user) {
|
|
476
|
+
return this.applyUpdateCore(id, payload, user);
|
|
477
|
+
}
|
|
478
|
+
async changeStatus(id, status, user) {
|
|
479
|
+
if (!this.isStatusPipelineConfigured()) {
|
|
480
|
+
throw new common_1.NotFoundException('Resource not found');
|
|
481
|
+
}
|
|
482
|
+
const payload = { [String(this.statusField)]: status };
|
|
483
|
+
return this.applyUpdateCore(id, payload, user);
|
|
484
|
+
}
|
|
485
|
+
async getApproval(id, user) {
|
|
486
|
+
if (!this.isApprovalPipelineConfigured()) {
|
|
487
|
+
throw new common_1.NotFoundException('Resource not found');
|
|
488
|
+
}
|
|
489
|
+
const approval = await this.loadApprovalStatus(id, user);
|
|
490
|
+
if (!approval) {
|
|
491
|
+
throw new common_1.NotFoundException('Resource not found');
|
|
492
|
+
}
|
|
493
|
+
const history = await this.getHistoryRepository().find({
|
|
494
|
+
where: { approvalStatusId: approval.id },
|
|
495
|
+
order: { requestedAt: 'DESC' },
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
approval: this.toApprovalStatusView(approval),
|
|
499
|
+
history: history.map((entry) => this.toApprovalHistoryView(entry)),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
async approveApproval(id, user) {
|
|
503
|
+
return this.transitionApproval(id, 'approve', user);
|
|
504
|
+
}
|
|
505
|
+
async rejectApproval(id, user) {
|
|
506
|
+
return this.transitionApproval(id, 'reject', user);
|
|
507
|
+
}
|
|
508
|
+
async requestModification(id, payload, user) {
|
|
509
|
+
return this.transitionApproval(id, 'requestModification', user, payload.modifications, payload.note);
|
|
510
|
+
}
|
|
511
|
+
async resubmitApproval(id, user) {
|
|
512
|
+
return this.transitionApproval(id, 'resubmit', user);
|
|
513
|
+
}
|
|
514
|
+
isApprovalPipelineConfigured() {
|
|
515
|
+
return Boolean(this.approvalPipeline && this.approvalPipeline.enabled !== false);
|
|
516
|
+
}
|
|
517
|
+
hasApprovalVisibilityFilter() {
|
|
518
|
+
if (!this.isApprovalPipelineConfigured()) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
const visible = this.approvalPipeline?.visibleStatuses;
|
|
522
|
+
return Boolean(visible && visible.length > 0);
|
|
523
|
+
}
|
|
524
|
+
getApprovalRepository() {
|
|
525
|
+
return this.repo.manager.getRepository(approval_status_entity_1.ApprovalStatusEntity);
|
|
526
|
+
}
|
|
527
|
+
getHistoryRepository() {
|
|
528
|
+
return this.repo.manager.getRepository(modification_request_history_entity_1.ModificationRequestHistoryEntity);
|
|
529
|
+
}
|
|
530
|
+
async createWithApproval(resolved, user) {
|
|
531
|
+
const entity = await this.repo.manager.transaction(async (manager) => {
|
|
532
|
+
const entityRepo = manager.getRepository(this.repo.target);
|
|
533
|
+
const saved = await entityRepo.save(resolved);
|
|
534
|
+
const approvalRepo = manager.getRepository(approval_status_entity_1.ApprovalStatusEntity);
|
|
535
|
+
const approval = approvalRepo.create({
|
|
536
|
+
entity: this.repo.metadata.tableName,
|
|
537
|
+
entityId: String(saved.id),
|
|
538
|
+
status: approval_pipeline_interface_1.APPROVAL_STATUS.pending,
|
|
539
|
+
requestedBy: user?.id != null ? String(user.id) : null,
|
|
540
|
+
});
|
|
541
|
+
await approvalRepo.save(approval);
|
|
542
|
+
return saved;
|
|
543
|
+
});
|
|
544
|
+
return entity;
|
|
545
|
+
}
|
|
546
|
+
async resolveEntityForApproval(id, user) {
|
|
547
|
+
if (this.enforceOwnershipFor(user)) {
|
|
548
|
+
return this.findOwnedEntity(id, user);
|
|
549
|
+
}
|
|
550
|
+
return this.repo.findOneBy({ id });
|
|
551
|
+
}
|
|
552
|
+
async loadApprovalStatus(id, user) {
|
|
553
|
+
const entity = await this.resolveEntityForApproval(id, user);
|
|
554
|
+
if (!entity) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
const approval = await this.getApprovalRepository().findOneBy({
|
|
558
|
+
entity: this.repo.metadata.tableName,
|
|
559
|
+
entityId: String(id),
|
|
560
|
+
});
|
|
561
|
+
return approval ?? null;
|
|
562
|
+
}
|
|
563
|
+
async transitionApproval(id, action, user, modifications, note) {
|
|
564
|
+
if (!this.isApprovalPipelineConfigured()) {
|
|
565
|
+
throw new common_1.NotFoundException('Resource not found');
|
|
566
|
+
}
|
|
567
|
+
const permission = this.approvalPipeline?.permissions?.[action];
|
|
568
|
+
if (permission && !this.userHasPermission(user, permission)) {
|
|
569
|
+
throw new common_1.ForbiddenException(`Missing required permission '${permission}' for ${action}`);
|
|
570
|
+
}
|
|
571
|
+
const approval = await this.loadApprovalStatus(id, user);
|
|
572
|
+
if (!approval) {
|
|
573
|
+
throw new common_1.NotFoundException('Resource not found');
|
|
574
|
+
}
|
|
575
|
+
const current = approval.status;
|
|
576
|
+
const now = new Date();
|
|
577
|
+
const userId = user?.id != null ? String(user.id) : null;
|
|
578
|
+
const transitionable = [
|
|
579
|
+
approval_pipeline_interface_1.APPROVAL_STATUS.pending,
|
|
580
|
+
approval_pipeline_interface_1.APPROVAL_STATUS.resubmitted,
|
|
581
|
+
];
|
|
582
|
+
switch (action) {
|
|
583
|
+
case 'approve':
|
|
584
|
+
this.assertTransitionAllowed(action, current, transitionable);
|
|
585
|
+
approval.status = approval_pipeline_interface_1.APPROVAL_STATUS.approved;
|
|
586
|
+
approval.decidedBy = userId;
|
|
587
|
+
approval.decidedAt = now;
|
|
588
|
+
approval.currentModifications = null;
|
|
589
|
+
break;
|
|
590
|
+
case 'reject':
|
|
591
|
+
this.assertTransitionAllowed(action, current, transitionable);
|
|
592
|
+
approval.status = approval_pipeline_interface_1.APPROVAL_STATUS.rejected;
|
|
593
|
+
approval.decidedBy = userId;
|
|
594
|
+
approval.decidedAt = now;
|
|
595
|
+
approval.currentModifications = null;
|
|
596
|
+
break;
|
|
597
|
+
case 'requestModification':
|
|
598
|
+
this.assertTransitionAllowed(action, current, transitionable);
|
|
599
|
+
approval.status = approval_pipeline_interface_1.APPROVAL_STATUS.modificationRequested;
|
|
600
|
+
approval.currentModifications = modifications ?? [];
|
|
601
|
+
await this.getHistoryRepository().save(this.getHistoryRepository().create({
|
|
602
|
+
approvalStatusId: approval.id,
|
|
603
|
+
modifications: modifications ?? [],
|
|
604
|
+
requestedBy: userId,
|
|
605
|
+
note: note ?? null,
|
|
606
|
+
}));
|
|
607
|
+
break;
|
|
608
|
+
case 'resubmit':
|
|
609
|
+
if (current !== approval_pipeline_interface_1.APPROVAL_STATUS.modificationRequested) {
|
|
610
|
+
throw new common_1.BadRequestException(`Cannot resubmit approval from status '${current}'`);
|
|
611
|
+
}
|
|
612
|
+
approval.status = approval_pipeline_interface_1.APPROVAL_STATUS.resubmitted;
|
|
613
|
+
approval.currentModifications = null;
|
|
614
|
+
approval.resubmittedBy = userId;
|
|
615
|
+
approval.resubmittedAt = now;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
const saved = await this.getApprovalRepository().save(approval);
|
|
619
|
+
return this.toApprovalStatusView(saved);
|
|
620
|
+
}
|
|
621
|
+
assertTransitionAllowed(action, current, allowedFrom) {
|
|
622
|
+
if (!allowedFrom.includes(current)) {
|
|
623
|
+
throw new common_1.BadRequestException(`Cannot ${action} approval from status '${current}'`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
userHasPermission(user, permission) {
|
|
627
|
+
if (!user) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
const permissions = this.resolveUserPermissions(user);
|
|
631
|
+
return permissions.includes(permission);
|
|
632
|
+
}
|
|
633
|
+
applyApprovalVisibility(qb) {
|
|
634
|
+
if (!this.hasApprovalVisibilityFilter()) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const visible = this.approvalPipeline?.visibleStatuses;
|
|
638
|
+
const tableName = this.repo.metadata.tableName;
|
|
639
|
+
qb.innerJoin(approval_status_entity_1.ApprovalStatusEntity, 'approvalStatus', `approvalStatus.entity = :approvalEntityName AND approvalStatus.entityId = ${this.approvalIdCastExpression()} AND approvalStatus.status IN (:...approvalVisibleStatuses)`, {
|
|
640
|
+
approvalEntityName: tableName,
|
|
641
|
+
approvalVisibleStatuses: [...visible],
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
approvalIdCastExpression() {
|
|
645
|
+
const type = this.repo.manager.connection.options.type;
|
|
646
|
+
if (type === 'mysql' || type === 'mariadb' || type === 'aurora-mysql') {
|
|
647
|
+
return 'CAST(e.id AS CHAR)';
|
|
648
|
+
}
|
|
649
|
+
if (type === 'better-sqlite3' || type === 'sqljs' || type === 'capacitor') {
|
|
650
|
+
return 'CAST(e.id AS TEXT)';
|
|
651
|
+
}
|
|
652
|
+
return 'CAST(e.id AS varchar)';
|
|
653
|
+
}
|
|
654
|
+
toApprovalStatusView(entity) {
|
|
655
|
+
return {
|
|
656
|
+
id: entity.id,
|
|
657
|
+
entity: entity.entity,
|
|
658
|
+
entityId: entity.entityId,
|
|
659
|
+
status: entity.status,
|
|
660
|
+
requestedBy: entity.requestedBy,
|
|
661
|
+
requestedAt: entity.requestedAt,
|
|
662
|
+
currentModifications: entity.currentModifications,
|
|
663
|
+
decidedBy: entity.decidedBy,
|
|
664
|
+
decidedAt: entity.decidedAt,
|
|
665
|
+
resubmittedBy: entity.resubmittedBy,
|
|
666
|
+
resubmittedAt: entity.resubmittedAt,
|
|
667
|
+
createdAt: entity.createdAt,
|
|
668
|
+
updatedAt: entity.updatedAt,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
toApprovalHistoryView(entity) {
|
|
672
|
+
return {
|
|
673
|
+
id: entity.id,
|
|
674
|
+
approvalStatusId: entity.approvalStatusId,
|
|
675
|
+
modifications: entity.modifications,
|
|
676
|
+
requestedBy: entity.requestedBy,
|
|
677
|
+
note: entity.note,
|
|
678
|
+
requestedAt: entity.requestedAt,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
async applyUpdateCore(id, payload, user) {
|
|
342
682
|
let existing;
|
|
343
683
|
if (this.enforceOwnershipFor(user)) {
|
|
344
684
|
existing = await this.findOwnedEntity(id, user);
|
|
@@ -352,12 +692,16 @@ let NestCrudService = class NestCrudService {
|
|
|
352
692
|
throw new common_1.NotFoundException('Resource not found');
|
|
353
693
|
}
|
|
354
694
|
await this.executeHook(this.hooks.beforeUpdate, { payload, entity: existing, id });
|
|
695
|
+
const transition = this.validateStatusTransition(existing, payload, user);
|
|
355
696
|
const payloadSnapshot = { ...payload };
|
|
356
697
|
const resolved = await this.resolveRelations(payload);
|
|
357
698
|
this.repo.merge(existing, resolved);
|
|
358
699
|
await this.repo.save(existing);
|
|
359
700
|
const result = await this.findOne(id, user);
|
|
360
701
|
await this.executeHook(this.hooks.afterUpdate, { entity: result, payload: payloadSnapshot, id });
|
|
702
|
+
if (transition) {
|
|
703
|
+
await this.runTransitionActions(transition, id, existing, user);
|
|
704
|
+
}
|
|
361
705
|
return result;
|
|
362
706
|
}
|
|
363
707
|
async remove(id, user) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crud-controller.test-suites.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/crud-controller.test-suites.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAMxC,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGhF,wBAAgB,mBAAmB,CACjC,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY,EAEZ,MAAM,EAAE,wBAAwB,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,GAC9E,eAAe,
|
|
1
|
+
{"version":3,"file":"crud-controller.test-suites.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/crud-controller.test-suites.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAMxC,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGhF,wBAAgB,mBAAmB,CACjC,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY,EAEZ,MAAM,EAAE,wBAAwB,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,GAC9E,eAAe,CA6OjB"}
|
|
@@ -17,6 +17,7 @@ function crudControllerTests(config) {
|
|
|
17
17
|
findOne: jest.fn(),
|
|
18
18
|
create: jest.fn(),
|
|
19
19
|
update: jest.fn(),
|
|
20
|
+
changeStatus: jest.fn(),
|
|
20
21
|
remove: jest.fn(),
|
|
21
22
|
findAuditLogs: jest.fn(),
|
|
22
23
|
findMine: jest.fn(),
|
|
@@ -95,6 +96,20 @@ function crudControllerTests(config) {
|
|
|
95
96
|
expect(result).toEqual(expectedResult);
|
|
96
97
|
});
|
|
97
98
|
});
|
|
99
|
+
describe('changeStatus', () => {
|
|
100
|
+
it('should call service.changeStatus with id and status', async () => {
|
|
101
|
+
const dto = { status: 'approved' };
|
|
102
|
+
const expectedResult = { id: 1, status: 'approved' };
|
|
103
|
+
service.changeStatus.mockResolvedValue(expectedResult);
|
|
104
|
+
const result = await controller.changeStatus?.(1, dto);
|
|
105
|
+
expect(service.changeStatus).toHaveBeenCalledWith(1, 'approved', undefined);
|
|
106
|
+
expect(result).toEqual(expectedResult);
|
|
107
|
+
});
|
|
108
|
+
it('should throw when status endpoint is not available', () => {
|
|
109
|
+
service.changeStatus = undefined;
|
|
110
|
+
expect(() => controller.changeStatus?.(1, { status: 'x' })).toThrow(common_1.NotFoundException);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
98
113
|
describe('remove', () => {
|
|
99
114
|
it('should call service.remove', async () => {
|
|
100
115
|
service.remove.mockResolvedValue(true);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-repository.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/mock-repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,wBAAgB,YAAY,IAAI,gBAAgB,
|
|
1
|
+
{"version":3,"file":"mock-repository.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/mock-repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,wBAAgB,YAAY,IAAI,gBAAgB,CAiB/C;AAED,wBAAgB,oBAAoB,CAAC,OAAO,SAAS,aAAa,EAChE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,EACrB,SAAS,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,GACpD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAkClC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,SAAS,aAAa,EACnE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,GACpB,OAAO,CAuCT"}
|
|
@@ -7,6 +7,7 @@ function createMockQb() {
|
|
|
7
7
|
const qb = {
|
|
8
8
|
where: jest.fn().mockReturnThis(),
|
|
9
9
|
andWhere: jest.fn().mockReturnThis(),
|
|
10
|
+
innerJoin: jest.fn().mockReturnThis(),
|
|
10
11
|
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
|
11
12
|
leftJoin: jest.fn().mockReturnThis(),
|
|
12
13
|
orderBy: jest.fn().mockReturnThis(),
|
|
@@ -33,6 +34,7 @@ function createMockRepository(entity, overrides) {
|
|
|
33
34
|
find: jest.fn().mockResolvedValue([mockEntity]),
|
|
34
35
|
metadata: {
|
|
35
36
|
name: entity.name,
|
|
37
|
+
tableName: entity.name.toLowerCase(),
|
|
36
38
|
primaryColumns: [{ propertyPath: 'id', type: () => Number }],
|
|
37
39
|
},
|
|
38
40
|
manager: {
|
|
@@ -28,7 +28,7 @@ export interface CrudTestConfig<TEntity extends ObjectLiteral, TCreateDto, TUpda
|
|
|
28
28
|
export interface CrudControllerTestConfig<TEntity extends ObjectLiteral, TCreateDto, TUpdateDto, TResponseDto> extends Omit<CrudTestConfig<TEntity, TCreateDto, TUpdateDto, TResponseDto>, 'serviceClass'> {
|
|
29
29
|
controllerFactory: () => Type<any>;
|
|
30
30
|
serviceClass: Type<any>;
|
|
31
|
-
permissions?: Record<CrudEndpoint, string | string[]
|
|
31
|
+
permissions?: Partial<Record<CrudEndpoint, string | string[]>>;
|
|
32
32
|
authOptions?: Record<string, unknown>;
|
|
33
33
|
}
|
|
34
34
|
export interface CrudTestContext {
|
|
@@ -39,6 +39,7 @@ export interface CrudTestContext {
|
|
|
39
39
|
export interface MockQueryBuilder {
|
|
40
40
|
where: jest.Mock;
|
|
41
41
|
andWhere: jest.Mock;
|
|
42
|
+
innerJoin: jest.Mock;
|
|
42
43
|
leftJoinAndSelect: jest.Mock;
|
|
43
44
|
leftJoin: jest.Mock;
|
|
44
45
|
orderBy: jest.Mock;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/testing.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D,MAAM,WAAW,cAAc,CAC7B,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY;IAEZ,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,SAAS,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACjC,cAAc,CAAC,EAAE,SAAS,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC;IAC5D,iBAAiB,CAAC,EAAE,SAAS,CAAC,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC;IAC/D,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC;IACnC,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IAC3D,iBAAiB,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAC5C,KAAK,CAAC,EAAE,kBAAkB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC;IAC3D,aAAa,CAAC,EAAE,CACd,MAAM,EAAE,OAAO,GAAG,OAAO,EAAE,KACxB,YAAY,GAAG,YAAY,EAAE,CAAC;IACnC,SAAS,CAAC,EAAE,kBAAkB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,WAAW,CAAC,CAAC;IACnE,IAAI,EAAE;QACJ,aAAa,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QACnC,aAAa,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;QACnC,UAAU,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAC9B,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;QACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;KAC/D,CAAC;CACH;AAED,MAAM,WAAW,wBAAwB,CACvC,OAAO,SAAS,aAAa,EAC7B,UAAU,EACV,UAAU,EACV,YAAY,CACZ,SAAQ,IAAI,CACV,cAAc,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,CAAC,EAC7D,cAAc,CACf;IACD,iBAAiB,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;IACnC,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"testing.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/testing.interface.ts"],"names":[],"mappings":"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"}
|