From 400bfb9c845de3c801101dbc2eb195ffb7a808ac Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Sun, 27 Oct 2024 18:39:54 +0100 Subject: [PATCH] Added deep equals to object comparison in handle method to support correctly mutable entities --- .../src/core/collection/pongoCollection.ts | 3 +- src/packages/pongo/src/core/index.ts | 1 + .../pongo/src/core/utils/deepEquals.ts | 56 +++++++++++++++++++ src/packages/pongo/src/core/utils/index.ts | 1 + .../pongo/src/e2e/postgres.e2e.spec.ts | 37 ++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/packages/pongo/src/core/utils/deepEquals.ts create mode 100644 src/packages/pongo/src/core/utils/index.ts diff --git a/src/packages/pongo/src/core/collection/pongoCollection.ts b/src/packages/pongo/src/core/collection/pongoCollection.ts index 4a48bbf..8873123 100644 --- a/src/packages/pongo/src/core/collection/pongoCollection.ts +++ b/src/packages/pongo/src/core/collection/pongoCollection.ts @@ -14,6 +14,7 @@ import { } from '@event-driven-io/dumbo'; import { v7 as uuid } from 'uuid'; import { + deepEquals, expectedVersionValue, operationResult, type CollectionOperationOptions, @@ -370,7 +371,7 @@ export const pongoCollection = < const result = await handle(existing as T); - if (existing === result) + if (deepEquals(existing as T, result)) return operationResult>( { successful: true, diff --git a/src/packages/pongo/src/core/index.ts b/src/packages/pongo/src/core/index.ts index a5475b6..7009e43 100644 --- a/src/packages/pongo/src/core/index.ts +++ b/src/packages/pongo/src/core/index.ts @@ -6,3 +6,4 @@ export * from './pongoSession'; export * from './pongoTransaction'; export * from './schema'; export * from './typing'; +export * from './utils'; diff --git a/src/packages/pongo/src/core/utils/deepEquals.ts b/src/packages/pongo/src/core/utils/deepEquals.ts new file mode 100644 index 0000000..f0c513e --- /dev/null +++ b/src/packages/pongo/src/core/utils/deepEquals.ts @@ -0,0 +1,56 @@ +export const deepEquals = (left: T, right: T): boolean => { + if (isEquatable(left)) { + return left.equals(right); + } + + if (Array.isArray(left)) { + return ( + Array.isArray(right) && + left.length === right.length && + left.every((val, index) => deepEquals(val, right[index])) + ); + } + + if ( + typeof left !== 'object' || + typeof right !== 'object' || + left === null || + right === null + ) { + return left === right; + } + + if (Array.isArray(right)) return false; + + const keys1 = Object.keys(left); + const keys2 = Object.keys(right); + + if ( + keys1.length !== keys2.length || + !keys1.every((key) => keys2.includes(key)) + ) + return false; + + for (const key in left) { + if (left[key] instanceof Function && right[key] instanceof Function) + continue; + + const isEqual = deepEquals(left[key], right[key]); + if (!isEqual) { + return false; + } + } + + return true; +}; + +export type Equatable = { equals: (right: T) => boolean } & T; + +export const isEquatable = (left: T): left is Equatable => { + return ( + left && + typeof left === 'object' && + 'equals' in left && + typeof left['equals'] === 'function' + ); +}; diff --git a/src/packages/pongo/src/core/utils/index.ts b/src/packages/pongo/src/core/utils/index.ts new file mode 100644 index 0000000..f8de580 --- /dev/null +++ b/src/packages/pongo/src/core/utils/index.ts @@ -0,0 +1 @@ +export * from './deepEquals'; diff --git a/src/packages/pongo/src/e2e/postgres.e2e.spec.ts b/src/packages/pongo/src/e2e/postgres.e2e.spec.ts index 821dbaa..bdff085 100644 --- a/src/packages/pongo/src/e2e/postgres.e2e.spec.ts +++ b/src/packages/pongo/src/e2e/postgres.e2e.spec.ts @@ -1177,6 +1177,43 @@ void describe('MongoDB Compatibility Tests', () => { }); }); + void it('should make the change if the handler returns the existing document changed', async () => { + const pongoCollection = pongoDb.collection('handleCollection'); + + const existingDoc: User = { name: 'John', age: 25 }; + + const pongoInsertResult = await pongoCollection.insertOne(existingDoc); + + const handle = (existing: User | null) => { + if (existing) existing.name = 'New'; + return existing; + }; + + const resultPongo = await pongoCollection.handle( + pongoInsertResult.insertedId!, + handle, + ); + + assert(resultPongo.successful); + assert.deepStrictEqual(resultPongo.document, { + ...existingDoc, + _id: pongoInsertResult.insertedId, + name: 'New', + _version: 2n, + }); + + const pongoDoc = await pongoCollection.findOne({ + _id: pongoInsertResult.insertedId!, + }); + + assert.deepStrictEqual(pongoDoc, { + ...existingDoc, + _id: pongoInsertResult.insertedId, + name: 'New', + _version: 2n, + }); + }); + void describe('No filter', () => { void it('should filter and count without filter specified', async () => { const pongoCollection = pongoDb.collection('nofilter');