@proteinjs/db 1.7.0 → 1.8.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 (46) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/generated/index.d.ts.map +1 -1
  3. package/dist/generated/index.js +3 -1
  4. package/dist/generated/index.js.map +1 -1
  5. package/dist/index.d.ts +5 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +5 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/src/Db.d.ts +30 -4
  10. package/dist/src/Db.d.ts.map +1 -1
  11. package/dist/src/Db.js +66 -75
  12. package/dist/src/Db.js.map +1 -1
  13. package/dist/src/Table.d.ts +4 -0
  14. package/dist/src/Table.d.ts.map +1 -1
  15. package/dist/src/Table.js +104 -1
  16. package/dist/src/Table.js.map +1 -1
  17. package/dist/src/services/TransactionRunnerService.d.ts +7 -0
  18. package/dist/src/services/TransactionRunnerService.d.ts.map +1 -0
  19. package/dist/src/services/TransactionRunnerService.js +6 -0
  20. package/dist/src/services/TransactionRunnerService.js.map +1 -0
  21. package/dist/src/transaction/Transaction.d.ts +69 -0
  22. package/dist/src/transaction/Transaction.d.ts.map +1 -0
  23. package/dist/src/transaction/Transaction.js +255 -0
  24. package/dist/src/transaction/Transaction.js.map +1 -0
  25. package/dist/src/transaction/TransactionRunner.d.ts +7 -0
  26. package/dist/src/transaction/TransactionRunner.d.ts.map +1 -0
  27. package/dist/src/transaction/TransactionRunner.js +88 -0
  28. package/dist/src/transaction/TransactionRunner.js.map +1 -0
  29. package/dist/test/TransactionDb.test.d.ts +2 -0
  30. package/dist/test/TransactionDb.test.d.ts.map +1 -0
  31. package/dist/test/TransactionDb.test.js +404 -0
  32. package/dist/test/TransactionDb.test.js.map +1 -0
  33. package/dist/test/reusable/TransactionTests.d.ts +52 -0
  34. package/dist/test/reusable/TransactionTests.d.ts.map +1 -0
  35. package/dist/test/reusable/TransactionTests.js +396 -0
  36. package/dist/test/reusable/TransactionTests.js.map +1 -0
  37. package/generated/index.ts +3 -1
  38. package/index.ts +5 -2
  39. package/package.json +2 -2
  40. package/src/Db.ts +63 -28
  41. package/src/Table.ts +23 -0
  42. package/src/services/TransactionRunnerService.ts +10 -0
  43. package/src/transaction/Transaction.ts +186 -0
  44. package/src/transaction/TransactionRunner.ts +18 -0
  45. package/test/TransactionDb.test.ts +335 -0
  46. package/test/reusable/TransactionTests.ts +234 -0
package/src/Table.ts CHANGED
@@ -42,6 +42,27 @@ export const getColumnByName = (table: Table<any>, columnName: string) => {
42
42
  return null;
43
43
  };
44
44
 
45
+ export const addDefaultFieldValues = async (table: Table<any>, record: any) => {
46
+ for (const columnPropertyName in table.columns) {
47
+ const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
48
+ if (
49
+ column.options?.defaultValue &&
50
+ (typeof record[columnPropertyName] === 'undefined' || column.options?.forceDefaultValue)
51
+ ) {
52
+ record[columnPropertyName] = await column.options.defaultValue(record);
53
+ }
54
+ }
55
+ };
56
+
57
+ export const addUpdateFieldValues = async (table: Table<any>, record: any) => {
58
+ for (const columnPropertyName in table.columns) {
59
+ const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
60
+ if (column.options?.updateValue) {
61
+ record[columnPropertyName] = await column.options.updateValue(record);
62
+ }
63
+ }
64
+ };
65
+
45
66
  /**
46
67
  * primary key is `id`
47
68
  */
