@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.
Files changed (38) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -0
  4. package/dist/lib/controllers/nest-crud.controller.d.ts +11 -0
  5. package/dist/lib/controllers/nest-crud.controller.d.ts.map +1 -1
  6. package/dist/lib/controllers/nest-crud.controller.js +113 -0
  7. package/dist/lib/dtos/request-modification.dto.d.ts +11 -0
  8. package/dist/lib/dtos/request-modification.dto.d.ts.map +1 -0
  9. package/dist/lib/dtos/request-modification.dto.js +57 -0
  10. package/dist/lib/dtos/status-change.dto.d.ts +5 -0
  11. package/dist/lib/dtos/status-change.dto.d.ts.map +1 -0
  12. package/dist/lib/dtos/status-change.dto.js +16 -0
  13. package/dist/lib/entities/approval-status.entity.d.ts +29 -0
  14. package/dist/lib/entities/approval-status.entity.d.ts.map +1 -0
  15. package/dist/lib/entities/approval-status.entity.js +72 -0
  16. package/dist/lib/entities/modification-request-history.entity.d.ts +20 -0
  17. package/dist/lib/entities/modification-request-history.entity.d.ts.map +1 -0
  18. package/dist/lib/entities/modification-request-history.entity.js +48 -0
  19. package/dist/lib/interfaces/approval-pipeline.interface.d.ts +90 -0
  20. package/dist/lib/interfaces/approval-pipeline.interface.d.ts.map +1 -0
  21. package/dist/lib/interfaces/approval-pipeline.interface.js +20 -0
  22. package/dist/lib/interfaces/crud.interface.d.ts +12 -1
  23. package/dist/lib/interfaces/crud.interface.d.ts.map +1 -1
  24. package/dist/lib/interfaces/status-pipeline.interface.d.ts +63 -0
  25. package/dist/lib/interfaces/status-pipeline.interface.d.ts.map +1 -0
  26. package/dist/lib/interfaces/status-pipeline.interface.js +2 -0
  27. package/dist/lib/nest-crud.module.d.ts.map +1 -1
  28. package/dist/lib/nest-crud.module.js +9 -1
  29. package/dist/lib/services/nest-crud.service.d.ts +44 -0
  30. package/dist/lib/services/nest-crud.service.d.ts.map +1 -1
  31. package/dist/lib/services/nest-crud.service.js +345 -1
  32. package/dist/lib/testing/crud-controller.test-suites.d.ts.map +1 -1
  33. package/dist/lib/testing/crud-controller.test-suites.js +15 -0
  34. package/dist/lib/testing/mock-repository.d.ts.map +1 -1
  35. package/dist/lib/testing/mock-repository.js +2 -0
  36. package/dist/lib/testing/testing.interface.d.ts +2 -1
  37. package/dist/lib/testing/testing.interface.d.ts.map +1 -1
  38. 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 = await this.repo.save(resolved);
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,CAuNjB"}
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,CAgB/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,CAiClC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,SAAS,aAAa,EACnE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,GACpB,OAAO,CAuCT"}
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":"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"}
1
+ {"version":3,"file":"testing.interface.d.ts","sourceRoot":"","sources":["../../../src/lib/testing/testing.interface.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nest-util/nest-crud",
3
- "version": "1.1.4",
3
+ "version": "1.2.1",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",