@woltz/rich-domain 1.9.1 → 1.9.2

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 (95) hide show
  1. package/dist/cjs/core/aggregate-changes.d.ts +14 -0
  2. package/dist/cjs/core/aggregate-changes.d.ts.map +1 -1
  3. package/dist/cjs/core/aggregate-changes.js +18 -0
  4. package/dist/cjs/core/aggregate-changes.js.map +1 -1
  5. package/dist/cjs/core/base-entity.d.ts +2 -0
  6. package/dist/cjs/core/base-entity.d.ts.map +1 -1
  7. package/dist/cjs/core/base-entity.js +39 -41
  8. package/dist/cjs/core/base-entity.js.map +1 -1
  9. package/dist/cjs/core/change-tracker.d.ts +8 -0
  10. package/dist/cjs/core/change-tracker.d.ts.map +1 -1
  11. package/dist/cjs/core/change-tracker.js +36 -6
  12. package/dist/cjs/core/change-tracker.js.map +1 -1
  13. package/dist/cjs/core/value-object.d.ts.map +1 -1
  14. package/dist/cjs/core/value-object.js +3 -5
  15. package/dist/cjs/core/value-object.js.map +1 -1
  16. package/dist/cjs/repository/entity-schema-registry.d.ts +56 -3
  17. package/dist/cjs/repository/entity-schema-registry.d.ts.map +1 -1
  18. package/dist/cjs/repository/entity-schema-registry.js +61 -6
  19. package/dist/cjs/repository/entity-schema-registry.js.map +1 -1
  20. package/dist/cjs/utils/helpers.d.ts +1 -0
  21. package/dist/cjs/utils/helpers.d.ts.map +1 -1
  22. package/dist/cjs/utils/helpers.js +4 -0
  23. package/dist/cjs/utils/helpers.js.map +1 -1
  24. package/dist/esm/core/aggregate-changes.d.ts +14 -0
  25. package/dist/esm/core/aggregate-changes.d.ts.map +1 -1
  26. package/dist/esm/core/aggregate-changes.js +18 -0
  27. package/dist/esm/core/aggregate-changes.js.map +1 -1
  28. package/dist/esm/core/base-entity.d.ts +2 -0
  29. package/dist/esm/core/base-entity.d.ts.map +1 -1
  30. package/dist/esm/core/base-entity.js +37 -39
  31. package/dist/esm/core/base-entity.js.map +1 -1
  32. package/dist/esm/core/change-tracker.d.ts +8 -0
  33. package/dist/esm/core/change-tracker.d.ts.map +1 -1
  34. package/dist/esm/core/change-tracker.js +36 -6
  35. package/dist/esm/core/change-tracker.js.map +1 -1
  36. package/dist/esm/core/value-object.d.ts.map +1 -1
  37. package/dist/esm/core/value-object.js +1 -3
  38. package/dist/esm/core/value-object.js.map +1 -1
  39. package/dist/esm/repository/entity-schema-registry.d.ts +56 -3
  40. package/dist/esm/repository/entity-schema-registry.d.ts.map +1 -1
  41. package/dist/esm/repository/entity-schema-registry.js +61 -6
  42. package/dist/esm/repository/entity-schema-registry.js.map +1 -1
  43. package/dist/esm/utils/helpers.d.ts +1 -0
  44. package/dist/esm/utils/helpers.d.ts.map +1 -1
  45. package/dist/esm/utils/helpers.js +3 -0
  46. package/dist/esm/utils/helpers.js.map +1 -1
  47. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  48. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  49. package/dist/tsconfig.types.tsbuildinfo +1 -1
  50. package/dist/types/core/aggregate-changes.d.ts +14 -0
  51. package/dist/types/core/aggregate-changes.d.ts.map +1 -1
  52. package/dist/types/core/base-entity.d.ts +2 -0
  53. package/dist/types/core/base-entity.d.ts.map +1 -1
  54. package/dist/types/core/change-tracker.d.ts +8 -0
  55. package/dist/types/core/change-tracker.d.ts.map +1 -1
  56. package/dist/types/core/value-object.d.ts.map +1 -1
  57. package/dist/types/repository/entity-schema-registry.d.ts +56 -3
  58. package/dist/types/repository/entity-schema-registry.d.ts.map +1 -1
  59. package/dist/types/utils/helpers.d.ts +1 -0
  60. package/dist/types/utils/helpers.d.ts.map +1 -1
  61. package/package.json +68 -67
  62. package/src/constants.ts +82 -0
  63. package/src/core/aggregate-changes.ts +466 -0
  64. package/src/core/base-aggregate.ts +76 -0
  65. package/src/core/base-entity.ts +552 -0
  66. package/src/core/change-tracker.ts +1327 -0
  67. package/src/core/domain-event.ts +41 -0
  68. package/src/core/entity-changes.ts +146 -0
  69. package/src/core/entity.ts +13 -0
  70. package/src/core/id.ts +124 -0
  71. package/src/core/index.ts +9 -0
  72. package/src/core/value-object.ts +179 -0
  73. package/src/criteria.ts +574 -0
  74. package/src/exceptions.ts +549 -0
  75. package/src/index.ts +74 -0
  76. package/src/repository/base-repository.ts +81 -0
  77. package/src/repository/entity-schema-registry.ts +620 -0
  78. package/src/repository/index.ts +5 -0
  79. package/src/repository/mapper.ts +7 -0
  80. package/src/repository/paginated-result.ts +251 -0
  81. package/src/repository/unit-of-work.ts +76 -0
  82. package/src/types/change-tracker.ts +268 -0
  83. package/src/types/criteria.ts +197 -0
  84. package/src/types/domain-event.ts +29 -0
  85. package/src/types/domain.ts +41 -0
  86. package/src/types/event-bus.ts +17 -0
  87. package/src/types/index.ts +9 -0
  88. package/src/types/outbox-store.ts +97 -0
  89. package/src/types/standard-schema.ts +19 -0
  90. package/src/types/unit-of-work.ts +46 -0
  91. package/src/types/utils.ts +24 -0
  92. package/src/utils/criteria-operator-validation.ts +209 -0
  93. package/src/utils/crypto.ts +31 -0
  94. package/src/utils/helpers.ts +50 -0
  95. package/src/validation-error.ts +219 -0
