@declaro/data 2.0.0-beta.120 → 2.0.0-beta.125

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 (47) hide show
  1. package/dist/browser/index.js +14 -14
  2. package/dist/browser/index.js.map +11 -11
  3. package/dist/node/index.cjs +163 -45
  4. package/dist/node/index.cjs.map +11 -11
  5. package/dist/node/index.js +163 -45
  6. package/dist/node/index.js.map +11 -11
  7. package/dist/ts/application/model-controller.d.ts +22 -1
  8. package/dist/ts/application/model-controller.d.ts.map +1 -1
  9. package/dist/ts/domain/events/domain-event.d.ts +1 -1
  10. package/dist/ts/domain/events/domain-event.d.ts.map +1 -1
  11. package/dist/ts/domain/events/event-types.d.ts +10 -1
  12. package/dist/ts/domain/events/event-types.d.ts.map +1 -1
  13. package/dist/ts/domain/events/mutation-event.d.ts +5 -2
  14. package/dist/ts/domain/events/mutation-event.d.ts.map +1 -1
  15. package/dist/ts/domain/events/query-event.d.ts +4 -2
  16. package/dist/ts/domain/events/query-event.d.ts.map +1 -1
  17. package/dist/ts/domain/events/request-event.d.ts +17 -2
  18. package/dist/ts/domain/events/request-event.d.ts.map +1 -1
  19. package/dist/ts/domain/interfaces/repository.d.ts +26 -0
  20. package/dist/ts/domain/interfaces/repository.d.ts.map +1 -1
  21. package/dist/ts/domain/services/model-service.d.ts +19 -1
  22. package/dist/ts/domain/services/model-service.d.ts.map +1 -1
  23. package/dist/ts/domain/services/read-only-model-service.d.ts +19 -0
  24. package/dist/ts/domain/services/read-only-model-service.d.ts.map +1 -1
  25. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts +21 -3
  26. package/dist/ts/test/mock/repositories/mock-memory-repository.d.ts.map +1 -1
  27. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts +2 -0
  28. package/dist/ts/test/mock/repositories/mock-memory-repository.trash.test.d.ts.map +1 -0
  29. package/package.json +5 -5
  30. package/src/application/model-controller.test.ts +191 -0
  31. package/src/application/model-controller.ts +44 -1
  32. package/src/domain/events/domain-event.ts +1 -1
  33. package/src/domain/events/event-types.ts +9 -0
  34. package/src/domain/events/mutation-event.test.ts +369 -17
  35. package/src/domain/events/mutation-event.ts +10 -2
  36. package/src/domain/events/query-event.test.ts +218 -18
  37. package/src/domain/events/query-event.ts +8 -2
  38. package/src/domain/events/request-event.test.ts +1 -1
  39. package/src/domain/events/request-event.ts +22 -7
  40. package/src/domain/interfaces/repository.ts +29 -0
  41. package/src/domain/services/model-service.normalization.test.ts +6 -6
  42. package/src/domain/services/model-service.test.ts +311 -7
  43. package/src/domain/services/model-service.ts +88 -1
  44. package/src/domain/services/read-only-model-service.test.ts +396 -0
  45. package/src/domain/services/read-only-model-service.ts +23 -3
  46. package/src/test/mock/repositories/mock-memory-repository.trash.test.ts +736 -0
  47. package/src/test/mock/repositories/mock-memory-repository.ts +146 -46
@@ -429,4 +429,400 @@ describe('ReadOnlyModelService', () => {
429
429
  expect(typeof record.publishedDate).toBe('string')
430
430
  })
431
431
  })
