@woltz/rich-domain 1.0.0 → 1.2.0

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 (129) hide show
  1. package/CHANGELOG.md +37 -86
  2. package/LICENSE +20 -20
  3. package/dist/base-entity.d.ts +1 -1
  4. package/dist/base-entity.d.ts.map +1 -1
  5. package/dist/base-entity.js +15 -19
  6. package/dist/base-entity.js.map +1 -1
  7. package/dist/constants.js +1 -4
  8. package/dist/constants.js.map +1 -1
  9. package/dist/criteria.d.ts +24 -14
  10. package/dist/criteria.d.ts.map +1 -1
  11. package/dist/criteria.js +154 -67
  12. package/dist/criteria.js.map +1 -1
  13. package/dist/deep-proxy.d.ts.map +1 -1
  14. package/dist/deep-proxy.js +19 -9
  15. package/dist/deep-proxy.js.map +1 -1
  16. package/dist/domain-event-bus.d.ts +5 -6
  17. package/dist/domain-event-bus.d.ts.map +1 -1
  18. package/dist/domain-event-bus.js +4 -17
  19. package/dist/domain-event-bus.js.map +1 -1
  20. package/dist/domain-event.d.ts +1 -31
  21. package/dist/domain-event.d.ts.map +1 -1
  22. package/dist/domain-event.js +4 -7
  23. package/dist/domain-event.js.map +1 -1
  24. package/dist/entity.d.ts +2 -2
  25. package/dist/entity.js +3 -8
  26. package/dist/entity.js.map +1 -1
  27. package/dist/exceptions.d.ts +251 -0
  28. package/dist/exceptions.d.ts.map +1 -0
  29. package/dist/exceptions.js +321 -0
  30. package/dist/exceptions.js.map +1 -0
  31. package/dist/id.d.ts.map +1 -1
  32. package/dist/id.js +14 -7
  33. package/dist/id.js.map +1 -1
  34. package/dist/index.d.ts +2 -4
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +11 -40
  37. package/dist/index.js.map +1 -1
  38. package/dist/mapper.js +1 -5
  39. package/dist/mapper.js.map +1 -1
  40. package/dist/paginated-result.d.ts.map +1 -1
  41. package/dist/paginated-result.js +17 -9
  42. package/dist/paginated-result.js.map +1 -1
  43. package/dist/repository/base-repository.js +4 -11
  44. package/dist/repository/base-repository.js.map +1 -1
  45. package/dist/repository/index.d.ts +1 -2
  46. package/dist/repository/index.d.ts.map +1 -1
  47. package/dist/repository/index.js +3 -26
  48. package/dist/repository/index.js.map +1 -1
  49. package/dist/repository/unit-of-work.d.ts +0 -11
  50. package/dist/repository/unit-of-work.d.ts.map +1 -1
  51. package/dist/repository/unit-of-work.js +2 -43
  52. package/dist/repository/unit-of-work.js.map +1 -1
  53. package/dist/types/criteria.d.ts +31 -7
  54. package/dist/types/criteria.d.ts.map +1 -1
  55. package/dist/types/criteria.js +1 -4
  56. package/dist/types/criteria.js.map +1 -1
  57. package/dist/types/domain-event.d.ts +32 -0
  58. package/dist/types/domain-event.d.ts.map +1 -0
  59. package/dist/types/domain-event.js +2 -0
  60. package/dist/types/domain-event.js.map +1 -0
  61. package/dist/types/domain.d.ts +2 -2
  62. package/dist/types/domain.d.ts.map +1 -1
  63. package/dist/types/domain.js +1 -2
  64. package/dist/types/history-tracker.d.ts +1 -1
  65. package/dist/types/history-tracker.d.ts.map +1 -1
  66. package/dist/types/history-tracker.js +1 -2
  67. package/dist/types/index.d.ts +1 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/dist/types/index.js +7 -22
  70. package/dist/types/index.js.map +1 -1
  71. package/dist/types/standard-schema.js +1 -2
  72. package/dist/types/unit-of-work.js +1 -2
  73. package/dist/types/utils.js +1 -2
  74. package/dist/utils/criteria-operator-validation.d.ts +5 -0
  75. package/dist/utils/criteria-operator-validation.d.ts.map +1 -0
  76. package/dist/utils/criteria-operator-validation.js +143 -0
  77. package/dist/utils/criteria-operator-validation.js.map +1 -0
  78. package/dist/utils/helpers.d.ts +2 -0
  79. package/dist/utils/helpers.d.ts.map +1 -0
  80. package/dist/utils/helpers.js +10 -0
  81. package/dist/utils/helpers.js.map +1 -0
  82. package/dist/validation-error.js +3 -9
  83. package/dist/validation-error.js.map +1 -1
  84. package/dist/value-object.d.ts +1 -1
  85. package/dist/value-object.d.ts.map +1 -1
  86. package/dist/value-object.js +7 -14
  87. package/dist/value-object.js.map +1 -1
  88. package/eslint.config.js +9 -3
  89. package/jest.config.js +1 -1
  90. package/package.json +14 -20
  91. package/src/base-entity.ts +3 -3
  92. package/src/criteria.ts +268 -87
  93. package/src/deep-proxy.ts +50 -38
  94. package/src/domain-event-bus.ts +152 -166
  95. package/src/domain-event.ts +53 -90
  96. package/src/entity.ts +16 -16
  97. package/src/exceptions.ts +435 -0
  98. package/src/id.ts +107 -94
  99. package/src/index.ts +32 -8
  100. package/src/paginated-result.ts +15 -3
  101. package/src/repository/index.ts +1 -6
  102. package/src/repository/unit-of-work.ts +1 -44
  103. package/src/types/criteria.ts +95 -17
  104. package/src/types/domain-event.ts +38 -0
  105. package/src/types/domain.ts +2 -3
  106. package/src/types/history-tracker.ts +1 -1
  107. package/src/types/index.ts +1 -0
  108. package/src/utils/criteria-operator-validation.ts +171 -0
  109. package/src/utils/helpers.ts +6 -0
  110. package/src/validation-error.ts +97 -97
  111. package/src/value-object.ts +3 -6
  112. package/tests/criteria.test.ts +324 -1
  113. package/tests/domain-events.test.ts +431 -445
  114. package/tests/entity-validation.test.ts +1 -1
  115. package/tests/entity.test.ts +33 -33
  116. package/tests/repository.test.ts +4 -2
  117. package/tests/utils.ts +254 -151
  118. package/tests/value-object-validation.test.ts +0 -9
  119. package/tsconfig.json +2 -24
  120. package/.github/workflows/ci.yml +0 -40
  121. package/.husky/commit-msg +0 -1
  122. package/.husky/pre-commit +0 -1
  123. package/.vscode/settings.json +0 -3
  124. package/commitlint.config.js +0 -23
  125. package/dist/repository/in-memory-repository.d.ts +0 -50
  126. package/dist/repository/in-memory-repository.d.ts.map +0 -1
  127. package/dist/repository/in-memory-repository.js +0 -97
  128. package/dist/repository/in-memory-repository.js.map +0 -1
  129. package/src/repository/in-memory-repository.ts +0 -116
