@woltz/rich-domain 0.2.1 → 1.0.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 (128) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +37 -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 +21 -15
  6. package/dist/base-entity.js.map +1 -1
  7. package/dist/constants.js +4 -1
  8. package/dist/constants.js.map +1 -1
  9. package/dist/criteria.d.ts.map +1 -1
  10. package/dist/criteria.js +7 -3
  11. package/dist/criteria.js.map +1 -1
  12. package/dist/deep-proxy.d.ts +3 -1
  13. package/dist/deep-proxy.d.ts.map +1 -1
  14. package/dist/deep-proxy.js +110 -33
  15. package/dist/deep-proxy.js.map +1 -1
  16. package/dist/domain-event-bus.js +7 -2
  17. package/dist/domain-event-bus.js.map +1 -1
  18. package/dist/domain-event.js +7 -3
  19. package/dist/domain-event.js.map +1 -1
  20. package/dist/entity.js +8 -3
  21. package/dist/entity.js.map +1 -1
  22. package/dist/id.d.ts +3 -3
  23. package/dist/id.d.ts.map +1 -1
  24. package/dist/id.js +10 -6
  25. package/dist/id.js.map +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +39 -16
  29. package/dist/index.js.map +1 -1
  30. package/dist/mapper.d.ts +4 -0
  31. package/dist/mapper.d.ts.map +1 -0
  32. package/dist/mapper.js +7 -0
  33. package/dist/mapper.js.map +1 -0
  34. package/dist/paginated-result.d.ts.map +1 -1
  35. package/dist/paginated-result.js +7 -6
  36. package/dist/paginated-result.js.map +1 -1
  37. package/dist/repository/base-repository.d.ts +25 -48
  38. package/dist/repository/base-repository.d.ts.map +1 -1
  39. package/dist/repository/base-repository.js +14 -51
  40. package/dist/repository/base-repository.js.map +1 -1
  41. package/dist/repository/in-memory-repository.d.ts +12 -8
  42. package/dist/repository/in-memory-repository.d.ts.map +1 -1
  43. package/dist/repository/in-memory-repository.js +24 -12
  44. package/dist/repository/in-memory-repository.js.map +1 -1
  45. package/dist/repository/index.d.ts +2 -39
  46. package/dist/repository/index.d.ts.map +1 -1
  47. package/dist/repository/index.js +26 -40
  48. package/dist/repository/index.js.map +1 -1
  49. package/dist/repository/unit-of-work.js +9 -3
  50. package/dist/repository/unit-of-work.js.map +1 -1
  51. package/dist/types/criteria.d.ts +2 -2
  52. package/dist/types/criteria.d.ts.map +1 -1
  53. package/dist/types/criteria.js +4 -1
  54. package/dist/types/criteria.js.map +1 -1
  55. package/dist/types/domain.js +2 -1
  56. package/dist/types/history-tracker.js +2 -1
  57. package/dist/types/index.d.ts +0 -1
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/types/index.js +22 -7
  60. package/dist/types/index.js.map +1 -1
  61. package/dist/types/standard-schema.js +2 -1
  62. package/dist/types/unit-of-work.d.ts +2 -2
  63. package/dist/types/unit-of-work.d.ts.map +1 -1
  64. package/dist/types/unit-of-work.js +2 -1
  65. package/dist/types/utils.js +2 -1
  66. package/dist/validation-error.js +9 -3
  67. package/dist/validation-error.js.map +1 -1
  68. package/dist/value-object.js +9 -5
  69. package/dist/value-object.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/base-entity.ts +3 -2
  72. package/src/criteria.ts +2 -2
  73. package/src/deep-proxy.ts +435 -339
  74. package/src/id.ts +4 -4
  75. package/src/index.ts +2 -3
  76. package/src/mapper.ts +3 -0
  77. package/src/paginated-result.ts +1 -8
  78. package/src/repository/base-repository.ts +27 -115
  79. package/src/repository/in-memory-repository.ts +28 -16
  80. package/src/repository/index.ts +2 -40
  81. package/src/types/criteria.ts +2 -2
  82. package/src/types/index.ts +0 -1
  83. package/src/types/unit-of-work.ts +3 -3
  84. package/tests/entity-validation.test.ts +1 -1
  85. package/tests/history-tracker.spec.ts +57 -17
  86. package/tests/id.test.ts +341 -341
  87. package/tests/repository.test.ts +95 -79
  88. package/tests/to-json.test.ts +103 -91
  89. package/tests/value-objects.test.ts +52 -52
  90. package/tsconfig.json +2 -2
  91. package/dist/filtering.d.ts +0 -107
  92. package/dist/filtering.d.ts.map +0 -1
  93. package/dist/filtering.js +0 -202
  94. package/dist/filtering.js.map +0 -1
  95. package/dist/ordering.d.ts +0 -93
  96. package/dist/ordering.d.ts.map +0 -1
  97. package/dist/ordering.js +0 -154
  98. package/dist/ordering.js.map +0 -1
  99. package/dist/pagination.d.ts +0 -218
  100. package/dist/pagination.d.ts.map +0 -1
  101. package/dist/pagination.js +0 -281
  102. package/dist/pagination.js.map +0 -1
  103. package/dist/repository/mapper.d.ts +0 -56
  104. package/dist/repository/mapper.d.ts.map +0 -1
  105. package/dist/repository/mapper.js +0 -15
  106. package/dist/repository/mapper.js.map +0 -1
  107. package/dist/repository/types.d.ts +0 -87
  108. package/dist/repository/types.d.ts.map +0 -1
  109. package/dist/repository/types.js +0 -6
  110. package/dist/repository/types.js.map +0 -1
  111. package/dist/repository.d.ts +0 -2
  112. package/dist/repository.d.ts.map +0 -1
  113. package/dist/repository.js +0 -21
  114. package/dist/repository.js.map +0 -1
  115. package/dist/specification.d.ts +0 -102
  116. package/dist/specification.d.ts.map +0 -1
  117. package/dist/specification.js +0 -187
  118. package/dist/specification.js.map +0 -1
  119. package/dist/types/repository.d.ts +0 -43
  120. package/dist/types/repository.d.ts.map +0 -1
  121. package/dist/types/repository.js +0 -2
  122. package/dist/types/repository.js.map +0 -1
  123. package/dist/types.d.ts +0 -88
  124. package/dist/types.d.ts.map +0 -1
  125. package/dist/types.js +0 -12
  126. package/dist/types.js.map +0 -1
  127. package/src/repository/mapper.ts +0 -74
  128. package/src/types/repository.ts +0 -51
