From 2ed595cb0c99114149b5299e34a8aa9ff040fc1d Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Mon, 23 Oct 2023 14:15:25 -0700 Subject: [PATCH] fix(mutator): match deep set/setIfMissing behavior --- packages/@sanity/mutator/src/patch/Patcher.ts | 20 ++++- .../mutator/src/patch/SetIfMissingPatch.ts | 9 ++- .../@sanity/mutator/src/patch/SetPatch.ts | 9 ++- .../@sanity/mutator/test/patchExamples/set.ts | 72 ++++++++++++++++++ .../test/patchExamples/setIfMissing.ts | 75 ++++++++++++++++++- 5 files changed, 180 insertions(+), 5 deletions(-) diff --git a/packages/@sanity/mutator/src/patch/Patcher.ts b/packages/@sanity/mutator/src/patch/Patcher.ts index 9db31b203a2..9cc3c4233f7 100644 --- a/packages/@sanity/mutator/src/patch/Patcher.ts +++ b/packages/@sanity/mutator/src/patch/Patcher.ts @@ -3,6 +3,8 @@ import {Matcher} from '../jsonpath' import {parsePatch} from './parse' import {ImmutableAccessor} from './ImmutableAccessor' import {PatchTypes, SingleDocumentPatch} from './types' +import {SetPatch} from './SetPatch' +import {SetIfMissingPatch} from './SetIfMissingPatch' export interface Patch { id: string @@ -57,6 +59,9 @@ export class Patcher { // a patch to be the payload. When matchers report a delivery, the // apply(targets, accessor) is called on the patch function process(matcher: Matcher, accessor: ImmutableAccessor) { + const isSetPatch = + matcher.payload instanceof SetPatch || matcher.payload instanceof SetIfMissingPatch + let result = accessor // Every time we execute the matcher a new set of leads is generated. Each lead // is a target (being an index, an attribute name or a range) in the form of an @@ -75,7 +80,20 @@ function process(matcher: Matcher, accessor: ImmutableAccessor) { result = result.setIndexAccessor(i, process(lead.matcher, item)) }) } else if (lead.target.isAttributeReference()) { - const oldValueAccessor = result.getAttribute(lead.target.name()) + // `set`/`setIfMissing` on a primitive value overwrites it + if (isSetPatch && result.containerType() === 'primitive') { + result = result.set({}) + } + + let oldValueAccessor = result.getAttribute(lead.target.name()) + + // If the patch is a set/setIfMissing patch, we allow deeply setting properties, + // creating missing segments as we go. + if (!oldValueAccessor && isSetPatch) { + result = result.setAttribute(lead.target.name(), {}) + oldValueAccessor = result.getAttribute(lead.target.name()) + } + if (!oldValueAccessor) { // Don't follow lead, no such attribute return diff --git a/packages/@sanity/mutator/src/patch/SetIfMissingPatch.ts b/packages/@sanity/mutator/src/patch/SetIfMissingPatch.ts index 198bb3ba77d..0cf5a529057 100644 --- a/packages/@sanity/mutator/src/patch/SetIfMissingPatch.ts +++ b/packages/@sanity/mutator/src/patch/SetIfMissingPatch.ts @@ -16,9 +16,14 @@ export class SetIfMissingPatch { let result = accessor targets.forEach((target) => { if (target.isIndexReference()) { - // setIfMissing do not apply to arrays, since Gradient will reject nulls in arrays + // setIfMissing do not apply to arrays, since Content Lake will reject nulls in arrays } else if (target.isAttributeReference()) { - if (!result.hasAttribute(target.name())) { + // setting a subproperty on a primitive value overwrites it, eg + // `{setIfMissing: {'address.street': 'California St'}}` on `{address: 'Fiction St'}` will + // result in `{address: {street: 'California St'}}` + if (result.containerType() === 'primitive') { + result = result.set({[target.name()]: this.value}) + } else if (!result.hasAttribute(target.name())) { result = accessor.setAttribute(target.name(), this.value) } } else { diff --git a/packages/@sanity/mutator/src/patch/SetPatch.ts b/packages/@sanity/mutator/src/patch/SetPatch.ts index 8e6401ec71b..41bc5fdd326 100644 --- a/packages/@sanity/mutator/src/patch/SetPatch.ts +++ b/packages/@sanity/mutator/src/patch/SetPatch.ts @@ -22,7 +22,14 @@ export class SetPatch { result = result.setIndex(i, this.value) }) } else if (target.isAttributeReference()) { - result = result.setAttribute(target.name(), this.value) + // setting a subproperty on a primitive value overwrites it, eg + // `{set: {'address.street': 'California St'}}` on `{address: 'Fiction St'}` will result in + // `{address: {street: 'California St'}}` + if (result.containerType() === 'primitive') { + result = result.set({[target.name()]: this.value}) + } else { + result = result.setAttribute(target.name(), this.value) + } } else { throw new Error(`Unable to apply to target ${target.toString()}`) } diff --git a/packages/@sanity/mutator/test/patchExamples/set.ts b/packages/@sanity/mutator/test/patchExamples/set.ts index 08ff855fe9f..653563f924a 100644 --- a/packages/@sanity/mutator/test/patchExamples/set.ts +++ b/packages/@sanity/mutator/test/patchExamples/set.ts @@ -125,6 +125,78 @@ const examples: PatchExample[] = [ a: 'hello', }, }, + { + name: 'Set new deep key', + before: {}, + patch: { + id: 'a', + set: { + 'a.b.c': 'hello', + }, + }, + after: { + a: { + b: { + c: 'hello', + }, + }, + }, + }, + { + name: 'Set deep key on previous string value', + before: { + a: 'stringValue', + }, + patch: { + id: 'a', + set: { + 'a.b.c': 'hello', + }, + }, + after: { + a: { + b: { + c: 'hello', + }, + }, + }, + }, + { + name: 'Set deep key on previous number value', + before: { + a: 123, + }, + patch: { + id: 'a', + set: { + 'a.b.c': 'hello', + }, + }, + after: { + a: { + b: { + c: 'hello', + }, + }, + }, + }, + { + name: 'Set key on previous number value', + before: { + a: 123, + }, + patch: { + id: 'a', + set: { + 'a.b': 'hello', + }, + }, + after: { + a: { + b: 'hello', + }, + }, + }, { name: 'Set range', before: { diff --git a/packages/@sanity/mutator/test/patchExamples/setIfMissing.ts b/packages/@sanity/mutator/test/patchExamples/setIfMissing.ts index 4de83eba82a..7dc7cf532e1 100644 --- a/packages/@sanity/mutator/test/patchExamples/setIfMissing.ts +++ b/packages/@sanity/mutator/test/patchExamples/setIfMissing.ts @@ -60,7 +60,80 @@ const examples: PatchExample[] = [ {b: 7, p: 'Thorvald Meyers gt.', zz: {yyy: 55, zzz: 10}}, ], }, - }, // Potentially redundant, added to exactly match a test case from @sanity/form-builder that was failing. + }, + { + name: 'Set new deep key', + before: {}, + patch: { + id: 'a', + setIfMissing: { + 'a.b.c': 'hello', + }, + }, + after: { + a: { + b: { + c: 'hello', + }, + }, + }, + }, + { + name: 'Set deep key on previous string value', + before: { + a: 'stringValue', + }, + patch: { + id: 'a', + setIfMissing: { + 'a.b.c': 'hello', + }, + }, + after: { + a: { + b: { + c: 'hello', + }, + }, + }, + }, + { + name: 'Set deep key on previous number value', + before: { + a: 123, + }, + patch: { + id: 'a', + setIfMissing: { + 'a.b.c': 'hello', + }, + }, + after: { + a: { + b: { + c: 'hello', + }, + }, + }, + }, + { + name: 'Set key on previous number value', + before: { + a: 123, + }, + patch: { + id: 'a', + setIfMissing: { + 'a.b': 'hello', + }, + }, + after: { + a: { + b: 'hello', + }, + }, + }, + // Potentially redundant, added to exactly match a test case from @sanity/form-builder that was failing. { name: 'Set if missing by key', before: {