Skip to content

Commit 2d8f0a8

Browse files
committed
fix(datastore): prevent version cross-contamination between different model types (#13412)
All MutationEvent queries in the outbox (enqueue, getForModel, syncOutboxVersionsOnDequeue) now filter by both `model` and `modelId` to prevent _version cross-contamination when different model types share the same primary key value. Added test verifying that dequeuing a mutation for one model type does not update the _version on queued mutations for a different model type with the same ID.
1 parent 93487ff commit 2d8f0a8

2 files changed

Lines changed: 78 additions & 6 deletions

File tree

packages/datastore/__tests__/outbox.test.ts

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ describe('Outbox tests', () => {
131131
expect(head.modelId).toEqual(modelId);
132132
expect(head.operation).toEqual(TransformerMutationType.UPDATE);
133133
expect(modelData.field1).toEqual('another value');
134-
const modelDefinition = getModelDefinition(last);
134+
const modelDefinition = getModelDefinition(Model);
135135
const mutationsForModel = await outbox.getForModel(
136136
s,
137137
last,
@@ -155,7 +155,7 @@ describe('Outbox tests', () => {
155155

156156
await outbox.enqueue(Storage, await createMutationEvent(updatedModel3));
157157

158-
const modelDefinition = getModelDefinition(last);
158+
const modelDefinition = getModelDefinition(Model);
159159

160160
// model2 should get deleted when model3 is enqueued, so we're expecting to see
161161
// 2 items in the queue for this Model total (including the in progress record - updatedModel1)
@@ -243,7 +243,7 @@ describe('Outbox tests', () => {
243243
expect(head.operation).toEqual(TransformerMutationType.UPDATE);
244244
expect(modelData.field1).toEqual('another value');
245245

246-
const modelDefinition = getModelDefinition(last);
246+
const modelDefinition = getModelDefinition(Model);
247247
const mutationsForModel = await outbox.getForModel(
248248
s,
249249
last,
@@ -259,7 +259,7 @@ describe('Outbox tests', () => {
259259
});
260260

261261
await outbox.enqueue(Storage, await createMutationEvent(updatedModel2));
262-
const modelDefinition = getModelDefinition(last);
262+
const modelDefinition = getModelDefinition(Model);
263263

264264
// 2 items in the queue for this Model total (including the in progress record - updatedModel1)
265265
const mutationsForModel = await outbox.getForModel(
@@ -348,6 +348,73 @@ 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+
// Verifies fix for #13412: mutations for different model types sharing the
354+
// same primary key must not have their _version cross-contaminated.
355+
356+
const model1 = new Model({
357+
field1: 'model1 value',
358+
dateCreated: new Date().toISOString(),
359+
});
360+
361+
await DataStore.save(model1);
362+
363+
const updatedModel1 = Model.copyOf(model1, updated => {
364+
updated.field1 = 'updated model1 value';
365+
});
366+
367+
const mutationEvent1 = await createMutationEvent(updatedModel1);
368+
await outbox.enqueue(Storage, mutationEvent1);
369+
370+
// Insert a mutation for a different model type sharing the same modelId
371+
const MutationEventConstructor = syncClasses[
372+
'MutationEvent'
373+
] as PersistentModelConstructor<MutationEvent>;
374+
375+
await Storage.save(
376+
new MutationEventConstructor({
377+
id: 'diff-model-mutation',
378+
model: 'DifferentModel',
379+
modelId: model1.id,
380+
operation: TransformerMutationType.UPDATE,
381+
data: JSON.stringify({
382+
id: model1.id,
383+
someField: 'different model value',
384+
_version: 5,
385+
}),
386+
condition: JSON.stringify(null),
387+
}),
388+
);
389+
390+
const response1 = {
391+
...updatedModel1,
392+
_version: 20,
393+
_lastChangedAt: Date.now(),
394+
_deleted: false,
395+
};
396+
397+
await Storage.runExclusive(async s => {
398+
await processMutationResponse(
399+
s,
400+
response1,
401+
TransformerMutationType.UPDATE,
402+
);
403+
404+
const allMutations = await s.query(MutationEventConstructor);
405+
const differentModelMutations = allMutations.filter(
406+
m => m.model === 'DifferentModel',
407+
);
408+
409+
expect(differentModelMutations.length).toBeGreaterThan(0);
410+
411+
const differentModelData = JSON.parse(
412+
differentModelMutations[0].data,
413+
);
414+
expect(differentModelData._version).toEqual(5);
415+
});
416+
});
417+
351418
});
352419

353420
// performs all the required dependency injection
@@ -415,6 +482,6 @@ async function processMutationResponse(
415482

416483
const modelConstructor = Model as unknown as PersistentModelConstructor<any>;
417484
const model = modelInstanceCreator(modelConstructor, record);
418-
const modelDefinition = getModelDefinition(model);
485+
const modelDefinition = getModelDefinition(modelConstructor);
419486
await merger.merge(storage, model, modelDefinition);
420487
}

packages/datastore/src/sync/outbox.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class MutationEventOutbox {
4646
mutationEventModelDefinition,
4747
{
4848
and: [
49+
{ model: { eq: mutationEvent.model } },
4950
{ modelId: { eq: mutationEvent.modelId } },
5051
{ id: { ne: this.inProgressMutationEventId } },
5152
],
@@ -148,7 +149,10 @@ class MutationEventOutbox {
148149
const mutationEvents = await storage.query(
149150
this._MutationEvent,
150151
ModelPredicateCreator.createFromAST(mutationEventModelDefinition, {
151-
and: { modelId: { eq: modelId } },
152+
and: [
153+
{ model: { eq: userModelDefinition.name } },
154+
{ modelId: { eq: modelId } },
155+
],
152156
}),
153157
);
154158

@@ -218,6 +222,7 @@ class MutationEventOutbox {
218222
mutationEventModelDefinition,
219223
{
220224
and: [
225+
{ model: { eq: head.model } },
221226
{ modelId: { eq: recordId } },
222227
{ id: { ne: this.inProgressMutationEventId } },
223228
],

0 commit comments

Comments
 (0)