@@ -112,6 +133,8 @@ export type ColumnOptions = {
112
133
  nullable?: boolean;
113
134
  /** Value stored on insert */
114
135
  defaultValue?: (insertObj: any) => Promise<any>;
136
+ /** If true, the `defaultValue` function will always provide the value and override any existing value */
137
+ forceDefaultValue?: boolean;
115
138
  /** Value stored on update */
116
139
  updateValue?: (updateObj: any) => Promise<any>;
117
140
  /** Add conditions to query; called on every query of this table */
@@ -0,0 +1,10 @@
1
+ import { Service, serviceFactory } from '@proteinjs/service';
2
+ import { Operation } from '../transaction/Transaction';
3
+
4
+ export const getTransactionRunnerService = serviceFactory<TransactionRunnerService>(
5
+ '@proteinjs/db/TransactionRunnerService'
6
+ );
7
+
8
+ export interface TransactionRunnerService extends Service {
9
+ run(ops: Operation<any>[]): Promise<void>;
10
+ }
@@ -0,0 +1,186 @@
1
+ import { Record } from '../Record';
2
+ import { DbService, ObjectQuery } from '../services/DbService';
3
+ import { getTransactionRunner } from './TransactionRunner';
4
+ import { addDefaultFieldValues, Table } from '../Table';
5
+ import { isInstanceOf } from '@proteinjs/util';
6
+ import { QueryBuilder } from '@proteinjs/db-query';
7
+
8
+ export type OperationQueue<R extends Record = Record> = {
9
+ insert: (...args: Parameters<DbService<R>['insert']>) => Promise<R>;
10
+ update: (...args: Parameters<DbService<R>['update']>) => void;
11
+ delete: (...args: Parameters<DbService<R>['delete']>) => void;
12
+ };
13
+
14
+ export type Operation<R extends Record = Record> = {
15
+ name: 'insert' | 'update' | 'delete';
16
+ args: Parameters<DbService<R>['insert']> | Parameters<DbService<R>['update']> | Parameters<DbService<R>['delete']>;
17
+ };
18
+
19
+ const hasAllProperties = (a: any, b: any) => Object.entries(b).every(([key, val]) => a[key] === val);
20
+
21
+ /**
22
+ * A queue of db write operations that are executed sequentially (not batched) as a single transaction when `run` is called.
23
+ *
24
+ * Intended to be used on the client to decrease network calls when performing an atomic unit of db write operations.
25
+ *
26
+ * A more robust server-side transaction api is available via `Db.runTransaction`.
27
+ */
28
+ export class Transaction implements OperationQueue {
29
+ /** Operations to be run in sequence in a transaction when `run` is called. */
30
+ ops: Operation<any>[] = [];
31
+
32
+ /**
33
+ * Local cache of tables.
34
+ *
35
+ * Data will be updated as operations are queued.
36
+ */
37
+ db: { [table: string]: { [id: string]: any } } = {};
38
+
39
+ constructor(
40
+ private onInsert?: (table: Table<any>, record: any) => void,
41
+ private onUpdate?: (table: Table<any>, prevRecord: any, currentRecord: any) => void,
42
+ private onDelete?: (table: Table<any>, record: any) => void
43
+ ) {}
44
+
45
+ /**
46
+ * @returns locally cached records for table
47
+ */
48
+ recordMap(table: string) {
49
+ if (!this.db[table]) {
50
+ this.db[table] = {};
51
+ }
52
+
53
+ return this.db[table];
54
+ }
55
+
56
+ /**
57
+ * Queue an insert.
58
+ */
59
+ async insert<R extends Record = Record>(...args: Parameters<DbService<R>['insert']>): Promise<R> {
60
+ const [table, record] = args;
61
+ const recordCopy = Object.assign({}, record) as R;
62
+ await addDefaultFieldValues(table, recordCopy);
63
+ const recordMap = this.recordMap(table.name);
64
+ if (recordMap[recordCopy.id]) {
65
+ throw new Error(`Attempting to insert with duplicate id: ${recordCopy.id}, record already exists`);
66
+ }
67
+
68
+ recordMap[recordCopy.id] = recordCopy;
69
+ if (this.onInsert) {
70
+ this.onInsert(table, recordCopy);
71
+ }
72
+
73
+ this.ops.push({ name: 'insert', args: [table, recordCopy] });
74
+
75
+ return recordCopy;
76
+ }
77
+
78
+ /**
79
+ * Queue an update.
80
+ *
81
+ * Updates are performed in-place on the existing object to ensure all references
82
+ * receive the changes.
83
+ *
84
+ * If a `QueryBuilder` is passed in for `query`, changes will not be made to the cached `db`.
85
+ * Passing in an `ObjectQuery` will update the cached `db`.
86
+ */
87
+ update<R extends Record = Record>(...args: Parameters<DbService<R>['update']>): void {
88
+ const [table, record, query] = args;
89
+ if (!query && !record.id) {
90
+ throw new Error(`Update must be called with either a Query or a record with an id property`);
91
+ }
92
+
93
+ if (record.id) {
94
+ const recordMap = this.recordMap(table.name);
95
+ const existingRecord = recordMap[record.id];
96
+ const prevRecord = { ...existingRecord };
97
+ if (existingRecord) {
98
+ Object.keys(record).forEach((key) => {
99
+ if (key !== 'id') {
100
+ existingRecord[key] = record[key as keyof R];
101
+ }
102
+ });
103
+ if (this.onUpdate) {
104
+ this.onUpdate(table, prevRecord, existingRecord);
105
+ }
106
+ } else {
107
+ throw new Error(`Attempting to update record not in the cached db`);
108
+ }
109
+ } else if (query && !isInstanceOf(query, QueryBuilder)) {
110
+ const objectQuery = query as ObjectQuery<any>;
111
+ const recordMap = this.recordMap(table.name);
112
+ if (objectQuery.id && recordMap[objectQuery.id]) {
113
+ const existingRecord = recordMap[objectQuery.id];
114
+ const prevRecord = { ...existingRecord };
115
+ Object.keys(record).forEach((key) => {
116
+ if (key !== 'id') {
117
+ existingRecord[key] = record[key as keyof R];
118
+ }
119
+ });
120
+ if (this.onUpdate) {
121
+ this.onUpdate(table, prevRecord, existingRecord);
122
+ }
123
+ } else {
124
+ for (const existingRecord of Object.values(recordMap)) {
125
+ if (hasAllProperties(existingRecord, objectQuery)) {
126
+ const prevRecord = { ...existingRecord };
127
+ Object.keys(record).forEach((key) => {
128
+ if (key !== 'id') {
129
+ existingRecord[key] = record[key as keyof R];
130
+ }
131
+ });
132
+ if (this.onUpdate) {
133
+ this.onUpdate(table, prevRecord, existingRecord);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ this.ops.push({ name: 'update', args });
141
+ }
142
+
143
+ /**
144
+ * Queue a delete.
145
+ *
146
+ * If a `QueryBuilder` is passed in for `query`, changes will not be made to the cached `db`.
147
+ * Passing in an `ObjectQuery` will update the cached `db`.
148
+ */
149
+ delete<R extends Record = Record>(...args: Parameters<DbService<R>['delete']>): void {
150
+ const [table, query] = args;
151
+ if (!isInstanceOf(query, QueryBuilder)) {
152
+ const objectQuery = query as ObjectQuery<any>;
153
+ const recordMap = this.recordMap(table.name);
154
+ if (objectQuery.id) {
155
+ if (this.onDelete && recordMap[objectQuery.id]) {
156
+ this.onDelete(table, recordMap[objectQuery.id]);
157
+ }
158
+ delete recordMap[objectQuery.id];
159
+ } else {
160
+ for (const record of Object.values(recordMap)) {
161
+ if (hasAllProperties(record, objectQuery)) {
162
+ if (this.onDelete && recordMap[record.id]) {
163
+ this.onDelete(table, recordMap[record.id]);
164
+ }
165
+ delete recordMap[record.id];
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ this.ops.push({ name: 'delete', args });
172
+ }
173
+
174
+ /**
175
+ * Run the operations in order (not a batch), as a single transaction.
176
+ */
177
+ async run(): Promise<void> {
178
+ if (this.ops.length === 0) {
179
+ return;
180
+ }
181
+
182
+ const runner = getTransactionRunner();
183
+ await runner.run(this.ops);
184
+ this.ops = [];
185
+ }
186
+ }
@@ -0,0 +1,18 @@
1
+ import { getDb } from '../Db';
2
+ import { TransactionRunnerService, getTransactionRunnerService } from '../services/TransactionRunnerService';
3
+ import { Operation } from './Transaction';
4
+
5
+ export const getTransactionRunner = () =>
6
+ typeof self === 'undefined' ? new TransactionRunner() : (getTransactionRunnerService() as TransactionRunner);
7
+
8
+ export class TransactionRunner implements TransactionRunnerService {
9
+ async run(ops: Operation<any>[]): Promise<void> {
10
+ const db = getDb();
11
+
12
+ await db.runTransaction(async () => {
13
+ for (const op of ops) {
14
+ await (db[op.name] as Function)(...op.args);
15
+ }
16
+ });
17
+ }
18
+ }
@@ -0,0 +1,335 @@
1
+ import moment from 'moment';
2
+ import { Transaction } from '../src/transaction/Transaction';
3
+ import { Table } from '../src/Table';
4
+ import { Record, withRecordColumns } from '../src/Record';
5
+ import { StringColumn, BooleanColumn, DateColumn } from '../src/Columns';
6
+
7
+ interface Employee extends Record {
8
+ name: string;
9
+ department?: string;
10
+ jobTitle?: string | null;
11
+ isRemote?: boolean;
12
+ startDate?: Date;
13
+ object?: string;
14
+ }
15
+
16
+ class EmployeeTestTable extends Table<Employee> {
17
+ name = 'db_test_employee';
18
+ columns = withRecordColumns<Employee>({
19
+ name: new StringColumn('name'),
20
+ department: new StringColumn('department'),
21
+ isRemote: new BooleanColumn('is_remote'),
22
+ jobTitle: new StringColumn('job_title'),
23
+ startDate: new DateColumn('start_date'),
24
+ object: new StringColumn('object'),
25
+ });
26
+ }
27
+
28
+ describe('Transaction db', () => {
29
+ const employeeTable = new EmployeeTestTable() as Table<Employee>;
30
+ let transaction: Transaction;
31
+ let insertCallback: jest.Mock;
32
+ let updateCallback: jest.Mock;
33
+ let deleteCallback: jest.Mock;
34
+
35
+ beforeEach(() => {
36
+ insertCallback = jest.fn();
37
+ updateCallback = jest.fn();
38
+ deleteCallback = jest.fn();
39
+ transaction = new Transaction(insertCallback, updateCallback, deleteCallback);
40
+ });
41
+
42
+ describe('insert', () => {
43
+ it('should add record to cached db and queue insert operation', async () => {
44
+ const record: Partial<Employee> = {
45
+ name: 'John Doe',
46
+ department: 'Engineering',
47
+ jobTitle: 'Software Engineer',
48
+ isRemote: true,
49
+ startDate: new Date('2024-01-01'),
50
+ };
51
+
52
+ const result = await transaction.insert(employeeTable, record);
53
+
54
+ // Verify record was added to cache
55
+ expect(transaction.recordMap(employeeTable.name)[result.id]).toEqual({
56
+ id: expect.any(String),
57
+ name: 'John Doe',
58
+ department: 'Engineering',
59
+ jobTitle: 'Software Engineer',
60
+ isRemote: true,
61
+ startDate: expect.any(Date),
62
+ created: expect.any(moment),
63
+ updated: expect.any(moment),
64
+ });
65
+
66
+ // Verify operation was queued
67
+ expect(transaction.ops).toHaveLength(1);
68
+ expect(transaction.ops[0]).toEqual({
69
+ name: 'insert',
70
+ args: [employeeTable, result],
71
+ });
72
+
73
+ // Verify callback was called
74
+ expect(insertCallback).toHaveBeenCalledTimes(1);
75
+ expect(insertCallback).toHaveBeenCalledWith(employeeTable, result);
76
+ });
77
+
78
+ it('should throw error when inserting record with duplicate id', async () => {
79
+ const record: Partial<Employee> = {
80
+ id: 'test-id',
81
+ name: 'John Doe',
82
+ department: 'Engineering',
83
+ };
84
+ await transaction.insert(employeeTable, record);
85
+
86
+ await expect(transaction.insert(employeeTable, record)).rejects.toThrow(
87
+ 'Attempting to insert with duplicate id: test-id, record already exists'
88
+ );
89
+ });
90
+ });
91
+
92
+ describe('update', () => {
93
+ it('should update record in cached db by id and queue update operation', async () => {
94
+ // First insert a record
95
+ const original = await transaction.insert(employeeTable, {
96
+ name: 'John Doe',
97
+ department: 'Engineering',
98
+ isRemote: true,
99
+ });
100
+
101
+ // Update the record
102
+ const updates = {
103
+ id: original.id,
104
+ department: 'Product',
105
+ jobTitle: 'Product Manager',
106
+ };
107
+ transaction.update(employeeTable, updates);
108
+
109
+ // Verify cache was updated
110
+ const cached = transaction.recordMap(employeeTable.name)[original.id];
111
+ expect(cached).toEqual({
112
+ id: original.id,
113
+ name: 'John Doe',
114
+ department: 'Product',
115
+ jobTitle: 'Product Manager',
116
+ isRemote: true,
117
+ created: expect.any(moment),
118
+ updated: expect.any(moment),
119
+ });
120
+
121
+ // Verify operation was queued
122
+ expect(transaction.ops[1]).toEqual({
123
+ name: 'update',
124
+ args: [employeeTable, updates],
125
+ });
126
+
127
+ // Verify callback was called
128
+ expect(updateCallback).toHaveBeenCalledTimes(1);
129
+ expect(updateCallback).toHaveBeenCalledWith(
130
+ employeeTable,
131
+ {
132
+ id: original.id,
133
+ name: 'John Doe',
134
+ department: 'Engineering',
135
+ isRemote: true,
136
+ created: expect.any(moment),
137
+ updated: expect.any(moment),
138
+ },
139
+ cached
140
+ );
141
+ });
142
+
143
+ it('should update records in cached db by object query and queue update operation', async () => {
144
+ // Insert two records
145
+ await transaction.insert(employeeTable, {
146
+ name: 'John Doe',
147
+ department: 'Engineering',
148
+ isRemote: true,
149
+ });
150
+ await transaction.insert(employeeTable, {
151
+ name: 'Jane Doe',
152
+ department: 'Engineering',
153
+ isRemote: false,
154
+ });
155
+
156
+ // Update all records in Engineering department
157
+ const updates = { jobTitle: 'Software Engineer' };
158
+ transaction.update(employeeTable, updates, { department: 'Engineering' });
159
+
160
+ // Verify all matching records were updated in cache
161
+ const cachedRecords = Object.values(transaction.recordMap(employeeTable.name));
162
+ cachedRecords.forEach((record) => {
163
+ expect(record.jobTitle).toBe('Software Engineer');
164
+ });
165
+
166
+ // Verify operation was queued
167
+ expect(transaction.ops[2]).toEqual({
168
+ name: 'update',
169
+ args: [employeeTable, updates, { department: 'Engineering' }],
170
+ });
171
+ });
172
+
173
+ it('should throw error when updating non-existent record by id', () => {
174
+ const update = { id: 'non-existent', name: 'John' };
175
+
176
+ expect(() => transaction.update(employeeTable, update)).toThrow(
177
+ 'Attempting to update record not in the cached db'
178
+ );
179
+ });
180
+ });
181
+
182
+ describe('delete', () => {
183
+ it('should remove record from cached db by id and queue delete operation', async () => {
184
+ const record = await transaction.insert(employeeTable, {
185
+ name: 'John Doe',
186
+ department: 'Engineering',
187
+ });
188
+
189
+ transaction.delete(employeeTable, { id: record.id });
190
+
191
+ expect(transaction.recordMap(employeeTable.name)[record.id]).toBeUndefined();
192
+ expect(transaction.ops[1]).toEqual({
193
+ name: 'delete',
194
+ args: [employeeTable, { id: record.id }],
195
+ });
196
+ expect(deleteCallback).toHaveBeenCalledTimes(1);
197
+ expect(deleteCallback).toHaveBeenCalledWith(employeeTable, record);
198
+ });
199
+
200
+ it('should remove records from cached db by object query and queue delete operation', async () => {
201
+ await transaction.insert(employeeTable, {
202
+ name: 'John Doe',
203
+ department: 'Engineering',
204
+ isRemote: true,
205
+ });
206
+ await transaction.insert(employeeTable, {
207
+ name: 'Jane Doe',
208
+ department: 'Engineering',
209
+ isRemote: true,
210
+ });
211
+
212
+ transaction.delete(employeeTable, { department: 'Engineering' });
213
+
214
+ const cachedRecords = Object.values(transaction.recordMap(employeeTable.name));
215
+ expect(cachedRecords).toHaveLength(0);
216
+ expect(transaction.ops[2]).toEqual({
217
+ name: 'delete',
218
+ args: [employeeTable, { department: 'Engineering' }],
219
+ });
220
+ });
221
+ });
222
+
223
+ describe('cache integrity', () => {
224
+ it('should preserve unspecified fields during partial updates', async () => {
225
+ // Insert a record with multiple fields
226
+ const record = await transaction.insert(employeeTable, {
227
+ name: 'John Doe',
228
+ department: 'Engineering',
229
+ jobTitle: 'Software Engineer',
230
+ isRemote: true,
231
+ startDate: new Date('2024-01-01'),
232
+ });
233
+
234
+ // Perform a partial update
235
+ transaction.update(employeeTable, {
236
+ id: record.id,
237
+ department: 'Product',
238
+ });
239
+
240
+ // Verify that only the specified field was updated and others remain unchanged
241
+ const cached = transaction.recordMap(employeeTable.name)[record.id];
242
+ expect(cached).toEqual({
243
+ id: record.id,
244
+ name: 'John Doe',
245
+ department: 'Product', // Only this field should change
246
+ jobTitle: 'Software Engineer',
247
+ isRemote: true,
248
+ startDate: expect.any(Date),
249
+ created: expect.any(moment),
250
+ updated: expect.any(moment),
251
+ });
252
+ });
253
+
254
+ it('should maintain referential integrity in the cache across operations', async () => {
255
+ // Insert initial record
256
+ const record = await transaction.insert(employeeTable, {
257
+ name: 'John Doe',
258
+ department: 'Engineering',
259
+ });
260
+
261
+ // Get a reference to the cached record
262
+ const initialCacheRef = transaction.recordMap(employeeTable.name)[record.id];
263
+
264
+ // Store references to verify they stay up to date
265
+ const references = [initialCacheRef, transaction.recordMap(employeeTable.name)[record.id]];
266
+
267
+ // Perform multiple updates
268
+ transaction.update(employeeTable, {
269
+ id: record.id,
270
+ jobTitle: 'Software Engineer',
271
+ });
272
+
273
+ transaction.update(employeeTable, {
274
+ id: record.id,
275
+ isRemote: true,
276
+ });
277
+
278
+ // Get another reference after updates
279
+ references.push(transaction.recordMap(employeeTable.name)[record.id]);
280
+
281
+ // All references should point to the same, updated object
282
+ const expectedState = {
283
+ id: record.id,
284
+ name: 'John Doe',
285
+ department: 'Engineering',
286
+ jobTitle: 'Software Engineer',
287
+ isRemote: true,
288
+ created: expect.any(moment),
289
+ updated: expect.any(moment),
290
+ };
291
+
292
+ // Verify all references have the same, updated state
293
+ references.forEach((ref) => {
294
+ expect(ref).toEqual(expectedState);
295
+ });
296
+
297
+ // Verify all references point to the exact same object
298
+ references.forEach((ref) => {
299
+ expect(ref).toBe(initialCacheRef);
300
+ });
301
+ });
302
+
303
+ it('should maintain referential integrity when updating via query', async () => {
304
+ // Insert records
305
+ const record = await transaction.insert(employeeTable, {
306
+ name: 'John Doe',
307
+ department: 'Engineering',
308
+ isRemote: true,
309
+ });
310
+
311
+ // Get initial reference
312
+ const initialRef = transaction.recordMap(employeeTable.name)[record.id];
313
+
314
+ // Update via query
315
+ transaction.update(employeeTable, { jobTitle: 'Remote Engineer' }, { department: 'Engineering', isRemote: true });
316
+
317
+ // Get new reference
318
+ const currentRef = transaction.recordMap(employeeTable.name)[record.id];
319
+
320
+ // Verify both references point to the same object
321
+ expect(currentRef).toBe(initialRef);
322
+
323
+ // Verify the object was updated correctly
324
+ expect(currentRef).toEqual({
325
+ id: record.id,
326
+ name: 'John Doe',
327
+ department: 'Engineering',
328
+ isRemote: true,
329
+ jobTitle: 'Remote Engineer',
330
+ created: expect.any(moment),
331
+ updated: expect.any(moment),
332
+ });
333
+ });
334
+ });
335
+ });