Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(mutator): match deep set/setIfMissing behavior #5048

Merged
merged 1 commit into from
Oct 27, 2023
Merged
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
20 changes: 19 additions & 1 deletion packages/@sanity/mutator/src/patch/Patcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
9 changes: 7 additions & 2 deletions packages/@sanity/mutator/src/patch/SetIfMissingPatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 8 additions & 1 deletion packages/@sanity/mutator/src/patch/SetPatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`)
}
Expand Down
72 changes: 72 additions & 0 deletions packages/@sanity/mutator/test/patchExamples/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
75 changes: 74 additions & 1 deletion packages/@sanity/mutator/test/patchExamples/setIfMissing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading