Skip to content

Commit aea7596

Browse files
authored
feat: Extend storage adapter interface to optionally return matchedCount and modifiedCount from DatabaseController.update with many: true (#10353)
1 parent 6183d4b commit aea7596

3 files changed

Lines changed: 125 additions & 1 deletion

File tree

spec/DatabaseController.spec.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,102 @@ describe('DatabaseController', function () {
841841
expect(findOneAndUpdateSpy).toHaveBeenCalled();
842842
});
843843
});
844+
845+
describe_only_db('mongo')('update with many', () => {
846+
it('should return matchedCount and modifiedCount when multiple docs are updated', async () => {
847+
const config = Config.get(Parse.applicationId);
848+
const obj1 = new Parse.Object('TestObject');
849+
const obj2 = new Parse.Object('TestObject');
850+
const obj3 = new Parse.Object('TestObject');
851+
obj1.set('status', 'pending');
852+
obj2.set('status', 'pending');
853+
obj3.set('status', 'pending');
854+
await Parse.Object.saveAll([obj1, obj2, obj3]);
855+
856+
const result = await config.database.update(
857+
'TestObject',
858+
{ status: 'pending' },
859+
{ status: 'done' },
860+
{ many: true }
861+
);
862+
863+
expect(result.matchedCount).toBe(3);
864+
expect(result.modifiedCount).toBe(3);
865+
});
866+
867+
it('should return matchedCount > 0 and modifiedCount 0 when values are already current', async () => {
868+
const config = Config.get(Parse.applicationId);
869+
const obj1 = new Parse.Object('TestObject');
870+
const obj2 = new Parse.Object('TestObject');
871+
obj1.set('status', 'done');
872+
obj2.set('status', 'done');
873+
await Parse.Object.saveAll([obj1, obj2]);
874+
875+
const result = await config.database.update(
876+
'TestObject',
877+
{ status: 'done' },
878+
{ status: 'done' },
879+
{ many: true }
880+
);
881+
882+
expect(result.matchedCount).toBe(2);
883+
expect(result.modifiedCount).toBe(0);
884+
});
885+
886+
it('should return matchedCount 0 and modifiedCount 0 when no docs match', async () => {
887+
const config = Config.get(Parse.applicationId);
888+
const result = await config.database.update(
889+
'TestObject',
890+
{ status: 'nonexistent' },
891+
{ status: 'done' },
892+
{ many: true }
893+
);
894+
895+
expect(result.matchedCount).toBe(0);
896+
expect(result.modifiedCount).toBe(0);
897+
});
898+
899+
it('should return only matchedCount and modifiedCount for op-based updates', async () => {
900+
const config = Config.get(Parse.applicationId);
901+
const obj1 = new Parse.Object('TestObject');
902+
const obj2 = new Parse.Object('TestObject');
903+
obj1.set('score', 1);
904+
obj2.set('score', 1);
905+
await Parse.Object.saveAll([obj1, obj2]);
906+
907+
const result = await config.database.update(
908+
'TestObject',
909+
{ score: { $exists: true } },
910+
{ score: { __op: 'Increment', amount: 5 } },
911+
{ many: true }
912+
);
913+
914+
expect(result.matchedCount).toBe(2);
915+
expect(result.modifiedCount).toBe(2);
916+
expect(Object.keys(result)).toEqual(['matchedCount', 'modifiedCount']);
917+
});
918+
919+
it('should return raw adapter result when skipSanitization is true', async () => {
920+
const config = Config.get(Parse.applicationId);
921+
const obj1 = new Parse.Object('TestObject');
922+
obj1.set('status', 'pending');
923+
await obj1.save();
924+
925+
const result = await config.database.update(
926+
'TestObject',
927+
{ status: 'pending' },
928+
{ status: 'done' },
929+
{ many: true },
930+
true // skipSanitization
931+
);
932+
933+
// skipSanitization returns raw adapter result, which for MongoDB
934+
// includes additional fields beyond matchedCount and modifiedCount
935+
expect(result.matchedCount).toBe(1);
936+
expect(result.modifiedCount).toBe(1);
937+
expect(result.acknowledged).toBe(true);
938+
});
939+
});
844940
});
845941

846942
function buildCLP(pointerNames) {

src/Adapters/Storage/StorageAdapter.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export type UpdateQueryOptions = {
2929

3030
export type FullQueryOptions = QueryOptions & UpdateQueryOptions;
3131

32+
export type UpdateManyResult = {
33+
matchedCount?: number,
34+
modifiedCount?: number,
35+
};
36+
3237
export interface StorageAdapter {
3338
canSortOnJoinTables: boolean;
3439
schemaCacheTtl: ?number;
@@ -56,13 +61,19 @@ export interface StorageAdapter {
5661
query: QueryType,
5762
transactionalSession: ?any
5863
): Promise<void>;
64+
/**
65+
* Updates all objects that match the given query.
66+
* Adapters may return an `UpdateManyResult` with optional `matchedCount` and `modifiedCount`
67+
* to indicate how many documents were matched and modified. If not provided, the caller
68+
* receives `undefined` for these fields.
69+
*/
5970
updateObjectsByQuery(
6071
className: string,
6172
schema: SchemaType,
6273
query: QueryType,
6374
update: any,
6475
transactionalSession: ?any
65-
): Promise<[any]>;
76+
): Promise<[any] | UpdateManyResult>;
6677
findOneAndUpdate(
6778
className: string,
6879
schema: SchemaType,

src/Controllers/DatabaseController.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,13 @@ class DatabaseController {
533533
});
534534
}
535535

536+
/**
537+
* Updates objects in the database that match the given query.
538+
* @param {Object} options
539+
* @param {boolean} [options.many=false] When true, updates all matching documents
540+
* and returns `{ matchedCount, modifiedCount }` where values are numbers if the
541+
* storage adapter supports `UpdateManyResult`, or `undefined` otherwise.
542+
*/
536543
update(
537544
className: string,
538545
query: any,
@@ -701,6 +708,16 @@ class DatabaseController {
701708
if (skipSanitization) {
702709
return Promise.resolve(result);
703710
}
711+
if (many) {
712+
return {
713+
matchedCount: typeof result?.matchedCount === 'number'
714+
? result.matchedCount
715+
: undefined,
716+
modifiedCount: typeof result?.modifiedCount === 'number'
717+
? result.modifiedCount
718+
: undefined,
719+
};
720+
}
704721
return this._sanitizeDatabaseResult(originalUpdate, result);
705722
});
706723
});

0 commit comments

Comments
 (0)