feat: Extend storage adapter interface to optionally return matchedCount and modifiedCount from DatabaseController.update with many: true (#10353)

This commit is contained in:
Manuel
2026-03-30 01:09:10 +01:00
committed by GitHub
parent 6183d4bfc6
commit aea7596cd2
3 changed files with 125 additions and 1 deletions
+96
View File
@@ -841,6 +841,102 @@ 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']);
});
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);
});
});
});
function buildCLP(pointerNames) {
+12 -1
View File
@@ -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<void>;
/**
* 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,
+17
View File
@@ -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,
@@ -701,6 +708,16 @@ class DatabaseController {
if (skipSanitization) {
return Promise.resolve(result);
}
if (many) {
return {
matchedCount: typeof result?.matchedCount === 'number'
? result.matchedCount
: undefined,
modifiedCount: typeof result?.modifiedCount === 'number'
? result.modifiedCount
: undefined,
};
}
return this._sanitizeDatabaseResult(originalUpdate, result);
});
});