@@ -1,6 +1,7 @@
1
1
  import { Pagination } from "../src";
2
2
  import { Criteria } from "../src/criteria";
3
3
  import { PaginatedResult } from "../src/paginated-result";
4
+ import { CriteriaAdapter } from "../src/types";
4
5
  import { Post } from "./utils";
5
6
 
6
7
  interface TestUser {
@@ -12,6 +13,17 @@ interface TestUser {
12
13
  createdAt: Date;
13
14
  }
14
15
 
16
+ interface UserWithPostsDto {
17
+ id: string;
18
+ name: string;
19
+ posts: { title: string; content: string }[];
20
+ leads: {
21
+ contact: {
22
+ name: string;
23
+ };
24
+ }[];
25
+ }
26
+
15
27
  const testUsers: TestUser[] = [
16
28
  {
17
29
  id: "1",
@@ -56,6 +68,20 @@ const testUsers: TestUser[] = [
56
68
  ];
57
69
 
58
70
  describe("Criteria", () => {
71
+ describe("Search", () => {
72
+ it("should search by all items ignoring pagination and limit", () => {
73
+ const criteria = Criteria.create<TestUser>()
74
+ .search(["name"], "Eve")
75
+ .paginate(3, 1);
76
+ const result = PaginatedResult.fromArray(testUsers, criteria);
77
+ expect(result.data).toHaveLength(1);
78
+ expect(result.data[0].name).toBe("Eve");
79
+ expect(result.meta.total).toBe(1);
80
+ expect(result.meta.totalPages).toBe(1);
81
+ expect(result.meta.page).toBe(1);
82
+ expect(result.meta.limit).toBe(1);
83
+ });
84
+ });
59
85
  describe("Fluent API", () => {
60
86
  it("should create empty criteria", () => {
61
87
  const criteria = Criteria.create<TestUser>();
@@ -113,6 +139,14 @@ describe("Criteria", () => {
113
139
  expect(result.data.every((u) => u.status === "inactive")).toBe(true);
114
140
  });
115
141
 
142
+ it("should filter by search", () => {
143
+ const criteria = Criteria.create<TestUser>().search(["name"], "Bob");
144
+
145
+ const result = PaginatedResult.fromArray(testUsers, criteria);
146
+ expect(result.data).toHaveLength(1);
147
+ expect(result.data[0].name).toBe("Bob");
148
+ });
149
+
116
150
  it("should filter by greaterThan", () => {
117
151
  const criteria = Criteria.create<TestUser>().where(
118
152
  "age",
@@ -289,7 +323,7 @@ describe("Criteria", () => {
289
323
 
290
324
  expect(result.data).toHaveLength(2);
291
325
  expect(result.data.map((u) => u.name)).toEqual(["Bob", "Diana"]);
292
- expect(result.meta.total).toBe(3); // Total active users
326
+ expect(result.meta.total).toBe(3);
293
327
  expect(result.meta.totalPages).toBe(2);
294
328
  });
295
329
  });
@@ -429,4 +463,293 @@ describe("Criteria", () => {
429
463
  expect(criteria.getPagination()?.limit).toBe(2);
430
464
  });
431
465
  });
466
+
467
+ describe("Quantifiers", () => {
468
+ describe("Fluent API methods", () => {
469
+ it("should create filter with whereSome", () => {
470
+ const criteria = Criteria.create<UserWithPostsDto>().whereSome(
471
+ "posts.title",
472
+ "contains",
473
+ "test"
474
+ );
475
+
476
+ const filters = criteria.getFilters();
477
+
478
+ expect(filters).toHaveLength(1);
479
+ expect(filters[0].field).toBe("posts.title");
480
+ expect(filters[0].operator).toBe("contains");
481
+ expect(filters[0].value).toBe("test");
482
+ expect(filters[0].options?.quantifier).toBe("some");
483
+ });
484
+
485
+ it("should create filter with whereEvery", () => {
486
+ const criteria = Criteria.create<UserWithPostsDto>().whereEvery(
487
+ "posts.title",
488
+ "contains",
489
+ "test"
490
+ );
491
+
492
+ const filters = criteria.getFilters();
493
+ expect(filters).toHaveLength(1);
494
+ expect(filters[0].options?.quantifier).toBe("every");
495
+ });
496
+
497
+ it("should create filter with whereNone", () => {
498
+ const criteria = Criteria.create<UserWithPostsDto>().whereNone(
499
+ "posts.title",
500
+ "contains",
501
+ "test"
502
+ );
503
+
504
+ const filters = criteria.getFilters();
505
+ expect(filters).toHaveLength(1);
506
+ expect(filters[0].options?.quantifier).toBe("none");
507
+ });
508
+
509
+ it("should create filter without quantifier using where", () => {
510
+ const criteria = Criteria.create<UserWithPostsDto>().where(
511
+ "posts.title",
512
+ "contains",
513
+ "test"
514
+ );
515
+
516
+ const filters = criteria.getFilters();
517
+ expect(filters).toHaveLength(1);
518
+ expect(filters[0].options).toBeUndefined();
519
+ });
520
+ });
521
+
522
+ describe("fromQueryParams with quantifier", () => {
523
+ it("should parse quantifier from query params with @some", () => {
524
+ const queryParams = {
525
+ "posts.title:contains@some": "test",
526
+ };
527
+
528
+ const criteria =
529
+ Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
530
+ const filters = criteria.getFilters();
531
+
532
+ expect(filters).toHaveLength(1);
533
+ expect(filters[0].field).toBe("posts.title");
534
+ expect(filters[0].operator).toBe("contains");
535
+ expect(filters[0].value).toBe("test");
536
+ expect(filters[0].options?.quantifier).toBe("some");
537
+ });
538
+
539
+ it("should parse quantifier from query params with @every", () => {
540
+ const queryParams = {
541
+ "posts.title:equals@every": "test",
542
+ };
543
+
544
+ const criteria =
545
+ Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
546
+ const filters = criteria.getFilters();
547
+
548
+ expect(filters).toHaveLength(1);
549
+ expect(filters[0].options?.quantifier).toBe("every");
550
+ });
551
+
552
+ it("should parse quantifier from query params with @none", () => {
553
+ const queryParams = {
554
+ "posts.title:contains@none": "spam",
555
+ };
556
+
557
+ const criteria =
558
+ Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
559
+ const filters = criteria.getFilters();
560
+
561
+ expect(filters).toHaveLength(1);
562
+ expect(filters[0].options?.quantifier).toBe("none");
563
+ });
564
+
565
+ it("should work with in operator and quantifier", () => {
566
+ const queryParams = {
567
+ "posts.title:in@some": "test1,test2,test3",
568
+ };
569
+
570
+ const criteria =
571
+ Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
572
+ const filters = criteria.getFilters();
573
+
574
+ expect(filters).toHaveLength(1);
575
+ expect(filters[0].operator).toBe("in");
576
+ expect(filters[0].value).toEqual(["test1", "test2", "test3"]);
577
+ expect(filters[0].options?.quantifier).toBe("some");
578
+ });
579
+
580
+ it("should work with between operator and quantifier", () => {
581
+ const queryParams = {
582
+ "posts.likes:between@some": "10,100",
583
+ };
584
+
585
+ const criteria =
586
+ Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
587
+ const filters = criteria.getFilters();
588
+
589
+ expect(filters).toHaveLength(1);
590
+ expect(filters[0].operator).toBe("between");
591
+ expect(filters[0].value).toEqual([10, 100]);
592
+ expect(filters[0].options?.quantifier).toBe("some");
593
+ });
594
+
595
+ it("should maintain backward compatibility without quantifier", () => {
596
+ const queryParams = {
597
+ "posts.title:contains": "test",
598
+ };
599
+
600
+ const criteria =
601
+ Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
602
+ const filters = criteria.getFilters();
603
+
604
+ expect(filters).toHaveLength(1);
605
+ expect(filters[0].field).toBe("posts.title");
606
+ expect(filters[0].operator).toBe("contains");
607
+ expect(filters[0].value).toBe("test");
608
+ expect(filters[0].options).toBeUndefined();
609
+ });
610
+
611
+ it("should throw error for invalid quantifier", () => {
612
+ const queryParams = {
613
+ "posts.title:contains@invalid": "test",
614
+ };
615
+
616
+ expect(() => {
617
+ Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
618
+ }).toThrow("Invalid quantifier");
619
+ });
620
+
621
+ it("should handle multiple filters with mixed quantifiers", () => {
622
+ const queryParams = {
623
+ "posts.title:contains@some": "test",
624
+ "posts.content:equals@every": "content",
625
+ "name:contains": "John",
626
+ };
627
+
628
+ const criteria =
629
+ Criteria.fromQueryParams<UserWithPostsDto>(queryParams);
630
+ const filters = criteria.getFilters();
631
+
632
+ expect(filters).toHaveLength(3);
633
+ expect(filters[0].options?.quantifier).toBe("some");
634
+ expect(filters[1].options?.quantifier).toBe("every");
635
+ expect(filters[2].options).toBeUndefined();
636
+ });
637
+ });
638
+
639
+ describe("fromObject with quantifier", () => {
640
+ it("should create criteria from object with quantifier", () => {
641
+ const criteria = Criteria.fromObject<UserWithPostsDto>({
642
+ filters: [
643
+ {
644
+ field: "posts.title",
645
+ operator: "contains",
646
+ value: "test",
647
+ options: { quantifier: "some" },
648
+ },
649
+ ],
650
+ });
651
+
652
+ const filters = criteria.getFilters();
653
+ expect(filters).toHaveLength(1);
654
+ expect(filters[0].options?.quantifier).toBe("some");
655
+ });
656
+
657
+ it("should preserve quantifier when cloning", () => {
658
+ const original = Criteria.create<UserWithPostsDto>().whereSome(
659
+ "posts.title",
660
+ "contains",
661
+ "test"
662
+ );
663
+
664
+ const cloned = original.clone();
665
+ const filters = cloned.getFilters();
666
+
667
+ expect(filters).toHaveLength(1);
668
+ expect(filters[0].options?.quantifier).toBe("some");
669
+ });
670
+ });
671
+
672
+ describe("toJSON with quantifier", () => {
673
+ it("should serialize quantifier to JSON", () => {
674
+ const criteria = Criteria.create<UserWithPostsDto>()
675
+ .whereSome("posts.title", "contains", "test")
676
+ .whereEvery("posts.content", "equals", "content");
677
+
678
+ const json = criteria.toJSON();
679
+
680
+ expect(json.filters).toHaveLength(2);
681
+ expect(json.filters[0].options?.quantifier).toBe("some");
682
+ expect(json.filters[1].options?.quantifier).toBe("every");
683
+ });
684
+ });
685
+ });
686
+
687
+ describe("Adapter", () => {
688
+ type UserInDatabase = {
689
+ id: string;
690
+ name: string;
691
+ user_posts: { title: string; content: string }[];
692
+ leads: {
693
+ contact: {
694
+ fullName: string;
695
+ };
696
+ };
697
+ };
698
+ const UserWithPostsAdapter: CriteriaAdapter<
699
+ UserWithPostsDto,
700
+ UserInDatabase
701
+ > = {
702
+ posts: "user_posts",
703
+ "leads.contact.name": "leads.contact.fullName",
704
+ };
705
+
706
+ let criteria: Criteria<UserWithPostsDto>;
707
+ beforeEach(() => {
708
+ criteria =
709
+ Criteria.create<UserWithPostsDto>().useAdapter(UserWithPostsAdapter);
710
+ });
711
+
712
+ it("should resolve field path", () => {
713
+ const filters = criteria
714
+ .where("leads.contact.name", "contains", "John")
715
+ .getFilters();
716
+
717
+ expect(filters[0].field).toBe("leads.contact.fullName");
718
+ });
719
+
720
+ it("should resolve field path from query params", () => {
721
+ const queryParams = {
722
+ "posts:contains": "test",
723
+ };
724
+
725
+ const result = Criteria.fromQueryParams<UserWithPostsDto>(
726
+ queryParams,
727
+ UserWithPostsAdapter
728
+ );
729
+ const filters = result.getFilters();
730
+
731
+ expect(filters).toHaveLength(1);
732
+ expect(filters[0].field).toBe("user_posts");
733
+ expect(filters[0].operator).toBe("contains");
734
+ expect(filters[0].value).toBe("test");
735
+ });
736
+
737
+ it("should resolve field path from object", () => {
738
+ const criteria = Criteria.fromObject<UserWithPostsDto>(
739
+ {
740
+ filters: [
741
+ {
742
+ field: "posts",
743
+ operator: "in",
744
+ value: [{ title: "test", content: "test" }],
745
+ },
746
+ ],
747
+ },
748
+ UserWithPostsAdapter
749
+ );
750
+ const filters = criteria.getFilters();
751
+ expect(filters).toHaveLength(1);
752
+ expect(filters[0].field).toBe("user_posts");
753
+ });
754
+ });
432
755
  });