Skip to content
Open
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
8 changes: 6 additions & 2 deletions packages/host/app/services/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { task } from 'ember-concurrency';

import { cloneDeep } from 'lodash-es';
import { isEqual } from 'lodash-es';
import { merge } from 'lodash-es';
import { merge, mergeWith } from 'lodash-es';

import { TrackedObject, TrackedMap } from 'tracked-built-ins';

Expand Down Expand Up @@ -926,7 +926,11 @@ export default class StoreService extends Service implements StoreInterface {
omitQueryFields: true,
});
if (patch.attributes) {
doc.data.attributes = merge(doc.data.attributes, patch.attributes);
doc.data.attributes = mergeWith(
doc.data.attributes,
patch.attributes,
(_dest, src) => (Array.isArray(src) ? src : undefined),
);
Comment on lines +929 to +933

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear stale array field metadata when replacing arrays

When the patched attribute is a shorter array for a field with per-index metadata (for example a polymorphic containsMany with data.meta.fields['items.1']), this replaces only data.attributes; the serialized doc still carries the old per-item metadata from serializeCard. updateFromSerialized can keep those stale field overrides on the live instance, so expanding the array again before a reload can write the removed element's type metadata back onto the new entry. Please clear or replace the matching doc.data.meta.fields entries when an array attribute is fully replaced.

Useful? React with 👍 / 👎.

}
if (patch.relationships) {
let mergedRel = mergeRelationships(
Expand Down
48 changes: 48 additions & 0 deletions packages/host/tests/integration/commands/patch-instance-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,54 @@ module('Integration | commands | patch-instance', function (hooks) {
);
});

test<TestContextWithSave>('patching a containsMany field with a shorter array fully replaces it (no stale trailing items)', async function (assert) {
let patchInstanceCommand = new PatchCardInstanceCommand(
commandService.commandContext,
{
cardType: PersonDef,
},
);
let url = new URL(`${testRealmURL}Person/hassan`);
let saves = 0;
this.onSave((saveURL) => {
if (saveURL.href === url.href) {
saves++;
}
});

await patchInstanceCommand.execute({
cardId: `${testRealmURL}Person/hassan`,
patch: { attributes: { nickNames: ['Paper', 'Pinky', 'Pix'] } },
});
await waitUntil(() => saves > 0, {
timeout: saveWaitTimeoutMs,
timeoutMessage: 'timed out waiting for the first save',
});

let savesAfterGrow = saves;
await patchInstanceCommand.execute({
cardId: `${testRealmURL}Person/hassan`,
patch: { attributes: { nickNames: ['Paper'] } },
});
await waitUntil(() => saves > savesAfterGrow, {
timeout: saveWaitTimeoutMs,
timeoutMessage: 'timed out waiting for the second save',
});

let result = await indexQuery.instance(url);
let instance =
result && result.type === 'instance' ? result.instance : undefined;
assert.ok(instance, 'instance payload is present');
if (!instance) {
throw new Error('expected instance payload');
}
assert.deepEqual(
instance.attributes?.nickNames,
['Paper'],
'the containsMany array was fully replaced, not index-merged',
);
});

test<TestContextWithSave>('can patch a linksTo field', async function (assert) {
let patchInstanceCommand = new PatchCardInstanceCommand(
commandService.commandContext,
Expand Down
Loading