@goodie-forms/core 1.1.5-alpha → 1.2.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +426 -426
- package/dist/field/FieldPath.d.ts +32 -0
- package/dist/field/FieldPath.d.ts.map +1 -0
- package/dist/field/FieldPathBuilder.d.ts +25 -0
- package/dist/field/FieldPathBuilder.d.ts.map +1 -0
- package/dist/field/Reconcile.d.ts +9 -0
- package/dist/field/Reconcile.d.ts.map +1 -0
- package/dist/form/FormController.d.ts +35 -39
- package/dist/form/FormController.d.ts.map +1 -1
- package/dist/form/FormField.d.ts +12 -10
- package/dist/form/FormField.d.ts.map +1 -1
- package/dist/form/NonullFormField.d.ts +3 -4
- package/dist/form/NonullFormField.d.ts.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +615 -881
- package/dist/index.js.map +1 -1
- package/dist/validation/CustomValidation.d.ts +10 -0
- package/dist/validation/CustomValidation.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/field/FieldPath.ts +292 -0
- package/src/field/FieldPathBuilder.ts +132 -0
- package/src/field/Reconcile.ts +99 -0
- package/src/form/FormController.ts +342 -310
- package/src/form/FormField.ts +200 -202
- package/src/form/NonullFormField.ts +15 -21
- package/src/index.ts +12 -5
- package/src/types/DeepPartial.ts +7 -7
- package/src/types/Mixin.ts +2 -2
- package/src/utils/ensureImmerability.ts +30 -30
- package/src/utils/getId.ts +5 -5
- package/src/utils/removeBy.ts +11 -11
- package/src/{form → validation}/CustomValidation.ts +52 -52
- package/tsconfig.json +8 -7
- package/vite.config.ts +18 -18
- package/dist/form/CustomValidation.d.ts +0 -10
- package/dist/form/CustomValidation.d.ts.map +0 -1
- package/dist/form/Field.d.ts +0 -31
- package/dist/form/Field.d.ts.map +0 -1
- package/src/form/Field.ts +0 -334
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { DeepPartial } from '../types/DeepPartial';
|
|
3
|
+
import { FieldPath } from '../field/FieldPath';
|
|
4
|
+
export type CustomValidationIssue<TOutput extends object> = {
|
|
5
|
+
path: FieldPath.StringPaths<TOutput>;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
export type CustomValidationStrategy<TOutput extends object> = (data: DeepPartial<TOutput>) => void | CustomValidationIssue<TOutput>[] | Promise<CustomValidationIssue<TOutput>[] | void>;
|
|
9
|
+
export declare function customValidation<TOutput extends object>(strategy: CustomValidationStrategy<TOutput>): StandardSchemaV1<TOutput, TOutput>;
|
|
10
|
+
//# sourceMappingURL=CustomValidation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CustomValidation.d.ts","sourceRoot":"","sources":["../../src/validation/CustomValidation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS,MAAM,IAAI;IAC1D,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,wBAAwB,CAAC,OAAO,SAAS,MAAM,IAAI,CAC7D,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,KAExB,IAAI,GACJ,qBAAqB,CAAC,OAAO,CAAC,EAAE,GAChC,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;AAErD,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,QAAQ,EAAE,wBAAwB,CAAC,OAAO,CAAC,GAiCtC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CACxC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
export namespace FieldPath {
|
|
2
|
+
export type Segments = readonly PropertyKey[];
|
|
3
|
+
export type CanonicalPath = string;
|
|
4
|
+
export type StringPath = string;
|
|
5
|
+
|
|
6
|
+
export function toCanonicalPath(path: Segments) {
|
|
7
|
+
return path.join(".") as CanonicalPath;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function toStringPath(path: Segments) {
|
|
11
|
+
const normalizedPath = normalize(path);
|
|
12
|
+
let result = "";
|
|
13
|
+
|
|
14
|
+
for (const fragment of normalizedPath) {
|
|
15
|
+
if (typeof fragment === "number") {
|
|
16
|
+
result += `[${fragment}]`;
|
|
17
|
+
} else {
|
|
18
|
+
if (result.length > 0) {
|
|
19
|
+
result += ".";
|
|
20
|
+
}
|
|
21
|
+
result += fragment.toString();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fromStringPath<TStrPath extends string>(
|
|
29
|
+
stringPath: TStrPath,
|
|
30
|
+
) {
|
|
31
|
+
const result: Array<string | number> = [];
|
|
32
|
+
|
|
33
|
+
let i = 0;
|
|
34
|
+
|
|
35
|
+
while (i < stringPath.length) {
|
|
36
|
+
const char = stringPath[i];
|
|
37
|
+
|
|
38
|
+
// dot separator
|
|
39
|
+
if (char === ".") {
|
|
40
|
+
i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// bracket index: [123]
|
|
45
|
+
if (char === "[") {
|
|
46
|
+
i++; // skip '['
|
|
47
|
+
let num = "";
|
|
48
|
+
|
|
49
|
+
while (i < stringPath.length && stringPath[i] !== "]") {
|
|
50
|
+
num += stringPath[i];
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
i++; // skip ']'
|
|
55
|
+
|
|
56
|
+
if (!num || !/^\d+$/.test(num)) {
|
|
57
|
+
throw new Error(`Invalid array index in path: ${stringPath}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
result.push(Number(num));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// identifier
|
|
65
|
+
let key = "";
|
|
66
|
+
while (i < stringPath.length && /[^\.\[]/.test(stringPath[i])) {
|
|
67
|
+
key += stringPath[i];
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (key) {
|
|
72
|
+
result.push(key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result as FieldPath.ParseStringPath<TStrPath>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function normalize<T extends readonly any[] | undefined>(path: T) {
|
|
80
|
+
return path?.map((segment) => {
|
|
81
|
+
if (typeof segment === "string") return segment;
|
|
82
|
+
if (typeof segment === "number") return segment;
|
|
83
|
+
if (typeof segment === "symbol") return segment;
|
|
84
|
+
if (typeof segment === "object" && "key" in segment) {
|
|
85
|
+
if (typeof segment === "string") return segment;
|
|
86
|
+
if (typeof segment === "number") return segment;
|
|
87
|
+
if (typeof segment === "symbol") return segment;
|
|
88
|
+
return segment.key;
|
|
89
|
+
}
|
|
90
|
+
}) as Segments;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function equals(path1?: Segments, path2?: Segments) {
|
|
94
|
+
if (path1 === path2) return true;
|
|
95
|
+
if (path1 == null) return false;
|
|
96
|
+
if (path2 == null) return false;
|
|
97
|
+
if (path1.length !== path2.length) return false;
|
|
98
|
+
for (let i = 0; i < path1.length; i++) {
|
|
99
|
+
if (path1[i] !== path2[i]) return false;
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
export function isDescendant(parentPath: Segments, childPath: Segments) {
|
|
104
|
+
if (parentPath.length >= childPath.length) return false;
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < parentPath.length; i++) {
|
|
107
|
+
if (parentPath[i] !== childPath[i]) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
type Unfoldable<T> = T & { _____foldMark?: never } & {};
|
|
116
|
+
|
|
117
|
+
export type Resolve<
|
|
118
|
+
TObject,
|
|
119
|
+
TPath extends readonly PropertyKey[],
|
|
120
|
+
> = TPath extends []
|
|
121
|
+
? TObject
|
|
122
|
+
: TPath extends readonly [infer Prop, ...infer Rest]
|
|
123
|
+
? Rest extends readonly PropertyKey[]
|
|
124
|
+
? Resolve<ResolveStep<TObject, Prop>, Rest>
|
|
125
|
+
: never
|
|
126
|
+
: never;
|
|
127
|
+
|
|
128
|
+
type ResolveStep<TObject, TKey> =
|
|
129
|
+
NonNullable<TObject> extends infer U
|
|
130
|
+
? U extends readonly unknown[]
|
|
131
|
+
? number extends U["length"]
|
|
132
|
+
? TKey extends number | `${number}`
|
|
133
|
+
? U[number]
|
|
134
|
+
: never
|
|
135
|
+
: TKey extends keyof U
|
|
136
|
+
? U[TKey]
|
|
137
|
+
: TKey extends `${infer N extends number}`
|
|
138
|
+
? U[N]
|
|
139
|
+
: never
|
|
140
|
+
: TKey extends keyof U
|
|
141
|
+
? U[TKey]
|
|
142
|
+
: never
|
|
143
|
+
: never;
|
|
144
|
+
|
|
145
|
+
export type StringPaths<TObject extends object> = StringPathsImpl<
|
|
146
|
+
CanonicalStringPaths<TObject>
|
|
147
|
+
>;
|
|
148
|
+
|
|
149
|
+
type StringPathsImpl<TCanonicalStringPaths extends string> =
|
|
150
|
+
TCanonicalStringPaths extends `${string}[*]${string}`
|
|
151
|
+
?
|
|
152
|
+
| ReplaceAll<TCanonicalStringPaths, "[*]", "[0]">
|
|
153
|
+
| Unfoldable<ReplaceAll<TCanonicalStringPaths, "[*]", `[${number}]`>>
|
|
154
|
+
: TCanonicalStringPaths;
|
|
155
|
+
|
|
156
|
+
type CanonicalStringPaths<TObject extends object> = {
|
|
157
|
+
[K in keyof TObject & string]: NonNullable<TObject[K]> extends (
|
|
158
|
+
...args: any[]
|
|
159
|
+
) => any
|
|
160
|
+
? never
|
|
161
|
+
: NonNullable<TObject[K]> extends (infer U)[]
|
|
162
|
+
? U extends object
|
|
163
|
+
? K | `${K}[*]` | `${K}[*].${CanonicalStringPaths<NonNullable<U>>}`
|
|
164
|
+
: K | `${K}[*]`
|
|
165
|
+
: NonNullable<TObject[K]> extends object
|
|
166
|
+
? K | `${K}.${CanonicalStringPaths<NonNullable<TObject[K]>>}`
|
|
167
|
+
: K;
|
|
168
|
+
}[keyof TObject & string];
|
|
169
|
+
|
|
170
|
+
type ReplaceAll<
|
|
171
|
+
TString extends string,
|
|
172
|
+
TMatch extends string,
|
|
173
|
+
TReplace extends string | number,
|
|
174
|
+
> = TString extends `${infer A}${TMatch}${infer B}`
|
|
175
|
+
? `${A}${TReplace}${ReplaceAll<B, TMatch, TReplace>}`
|
|
176
|
+
: TString;
|
|
177
|
+
|
|
178
|
+
export type ParseStringPath<TStrPath extends string> = string extends TStrPath
|
|
179
|
+
? never
|
|
180
|
+
: ParseStringPathImpl<NormalizeStrPath<TStrPath>, []>;
|
|
181
|
+
|
|
182
|
+
type ParseStringPathImpl<
|
|
183
|
+
TStrPath extends string,
|
|
184
|
+
TPath extends PropertyKey[],
|
|
185
|
+
> = TStrPath extends ""
|
|
186
|
+
? TPath
|
|
187
|
+
: TStrPath extends `.${infer Rest}`
|
|
188
|
+
? ParseStringPathImpl<Rest, TPath>
|
|
189
|
+
: TStrPath extends `[${infer Bracket}]${infer Rest}`
|
|
190
|
+
? ParseStringPathImpl<Rest, [...TPath, ParseBracket<Bracket>]>
|
|
191
|
+
: TStrPath extends `${infer Head}.${infer Rest}`
|
|
192
|
+
? ParseStringPathImpl<Rest, [...TPath, Head]>
|
|
193
|
+
: TStrPath extends `${infer Head}[${infer Tail}`
|
|
194
|
+
? ParseStringPathImpl<`[${Tail}`, [...TPath, Head]>
|
|
195
|
+
: [...TPath, TStrPath];
|
|
196
|
+
|
|
197
|
+
type NormalizeStrPath<TStringPath extends string> =
|
|
198
|
+
TStringPath extends `${infer A}[${infer B}]${infer Rest}`
|
|
199
|
+
? NormalizeStrPath<`${A}.${B}${Rest}`>
|
|
200
|
+
: TStringPath extends `.${infer R}`
|
|
201
|
+
? NormalizeStrPath<R>
|
|
202
|
+
: TStringPath;
|
|
203
|
+
|
|
204
|
+
type ParseBracket<TString extends string> =
|
|
205
|
+
TString extends `${infer N extends number}`
|
|
206
|
+
? N
|
|
207
|
+
: TString extends `"${infer K}"`
|
|
208
|
+
? K
|
|
209
|
+
: TString extends `'${infer K}'`
|
|
210
|
+
? K
|
|
211
|
+
: never;
|
|
212
|
+
|
|
213
|
+
export function getValue<
|
|
214
|
+
TObject extends object,
|
|
215
|
+
const TPath extends readonly PropertyKey[],
|
|
216
|
+
>(
|
|
217
|
+
object: TObject,
|
|
218
|
+
path: TPath,
|
|
219
|
+
): FieldPath.Resolve<TObject, TPath> | undefined {
|
|
220
|
+
let current: any = object;
|
|
221
|
+
|
|
222
|
+
for (const pathFragment of path) {
|
|
223
|
+
if (current == null) return undefined;
|
|
224
|
+
current = current[pathFragment];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return current;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function setValue<
|
|
231
|
+
TObject extends object,
|
|
232
|
+
const TPath extends readonly PropertyKey[],
|
|
233
|
+
>(object: TObject, key: TPath, value: FieldPath.Resolve<TObject, TPath>) {
|
|
234
|
+
return FieldPath.modifyValue(object, key, () => value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function modifyValue<
|
|
238
|
+
TObject extends object,
|
|
239
|
+
const TPath extends readonly PropertyKey[],
|
|
240
|
+
>(
|
|
241
|
+
object: TObject,
|
|
242
|
+
path: TPath,
|
|
243
|
+
modifier: (
|
|
244
|
+
currentValue: FieldPath.Resolve<TObject, TPath>,
|
|
245
|
+
) => FieldPath.Resolve<TObject, TPath> | void,
|
|
246
|
+
) {
|
|
247
|
+
let current: any = object;
|
|
248
|
+
|
|
249
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
250
|
+
const pathFragment = path[i];
|
|
251
|
+
const nextFragment = path[i + 1];
|
|
252
|
+
|
|
253
|
+
if (current[pathFragment] == null) {
|
|
254
|
+
current[pathFragment] = typeof nextFragment === "number" ? [] : {};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
current = current[pathFragment];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const lastFragment = path[path.length - 1];
|
|
261
|
+
|
|
262
|
+
const oldValue = current[lastFragment];
|
|
263
|
+
const newValue = modifier(oldValue);
|
|
264
|
+
|
|
265
|
+
if (newValue !== undefined) {
|
|
266
|
+
current[lastFragment] = newValue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function deleteValue<
|
|
271
|
+
TObject extends object,
|
|
272
|
+
TPath extends readonly PropertyKey[],
|
|
273
|
+
>(object: TObject, path: TPath) {
|
|
274
|
+
let current: any = object;
|
|
275
|
+
|
|
276
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
277
|
+
const pathFragment = path[i];
|
|
278
|
+
|
|
279
|
+
if (current[pathFragment] == null) return;
|
|
280
|
+
|
|
281
|
+
current = current[pathFragment];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const lastFragment = path[path.length - 1];
|
|
285
|
+
|
|
286
|
+
delete current[lastFragment];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// const x = {} as { foo: { bar: string[] } };
|
|
291
|
+
// FieldPath.setValue(x, FieldPath.fromStringPath("foo.bar[9]"), "C");
|
|
292
|
+
// console.log(x); // <-- { foo: { bar: [<9xempty>, "C"] } }
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { FieldPath } from "./FieldPath";
|
|
2
|
+
|
|
3
|
+
const resolverCall = Symbol("PathResolver");
|
|
4
|
+
|
|
5
|
+
export namespace FieldPathBuilder {
|
|
6
|
+
export type Proxy<TObject, TPath extends readonly PropertyKey[]> =
|
|
7
|
+
NonNullable<TObject> extends readonly (infer E)[]
|
|
8
|
+
? {
|
|
9
|
+
[K in number]: Proxy<NonNullable<E>, [...TPath, K]>;
|
|
10
|
+
} & {
|
|
11
|
+
[resolverCall]: TPath;
|
|
12
|
+
}
|
|
13
|
+
: NonNullable<TObject> extends object
|
|
14
|
+
? {
|
|
15
|
+
[K in keyof TObject]-?: Proxy<
|
|
16
|
+
NonNullable<TObject[K]>,
|
|
17
|
+
[...TPath, K]
|
|
18
|
+
>;
|
|
19
|
+
} & {
|
|
20
|
+
[resolverCall]: TPath;
|
|
21
|
+
}
|
|
22
|
+
: {
|
|
23
|
+
[resolverCall]: TPath;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class FieldPathBuilder<TOutput extends object> {
|
|
28
|
+
protected static wrap<
|
|
29
|
+
TObject extends object,
|
|
30
|
+
TPath extends readonly PropertyKey[],
|
|
31
|
+
>(paths: TPath): FieldPathBuilder.Proxy<TObject, TPath> {
|
|
32
|
+
return new Proxy<TObject>(paths as any, {
|
|
33
|
+
get(_target, p, _receiver) {
|
|
34
|
+
if (p === resolverCall) return paths;
|
|
35
|
+
const key =
|
|
36
|
+
typeof p === "string" ? (/^\d+$/.test(p) ? Number(p) : p) : p;
|
|
37
|
+
|
|
38
|
+
// TODO: Branch instead of reallocating a copy
|
|
39
|
+
return FieldPathBuilder.wrap<any, [...TPath, typeof key]>([
|
|
40
|
+
...paths,
|
|
41
|
+
key,
|
|
42
|
+
]);
|
|
43
|
+
},
|
|
44
|
+
}) as any;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public fromStringPath<TStrPath extends FieldPath.StringPaths<TOutput>>(
|
|
48
|
+
stringPath: TStrPath,
|
|
49
|
+
) {
|
|
50
|
+
return FieldPath.fromStringPath(
|
|
51
|
+
stringPath,
|
|
52
|
+
) as unknown as FieldPath.ParseStringPath<TStrPath>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public fromProxy<TProxy extends FieldPathBuilder.Proxy<any, any>>(
|
|
56
|
+
consumer: (data: FieldPathBuilder.Proxy<TOutput, []>) => TProxy,
|
|
57
|
+
): TProxy[typeof resolverCall] {
|
|
58
|
+
return consumer(FieldPathBuilder.wrap<TOutput, []>([]))[resolverCall];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ---- TESTS ---------------- */
|
|
63
|
+
|
|
64
|
+
// interface User {
|
|
65
|
+
// name: string;
|
|
66
|
+
// address: {
|
|
67
|
+
// city: string;
|
|
68
|
+
// street: string;
|
|
69
|
+
// };
|
|
70
|
+
// friends: {
|
|
71
|
+
// name: string;
|
|
72
|
+
// tags: string[];
|
|
73
|
+
// }[];
|
|
74
|
+
// coords: [100, 200];
|
|
75
|
+
// }
|
|
76
|
+
|
|
77
|
+
// const builder = new FieldPathBuilder<User>();
|
|
78
|
+
|
|
79
|
+
// const data: User = {
|
|
80
|
+
// name: "",
|
|
81
|
+
// address: { city: "", street: "" },
|
|
82
|
+
// friends: [{ name: "", tags: ["A", "B"] }],
|
|
83
|
+
// coords: [100, 200] as const,
|
|
84
|
+
// };
|
|
85
|
+
|
|
86
|
+
// const path = builder.fromProxy((data) => data.friends[0].tags[1]);
|
|
87
|
+
// // ^?
|
|
88
|
+
// const value = FieldPath.getValue(data, path);
|
|
89
|
+
// // ^?
|
|
90
|
+
// console.log(path, "=", value);
|
|
91
|
+
|
|
92
|
+
// const path2 = builder.fromStringPath("friends[0].tags[0]");
|
|
93
|
+
// // ^?
|
|
94
|
+
// const value2 = FieldPath.getValue(data, path2);
|
|
95
|
+
// // ^?
|
|
96
|
+
// console.log(path2, "=", value2);
|
|
97
|
+
|
|
98
|
+
// const path3 = builder.fromStringPath("coords[0]");
|
|
99
|
+
// // ^?
|
|
100
|
+
// const value3 = FieldPath.getValue(data, path3);
|
|
101
|
+
// console.log(path3, "=", value3);
|
|
102
|
+
|
|
103
|
+
// const path4 = builder.fromStringPath("coords[1]");
|
|
104
|
+
// // ^?
|
|
105
|
+
// const value4 = FieldPath.getValue(data, path4);
|
|
106
|
+
// // ^?
|
|
107
|
+
// console.log(path4, "=", value4);
|
|
108
|
+
|
|
109
|
+
// type Shape = {
|
|
110
|
+
// user?: {
|
|
111
|
+
// profile?: { name?: string };
|
|
112
|
+
// tags?: boolean[];
|
|
113
|
+
// };
|
|
114
|
+
// };
|
|
115
|
+
|
|
116
|
+
// type A = FieldPath.Resolve<Shape, ["user"]>;
|
|
117
|
+
// // ^?
|
|
118
|
+
|
|
119
|
+
// type A2 = FieldPath.Resolve<Shape, ["user", "profile"]>;
|
|
120
|
+
// // ^?
|
|
121
|
+
|
|
122
|
+
// type B = FieldPath.Resolve<Shape, ["user", "profile", "name"]>;
|
|
123
|
+
// // ^?
|
|
124
|
+
|
|
125
|
+
// type C = FieldPath.Resolve<Shape, ["user", "tags", number]>;
|
|
126
|
+
// // ^?
|
|
127
|
+
|
|
128
|
+
// type C2 = FieldPath.Resolve<Shape, ["user", "tags", 0]>;
|
|
129
|
+
// // ^?
|
|
130
|
+
|
|
131
|
+
// type D = FieldPath.Resolve<Shape, ["user", "missing"]>;
|
|
132
|
+
// // ^?
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export namespace Reconsile {
|
|
2
|
+
export function deepEqual(
|
|
3
|
+
a: any,
|
|
4
|
+
b: any,
|
|
5
|
+
customComparator?: (a: any, b: any) => boolean | undefined,
|
|
6
|
+
) {
|
|
7
|
+
if (a === b) return true;
|
|
8
|
+
|
|
9
|
+
if (a === null || b === null) return false;
|
|
10
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
11
|
+
|
|
12
|
+
// Arrays
|
|
13
|
+
if (Array.isArray(a)) {
|
|
14
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
15
|
+
for (let i = 0; i < a.length; i++) {
|
|
16
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Allow custom comparison
|
|
22
|
+
if (customComparator != null) {
|
|
23
|
+
const result = customComparator(a, b);
|
|
24
|
+
if (result !== undefined) return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Dates
|
|
28
|
+
if (a instanceof Date && b instanceof Date) {
|
|
29
|
+
return a.getTime() === b.getTime();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// RegExp
|
|
33
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
34
|
+
return a.source === b.source && a.flags === b.flags;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Map
|
|
38
|
+
if (a instanceof Map && b instanceof Map) {
|
|
39
|
+
if (a.size !== b.size) return false;
|
|
40
|
+
for (const [key, val] of a) {
|
|
41
|
+
if (!b.has(key) || !deepEqual(val, b.get(key))) return false;
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Set
|
|
47
|
+
if (a instanceof Set && b instanceof Set) {
|
|
48
|
+
if (a.size !== b.size) return false;
|
|
49
|
+
for (const val of a) {
|
|
50
|
+
if (!b.has(val)) return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Plain / class objects
|
|
56
|
+
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const keysA = Object.keys(a);
|
|
61
|
+
const keysB = Object.keys(b);
|
|
62
|
+
|
|
63
|
+
if (keysA.length !== keysB.length) return false;
|
|
64
|
+
|
|
65
|
+
for (const key of keysA) {
|
|
66
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
67
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function diff<T>(
|
|
74
|
+
prev: readonly T[],
|
|
75
|
+
next: readonly T[],
|
|
76
|
+
equals: (a: T, b: T) => boolean,
|
|
77
|
+
filter?: (a: T) => boolean,
|
|
78
|
+
) {
|
|
79
|
+
const added: T[] = [];
|
|
80
|
+
const removed: T[] = [];
|
|
81
|
+
const unchanged: T[] = [];
|
|
82
|
+
|
|
83
|
+
for (const n of next) {
|
|
84
|
+
if (prev.some((p) => equals(p, n))) {
|
|
85
|
+
if (filter?.(n) ?? true) unchanged.push(n);
|
|
86
|
+
} else {
|
|
87
|
+
if (filter?.(n) ?? true) added.push(n);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const p of prev) {
|
|
92
|
+
if (!next.some((n) => equals(p, n))) {
|
|
93
|
+
if (filter?.(p) ?? true) removed.push(p);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { added, removed, unchanged };
|
|
98
|
+
}
|
|
99
|
+
}
|