@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.
Files changed (40) hide show
  1. package/LICENSE +426 -426
  2. package/dist/field/FieldPath.d.ts +32 -0
  3. package/dist/field/FieldPath.d.ts.map +1 -0
  4. package/dist/field/FieldPathBuilder.d.ts +25 -0
  5. package/dist/field/FieldPathBuilder.d.ts.map +1 -0
  6. package/dist/field/Reconcile.d.ts +9 -0
  7. package/dist/field/Reconcile.d.ts.map +1 -0
  8. package/dist/form/FormController.d.ts +35 -39
  9. package/dist/form/FormController.d.ts.map +1 -1
  10. package/dist/form/FormField.d.ts +12 -10
  11. package/dist/form/FormField.d.ts.map +1 -1
  12. package/dist/form/NonullFormField.d.ts +3 -4
  13. package/dist/form/NonullFormField.d.ts.map +1 -1
  14. package/dist/index.d.ts +7 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +615 -881
  17. package/dist/index.js.map +1 -1
  18. package/dist/validation/CustomValidation.d.ts +10 -0
  19. package/dist/validation/CustomValidation.d.ts.map +1 -0
  20. package/package.json +1 -1
  21. package/src/field/FieldPath.ts +292 -0
  22. package/src/field/FieldPathBuilder.ts +132 -0
  23. package/src/field/Reconcile.ts +99 -0
  24. package/src/form/FormController.ts +342 -310
  25. package/src/form/FormField.ts +200 -202
  26. package/src/form/NonullFormField.ts +15 -21
  27. package/src/index.ts +12 -5
  28. package/src/types/DeepPartial.ts +7 -7
  29. package/src/types/Mixin.ts +2 -2
  30. package/src/utils/ensureImmerability.ts +30 -30
  31. package/src/utils/getId.ts +5 -5
  32. package/src/utils/removeBy.ts +11 -11
  33. package/src/{form → validation}/CustomValidation.ts +52 -52
  34. package/tsconfig.json +8 -7
  35. package/vite.config.ts +18 -18
  36. package/dist/form/CustomValidation.d.ts +0 -10
  37. package/dist/form/CustomValidation.d.ts.map +0 -1
  38. package/dist/form/Field.d.ts +0 -31
  39. package/dist/form/Field.d.ts.map +0 -1
  40. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodie-forms/core",
3
- "version": "1.1.5-alpha",
3
+ "version": "1.2.0-alpha",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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
+ }