@@ -0,0 +1,41 @@
1
+ import { IDomainEvent } from "..";
2
+ import UUID from "../utils/crypto";
3
+
4
+ /**
5
+ * Base class for domain events
6
+ */
7
+ export abstract class DomainEvent<P> implements IDomainEvent<P> {
8
+ public readonly eventId: string;
9
+ public readonly occurredOn: Date;
10
+ public readonly payload: P;
11
+ static readonly queueName?: string;
12
+
13
+ constructor(payload: P) {
14
+ this.eventId = this.generateEventId();
15
+ this.occurredOn = new Date();
16
+ this.payload = payload;
17
+ }
18
+
19
+ /**
20
+ * Get the event name (defaults to class name)
21
+ */
22
+ get eventName(): string {
23
+ return this.constructor.name;
24
+ }
25
+
26
+ /**
27
+ * Generate a UUID v4
28
+ */
29
+ private generateEventId(): string {
30
+ return UUID();
31
+ }
32
+
33
+ toJSON() {
34
+ return {
35
+ eventId: this.eventId,
36
+ eventName: this.eventName,
37
+ occurredOn: this.occurredOn.toISOString(),
38
+ payload: this.payload,
39
+ };
40
+ }
41
+ }
@@ -0,0 +1,146 @@
1
+ import {
2
+ Operation,
3
+ CreateOperation,
4
+ UpdateOperation,
5
+ DeleteOperation,
6
+ } from "../types/change-tracker.js";
7
+
8
+ /**
9
+ * Represents the changes filtered for a specific entity.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const changes = user.getChanges();
14
+ * const postChanges = changes.of('Post');
15
+ *
16
+ * if (postChanges.hasCreates()) {
17
+ * console.log('Created posts:', postChanges.creates);
18
+ * }
19
+ *
20
+ * if (postChanges.hasUpdates()) {
21
+ * postChanges.updates.forEach(({ entity, changed }) => {
22
+ * console.log(`Post ${entity.id} has changed:`, changed);
23
+ * });
24
+ * }
25
+ * ```
26
+ */
27
+ export class EntityChanges<T = any> {
28
+ constructor(private readonly operations: Operation<T>[]) {}
29
+
30
+ /**
31
+ * Returns all created entities
32
+ */
33
+ get creates(): T[] {
34
+ return this.operations
35
+ .filter((op): op is CreateOperation<T> => op.type === "create")
36
+ .map((op) => op.data);
37
+ }
38
+
39
+ /**
40
+ * Returns all updated entities with their changed fields
41
+ */
42
+ get updates(): Array<{ entity: T; changed: Record<string, any> }> {
43
+ return this.operations
44
+ .filter((op): op is UpdateOperation<T> => op.type === "update")
45
+ .map((op) => ({
46
+ entity: op.data,
47
+ changed: op.changedFields,
48
+ }));
49
+ }
50
+
51
+ /**
52
+ * Returns all deleted entities
53
+ */
54
+ get deletes(): T[] {
55
+ return this.operations
56
+ .filter((op): op is DeleteOperation<T> => op.type === "delete")
57
+ .map((op) => op.data);
58
+ }
59
+
60
+ /**
61
+ * Returns the IDs of the created entities
62
+ */
63
+ get createIds(): string[] {
64
+ return this.operations
65
+ .filter((op): op is CreateOperation<T> => op.type === "create")
66
+ .map((op) => this.extractId(op.data))
67
+ .filter((id): id is string => id !== undefined);
68
+ }
69
+
70
+ /**
71
+ * Returns the IDs of the updated entities
72
+ */
73
+ get updateIds(): string[] {
74
+ return this.operations
75
+ .filter((op): op is UpdateOperation<T> => op.type === "update")
76
+ .map((op) => op.id);
77
+ }
78
+
79
+ /**
80
+ * Returns the IDs of the deleted entities
81
+ */
82
+ get deleteIds(): string[] {
83
+ return this.operations
84
+ .filter((op): op is DeleteOperation<T> => op.type === "delete")
85
+ .map((op) => op.id);
86
+ }
87
+
88
+ /**
89
+ * Checks if there are any creates
90
+ */
91
+ hasCreates(): boolean {
92
+ return this.creates.length > 0;
93
+ }
94
+
95
+ /**
96
+ * Checks if there are any updates
97
+ */
98
+ hasUpdates(): boolean {
99
+ return this.updates.length > 0;
100
+ }
101
+
102
+ /**
103
+ * Checks if there are any deletes
104
+ */
105
+ hasDeletes(): boolean {
106
+ return this.deletes.length > 0;
107
+ }
108
+
109
+ /**
110
+ * Checks if there is any change
111
+ */
112
+ hasChanges(): boolean {
113
+ return this.operations.length > 0;
114
+ }
115
+
116
+ /**
117
+ * Checks if it is empty (no changes)
118
+ */
119
+ isEmpty(): boolean {
120
+ return this.operations.length === 0;
121
+ }
122
+
123
+ /**
124
+ * Returns the total number of operations
125
+ */
126
+ get count(): number {
127
+ return this.operations.length;
128
+ }
129
+
130
+ /**
131
+ * Returns the raw operations (for advanced use cases)
132
+ */
133
+ get rawOperations(): Operation<T>[] {
134
+ return [...this.operations];
135
+ }
136
+
137
+ /**
138
+ * Extracts the ID from an entity
139
+ */
140
+ private extractId(entity: any): string | undefined {
141
+ if (!entity) return undefined;
142
+ if (entity.id?.value) return entity.id.value;
143
+ if (typeof entity.id === "string") return entity.id;
144
+ return undefined;
145
+ }
146
+ }
@@ -0,0 +1,13 @@
1
+ import { BaseEntity } from "./base-entity.js";
2
+ import { BaseAggregate } from "./base-aggregate.js";
3
+ import { BaseProps } from "../types/index.js";
4
+
5
+ export class Entity<
6
+ T extends BaseProps,
7
+ TOptionalInput extends keyof T = never,
8
+ > extends BaseEntity<T, TOptionalInput> {}
9
+
10
+ export class Aggregate<
11
+ T extends BaseProps,
12
+ TOptionalInput extends keyof T = never,
13
+ > extends BaseAggregate<T, TOptionalInput> {}
package/src/core/id.ts ADDED
@@ -0,0 +1,124 @@
1
+ import UUID from "../utils/crypto.js";
2
+
3
+ export class Id {
4
+ private readonly _value: string;
5
+ private _isNew: boolean;
6
+
7
+ /**
8
+ * Create a new Id
9
+ * @param value - Optional existing ID value. If not provided, generates a new UUID.
10
+ *
11
+ * @example
12
+ * // New entity (generates UUID)
13
+ * const newId = new Id();
14
+ * newuser.isNew() // true
15
+ *
16
+ * // Existing entity (uses provided ID)
17
+ * const existingId = new Id("550e8400-e29b-41d4-a716-446655440000");
18
+ * existinguser.isNew() // false
19
+ */
20
+ constructor(value: string, isNew?: boolean);
21
+ constructor(value?: string);
22
+ constructor(value?: string, isNew?: boolean) {
23
+ if (value !== undefined) {
24
+ // ID was provided - this is an existing entity
25
+ this._value = value;
26
+ this._isNew = isNew ?? false;
27
+ } else {
28
+ // No ID provided - generate new one, this is a new entity
29
+ this._value = this.generateUUID();
30
+ this._isNew = true;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Get the string value of the ID
36
+ */
37
+ get value(): string {
38
+ return this._value;
39
+ }
40
+
41
+ /**
42
+ * Check if this ID represents a new entity
43
+ */
44
+ public isNew(): boolean {
45
+ return this._isNew;
46
+ }
47
+
48
+ /**
49
+ * Convert to string (for JSON serialization and comparisons)
50
+ */
51
+ toString(): string {
52
+ return this._value;
53
+ }
54
+
55
+ /**
56
+ * Convert to JSON (returns the string value)
57
+ */
58
+ toJSON(): string {
59
+ return this._value;
60
+ }
61
+
62
+ /**
63
+ * Check equality with another Id or string
64
+ */
65
+ equals(other: Id | string): boolean {
66
+ if (other instanceof Id) {
67
+ return this._value === other._value;
68
+ }
69
+ return this._value === other;
70
+ }
71
+
72
+ /**
73
+ * Generate a UUID v4
74
+ */
75
+ private generateUUID(): string {
76
+ // Simple UUID v4 implementation
77
+ return UUID();
78
+ }
79
+
80
+ /**
81
+ * Create a new Id (convenience static method)
82
+ */
83
+ static create(): Id {
84
+ return new Id();
85
+ }
86
+
87
+ /**
88
+ * Mark the Id as not new
89
+ */
90
+ public markAsNotNew(): void {
91
+ this._isNew = false;
92
+ }
93
+
94
+ /**
95
+ * Mark the Id as new
96
+ */
97
+ public markAsNew(): void {
98
+ this._isNew = true;
99
+ }
100
+
101
+ /**
102
+ * Create an Id from an existing value
103
+ */
104
+ static from(value: string): Id {
105
+ return new Id(value);
106
+ }
107
+
108
+ /**
109
+ * Compose two IDs into a single ID
110
+ * @param a - The first ID
111
+ * @param b - The second ID
112
+ * @returns The composed ID
113
+ */
114
+ static compose(a: string | Id, b: string | Id): string {
115
+ if (a instanceof Id) {
116
+ a = a.value;
117
+ }
118
+ if (b instanceof Id) {
119
+ b = b.value;
120
+ }
121
+
122
+ return `${a}-${b}`;
123
+ }
124
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./base-entity";
2
+ export * from "./base-aggregate";
3
+ export * from "./aggregate-changes";
4
+ export * from "./change-tracker";
5
+ export * from "./domain-event";
6
+ export * from "./entity-changes";
7
+ export * from "./entity";
8
+ export * from "./id";
9
+ export * from "./value-object";
@@ -0,0 +1,179 @@
1
+ import {
2
+ ValidationError,
3
+ ValidationIssue,
4
+ ValidationIssueCollector,
5
+ } from "../validation-error.js";
6
+ import {
7
+ VOHooks,
8
+ ValidationConfig,
9
+ StandardSchema,
10
+ EntityValidation,
11
+ Primitive,
12
+ } from "../types/index.js";
13
+ import { DEFAULT_VALIDATION_CONFIG } from "../constants.js";
14
+ import { DomainError } from "../exceptions.js";
15
+ import { getStaticProperty } from "../utils/helpers.js";
16
+
17
+ export abstract class ValueObject<T extends Primitive> {
18
+ public readonly value!: T;
19
+ private validationConfig: Required<ValidationConfig>;
20
+ private domainHooks?: VOHooks<T, any>;
21
+ private domainSchema?: StandardSchema<T>;
22
+ private readonly issueCollector = new ValidationIssueCollector();
23
+
24
+ protected static validation?: EntityValidation<any>;
25
+ protected static hooks?: VOHooks<any, any>;
26
+
27
+ constructor(value: T) {
28
+ const validation = getStaticProperty<EntityValidation<T>>(
29
+ this,
30
+ "validation"
31
+ );
32
+ const hooks = getStaticProperty<VOHooks<T, any>>(this, "hooks");
33
+
34
+ if (hooks?.onBeforeCreate) {
35
+ hooks.onBeforeCreate(value);
36
+ }
37
+
38
+ this.domainHooks = hooks;
39
+
40
+ if (validation?.schema) {
41
+ this.domainSchema = validation.schema;
42
+ }
43
+
44
+ this.validationConfig = {
45
+ ...DEFAULT_VALIDATION_CONFIG,
46
+ ...validation?.config,
47
+ };
48
+
49
+ if (this.domainSchema && this.validationConfig.onCreate) {
50
+ this.validateValue(value);
51
+ }
52
+
53
+ this.value = value;
54
+
55
+ if (hooks?.rules) {
56
+ this.runRulesHook();
57
+ }
58
+
59
+ Object.freeze(this);
60
+ }
61
+
62
+ /**
63
+ * Add a validation issue during rules hook execution (non-throwing mode).
64
+ */
65
+ public addValidationIssue(path: string | string[], message: string): void {
66
+ this.issueCollector.add(path, message);
67
+ }
68
+
69
+ private beginValidationCycle(): void {
70
+ this.issueCollector.clear();
71
+ }
72
+
73
+ private finalizeValidation(collectedIssues: ValidationIssue[] = []): void {
74
+ const existing = (this as any)._validationError as
75
+ | ValidationError
76
+ | undefined;
77
+ const merged = ValidationError.merge(existing, collectedIssues);
78
+
79
+ if (!merged) {
80
+ delete (this as any)._validationError;
81
+ return;
82
+ }
83
+
84
+ if (this.validationConfig.throwOnError) {
85
+ throw merged;
86
+ }
87
+
88
+ (this as any)._validationError = merged;
89
+ }
90
+
91
+ private runRulesHook(): void {
92
+ if (!this.domainHooks?.rules) return;
93
+
94
+ this.beginValidationCycle();
95
+ this.domainHooks.rules(this as any);
96
+ this.finalizeValidation([...this.issueCollector.getIssues()]);
97
+ }
98
+
99
+ private validateValue(value: T): void {
100
+ const schemaError = this.validateSchema(value);
101
+ if (!schemaError) return;
102
+
103
+ if (this.validationConfig.throwOnError) {
104
+ throw schemaError;
105
+ }
106
+
107
+ (this as any)._validationError = schemaError;
108
+ }
109
+
110
+ private validateSchema(value: T): ValidationError | null {
111
+ if (!this.domainSchema) return null;
112
+
113
+ const result = this.domainSchema["~standard"].validate(value);
114
+
115
+ if (result instanceof Promise) {
116
+ throw new DomainError(
117
+ "Async validation not supported in constructor. Use sync validation schema."
118
+ );
119
+ }
120
+
121
+ if (result.issues && result.issues.length > 0) {
122
+ return new ValidationError(
123
+ result.issues.map((issue) => ({
124
+ path: issue.path?.map((p) => this.extractPathKey(p)) || [],
125
+ message: issue.message,
126
+ }))
127
+ );
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ private extractPathKey(pathSegment: unknown): string {
134
+ if (pathSegment === null || pathSegment === undefined) {
135
+ return "";
136
+ }
137
+ if (typeof pathSegment === "string" || typeof pathSegment === "number") {
138
+ return String(pathSegment);
139
+ }
140
+ if (typeof pathSegment === "symbol") {
141
+ return pathSegment.toString();
142
+ }
143
+ if (typeof pathSegment === "object" && "key" in pathSegment) {
144
+ return String((pathSegment as { key: unknown }).key);
145
+ }
146
+ return String(pathSegment);
147
+ }
148
+
149
+ /**
150
+ * Returns true if the value object has validation errors (when throwOnError is false).
151
+ */
152
+ get hasValidationErrors(): boolean {
153
+ return !!(this as any)._validationError;
154
+ }
155
+
156
+ /**
157
+ * Returns the validation errors (when throwOnError is false).
158
+ */
159
+ get validationErrors(): ValidationError | undefined {
160
+ return (this as any)._validationError;
161
+ }
162
+
163
+ /**
164
+ * Compare this ValueObject with another for equality based on their properties.
165
+ */
166
+ equals(other: ValueObject<T>): boolean {
167
+ if (!other || !(other instanceof ValueObject)) return false;
168
+ return this.value === other.value;
169
+ }
170
+
171
+ /**
172
+ * Creates a new ValueObject with updated value.
173
+ * ValueObjects are immutable, so this returns a new instance.
174
+ */
175
+ protected clone(value: T): this {
176
+ const Constructor = this.constructor as new (value: T) => this;
177
+ return new Constructor(value);
178
+ }
179
+ }