package/src/id.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { randomUUID } from 'crypto';
1
+ import { randomUUID } from "crypto";
2
2
  // ============================================================================
3
3
  // Id Class - Smart Identity Management
4
4
  // ============================================================================
@@ -14,11 +14,11 @@ export class Id {
14
14
  * @example
15
15
  * // New entity (generates UUID)
16
16
  * const newId = new Id();
17
- * newId.isNew // true
17
+ * newuser.isNew() // true
18
18
  *
19
19
  * // Existing entity (uses provided ID)
20
20
  * const existingId = new Id("550e8400-e29b-41d4-a716-446655440000");
21
- * existingId.isNew // false
21
+ * existinguser.isNew() // false
22
22
  */
23
23
  constructor(value?: string) {
24
24
  if (value !== undefined) {
@@ -42,7 +42,7 @@ export class Id {
42
42
  /**
43
43
  * Check if this ID represents a new entity
44
44
  */
45
- get isNew(): boolean {
45
+ public isNew(): boolean {
46
46
  return this._isNew;
47
47
  }
48
48
 
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export { Id } from "./id";
7
7
  export { BaseEntity } from "./base-entity";
8
8
  export { Entity, Aggregate } from "./entity";
9
9
  export { ValueObject } from "./value-object";
10
+ export { Mapper } from "./mapper";
10
11
 
11
12
  export * from "./validation-error";
12
13
 
@@ -15,14 +16,12 @@ export * from "./domain-event";
15
16
 
16
17
  export * from "./domain-event-bus";
17
18
 
18
- // Criteria & Repository
19
+ // Criteria
19
20
  export * from "./criteria";
20
21
  export * from "./paginated-result";
21
22
 
22
23
  // Repository
23
24
  export * from "./repository";
24
- // Backward compatibility - re-export InMemoryRepository at top level
25
- export { InMemoryRepository } from "./repository";
26
25
 
27
26
  // Types
28
27
  export * from "./types";
package/src/mapper.ts ADDED
@@ -0,0 +1,3 @@
1
+ export abstract class Mapper<Input, Output> {
2
+ public abstract build(input: Input, ...args: unknown[]): Output;
3
+ }
@@ -1,7 +1,3 @@
1
- // ============================================================================
2
- // PaginatedResult - Container for paginated data with deep serialization
3
- // ============================================================================
4
-
5
1
  import { Id } from "./id";
6
2
  import type { Criteria } from "./criteria";
7
3
  import type { Pagination, PaginationMeta, Filter } from "./types";
@@ -66,10 +62,7 @@ export class PaginatedResult<T> {
66
62
  /**
67
63
  * Applies criteria to an in-memory array (useful for testing)
68
64
  */
69
- static fromArray<T>(
70
- items: T[],
71
- criteria: Criteria<T>
72
- ): PaginatedResult<T> {
65
+ static fromArray<T>(items: T[], criteria: Criteria<T>): PaginatedResult<T> {
73
66
  let result = [...items];
74
67
 
75
68
  // Apply filters
@@ -2,12 +2,10 @@
2
2
  // Base Repository - Abstract implementation with common logic
3
3
  // ============================================================================
4
4
 
5
- import type { Id } from "../id";
6
5
  import type { Aggregate } from "../entity";
7
6
  import type { Criteria } from "../criteria";
8
7
  import { PaginatedResult } from "../paginated-result";
9
- import type { IRepository } from "../types";
10
- import type { IMapper } from "./mapper";
8
+ import { Mapper } from "../mapper";
11
9
 
12
10
  /**
13
11
  * Abstract base repository
@@ -33,120 +31,34 @@ import type { IMapper } from "./mapper";
33
31
  * }
34
32
  * ```
35
33
  */
36
- export abstract class BaseRepository<
37
- TDomain extends Aggregate<any>,
38
- TPersistence = any
39
- > implements IRepository<TDomain>
40
- {
41
- constructor(protected readonly mapper: IMapper<TDomain, TPersistence>) {}
42
34
 
43
- // ============================================================================
44
- // Abstract methods - Must be implemented by subclasses
45
- // ============================================================================
46
-
47
- /**
48
- * Insert new record in database
49
- */
50
- protected abstract insertOne(data: TPersistence): Promise<TPersistence>;
51
-
52
- /**
53
- * Update existing record in database
54
- */
55
- protected abstract updateOne(
56
- id: string,
57
- data: TPersistence
58
- ): Promise<TPersistence>;
59
-
60
- /**
61
- * Delete record from database
62
- */
63
- protected abstract deleteOne(id: string): Promise<void>;
64
-
65
- /**
66
- * Find record by ID in database
67
- */
68
- protected abstract findOneById(id: string): Promise<TDomain | null>;
69
-
70
- /**
71
- * Find all records in database (no filtering)
72
- */
73
- protected abstract findMany(): Promise<TDomain[]>;
74
-
75
- /**
76
- * Apply criteria to query (filtering, ordering, pagination)
77
- * Returns [data, total]
78
- */
79
- protected abstract applyCriteria(
80
- criteria: Criteria<TDomain>
81
- ): Promise<PaginatedResult<TDomain>>;
82
-
83
- /**
84
- * Count records matching criteria
85
- */
86
- protected abstract countByCriteria(
87
- criteria?: Criteria<TDomain>
88
- ): Promise<number>;
89
-
90
- /**
91
- * Check if record exists by ID
92
- */
93
- protected abstract existsById(id: string): Promise<boolean>;
94
-
95
- // ============================================================================
96
- // Public API - Implemented using abstract methods
97
- // ============================================================================
98
-
99
- async findById(id: Id): Promise<TDomain | null> {
100
- const domain = await this.findOneById(id.value);
101
- if (!domain) return null;
102
- return domain;
103
- }
104
-
105
- async find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>> {
106
- return await this.applyCriteria(criteria);
107
- }
108
-
109
- async findAll(criteria?: Criteria<TDomain>): Promise<TDomain[]> {
110
- if (criteria) {
111
- const result = await this.find(criteria);
112
- return result.data;
113
- }
114
-
115
- const domains = await this.findMany();
116
- return domains;
117
- }
118
-
119
- async findOne(criteria: Criteria<TDomain>): Promise<TDomain | null> {
120
- // Limit to 1 result
121
- const limitedCriteria = criteria.clone().limit(1);
122
- const result = await this.find(limitedCriteria);
123
-
124
- return result.data.length > 0 ? result.data[0] : null;
125
- }
126
-
127
- async save(aggregate: TDomain): Promise<void> {
128
- const persistence = this.mapper.toPersistence(aggregate);
129
-
130
- if (aggregate.isNew) {
131
- await this.insertOne(persistence);
132
- } else {
133
- await this.updateOne(aggregate.id.value, persistence);
134
- }
135
- }
136
-
137
- async delete(aggregate: TDomain): Promise<void> {
138
- await this.deleteOne(aggregate.id.value);
139
- }
35
+ export abstract class ReadRepository<Agg extends Aggregate<any>> {
36
+ abstract find(criteria: Criteria<Agg>): Promise<PaginatedResult<Agg>>;
37
+ abstract findById(id: string): Promise<Agg | null>;
38
+ abstract count(criteria: Criteria<Agg>): Promise<number>;
39
+ abstract exists(id: string): Promise<boolean>;
40
+ }
140
41
 
141
- async deleteById(id: Id): Promise<void> {
142
- await this.deleteOne(id.value);
143
- }
42
+ export abstract class WriteRepository<Agg extends Aggregate<any>> {
43
+ abstract create(entity: Agg): Promise<void>;
44
+ abstract update(entity: Agg): Promise<void>;
45
+ abstract delete(entity: Agg): Promise<void>;
46
+ }
144
47
 
145
- async exists(id: Id): Promise<boolean> {
146
- return this.existsById(id.value);
147
- }
48
+ export abstract class WriteAndRead<Agg extends Aggregate<any>> {
49
+ abstract find(criteria: Criteria<Agg>): Promise<PaginatedResult<Agg>>;
50
+ abstract findById(id: string): Promise<Agg | null>;
51
+ abstract create(entity: Agg): Promise<void>;
52
+ abstract update(entity: Agg): Promise<void>;
53
+ abstract delete(entity: Agg): Promise<void>;
54
+ abstract count(criteria: Criteria<Agg>): Promise<number>;
55
+ abstract exists(id: string): Promise<boolean>;
56
+ }
148
57
 
149
- async count(criteria?: Criteria<TDomain>): Promise<number> {
150
- return this.countByCriteria(criteria);
151
- }
58
+ export abstract class Repository<
59
+ TDomain extends Aggregate<any>
60
+ > extends WriteAndRead<TDomain> {
61
+ protected abstract readonly mapperToDomain: Mapper<unknown, TDomain>;
62
+ protected abstract readonly mapperToPersistence: Mapper<TDomain, unknown>;
63
+ abstract get model(): any;
152
64
  }
@@ -2,11 +2,11 @@
2
2
  // In-Memory Repository - Perfect for testing
3
3
  // ============================================================================
4
4
 
5
- import type { Id } from "../id";
6
5
  import type { Aggregate } from "../entity";
7
6
  import type { Criteria } from "../criteria";
8
7
  import { PaginatedResult } from "../paginated-result";
9
- import type { IRepository } from "../types";
8
+ import { Repository } from "./base-repository";
9
+ import { Mapper } from "../mapper";
10
10
 
11
11
  /**
12
12
  * In-memory repository implementation
@@ -23,13 +23,25 @@ import type { IRepository } from "../types";
23
23
  * );
24
24
  * ```
25
25
  */
26
- export class InMemoryRepository<TDomain extends Aggregate<any>>
27
- implements IRepository<TDomain>
28
- {
26
+ export class InMemoryRepository<
27
+ TDomain extends Aggregate<any>
28
+ > extends Repository<TDomain> {
29
29
  protected items: Map<string, TDomain> = new Map();
30
30
 
31
- async findById(id: Id): Promise<TDomain | null> {
32
- return this.items.get(id.value) || null;
31
+ constructor(
32
+ protected readonly mapperToDomain: Mapper<unknown, TDomain>,
33
+ protected readonly mapperToPersistence: Mapper<TDomain, unknown>
34
+ ) {
35
+ super();
36
+ }
37
+
38
+ get model(): any {
39
+ // your database table name
40
+ return "inMemory";
41
+ }
42
+
43
+ async findById(id: string): Promise<TDomain | null> {
44
+ return this.items.get(id) || null;
33
45
  }
34
46
 
35
47
  async find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>> {
@@ -51,13 +63,17 @@ export class InMemoryRepository<TDomain extends Aggregate<any>>
51
63
  return result.data.length > 0 ? result.data[0] : null;
52
64
  }
53
65
 
54
- async save(aggregate: TDomain): Promise<void> {
66
+ async create(aggregate: TDomain): Promise<void> {
55
67
  this.items.set(aggregate.id.value, aggregate);
56
68
  }
57
69
 
58
- async saveMany(aggregates: TDomain[]): Promise<void> {
70
+ async update(entity: TDomain): Promise<void> {
71
+ this.items.set(entity.id.value, entity);
72
+ }
73
+
74
+ async createMany(aggregates: TDomain[]): Promise<void> {
59
75
  for (const aggregate of aggregates) {
60
- await this.save(aggregate);
76
+ await this.create(aggregate);
61
77
  }
62
78
  }
63
79
 
@@ -65,12 +81,8 @@ export class InMemoryRepository<TDomain extends Aggregate<any>>
65
81
  this.items.delete(aggregate.id.value);
66
82
  }
67
83
 
68
- async deleteById(id: Id): Promise<void> {
69
- this.items.delete(id.value);
70
- }
71
-
72
- async exists(id: Id): Promise<boolean> {
73
- return this.items.has(id.value);
84
+ async exists(id: string): Promise<boolean> {
85
+ return this.items.has(id);
74
86
  }
75
87
 
76
88
  async count(criteria?: Criteria<TDomain>): Promise<number> {
@@ -3,11 +3,10 @@
3
3
  // ============================================================================
4
4
 
5
5
  // Mapper
6
- export { BaseMapper } from "./mapper";
7
- export type { IMapper } from "./mapper";
6
+ export { Mapper } from "../mapper";
8
7
 
9
8
  // Base implementations
10
- export { BaseRepository } from "./base-repository";
9
+ export * from "./base-repository";
11
10
  export { InMemoryRepository } from "./in-memory-repository";
12
11
 
13
12
  // Unit of Work
@@ -16,40 +15,3 @@ export {
16
15
  BaseTransactionContext,
17
16
  InMemoryUnitOfWork,
18
17
  } from "./unit-of-work";
19
-
20
- /**
21
- * QUICK START:
22
- *
23
- * 1. For Testing:
24
- * ```ts
25
- * import { InMemoryRepository } from 'rich-domain';
26
- *
27
- * const userRepo = new InMemoryRepository<User>();
28
- * await userRepo.save(user);
29
- * const found = await userRepo.findById(user.id);
30
- * ```
31
- *
32
- * 2. For Production (Prisma, TypeORM, etc):
33
- * - Extend BaseRepository
34
- * - Implement abstract methods
35
- * - Create a Mapper
36
- * - See examples/ folder for reference
37
- *
38
- * 3. With Criteria:
39
- * ```ts
40
- * const result = await userRepo.find(
41
- * Criteria.create<User>()
42
- * .whereEquals('status', 'active')
43
- * .orderByDesc('createdAt')
44
- * .paginate(1, 10)
45
- * );
46
- * ```
47
- *
48
- * 4. With Unit of Work:
49
- * ```ts
50
- * await uow.transaction(async (ctx) => {
51
- * const userRepo = uow.getRepository(UserRepository);
52
- * await userRepo.save(user);
53
- * });
54
- * ```
55
- */
@@ -1,6 +1,6 @@
1
1
  import { Primitive } from "./utils";
2
2
 
3
- export const FilterOperator = [
3
+ export const FILTER_OPERATORS = [
4
4
  "equals",
5
5
  "notEquals",
6
6
  "greaterThan",
@@ -36,7 +36,7 @@ export type PathValue<
36
36
  ? T[P]
37
37
  : never;
38
38
 
39
- export type FilterOperator = (typeof FilterOperator)[number];
39
+ export type FilterOperator = (typeof FILTER_OPERATORS)[number];
40
40
 
41
41
  export interface Filter<TField = string, TValue = unknown> {
42
42
  field: TField;
@@ -3,5 +3,4 @@ export * from "./domain";
3
3
  export * from "./history-tracker";
4
4
  export * from "./standard-schema";
5
5
  export * from "./utils";
6
- export * from "./repository";
7
6
  export * from "./unit-of-work";
@@ -1,5 +1,5 @@
1
1
  import { Aggregate } from "../entity";
2
- import { IRepository } from "./repository";
2
+ import { Repository } from "../repository/base-repository";
3
3
 
4
4
  /**
5
5
  * Transaction context for Unit of Work
@@ -41,6 +41,6 @@ export interface IUnitOfWork {
41
41
  * Get repository within transaction context
42
42
  */
43
43
  getRepository<TDomain extends Aggregate<any>>(
44
- repository: new (...args: any[]) => IRepository<TDomain>
45
- ): IRepository<TDomain>;
44
+ repository: new (...args: any[]) => Repository<TDomain>
45
+ ): Repository<TDomain>;
46
46
  }
@@ -134,7 +134,7 @@ describe("Rich Domain with Standard Schema Validation", () => {
134
134
  expect(user.name).toBe("John Doe");
135
135
  expect(user.email).toBe("john@example.com");
136
136
  expect(user.age).toBe(25);
137
- expect(user.isNew).toBe(true);
137
+ expect(user.isNew()).toBe(true);
138
138
  });
139
139
 
140
140
  it("should throw on invalid email", () => {
@@ -4,28 +4,68 @@ import { Post, User, Address, Comment } from "./utils";
4
4
  describe("History Tracker Tests", () => {
5
5
  describe("Simple Property Changes", () => {
6
6
  it("should track simple property changes", (done) => {
7
- const post = new Post({
7
+ const user = new User({
8
8
  id: new Id("1"),
9
- title: "First Post",
10
- content: "Hello World",
11
- likes: 0,
9
+ name: "John Doe",
10
+ email: "john@example.com",
11
+ posts: [
12
+ new Post({
13
+ id: new Id("1"),
14
+ title: "First Post",
15
+ content: "Hello World",
16
+ likes: 0,
17
+ }),
18
+ ],
19
+ address: new Address({
20
+ street: "Main St",
21
+ city: "NYC",
22
+ zipCode: "10001",
23
+ }),
24
+ comments: [],
12
25
  });
13
26
 
14
- let changeCount = 0;
27
+ user.changeEmail("new@example.com");
28
+ user.name = "New Name";
29
+ user.addPost(
30
+ new Post({
31
+ id: new Id("2"),
32
+ title: "Second Post",
33
+ content: "Hello World 2",
34
+ likes: 0,
35
+ })
36
+ );
15
37
 
16
- post.subscribe({
17
- title: {
18
- onChange: ({ previous, current, path }) => {
19
- changeCount++;
20
- expect(previous).toBe("First Post");
21
- expect(current).toBe("Updated Title");
22
- expect(path).toBe("title");
23
- done();
38
+ function dispatch(entity: User) {
39
+ entity.subscribe({
40
+ email: {
41
+ onChange: ({ previous, current, path }) => {
42
+ expect(previous).toBe("john@example.com");
43
+ expect(current).toBe("new@example.com");
44
+ expect(path).toBe("email");
45
+ },
24
46
  },
25
- },
26
- });
27
-
28
- post.title = "Updated Title";
47
+ posts: {
48
+ onChange: ({ toCreate, toUpdate, toDelete }) => {
49
+ expect(toCreate).toHaveLength(1);
50
+ expect(toUpdate).toHaveLength(1);
51
+ expect(toDelete).toHaveLength(0);
52
+ },
53
+ },
54
+ name: {
55
+ onChange: ({ previous, current, path }) => {
56
+ expect(previous).toBe("John Doe");
57
+ expect(current).toBe("New Name");
58
+ expect(path).toBe("name");
59
+ },
60
+ },
61
+ });
62
+ }
63
+ user.posts[0].title = "Updated Title";
64
+ dispatch(user);
65
+
66
+ setTimeout(() => {
67
+ done();
68
+ }, 100);
29
69
  });
30
70
 
31
71
  it("should track multiple property changes", () => {