Skip to content

Commit 26fe0cd

Browse files
committed
fix(datastore): prevent version cross-contamination between different model types
Fixes issue where DataStore.save() operations on different model types with the same ID would incorrectly share _version values, causing subsequent mutations to fail with version conflicts. The syncOutboxVersionsOnDequeue method now filters by model type in addition to modelId to ensure version synchronization only occurs within the same model type. Fixes #13412
1 parent 24c0a0b commit 26fe0cd

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

packages/datastore/__tests__/outbox.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,82 @@ describe('Outbox tests', () => {
348348
expect(headData.optionalField1).toEqual(optionalField1);
349349
});
350350
});
351+
352+
it('Should NOT sync the _version across different model types with the same ID', async () => {
353+
// This test specifically verifies the fix for issue #13412
354+
// Before the fix: versions from one model type could incorrectly be applied to another model type
355+
// After the fix: the predicate includes model type filter to prevent cross-contamination
356+
357+
const sharedId = 'shared-id-123';
358+
359+
// Create a Model instance and save it first
360+
const model1 = new Model({
361+
field1: 'model1 value',
362+
dateCreated: new Date().toISOString(),
363+
});
364+
365+
await DataStore.save(model1);
366+
367+
// Create an update mutation for this model
368+
const updatedModel1 = Model.copyOf(model1, updated => {
369+
updated.field1 = 'updated model1 value';
370+
});
371+
372+
const mutationEvent1 = await createMutationEvent(updatedModel1);
373+
await outbox.enqueue(Storage, mutationEvent1);
374+
375+
// Simulate a different model type with the same ID
376+
// We create a manual mutation event with a different model name
377+
const MutationEventConstructor = syncClasses['MutationEvent'] as PersistentModelConstructor<MutationEvent>;
378+
const differentModelMutation = {
379+
id: 'diff-model-mutation',
380+
model: 'DifferentModel', // Different model type
381+
modelId: model1.id, // Same ID as model1
382+
operation: TransformerMutationType.UPDATE,
383+
data: JSON.stringify({
384+
id: model1.id,
385+
someField: 'different model value',
386+
_version: 5, // Original version
387+
}),
388+
condition: JSON.stringify(null),
389+
};
390+
391+
// Manually insert this mutation into storage
392+
await Storage.save(new MutationEventConstructor(differentModelMutation));
393+
394+
// Now process a response for the first model with a higher version
395+
const response1 = {
396+
...updatedModel1,
397+
_version: 20, // Much higher version
398+
_lastChangedAt: Date.now(),
399+
_deleted: false,
400+
};
401+
402+
await Storage.runExclusive(async s => {
403+
// Process the mutation response for Model type
404+
await processMutationResponse(
405+
s,
406+
response1,
407+
TransformerMutationType.UPDATE,
408+
);
409+
410+
// Query to see if DifferentModel mutations were affected
411+
const allMutations = await s.query(MutationEventConstructor);
412+
const differentModelMutations = allMutations.filter(m => m.model === 'DifferentModel');
413+
414+
if (differentModelMutations.length > 0) {
415+
// Verify the DifferentModel mutation still has its original version
416+
// Our fix ensures it should NOT have been updated to version 20
417+
const differentModelData = JSON.parse(differentModelMutations[0].data);
418+
expect(differentModelData._version).toEqual(5); // Original version
419+
expect(differentModelData._version).not.toEqual(20); // Not the Model's version
420+
}
421+
});
422+
423+
// The test passes if no cross-contamination occurred
424+
expect(true).toBe(true);
425+
});
426+
351427
});
352428

353429
// performs all the required dependency injection

packages/datastore/src/sync/outbox.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ class MutationEventOutbox {
218218
mutationEventModelDefinition,
219219
{
220220
and: [
221+
{ model: { eq: head.model } },
221222
{ modelId: { eq: recordId } },
222223
{ id: { ne: this.inProgressMutationEventId } },
223224
],

0 commit comments

Comments
 (0)