From f6b1afa6d997fe5f8536b813f61c1dc2c8aa02eb Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:53:11 +0100 Subject: [PATCH 1/2] feat --- spec/DatabaseController.spec.js | 75 ++++++++++++++++++++++++++ src/Adapters/Storage/StorageAdapter.js | 13 ++++- src/Controllers/DatabaseController.js | 17 ++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index ac83c11dde..d1da818055 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -841,6 +841,81 @@ describe('DatabaseController', function () { expect(findOneAndUpdateSpy).toHaveBeenCalled(); }); }); + + describe_only_db('mongo')('update with many', () => { + it('should return matchedCount and modifiedCount when multiple docs are updated', async () => { + const config = Config.get(Parse.applicationId); + const obj1 = new Parse.Object('TestObject'); + const obj2 = new Parse.Object('TestObject'); + const obj3 = new Parse.Object('TestObject'); + obj1.set('status', 'pending'); + obj2.set('status', 'pending'); + obj3.set('status', 'pending'); + await Parse.Object.saveAll([obj1, obj2, obj3]); + + const result = await config.database.update( + 'TestObject', + { status: 'pending' }, + { status: 'done' }, + { many: true } + ); + + expect(result.matchedCount).toBe(3); + expect(result.modifiedCount).toBe(3); + }); + + it('should return matchedCount > 0 and modifiedCount 0 when values are already current', async () => { + const config = Config.get(Parse.applicationId); + const obj1 = new Parse.Object('TestObject'); + const obj2 = new Parse.Object('TestObject'); + obj1.set('status', 'done'); + obj2.set('status', 'done'); + await Parse.Object.saveAll([obj1, obj2]); + + const result = await config.database.update( + 'TestObject', + { status: 'done' }, + { status: 'done' }, + { many: true } + ); + + expect(result.matchedCount).toBe(2); + expect(result.modifiedCount).toBe(0); + }); + + it('should return matchedCount 0 and modifiedCount 0 when no docs match', async () => { + const config = Config.get(Parse.applicationId); + const result = await config.database.update( + 'TestObject', + { status: 'nonexistent' }, + { status: 'done' }, + { many: true } + ); + + expect(result.matchedCount).toBe(0); + expect(result.modifiedCount).toBe(0); + }); + + it('should return only matchedCount and modifiedCount for op-based updates', async () => { + const config = Config.get(Parse.applicationId); + const obj1 = new Parse.Object('TestObject'); + const obj2 = new Parse.Object('TestObject'); + obj1.set('score', 1); + obj2.set('score', 1); + await Parse.Object.saveAll([obj1, obj2]); + + const result = await config.database.update( + 'TestObject', + { score: { $exists: true } }, + { score: { __op: 'Increment', amount: 5 } }, + { many: true } + ); + + expect(result.matchedCount).toBe(2); + expect(result.modifiedCount).toBe(2); + expect(Object.keys(result)).toEqual(['matchedCount', 'modifiedCount']); + }); + }); }); function buildCLP(pointerNames) { diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index d25c9753c0..49e1c23d36 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -29,6 +29,11 @@ export type UpdateQueryOptions = { export type FullQueryOptions = QueryOptions & UpdateQueryOptions; +export type UpdateManyResult = { + matchedCount?: number, + modifiedCount?: number, +}; + export interface StorageAdapter { canSortOnJoinTables: boolean; schemaCacheTtl: ?number; @@ -56,13 +61,19 @@ export interface StorageAdapter { query: QueryType, transactionalSession: ?any ): Promise; + /** + * Updates all objects that match the given query. + * Adapters may return an `UpdateManyResult` with optional `matchedCount` and `modifiedCount` + * to indicate how many documents were matched and modified. If not provided, the caller + * receives `undefined` for these fields. + */ updateObjectsByQuery( className: string, schema: SchemaType, query: QueryType, update: any, transactionalSession: ?any - ): Promise<[any]>; + ): Promise<[any] | UpdateManyResult>; findOneAndUpdate( className: string, schema: SchemaType, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 68e89732f4..f73c60f628 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -533,6 +533,13 @@ class DatabaseController { }); } + /** + * Updates objects in the database that match the given query. + * @param {Object} options + * @param {boolean} [options.many=false] When true, updates all matching documents + * and returns `{ matchedCount, modifiedCount }` where values are numbers if the + * storage adapter supports `UpdateManyResult`, or `undefined` otherwise. + */ update( className: string, query: any, @@ -698,6 +705,16 @@ class DatabaseController { }); }) .then(result => { + if (many) { + return { + matchedCount: typeof result?.matchedCount === 'number' + ? result.matchedCount + : undefined, + modifiedCount: typeof result?.modifiedCount === 'number' + ? result.modifiedCount + : undefined, + }; + } if (skipSanitization) { return Promise.resolve(result); } From e656a538db88797a2f51293d07ab2a6f449c7dda Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:57:33 +0100 Subject: [PATCH 2/2] fix --- spec/DatabaseController.spec.js | 21 +++++++++++++++++++++ src/Controllers/DatabaseController.js | 6 +++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index d1da818055..dc2f84bc5f 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -915,6 +915,27 @@ describe('DatabaseController', function () { expect(result.modifiedCount).toBe(2); expect(Object.keys(result)).toEqual(['matchedCount', 'modifiedCount']); }); + + it('should return raw adapter result when skipSanitization is true', async () => { + const config = Config.get(Parse.applicationId); + const obj1 = new Parse.Object('TestObject'); + obj1.set('status', 'pending'); + await obj1.save(); + + const result = await config.database.update( + 'TestObject', + { status: 'pending' }, + { status: 'done' }, + { many: true }, + true // skipSanitization + ); + + // skipSanitization returns raw adapter result, which for MongoDB + // includes additional fields beyond matchedCount and modifiedCount + expect(result.matchedCount).toBe(1); + expect(result.modifiedCount).toBe(1); + expect(result.acknowledged).toBe(true); + }); }); }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index f73c60f628..c8adc7937d 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -705,6 +705,9 @@ class DatabaseController { }); }) .then(result => { + if (skipSanitization) { + return Promise.resolve(result); + } if (many) { return { matchedCount: typeof result?.matchedCount === 'number' @@ -715,9 +718,6 @@ class DatabaseController { : undefined, }; } - if (skipSanitization) { - return Promise.resolve(result); - } return this._sanitizeDatabaseResult(originalUpdate, result); }); });