Skip to content

Commit 59e599b

Browse files
author
Hong Sukhoon
committed
fix: Prevent stale data reads during update validation with primary readPreference
- Ensures update validation always reads from primary replica in DB - Fixes potential data consistency issues in distributed database environments - Adds comprehensive tests for validateOnly behavior with primary readPreference
1 parent 1a24ede commit 59e599b

File tree

2 files changed

+71
-1
lines changed

2 files changed

+71
-1
lines changed

spec/DatabaseController.spec.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,76 @@ describe('DatabaseController', function () {
615615
expect(result2.length).toEqual(1);
616616
});
617617
});
618+
619+
describe('update with validateOnly', () => {
620+
const mockStorageAdapter = {
621+
findOneAndUpdate: () => Promise.resolve({}),
622+
find: () => Promise.resolve([{ objectId: 'test123', testField: 'initialValue' }]),
623+
watch: () => Promise.resolve(),
624+
getAllClasses: () =>
625+
Promise.resolve([
626+
{
627+
className: 'TestObject',
628+
fields: { testField: 'String' },
629+
indexes: {},
630+
classLevelPermissions: { protectedFields: {} },
631+
},
632+
]),
633+
};
634+
635+
it('should use primary readPreference when validateOnly is true', async () => {
636+
const databaseController = new DatabaseController(mockStorageAdapter, {});
637+
const findSpy = spyOn(mockStorageAdapter, 'find').and.callThrough();
638+
const findOneAndUpdateSpy = spyOn(mockStorageAdapter, 'findOneAndUpdate').and.callThrough();
639+
640+
try {
641+
// Call update with validateOnly: true (same as RestWrite.runBeforeSaveTrigger)
642+
await databaseController.update(
643+
'TestObject',
644+
{ objectId: 'test123' },
645+
{ testField: 'newValue' },
646+
{},
647+
true, // skipSanitization: true (matches RestWrite behavior)
648+
true // validateOnly: true
649+
);
650+
} catch (error) {
651+
// validateOnly may throw, but we're checking the find call options
652+
}
653+
654+
// Verify that find was called with primary readPreference
655+
expect(findSpy).toHaveBeenCalled();
656+
const findCall = findSpy.calls.mostRecent();
657+
expect(findCall.args[3]).toEqual({ readPreference: 'primary' }); // options parameter
658+
659+
// Verify that findOneAndUpdate was NOT called (only validation, no actual update)
660+
expect(findOneAndUpdateSpy).not.toHaveBeenCalled();
661+
});
662+
663+
it('should not use primary readPreference when validateOnly is false', async () => {
664+
const databaseController = new DatabaseController(mockStorageAdapter, {});
665+
const findSpy = spyOn(mockStorageAdapter, 'find').and.callThrough();
666+
const findOneAndUpdateSpy = spyOn(mockStorageAdapter, 'findOneAndUpdate').and.callThrough();
667+
668+
try {
669+
// Call update with validateOnly: false
670+
await databaseController.update(
671+
'TestObject',
672+
{ objectId: 'test123' },
673+
{ testField: 'newValue' },
674+
{},
675+
false, // skipSanitization
676+
false // validateOnly
677+
);
678+
} catch (error) {
679+
// May throw for other reasons, but we're checking the call pattern
680+
}
681+
682+
// When validateOnly is false, find should not be called for validation
683+
// Instead, findOneAndUpdate should be called
684+
expect(findSpy).not.toHaveBeenCalled();
685+
expect(findOneAndUpdateSpy).toHaveBeenCalled();
686+
});
687+
});
618688
});
619689

620690
function buildCLP(pointerNames) {

src/Controllers/DatabaseController.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ class DatabaseController {
593593
convertUsernameToLowercase(update, className, this.options);
594594
transformAuthData(className, update, schema);
595595
if (validateOnly) {
596-
return this.adapter.find(className, schema, query, {}).then(result => {
596+
return this.adapter.find(className, schema, query, { readPreference: 'primary' }).then(result => {
597597
if (!result || !result.length) {
598598
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
599599
}

0 commit comments

Comments
 (0)