432
+
433
+ describe('Trash Functionality', () => {
434
+ beforeEach(async () => {
435
+ repository = new MockMemoryRepository({ schema: mockSchema })
436
+ emitter = new EventManager()
437
+ service = new ReadOnlyModelService({ repository, emitter, schema: mockSchema, namespace })
438
+ })
439
+
440
+ describe('load with trash options', () => {
441
+ it('should not load removed items by default', async () => {
442
+ const input = { id: 1, title: 'Book to Remove', author: 'Author', publishedDate: new Date() }
443
+ await repository.create(input)
444
+ await repository.remove({ id: 1 })
445
+
446
+ const record = await service.load({ id: 1 })
447
+ expect(record).toBeNull()
448
+ })
449
+
450
+ it('should load removed items with removedOnly option', async () => {
451
+ const input = { id: 2, title: 'Removed Book', author: 'Author', publishedDate: new Date() }
452
+ await repository.create(input)
453
+ await repository.remove({ id: 2 })
454
+
455
+ const record = await service.load({ id: 2 }, { removedOnly: true })
456
+ expect(record).not.toBeNull()
457
+ expect(record?.title).toBe('Removed Book')
458
+ })
459
+
460
+ it('should not load active items with removedOnly option', async () => {
461
+ const input = { id: 3, title: 'Active Book', author: 'Author', publishedDate: new Date() }
462
+ await repository.create(input)
463
+
464
+ const record = await service.load({ id: 3 }, { removedOnly: true })
465
+ expect(record).toBeNull()
466
+ })
467
+
468
+ it('should load removed items with includeRemoved option', async () => {
469
+ const input = { id: 4, title: 'Removed Book', author: 'Author', publishedDate: new Date() }
470
+ await repository.create(input)
471
+ await repository.remove({ id: 4 })
472
+
473
+ const record = await service.load({ id: 4 }, { includeRemoved: true })
474
+ expect(record).not.toBeNull()
475
+ expect(record?.title).toBe('Removed Book')
476
+ })
477
+
478
+ it('should load active items with includeRemoved option', async () => {
479
+ const input = { id: 5, title: 'Active Book', author: 'Author', publishedDate: new Date() }
480
+ await repository.create(input)
481
+
482
+ const record = await service.load({ id: 5 }, { includeRemoved: true })
483
+ expect(record).not.toBeNull()
484
+ expect(record?.title).toBe('Active Book')
485
+ })
486
+ })
487
+
488
+ describe('search with trash options', () => {
489
+ it('should not return removed items by default', async () => {
490
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
491
+ await repository.create({
492
+ id: 2,
493
+ title: 'Removed Book',
494
+ author: 'Author 2',
495
+ publishedDate: new Date(),
496
+ })
497
+ await repository.remove({ id: 2 })
498
+
499
+ const results = await service.search({})
500
+ expect(results.results).toHaveLength(1)
501
+ expect(results.results[0].title).toBe('Active Book')
502
+ })
503
+
504
+ it('should return only removed items with removedOnly option', async () => {
505
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
506
+ await repository.create({
507
+ id: 2,
508
+ title: 'Removed Book 1',
509
+ author: 'Author 2',
510
+ publishedDate: new Date(),
511
+ })
512
+ await repository.create({
513
+ id: 3,
514
+ title: 'Removed Book 2',
515
+ author: 'Author 3',
516
+ publishedDate: new Date(),
517
+ })
518
+
519
+ await repository.remove({ id: 2 })
520
+ await repository.remove({ id: 3 })
521
+
522
+ const results = await service.search({}, { removedOnly: true })
523
+ expect(results.results).toHaveLength(2)
524
+ expect(results.results.every((book) => book.title.startsWith('Removed'))).toBe(true)
525
+ })
526
+
527
+ it('should return both active and removed items with includeRemoved option', async () => {
528
+ await repository.create({
529
+ id: 1,
530
+ title: 'Active Book 1',
531
+ author: 'Author 1',
532
+ publishedDate: new Date(),
533
+ })
534
+ await repository.create({
535
+ id: 2,
536
+ title: 'Active Book 2',
537
+ author: 'Author 2',
538
+ publishedDate: new Date(),
539
+ })
540
+ await repository.create({
541
+ id: 3,
542
+ title: 'Removed Book',
543
+ author: 'Author 3',
544
+ publishedDate: new Date(),
545
+ })
546
+
547
+ await repository.remove({ id: 3 })
548
+
549
+ const results = await service.search({}, { includeRemoved: true })
550
+ expect(results.results).toHaveLength(3)
551
+ })
552
+
553
+ it('should filter removed items with removedOnly option', async () => {
554
+ const repositoryWithFilter = new MockMemoryRepository({
555
+ schema: mockSchema,
556
+ filter: (data, filters) => {
557
+ if (filters.text) {
558
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
559
+ }
560
+ return true
561
+ },
562
+ })
563
+
564
+ const serviceWithFilter = new ReadOnlyModelService({
565
+ repository: repositoryWithFilter,
566
+ emitter,
567
+ namespace,
568
+ schema: mockSchema,
569
+ })
570
+
571
+ const removed1 = await repositoryWithFilter.create({
572
+ title: 'Test Removed Book',
573
+ author: 'Author 1',
574
+ publishedDate: new Date(),
575
+ })
576
+ const removed2 = await repositoryWithFilter.create({
577
+ title: 'Other Removed Book',
578
+ author: 'Author 2',
579
+ publishedDate: new Date(),
580
+ })
581
+
582
+ await repositoryWithFilter.remove({ id: removed1.id })
583
+ await repositoryWithFilter.remove({ id: removed2.id })
584
+
585
+ const results = await serviceWithFilter.search({ text: 'Test' }, { removedOnly: true })
586
+ expect(results.results).toHaveLength(1)
587
+ expect(results.results[0].title).toBe('Test Removed Book')
588
+ })
589
+
590
+ it('should filter across active and removed items with includeRemoved option', async () => {
591
+ const repositoryWithFilter = new MockMemoryRepository({
592
+ schema: mockSchema,
593
+ filter: (data, filters) => {
594
+ if (filters.text) {
595
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
596
+ }
597
+ return true
598
+ },
599
+ })
600
+
601
+ const serviceWithFilter = new ReadOnlyModelService({
602
+ repository: repositoryWithFilter,
603
+ emitter,
604
+ namespace,
605
+ schema: mockSchema,
606
+ })
607
+
608
+ await repositoryWithFilter.create({
609
+ title: 'Test Active Book',
610
+ author: 'Author 1',
611
+ publishedDate: new Date(),
612
+ })
613
+ const removed = await repositoryWithFilter.create({
614
+ title: 'Test Removed Book',
615
+ author: 'Author 2',
616
+ publishedDate: new Date(),
617
+ })
618
+ await repositoryWithFilter.create({
619
+ title: 'Other Book',
620
+ author: 'Author 3',
621
+ publishedDate: new Date(),
622
+ })
623
+
624
+ await repositoryWithFilter.remove({ id: removed.id })
625
+
626
+ const results = await serviceWithFilter.search({ text: 'Test' }, { includeRemoved: true })
627
+ expect(results.results).toHaveLength(2)
628
+ expect(results.results.some((book) => book.title === 'Test Active Book')).toBe(true)
629
+ expect(results.results.some((book) => book.title === 'Test Removed Book')).toBe(true)
630
+ })
631
+ })
632
+
633
+ describe('count with trash options', () => {
634
+ it('should count only active items by default', async () => {
635
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
636
+ await repository.create({
637
+ id: 2,
638
+ title: 'Removed Book',
639
+ author: 'Author 2',
640
+ publishedDate: new Date(),
641
+ })
642
+ await repository.remove({ id: 2 })
643
+
644
+ const count = await service.count({})
645
+ expect(count).toBe(1)
646
+ })
647
+
648
+ it('should count only removed items with removedOnly option', async () => {
649
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
650
+ await repository.create({
651
+ id: 2,
652
+ title: 'Removed Book 1',
653
+ author: 'Author 2',
654
+ publishedDate: new Date(),
655
+ })
656
+ await repository.create({
657
+ id: 3,
658
+ title: 'Removed Book 2',
659
+ author: 'Author 3',
660
+ publishedDate: new Date(),
661
+ })
662
+
663
+ await repository.remove({ id: 2 })
664
+ await repository.remove({ id: 3 })
665
+
666
+ const count = await service.count({}, { removedOnly: true })
667
+ expect(count).toBe(2)
668
+ })
669
+
670
+ it('should count both active and removed items with includeRemoved option', async () => {
671
+ await repository.create({ id: 1, title: 'Active Book', author: 'Author 1', publishedDate: new Date() })
672
+ await repository.create({
673
+ id: 2,
674
+ title: 'Removed Book',
675
+ author: 'Author 2',
676
+ publishedDate: new Date(),
677
+ })
678
+ await repository.remove({ id: 2 })
679
+
680
+ const count = await service.count({}, { includeRemoved: true })
681
+ expect(count).toBe(2)
682
+ })
683
+
684
+ it('should count filtered active items by default', async () => {
685
+ const repositoryWithFilter = new MockMemoryRepository({
686
+ schema: mockSchema,
687
+ filter: (data, filters) => {
688
+ if (filters.text) {
689
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
690
+ }
691
+ return true
692
+ },
693
+ })
694
+
695
+ const serviceWithFilter = new ReadOnlyModelService({
696
+ repository: repositoryWithFilter,
697
+ emitter,
698
+ namespace,
699
+ schema: mockSchema,
700
+ })
701
+
702
+ await repositoryWithFilter.create({
703
+ title: 'Test Book 1',
704
+ author: 'Author 1',
705
+ publishedDate: new Date(),
706
+ })
707
+ await repositoryWithFilter.create({
708
+ title: 'Test Book 2',
709
+ author: 'Author 2',
710
+ publishedDate: new Date(),
711
+ })
712
+ await repositoryWithFilter.create({
713
+ title: 'Other Book',
714
+ author: 'Author 3',
715
+ publishedDate: new Date(),
716
+ })
717
+ const removed = await repositoryWithFilter.create({
718
+ title: 'Test Book 3',
719
+ author: 'Author 4',
720
+ publishedDate: new Date(),
721
+ })
722
+
723
+ await repositoryWithFilter.remove({ id: removed.id })
724
+
725
+ const count = await serviceWithFilter.count({ text: 'Test' })
726
+ expect(count).toBe(2)
727
+ })
728
+
729
+ it('should count filtered removed items with removedOnly option', async () => {
730
+ const repositoryWithFilter = new MockMemoryRepository({
731
+ schema: mockSchema,
732
+ filter: (data, filters) => {
733
+ if (filters.text) {
734
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
735
+ }
736
+ return true
737
+ },
738
+ })
739
+
740
+ const serviceWithFilter = new ReadOnlyModelService({
741
+ repository: repositoryWithFilter,
742
+ emitter,
743
+ namespace,
744
+ schema: mockSchema,
745
+ })
746
+
747
+ const removed1 = await repositoryWithFilter.create({
748
+ title: 'Test Removed Book 1',
749
+ author: 'Author 1',
750
+ publishedDate: new Date(),
751
+ })
752
+ const removed2 = await repositoryWithFilter.create({
753
+ title: 'Test Removed Book 2',
754
+ author: 'Author 2',
755
+ publishedDate: new Date(),
756
+ })
757
+ const removed3 = await repositoryWithFilter.create({
758
+ title: 'Other Removed Book',
759
+ author: 'Author 3',
760
+ publishedDate: new Date(),
761
+ })
762
+ await repositoryWithFilter.create({
763
+ title: 'Test Active Book',
764
+ author: 'Author 4',
765
+ publishedDate: new Date(),
766
+ })
767
+
768
+ await repositoryWithFilter.remove({ id: removed1.id })
769
+ await repositoryWithFilter.remove({ id: removed2.id })
770
+ await repositoryWithFilter.remove({ id: removed3.id })
771
+
772
+ const count = await serviceWithFilter.count({ text: 'Test' }, { removedOnly: true })
773
+ expect(count).toBe(2)
774
+ })
775
+
776
+ it('should count filtered items across active and removed with includeRemoved option', async () => {
777
+ const repositoryWithFilter = new MockMemoryRepository({
778
+ schema: mockSchema,
779
+ filter: (data, filters) => {
780
+ if (filters.text) {
781
+ return data.title.toLowerCase().includes(filters.text.toLowerCase())
782
+ }
783
+ return true
784
+ },
785
+ })
786
+
787
+ const serviceWithFilter = new ReadOnlyModelService({
788
+ repository: repositoryWithFilter,
789
+ emitter,
790
+ namespace,
791
+ schema: mockSchema,
792
+ })
793
+
794
+ await repositoryWithFilter.create({
795
+ title: 'Test Active Book 1',
796
+ author: 'Author 1',
797
+ publishedDate: new Date(),
798
+ })
799
+ await repositoryWithFilter.create({
800
+ title: 'Test Active Book 2',
801
+ author: 'Author 2',
802
+ publishedDate: new Date(),
803
+ })
804
+ const removed1 = await repositoryWithFilter.create({
805
+ title: 'Test Removed Book 1',
806
+ author: 'Author 3',
807
+ publishedDate: new Date(),
808
+ })
809
+ const removed2 = await repositoryWithFilter.create({
810
+ title: 'Test Removed Book 2',
811
+ author: 'Author 4',
812
+ publishedDate: new Date(),
813
+ })
814
+ await repositoryWithFilter.create({
815
+ title: 'Other Active Book',
816
+ author: 'Author 5',
817
+ publishedDate: new Date(),
818
+ })
819
+
820
+ await repositoryWithFilter.remove({ id: removed1.id })
821
+ await repositoryWithFilter.remove({ id: removed2.id })
822
+
823
+ const count = await serviceWithFilter.count({ text: 'Test' }, { includeRemoved: true })
824
+ expect(count).toBe(4)
825
+ })
826
+ })
827
+ })
432
828
  })
