Skip to content

Commit 82a2a05

Browse files
committed
fix: refactor update data to avoid lodash issues
Replace the lodash/fp/set call in UPDATE_DATA with a dedicated helper. This fixes issues with numeric segments (e.g. "group-key.15") and bracket characters in property names (e.g. "test[0]"). Fixes #2102 Fixes #2397
1 parent 53918bc commit 82a2a05

6 files changed

Lines changed: 595 additions & 7 deletions

File tree

packages/core/src/reducers/core.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@
2424
*/
2525

2626
import cloneDeep from 'lodash/cloneDeep';
27-
import setFp from 'lodash/fp/set';
28-
import unsetFp from 'lodash/fp/unset';
2927
import get from 'lodash/get';
3028
import isEqual from 'lodash/isEqual';
29+
import { setDataAt, unsetDataAt } from '../util/setData';
3130
import {
3231
CoreActions,
3332
INIT,
@@ -242,15 +241,16 @@ export const coreReducer: Reducer<JsonFormsCore, CoreActions> = (
242241
const newData = action.updater(cloneDeep(oldData));
243242
let newState: any;
244243
if (newData !== undefined) {
245-
newState = setFp(
244+
newState = setDataAt(
245+
state.data === undefined ? {} : state.data,
246246
action.path,
247247
newData,
248-
state.data === undefined ? {} : state.data
248+
state.schema
249249
);
250250
} else {
251-
newState = unsetFp(
252-
action.path,
253-
state.data === undefined ? {} : state.data
251+
newState = unsetDataAt(
252+
state.data === undefined ? {} : state.data,
253+
action.path
254254
);
255255
}
256256
const errors = validate(state.validator, newState);

packages/core/src/util/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export * from './ids';
2828
export * from './label';
2929
export * from './path';
3030
export * from './resolvers';
31+
export * from './setData';
3132
export * from './runtime';
3233
export * from './schema';
3334
export * from './uischema';

packages/core/src/util/setData.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
The MIT License
3+
4+
Copyright (c) 2017-2019 EclipseSource Munich
5+
https://github.com/eclipsesource/jsonforms
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
THE SOFTWARE.
24+
*/
25+
26+
import type { JsonSchema } from '../models';
27+
import { encode } from './path';
28+
import { resolveSchema } from './resolvers';
29+
import { hasType } from './util';
30+
31+
const splitPath = (path: string): string[] => path.split('.');
32+
33+
/**
34+
* Walks one step in the schema along the given data path segment, so we can
35+
* tell whether a missing intermediate container should be created as an
36+
* array or as an object.
37+
*/
38+
const stepSchema = (
39+
schema: JsonSchema | undefined,
40+
segment: string,
41+
rootSchema: JsonSchema | undefined
42+
): JsonSchema | undefined => {
43+
if (!schema || !rootSchema) {
44+
return undefined;
45+
}
46+
const pointer = hasType(schema, 'array')
47+
? Array.isArray(schema.items)
48+
? `/items/${segment}`
49+
: '/items'
50+
: `/properties/${encode(segment)}`;
51+
return resolveSchema(schema, pointer, rootSchema);
52+
};
53+
54+
const cloneContainer = (data: any): any => {
55+
if (Array.isArray(data)) {
56+
return [...data];
57+
}
58+
return { ...(data ?? {}) };
59+
};
60+
61+
const assign = (container: any, segment: string, value: any): any => {
62+
if (Array.isArray(container)) {
63+
const index = Number(segment);
64+
container[Number.isInteger(index) ? index : (segment as any)] = value;
65+
} else {
66+
container[segment] = value;
67+
}
68+
return container;
69+
};
70+
71+
/**
72+
* Immutably sets `value` at the dotted `path` within `data`.
73+
*
74+
* Numeric path segments and segments containing bracket notation are treated
75+
* as plain object property names. The optional `rootSchema` is consulted when
76+
* a new intermediate container has to be created, so that arrays are still
77+
* created where the schema declares an array type.
78+
*/
79+
export const setDataAt = (
80+
data: any,
81+
path: string,
82+
value: any,
83+
rootSchema?: JsonSchema
84+
): any => {
85+
const segments = splitPath(path);
86+
if (segments.length === 0) {
87+
return value;
88+
}
89+
return doSet(data, segments, 0, value, rootSchema, rootSchema);
90+
};
91+
92+
const doSet = (
93+
data: any,
94+
segments: string[],
95+
index: number,
96+
value: any,
97+
currentSchema: JsonSchema | undefined,
98+
rootSchema: JsonSchema | undefined
99+
): any => {
100+
const segment = segments[index];
101+
const childSchema = stepSchema(currentSchema, segment, rootSchema);
102+
const container = cloneContainer(data);
103+
104+
if (index === segments.length - 1) {
105+
return assign(container, segment, value);
106+
}
107+
108+
const existingChild = data?.[segment];
109+
let nextValue;
110+
if (
111+
existingChild !== undefined &&
112+
existingChild !== null &&
113+
typeof existingChild === 'object'
114+
) {
115+
nextValue = doSet(
116+
existingChild,
117+
segments,
118+
index + 1,
119+
value,
120+
childSchema,
121+
rootSchema
122+
);
123+
} else {
124+
const initial = hasType(childSchema, 'array') ? [] : {};
125+
nextValue = doSet(
126+
initial,
127+
segments,
128+
index + 1,
129+
value,
130+
childSchema,
131+
rootSchema
132+
);
133+
}
134+
return assign(container, segment, nextValue);
135+
};
136+
137+
/**
138+
* Immutably unsets the value at the dotted `path` within `data`.
139+
*
140+
* Numeric path segments and bracket notation in segments are treated as
141+
* plain object property names, mirroring the semantics of {@link setDataAt}.
142+
*/
143+
export const unsetDataAt = (data: any, path: string): any => {
144+
const segments = splitPath(path);
145+
if (segments.length === 0) {
146+
return data;
147+
}
148+
return doUnset(data, segments, 0);
149+
};
150+
151+
const doUnset = (data: any, segments: string[], index: number): any => {
152+
if (data === undefined || data === null) {
153+
return data;
154+
}
155+
const segment = segments[index];
156+
if (index === segments.length - 1) {
157+
if (Array.isArray(data)) {
158+
const container = [...data];
159+
const numericIndex = Number(segment);
160+
if (Number.isInteger(numericIndex)) {
161+
delete container[numericIndex];
162+
}
163+
return container;
164+
}
165+
if (!Object.prototype.hasOwnProperty.call(data, segment)) {
166+
return data;
167+
}
168+
const container: { [key: string]: any } = { ...data };
169+
delete container[segment];
170+
return container;
171+
}
172+
173+
const existingChild = data[segment];
174+
if (
175+
existingChild === undefined ||
176+
existingChild === null ||
177+
typeof existingChild !== 'object'
178+
) {
179+
return data;
180+
}
181+
const nextValue = doUnset(existingChild, segments, index + 1);
182+
if (nextValue === existingChild) {
183+
return data;
184+
}
185+
const container = cloneContainer(data);
186+
return assign(container, segment, nextValue);
187+
};

0 commit comments

Comments
 (0)