@goodie-forms/core 1.2.5-alpha → 1.3.0
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/README.md +61 -0
- package/dist/field/FieldPath.d.ts +18 -9
- package/dist/field/FieldPath.d.ts.map +1 -1
- package/dist/field/FieldPathBuilder.d.ts +2 -2
- package/dist/field/FieldPathBuilder.d.ts.map +1 -1
- package/dist/field/Reconcile.d.ts +5 -2
- package/dist/field/Reconcile.d.ts.map +1 -1
- package/dist/form/FormController.d.ts +51 -20
- package/dist/form/FormController.d.ts.map +1 -1
- package/dist/form/FormField.d.ts +15 -10
- package/dist/form/FormField.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +550 -505
- package/dist/index.js.map +1 -1
- package/dist/types/DeepHelpers.d.ts +11 -0
- package/dist/types/DeepHelpers.d.ts.map +1 -0
- package/dist/types/FormHelpers.d.ts +5 -0
- package/dist/types/FormHelpers.d.ts.map +1 -0
- package/dist/types/Suppliable.d.ts +3 -0
- package/dist/types/Suppliable.d.ts.map +1 -0
- package/dist/validation/CustomValidation.d.ts +1 -1
- package/package.json +23 -3
- package/src/field/FieldPath.spec.ts +204 -0
- package/src/field/FieldPath.ts +63 -59
- package/src/field/FieldPathBuilder.spec.ts +47 -0
- package/src/field/FieldPathBuilder.ts +15 -81
- package/src/field/Reconcile.ts +12 -7
- package/src/form/FormController.spec.ts +55 -0
- package/src/form/FormController.ts +152 -121
- package/src/form/FormField.ts +66 -30
- package/src/index.ts +2 -2
- package/src/types/DeepHelpers.ts +15 -0
- package/src/types/FormHelpers.ts +13 -0
- package/src/types/Suppliable.ts +7 -0
- package/src/validation/CustomValidation.ts +1 -1
- package/dist/form/NonullFormField.d.ts +0 -9
- package/dist/form/NonullFormField.d.ts.map +0 -1
- package/dist/types/DeepPartial.d.ts +0 -6
- package/dist/types/DeepPartial.d.ts.map +0 -1
- package/src/form/NonullFormField.ts +0 -15
- package/src/types/DeepPartial.ts +0 -7
- package/tsconfig.json +0 -8
- package/vite.config.ts +0 -18
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type DeepPartial<T> = {
|
|
2
|
+
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
|
|
3
|
+
} & {
|
|
4
|
+
[K in keyof T as T[K] extends (...args: any[]) => any ? never : K]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
5
|
+
};
|
|
6
|
+
export type DeepReadonly<T> = {
|
|
7
|
+
[K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
|
|
8
|
+
} & {
|
|
9
|
+
readonly [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=DeepHelpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeepHelpers.d.ts","sourceRoot":"","sources":["../../src/types/DeepHelpers.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;KAC1B,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;CACzE,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GACjD,KAAK,GACL,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI;KAC3B,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;CACzE,GAAG;IACF,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAC1D,KAAK,GACL,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACxD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FormHelpers.d.ts","sourceRoot":"","sources":["../../src/types/FormHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAG7D,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,IAClD,WAAW,CAAC,CAAC,CAAC,SAAS;IAAE,KAAK,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Suppliable.d.ts","sourceRoot":"","sources":["../../src/types/Suppliable.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAE1C,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAItD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
-
import { DeepPartial } from '../types/
|
|
2
|
+
import { DeepPartial } from '../types/DeepHelpers';
|
|
3
3
|
import { FieldPath } from '../field/FieldPath';
|
|
4
4
|
export type CustomValidationIssue<TOutput extends object> = {
|
|
5
5
|
path: FieldPath.StringPaths<TOutput>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodie-forms/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/iGoodie/goodie-forms"
|
|
7
|
+
},
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Taha Anılcan Metinyurt",
|
|
10
|
+
"email": "igoodie@programmer.net",
|
|
11
|
+
"url": "https://github.com/iGoodie"
|
|
12
|
+
},
|
|
13
|
+
"license": "CC BY-SA 4.0",
|
|
4
14
|
"type": "module",
|
|
5
15
|
"main": "dist/index.js",
|
|
6
16
|
"types": "dist/index.d.ts",
|
|
@@ -10,13 +20,20 @@
|
|
|
10
20
|
"default": "./dist/index.js"
|
|
11
21
|
}
|
|
12
22
|
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
13
27
|
"publishConfig": {
|
|
14
28
|
"access": "public"
|
|
15
29
|
},
|
|
16
30
|
"devDependencies": {
|
|
31
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
32
|
+
"type-testing": "^0.2.0",
|
|
17
33
|
"typescript": "^5.9.3",
|
|
18
34
|
"vite": "^7.3.1",
|
|
19
|
-
"vite-plugin-dts": "^4.5.4"
|
|
35
|
+
"vite-plugin-dts": "^4.5.4",
|
|
36
|
+
"vitest": "^4.0.18"
|
|
20
37
|
},
|
|
21
38
|
"dependencies": {
|
|
22
39
|
"@standard-schema/spec": "^1.1.0",
|
|
@@ -24,6 +41,9 @@
|
|
|
24
41
|
"nanoevents": "^9.1.0"
|
|
25
42
|
},
|
|
26
43
|
"scripts": {
|
|
27
|
-
"build": "vite build"
|
|
44
|
+
"build": "vite build",
|
|
45
|
+
"coverage": "vitest run --coverage",
|
|
46
|
+
"test": "tsc --noEmit && vitest run",
|
|
47
|
+
"test:watch": "vitest"
|
|
28
48
|
}
|
|
29
49
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Equal, Expect } from "type-testing";
|
|
2
|
+
import { expect, test } from "vitest";
|
|
3
|
+
import { FieldPath } from "../field/FieldPath";
|
|
4
|
+
|
|
5
|
+
interface Person {
|
|
6
|
+
name: string;
|
|
7
|
+
surname: string;
|
|
8
|
+
address?: {
|
|
9
|
+
city: string;
|
|
10
|
+
street: string;
|
|
11
|
+
};
|
|
12
|
+
friends?: string[];
|
|
13
|
+
relations?: {
|
|
14
|
+
name: string;
|
|
15
|
+
points: number;
|
|
16
|
+
metAddress?: {
|
|
17
|
+
city: string;
|
|
18
|
+
street: string;
|
|
19
|
+
};
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const person: Person = {
|
|
24
|
+
name: "John",
|
|
25
|
+
surname: "Doe",
|
|
26
|
+
address: {
|
|
27
|
+
city: "Arkham City",
|
|
28
|
+
street: "Sesame Street",
|
|
29
|
+
},
|
|
30
|
+
friends: ["Alice", "Bob"],
|
|
31
|
+
relations: [
|
|
32
|
+
{
|
|
33
|
+
name: "Alice",
|
|
34
|
+
points: 99,
|
|
35
|
+
metAddress: {
|
|
36
|
+
city: "Arkham City",
|
|
37
|
+
street: "Sesame Street",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "Bob",
|
|
42
|
+
points: 0,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/* ------------------------------ */
|
|
48
|
+
|
|
49
|
+
test("resolves field types by path", () => {
|
|
50
|
+
type Shape = {
|
|
51
|
+
user?: {
|
|
52
|
+
profile?: { name?: string };
|
|
53
|
+
tags?: boolean[];
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type typeTests = [
|
|
58
|
+
Expect<
|
|
59
|
+
Equal<
|
|
60
|
+
FieldPath.Resolve<Shape, ["user"]>,
|
|
61
|
+
{ profile?: { name?: string }; tags?: boolean[] } | undefined
|
|
62
|
+
>
|
|
63
|
+
>,
|
|
64
|
+
|
|
65
|
+
Expect<
|
|
66
|
+
Equal<
|
|
67
|
+
FieldPath.Resolve<Shape, ["user", "profile"]>,
|
|
68
|
+
{ name?: string } | undefined
|
|
69
|
+
>
|
|
70
|
+
>,
|
|
71
|
+
|
|
72
|
+
Expect<
|
|
73
|
+
Equal<
|
|
74
|
+
FieldPath.Resolve<Shape, ["user", "profile", "name"]>,
|
|
75
|
+
string | undefined
|
|
76
|
+
>
|
|
77
|
+
>,
|
|
78
|
+
|
|
79
|
+
Expect<
|
|
80
|
+
Equal<
|
|
81
|
+
FieldPath.Resolve<Shape, ["user", "tags", number]>,
|
|
82
|
+
//
|
|
83
|
+
boolean
|
|
84
|
+
>
|
|
85
|
+
>,
|
|
86
|
+
|
|
87
|
+
Expect<
|
|
88
|
+
Equal<
|
|
89
|
+
FieldPath.Resolve<Shape, ["user", "tags", 0]>,
|
|
90
|
+
//
|
|
91
|
+
boolean
|
|
92
|
+
>
|
|
93
|
+
>,
|
|
94
|
+
|
|
95
|
+
Expect<
|
|
96
|
+
Equal<
|
|
97
|
+
FieldPath.Resolve<Shape, ["user", "missing"]>,
|
|
98
|
+
//
|
|
99
|
+
never
|
|
100
|
+
>
|
|
101
|
+
>,
|
|
102
|
+
];
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("parses string paths", () => {
|
|
106
|
+
type PersonPaths = FieldPath.StringPaths<Person>;
|
|
107
|
+
|
|
108
|
+
const path1 = FieldPath.fromStringPath("a.b");
|
|
109
|
+
expect(path1).toStrictEqual(["a", "b"]);
|
|
110
|
+
|
|
111
|
+
const path2 = FieldPath.fromStringPath("a.b.c");
|
|
112
|
+
expect(path2).toStrictEqual(["a", "b", "c"]);
|
|
113
|
+
|
|
114
|
+
const path3 = FieldPath.fromStringPath("a[99].b.c[3]");
|
|
115
|
+
expect(path3).toStrictEqual(["a", 99, "b", "c", 3]);
|
|
116
|
+
|
|
117
|
+
type typeTests = [
|
|
118
|
+
Expect<
|
|
119
|
+
Equal<
|
|
120
|
+
PersonPaths,
|
|
121
|
+
| "name"
|
|
122
|
+
| "surname"
|
|
123
|
+
| "address"
|
|
124
|
+
| "friends"
|
|
125
|
+
| "relations"
|
|
126
|
+
| "address.city"
|
|
127
|
+
| "address.street"
|
|
128
|
+
| "friends[0]"
|
|
129
|
+
| FieldPath._Unfoldable<`friends[${number}]`>
|
|
130
|
+
| "relations[0]"
|
|
131
|
+
| FieldPath._Unfoldable<`relations[${number}]`>
|
|
132
|
+
| "relations[0].metAddress"
|
|
133
|
+
| FieldPath._Unfoldable<`relations[${number}].metAddress`>
|
|
134
|
+
| "relations[0].name"
|
|
135
|
+
| FieldPath._Unfoldable<`relations[${number}].name`>
|
|
136
|
+
| "relations[0].points"
|
|
137
|
+
| FieldPath._Unfoldable<`relations[${number}].points`>
|
|
138
|
+
| "relations[0].metAddress.city"
|
|
139
|
+
| FieldPath._Unfoldable<`relations[${number}].metAddress.city`>
|
|
140
|
+
| "relations[0].metAddress.street"
|
|
141
|
+
| FieldPath._Unfoldable<`relations[${number}].metAddress.street`>
|
|
142
|
+
>
|
|
143
|
+
>,
|
|
144
|
+
];
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("gets fields properly", () => {
|
|
148
|
+
const getFieldValue = (stringPath: FieldPath.StringPaths<Person>) =>
|
|
149
|
+
FieldPath.getValue(person, FieldPath.fromStringPath(stringPath));
|
|
150
|
+
|
|
151
|
+
expect(getFieldValue("name")).toBe(person.name);
|
|
152
|
+
expect(getFieldValue("surname")).toBe(person.surname);
|
|
153
|
+
expect(getFieldValue("address")).toBe(person.address);
|
|
154
|
+
expect(getFieldValue("address.city")).toBe(person.address?.city);
|
|
155
|
+
expect(getFieldValue("address.street")).toBe(person.address?.street);
|
|
156
|
+
expect(getFieldValue("friends")).toBe(person.friends);
|
|
157
|
+
expect(getFieldValue("friends[0]")).toBe(person.friends?.[0]);
|
|
158
|
+
expect(getFieldValue("friends[1]")).toBe(person.friends?.[1]);
|
|
159
|
+
expect(getFieldValue("relations")).toBe(person.relations);
|
|
160
|
+
expect(getFieldValue("relations[0]")).toBe(person.relations?.[0]);
|
|
161
|
+
expect(getFieldValue("relations[1]")).toBe(person.relations?.[1]);
|
|
162
|
+
expect(getFieldValue("relations[1].metAddress.city")).toBe(
|
|
163
|
+
person.relations?.[1].metAddress?.city,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("sets fields properly", () => {
|
|
168
|
+
const obj = {
|
|
169
|
+
a: 1,
|
|
170
|
+
b: 2,
|
|
171
|
+
c: {
|
|
172
|
+
d: 3,
|
|
173
|
+
e: [4, 5],
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
type ObjPaths = FieldPath.StringPaths<typeof obj>;
|
|
178
|
+
|
|
179
|
+
const getPath = <TPath extends ObjPaths>(stringPath: TPath) => {
|
|
180
|
+
return FieldPath.fromStringPath(stringPath);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const setFieldValue = <TPath extends FieldPath.Segments>(
|
|
184
|
+
path: TPath,
|
|
185
|
+
value: FieldPath.Resolve<typeof obj, TPath>,
|
|
186
|
+
) => {
|
|
187
|
+
FieldPath.setValue(obj, path, value);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
setFieldValue(getPath("a"), 99);
|
|
191
|
+
setFieldValue(getPath("b"), 99);
|
|
192
|
+
setFieldValue(getPath("c.d"), 99);
|
|
193
|
+
setFieldValue(getPath("c.e"), [99, 99]);
|
|
194
|
+
setFieldValue(getPath("c.e[2]"), 99);
|
|
195
|
+
|
|
196
|
+
expect(obj).toMatchObject({
|
|
197
|
+
a: 99,
|
|
198
|
+
b: 99,
|
|
199
|
+
c: {
|
|
200
|
+
d: 99,
|
|
201
|
+
e: [99, 99, 99],
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
});
|
package/src/field/FieldPath.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
export namespace FieldPath {
|
|
2
2
|
export type Segments = readonly PropertyKey[];
|
|
3
|
-
export type CanonicalPath = string;
|
|
4
3
|
export type StringPath = string;
|
|
5
4
|
|
|
6
|
-
export function toCanonicalPath(path: Segments) {
|
|
7
|
-
return path.join(".") as CanonicalPath;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
5
|
export function toStringPath(path: Segments) {
|
|
11
6
|
const normalizedPath = normalize(path);
|
|
12
7
|
let result = "";
|
|
@@ -100,6 +95,7 @@ export namespace FieldPath {
|
|
|
100
95
|
}
|
|
101
96
|
return true;
|
|
102
97
|
}
|
|
98
|
+
|
|
103
99
|
export function isDescendant(parentPath: Segments, childPath: Segments) {
|
|
104
100
|
if (parentPath.length >= childPath.length) return false;
|
|
105
101
|
|
|
@@ -112,15 +108,13 @@ export namespace FieldPath {
|
|
|
112
108
|
return true;
|
|
113
109
|
}
|
|
114
110
|
|
|
115
|
-
|
|
111
|
+
/** @internal used to mark and prevent array types, so IntellSense can suggest and accept array indices */
|
|
112
|
+
export type _Unfoldable<T> = T & { _____foldMark?: never } & {};
|
|
116
113
|
|
|
117
|
-
export type Resolve<
|
|
118
|
-
TObject,
|
|
119
|
-
TPath extends readonly PropertyKey[],
|
|
120
|
-
> = TPath extends []
|
|
114
|
+
export type Resolve<TObject, TPath extends Segments> = TPath extends []
|
|
121
115
|
? TObject
|
|
122
116
|
: TPath extends readonly [infer Prop, ...infer Rest]
|
|
123
|
-
? Rest extends
|
|
117
|
+
? Rest extends Segments
|
|
124
118
|
? Resolve<ResolveStep<TObject, Prop>, Rest>
|
|
125
119
|
: never
|
|
126
120
|
: never;
|
|
@@ -150,7 +144,7 @@ export namespace FieldPath {
|
|
|
150
144
|
TCanonicalStringPaths extends `${string}[*]${string}`
|
|
151
145
|
?
|
|
152
146
|
| ReplaceAll<TCanonicalStringPaths, "[*]", "[0]">
|
|
153
|
-
|
|
|
147
|
+
| _Unfoldable<ReplaceAll<TCanonicalStringPaths, "[*]", `[${number}]`>>
|
|
154
148
|
: TCanonicalStringPaths;
|
|
155
149
|
|
|
156
150
|
type CanonicalStringPaths<TObject extends object> = {
|
|
@@ -210,9 +204,47 @@ export namespace FieldPath {
|
|
|
210
204
|
? K
|
|
211
205
|
: never;
|
|
212
206
|
|
|
207
|
+
export function walkPath<
|
|
208
|
+
TObject extends object,
|
|
209
|
+
const TPath extends FieldPath.Segments,
|
|
210
|
+
>(object: TObject, path: TPath): { target: any; key: PropertyKey };
|
|
211
|
+
export function walkPath<
|
|
212
|
+
TObject extends object,
|
|
213
|
+
const TPath extends FieldPath.Segments,
|
|
214
|
+
>(
|
|
215
|
+
object: TObject,
|
|
216
|
+
path: TPath,
|
|
217
|
+
opts: { returnOnEmptyBranch: true },
|
|
218
|
+
): { target: any; key: PropertyKey | null };
|
|
219
|
+
export function walkPath<
|
|
220
|
+
TObject extends object,
|
|
221
|
+
const TPath extends FieldPath.Segments,
|
|
222
|
+
>(object: TObject, path: TPath, opts?: { returnOnEmptyBranch?: boolean }) {
|
|
223
|
+
let current: any = object;
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
226
|
+
const pathFragment = path[i];
|
|
227
|
+
const nextFragment = path[i + 1];
|
|
228
|
+
|
|
229
|
+
if (current[pathFragment] == null) {
|
|
230
|
+
if (opts?.returnOnEmptyBranch) return { target: null, key: null };
|
|
231
|
+
current[pathFragment] = typeof nextFragment === "number" ? [] : {};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
current = current[pathFragment];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const lastFragment = path[path.length - 1];
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
target: current,
|
|
241
|
+
key: lastFragment,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
213
245
|
export function getValue<
|
|
214
246
|
TObject extends object,
|
|
215
|
-
const TPath extends
|
|
247
|
+
const TPath extends Segments,
|
|
216
248
|
>(
|
|
217
249
|
object: TObject,
|
|
218
250
|
path: TPath,
|
|
@@ -229,64 +261,36 @@ export namespace FieldPath {
|
|
|
229
261
|
|
|
230
262
|
export function setValue<
|
|
231
263
|
TObject extends object,
|
|
232
|
-
const TPath extends
|
|
233
|
-
>(object: TObject,
|
|
234
|
-
|
|
264
|
+
const TPath extends Segments,
|
|
265
|
+
>(object: TObject, path: TPath, value: FieldPath.Resolve<TObject, TPath>) {
|
|
266
|
+
const { target, key } = walkPath(object, path);
|
|
267
|
+
target[key] = value;
|
|
235
268
|
}
|
|
236
269
|
|
|
237
270
|
export function modifyValue<
|
|
238
271
|
TObject extends object,
|
|
239
|
-
const TPath extends
|
|
272
|
+
const TPath extends Segments,
|
|
240
273
|
>(
|
|
241
274
|
object: TObject,
|
|
242
275
|
path: TPath,
|
|
243
276
|
modifier: (
|
|
244
|
-
currentValue: FieldPath.Resolve<TObject, TPath
|
|
245
|
-
) =>
|
|
277
|
+
currentValue: FieldPath.Resolve<TObject, TPath> | undefined,
|
|
278
|
+
) => void,
|
|
246
279
|
) {
|
|
247
|
-
|
|
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
|
-
}
|
|
280
|
+
const { target, key } = walkPath(object, path);
|
|
281
|
+
modifier(target[key]);
|
|
268
282
|
}
|
|
269
283
|
|
|
270
|
-
export function deleteValue<
|
|
271
|
-
TObject
|
|
272
|
-
TPath
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const pathFragment = path[i];
|
|
278
|
-
|
|
279
|
-
if (current[pathFragment] == null) return;
|
|
280
|
-
|
|
281
|
-
current = current[pathFragment];
|
|
282
|
-
}
|
|
284
|
+
export function deleteValue<TObject extends object, TPath extends Segments>(
|
|
285
|
+
object: TObject,
|
|
286
|
+
path: TPath,
|
|
287
|
+
) {
|
|
288
|
+
const { target, key } = walkPath(object, path, {
|
|
289
|
+
returnOnEmptyBranch: true,
|
|
290
|
+
});
|
|
283
291
|
|
|
284
|
-
|
|
292
|
+
if (key == null) return;
|
|
285
293
|
|
|
286
|
-
delete
|
|
294
|
+
delete target[key];
|
|
287
295
|
}
|
|
288
296
|
}
|
|
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,47 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { FieldPath } from "../field/FieldPath";
|
|
3
|
+
import { FieldPathBuilder } from "../field/FieldPathBuilder";
|
|
4
|
+
|
|
5
|
+
interface User {
|
|
6
|
+
name: string;
|
|
7
|
+
address: {
|
|
8
|
+
city: string;
|
|
9
|
+
street: string;
|
|
10
|
+
};
|
|
11
|
+
friends: {
|
|
12
|
+
name: string;
|
|
13
|
+
tags: string[];
|
|
14
|
+
}[];
|
|
15
|
+
coords: [100, 200];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const builder = new FieldPathBuilder<User>();
|
|
19
|
+
|
|
20
|
+
const data: User = {
|
|
21
|
+
name: "",
|
|
22
|
+
address: { city: "", street: "" },
|
|
23
|
+
friends: [{ name: "", tags: ["A", "B"] }],
|
|
24
|
+
coords: [100, 200] as const,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
test("builds path segments from proxy and string paths", () => {
|
|
28
|
+
const path1 = builder.of((data) => data.friends[0].tags[1]);
|
|
29
|
+
const value1 = FieldPath.getValue(data, path1);
|
|
30
|
+
expect(path1).toStrictEqual(["friends", 0, "tags", 1]);
|
|
31
|
+
expect(value1).toStrictEqual(data.friends[0].tags[1]);
|
|
32
|
+
|
|
33
|
+
const path2 = builder.of("friends[0].tags[0]");
|
|
34
|
+
const value2 = FieldPath.getValue(data, path2);
|
|
35
|
+
expect(path2).toStrictEqual(["friends", 0, "tags", 0]);
|
|
36
|
+
expect(value2).toStrictEqual(data.friends[0].tags[0]);
|
|
37
|
+
|
|
38
|
+
const path3 = builder.of("friends[0].tags");
|
|
39
|
+
const value3 = FieldPath.getValue(data, path3);
|
|
40
|
+
expect(path3).toStrictEqual(["friends", 0, "tags"]);
|
|
41
|
+
expect(value3).toStrictEqual(data.friends[0].tags);
|
|
42
|
+
|
|
43
|
+
const path4 = builder.of("coords[1]");
|
|
44
|
+
const value4 = FieldPath.getValue(data, path4);
|
|
45
|
+
expect(path4).toStrictEqual(["coords", 1]);
|
|
46
|
+
expect(value4).toStrictEqual(data.coords[1]);
|
|
47
|
+
});
|
|
@@ -44,89 +44,23 @@ export class FieldPathBuilder<TOutput extends object> {
|
|
|
44
44
|
}) as any;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
public
|
|
47
|
+
public of<TStrPath extends FieldPath.StringPaths<TOutput>>(
|
|
48
48
|
stringPath: TStrPath,
|
|
49
|
-
)
|
|
50
|
-
return FieldPath.fromStringPath(
|
|
51
|
-
stringPath,
|
|
52
|
-
) as unknown as FieldPath.ParseStringPath<TStrPath>;
|
|
53
|
-
}
|
|
49
|
+
): FieldPath.ParseStringPath<TStrPath>;
|
|
54
50
|
|
|
55
|
-
public
|
|
51
|
+
public of<TProxy extends FieldPathBuilder.Proxy<any, any>>(
|
|
56
52
|
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);
|
|
53
|
+
): TProxy[typeof resolverCall];
|
|
97
54
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
// // ^?
|
|
55
|
+
public of(
|
|
56
|
+
arg:
|
|
57
|
+
| FieldPath.StringPaths<TOutput>
|
|
58
|
+
| ((data: FieldPathBuilder.Proxy<TOutput, []>) => any),
|
|
59
|
+
) {
|
|
60
|
+
if (typeof arg === "function") {
|
|
61
|
+
return arg(FieldPathBuilder.wrap<TOutput, []>([]))[resolverCall];
|
|
62
|
+
}
|
|
130
63
|
|
|
131
|
-
|
|
132
|
-
|
|
64
|
+
return FieldPath.fromStringPath(arg as any);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/field/Reconcile.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export namespace
|
|
1
|
+
export namespace Reconcile {
|
|
2
2
|
export function deepEqual(
|
|
3
3
|
a: any,
|
|
4
4
|
b: any,
|
|
@@ -70,27 +70,32 @@ export namespace Reconsile {
|
|
|
70
70
|
return true;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export function
|
|
73
|
+
export function arrayDiff<T>(
|
|
74
74
|
prev: readonly T[],
|
|
75
75
|
next: readonly T[],
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
opts?: {
|
|
77
|
+
equals?: (a: T, b: T) => boolean;
|
|
78
|
+
include?: (a: T) => boolean;
|
|
79
|
+
},
|
|
78
80
|
) {
|
|
79
81
|
const added: T[] = [];
|
|
80
82
|
const removed: T[] = [];
|
|
81
83
|
const unchanged: T[] = [];
|
|
82
84
|
|
|
85
|
+
const equals = (a: T, b: T) => (opts?.equals ? opts.equals(a, b) : a === b);
|
|
86
|
+
const include = (a: T) => (opts?.include ? opts.include(a) : true);
|
|
87
|
+
|
|
83
88
|
for (const n of next) {
|
|
84
89
|
if (prev.some((p) => equals(p, n))) {
|
|
85
|
-
if (
|
|
90
|
+
if (include(n)) unchanged.push(n);
|
|
86
91
|
} else {
|
|
87
|
-
if (
|
|
92
|
+
if (include(n)) added.push(n);
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
for (const p of prev) {
|
|
92
97
|
if (!next.some((n) => equals(p, n))) {
|
|
93
|
-
if (
|
|
98
|
+
if (include(p)) removed.push(p);
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|