@@ -11,10 +11,30 @@ import { QueryEvent } from '../events/query-event'
11
11
  import { BaseModelService, type IActionOptions } from './base-model-service'
12
12
  import type { IPaginationInput } from '../models/pagination'
13
13
 
14
- export interface ILoadOptions extends IActionOptions {}
14
+ /**
15
+ * Options for loading records.
16
+ */
17
+ export interface ILoadOptions extends IActionOptions {
18
+ /**
19
+ * If true, only removed (soft-deleted) records will be returned.
20
+ */
21
+ removedOnly?: boolean
22
+ /**
23
+ * If true, both removed and non-removed records will be returned.
24
+ */
25
+ includeRemoved?: boolean
26
+ }
15
27
  export interface ISearchOptions<TSchema extends AnyModelSchema> extends IActionOptions {
16
28
  pagination?: IPaginationInput
17
29
  sort?: InferSort<TSchema>
30
+ /**
31
+ * If true, only removed (soft-deleted) records will be returned.
32
+ */
33
+ removedOnly?: boolean
34
+ /**
35
+ * If true, both removed and non-removed records will be returned.
36
+ */
37
+ includeRemoved?: boolean
18
38
  }
19
39
 
20
40
  export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseModelService<TSchema> {
@@ -56,7 +76,7 @@ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseMo
56
76
  await this.emitter.emitAsync(beforeLoadEvent)
57
77
 
58
78
  // Load the details from the repository
59
- const details = await this.repository.load(lookup)
79
+ const details = await this.repository.load(lookup, options)
60
80
 
61
81
  // Emit the after load event
62
82
  const afterLoadEvent = new QueryEvent<InferDetail<TSchema>, InferLookup<TSchema>>(
@@ -83,7 +103,7 @@ export class ReadOnlyModelService<TSchema extends AnyModelSchema> extends BaseMo
83
103
  await this.emitter.emitAsync(beforeLoadManyEvent)
84
104
 
85
105
  // Load the details from the repository
86
- const details = await this.repository.loadMany(lookups)
106
+ const details = await this.repository.loadMany(lookups, options)
87
107
 
88
108
  // Emit the after load many event
89
109
  const afterLoadManyEvent = new QueryEvent<InferDetail<TSchema>[], InferLookup<TSchema>[]>(