Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions spec/DatabaseController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
13 changes: 12 additions & 1 deletion src/Adapters/Storage/StorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
});
});
Expand Down
Loading