@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
@@ -1,52 +1,52 @@
1
- import { StandardSchemaV1 } from "@standard-schema/spec";
2
- import { Field } from "../form/Field";
3
- import { DeepPartial } from "../types/DeepPartial";
4
-
5
- export type CustomValidationIssue<TShape extends object> = {
6
- path: Field.Paths<TShape>;
7
- message: string;
8
- };
9
-
10
- export type CustomValidationStrategy<TShape extends object> = (
11
- data: DeepPartial<TShape>,
12
- ) =>
13
- | void
14
- | CustomValidationIssue<TShape>[]
15
- | Promise<CustomValidationIssue<TShape>[] | void>;
16
-
17
- export function customValidation<TShape extends object>(
18
- strategy: CustomValidationStrategy<TShape>,
19
- ): StandardSchemaV1<TShape, TShape> {
20
- return {
21
- "~standard": {
22
- version: 1 as const,
23
-
24
- vendor: "goodie-forms/custom" as const,
25
-
26
- validate: async (input: unknown) => {
27
- try {
28
- const customIssues = await strategy(input as DeepPartial<TShape>);
29
-
30
- if (customIssues == null) {
31
- return { value: input as TShape };
32
- }
33
-
34
- return {
35
- issues: customIssues.map((i) => ({
36
- path: Field.parsePathFragments(i.path),
37
- message: i.message,
38
- })),
39
- };
40
- } catch (err) {
41
- return {
42
- issues: [
43
- {
44
- message: err instanceof Error ? err.message : "Unknown error",
45
- },
46
- ],
47
- };
48
- }
49
- },
50
- },
51
- };
52
- }
1
+ import { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import { DeepPartial } from "../types/DeepPartial";
3
+ import { FieldPath } from "../field/FieldPath";
4
+
5
+ export type CustomValidationIssue<TOutput extends object> = {
6
+ path: FieldPath.StringPaths<TOutput>;
7
+ message: string;
8
+ };
9
+
10
+ export type CustomValidationStrategy<TOutput extends object> = (
11
+ data: DeepPartial<TOutput>,
12
+ ) =>
13
+ | void
14
+ | CustomValidationIssue<TOutput>[]
15
+ | Promise<CustomValidationIssue<TOutput>[] | void>;
16
+
17
+ export function customValidation<TOutput extends object>(
18
+ strategy: CustomValidationStrategy<TOutput>,
19
+ ) {
20
+ return {
21
+ "~standard": {
22
+ version: 1 as const,
23
+
24
+ vendor: "goodie-forms/custom" as const,
25
+
26
+ validate: async (input: unknown) => {
27
+ try {
28
+ const customIssues = await strategy(input as DeepPartial<TOutput>);
29
+
30
+ if (customIssues == null) {
31
+ return { value: input as TOutput };
32
+ }
33
+
34
+ return {
35
+ issues: customIssues.map((i) => ({
36
+ path: FieldPath.fromStringPath(i.path as string),
37
+ message: i.message,
38
+ })),
39
+ };
40
+ } catch (err) {
41
+ return {
42
+ issues: [
43
+ {
44
+ message: err instanceof Error ? err.message : "Unknown error",
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ },
50
+ },
51
+ } as StandardSchemaV1<TOutput, TOutput>;
52
+ }
package/tsconfig.json CHANGED
@@ -1,7 +1,8 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist"
5
- },
6
- "include": ["src"]
7
- }
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "."
6
+ },
7
+ "include": ["src"]
8
+ }
package/vite.config.ts CHANGED
@@ -1,18 +1,18 @@
1
- import { defineConfig } from "vite";
2
-
3
- import dts from "vite-plugin-dts";
4
-
5
- export default defineConfig({
6
- plugins: [dts({ entryRoot: "src" })],
7
- build: {
8
- lib: {
9
- entry: "src/index.ts",
10
- formats: ["es"],
11
- fileName: "index",
12
- },
13
- rollupOptions: {
14
- external: [],
15
- },
16
- sourcemap: true,
17
- },
18
- });
1
+ import { defineConfig } from "vite";
2
+
3
+ import dts from "vite-plugin-dts";
4
+
5
+ export default defineConfig({
6
+ plugins: [dts({ entryRoot: "src" })],
7
+ build: {
8
+ lib: {
9
+ entry: "src/index.ts",
10
+ formats: ["es"],
11
+ fileName: "index",
12
+ },
13
+ rollupOptions: {
14
+ external: [],
15
+ },
16
+ sourcemap: true,
17
+ },
18
+ });
@@ -1,10 +0,0 @@
1
- import { StandardSchemaV1 } from '@standard-schema/spec';
2
- import { Field } from '../form/Field';
3
- import { DeepPartial } from '../types/DeepPartial';
4
- export type CustomValidationIssue<TShape extends object> = {
5
- path: Field.Paths<TShape>;
6
- message: string;
7
- };
8
- export type CustomValidationStrategy<TShape extends object> = (data: DeepPartial<TShape>) => void | CustomValidationIssue<TShape>[] | Promise<CustomValidationIssue<TShape>[] | void>;
9
- export declare function customValidation<TShape extends object>(strategy: CustomValidationStrategy<TShape>): StandardSchemaV1<TShape, TShape>;
10
- //# sourceMappingURL=CustomValidation.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"CustomValidation.d.ts","sourceRoot":"","sources":["../../src/form/CustomValidation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,MAAM,qBAAqB,CAAC,MAAM,SAAS,MAAM,IAAI;IACzD,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,wBAAwB,CAAC,MAAM,SAAS,MAAM,IAAI,CAC5D,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,KAEvB,IAAI,GACJ,qBAAqB,CAAC,MAAM,CAAC,EAAE,GAC/B,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;AAEpD,wBAAgB,gBAAgB,CAAC,MAAM,SAAS,MAAM,EACpD,QAAQ,EAAE,wBAAwB,CAAC,MAAM,CAAC,GACzC,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAiClC"}
@@ -1,31 +0,0 @@
1
- import { StandardSchemaV1 } from '@standard-schema/spec';
2
- export declare namespace Field {
3
- type Unfoldable<T> = T & {
4
- _____foldMark?: never;
5
- } & {};
6
- export type Paths<TShape extends object> = PathsImpl<CanonicalPaths<TShape>>;
7
- type PathsImpl<T> = T extends string ? T extends `${string}[*]${string}` ? ReplaceAll<T, "[*]", "[0]"> | Unfoldable<ReplaceAll<T, "[*]", `[${number}]`>> : T : never;
8
- type CanonicalPaths<TShape extends object> = {
9
- [K in keyof TShape & string]: NonNullable<TShape[K]> extends (...args: any[]) => any ? never : NonNullable<TShape[K]> extends (infer U)[] ? U extends object ? K | `${K}[*]` | `${K}[*].${CanonicalPaths<NonNullable<U>>}` : K | `${K}[*]` : NonNullable<TShape[K]> extends object ? K | `${K}.${CanonicalPaths<NonNullable<TShape[K]>>}` : K;
10
- }[keyof TShape & string];
11
- type ReplaceAll<TString extends string, TMatch extends string, TReplace extends string | number> = TString extends `${infer A}${TMatch}${infer B}` ? `${A}${TReplace}${ReplaceAll<B, TMatch, TReplace>}` : TString;
12
- export type GetValue<TShape, TPath extends string> = GetValueImpl<TShape, NormalizePath<TPath>>;
13
- type GetValueImpl<TShape, TPath extends string> = TPath extends `${infer Head}.${infer Tail}` ? GetValueImpl<ResolveFragment<NonNullable<TShape>, Head>, Tail> : ResolveFragment<NonNullable<TShape>, TPath>;
14
- type NormalizePath<TPath extends string> = TPath extends `${infer A}[${infer B}]${infer Rest}` ? NormalizePath<`${A}.${B}${Rest}`> : TPath extends `.${infer R}` ? NormalizePath<R> : TPath;
15
- type ResolveFragment<TShape, TFragment extends string> = TFragment extends `${number}` ? TShape extends readonly (infer U)[] ? U : never : TFragment extends keyof TShape ? TShape[TFragment] : never;
16
- export function deepEqual(a: any, b: any, customComparator?: (a: any, b: any) => boolean | undefined): boolean;
17
- export function diff<T>(prev: readonly T[], next: readonly T[], equals: (a: T, b: T) => boolean, filter?: (a: T) => boolean): {
18
- added: T[];
19
- removed: T[];
20
- unchanged: T[];
21
- };
22
- export function parsePathFragments(path: string): (string | number)[];
23
- export function parsePath(fragments: readonly (PropertyKey | StandardSchemaV1.PathSegment)[]): string;
24
- export function isDescendant(parentPath: string, childPath: string): boolean;
25
- export function getValue<TShape extends object, TPath extends Field.Paths<TShape>>(data: TShape, path: TPath): Field.GetValue<TShape, TPath> | undefined;
26
- export function setValue<TShape extends object, TPath extends Field.Paths<TShape>>(data: TShape, key: TPath, value: Field.GetValue<TShape, TPath>): void;
27
- export function modifyValue<TShape extends object, TPath extends Field.Paths<TShape>>(data: TShape, path: TPath, modifier: (currentValue: Field.GetValue<TShape, TPath>) => Field.GetValue<TShape, TPath> | void): void;
28
- export function deleteValue<TShape extends object, TPath extends Field.Paths<TShape>>(data: TShape, path: TPath): void;
29
- export {};
30
- }
31
- //# sourceMappingURL=Field.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Field.d.ts","sourceRoot":"","sources":["../../src/form/Field.ts"],"names":[],"mappings":"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"}
package/src/form/Field.ts DELETED
@@ -1,334 +0,0 @@
1
- import { StandardSchemaV1 } from "@standard-schema/spec";
2
-
3
- export namespace Field {
4
- type Unfoldable<T> = T & { _____foldMark?: never } & {};
5
-
6
- export type Paths<TShape extends object> = PathsImpl<CanonicalPaths<TShape>>;
7
-
8
- type PathsImpl<T> = T extends string
9
- ? T extends `${string}[*]${string}`
10
- ?
11
- | ReplaceAll<T, "[*]", "[0]">
12
- | Unfoldable<ReplaceAll<T, "[*]", `[${number}]`>>
13
- : T
14
- : never;
15
-
16
- type CanonicalPaths<TShape extends object> = {
17
- [K in keyof TShape & string]: NonNullable<TShape[K]> extends (
18
- ...args: any[]
19
- ) => any
20
- ? never
21
- : NonNullable<TShape[K]> extends (infer U)[]
22
- ? U extends object
23
- ? K | `${K}[*]` | `${K}[*].${CanonicalPaths<NonNullable<U>>}`
24
- : K | `${K}[*]`
25
- : NonNullable<TShape[K]> extends object
26
- ? K | `${K}.${CanonicalPaths<NonNullable<TShape[K]>>}`
27
- : K;
28
- }[keyof TShape & string];
29
-
30
- type ReplaceAll<
31
- TString extends string,
32
- TMatch extends string,
33
- TReplace extends string | number
34
- > = TString extends `${infer A}${TMatch}${infer B}`
35
- ? `${A}${TReplace}${ReplaceAll<B, TMatch, TReplace>}`
36
- : TString;
37
-
38
- export type GetValue<TShape, TPath extends string> = GetValueImpl<
39
- TShape,
40
- NormalizePath<TPath>
41
- >;
42
-
43
- type GetValueImpl<
44
- TShape,
45
- TPath extends string
46
- > = TPath extends `${infer Head}.${infer Tail}`
47
- ? GetValueImpl<ResolveFragment<NonNullable<TShape>, Head>, Tail>
48
- : ResolveFragment<NonNullable<TShape>, TPath>;
49
-
50
- type NormalizePath<TPath extends string> =
51
- TPath extends `${infer A}[${infer B}]${infer Rest}`
52
- ? NormalizePath<`${A}.${B}${Rest}`>
53
- : TPath extends `.${infer R}`
54
- ? NormalizePath<R>
55
- : TPath;
56
-
57
- type ResolveFragment<
58
- TShape,
59
- TFragment extends string
60
- > = TFragment extends `${number}`
61
- ? TShape extends readonly (infer U)[]
62
- ? U
63
- : never
64
- : TFragment extends keyof TShape
65
- ? TShape[TFragment]
66
- : never;
67
-
68
- export function deepEqual(
69
- a: any,
70
- b: any,
71
- customComparator?: (a: any, b: any) => boolean | undefined
72
- ) {
73
- if (a === b) return true;
74
-
75
- if (a === null || b === null) return false;
76
- if (typeof a !== "object" || typeof b !== "object") return false;
77
-
78
- // Arrays
79
- if (Array.isArray(a)) {
80
- if (!Array.isArray(b) || a.length !== b.length) return false;
81
- for (let i = 0; i < a.length; i++) {
82
- if (!deepEqual(a[i], b[i])) return false;
83
- }
84
- return true;
85
- }
86
-
87
- // Allow custom comparison
88
- if (customComparator != null) {
89
- const result = customComparator(a, b);
90
- if (result !== undefined) return result;
91
- }
92
-
93
- // Dates
94
- if (a instanceof Date && b instanceof Date) {
95
- return a.getTime() === b.getTime();
96
- }
97
-
98
- // RegExp
99
- if (a instanceof RegExp && b instanceof RegExp) {
100
- return a.source === b.source && a.flags === b.flags;
101
- }
102
-
103
- // Map
104
- if (a instanceof Map && b instanceof Map) {
105
- if (a.size !== b.size) return false;
106
- for (const [key, val] of a) {
107
- if (!b.has(key) || !deepEqual(val, b.get(key))) return false;
108
- }
109
- return true;
110
- }
111
-
112
- // Set
113
- if (a instanceof Set && b instanceof Set) {
114
- if (a.size !== b.size) return false;
115
- for (const val of a) {
116
- if (!b.has(val)) return false;
117
- }
118
- return true;
119
- }
120
-
121
- // Plain / class objects
122
- if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
123
- return false;
124
- }
125
-
126
- const keysA = Object.keys(a);
127
- const keysB = Object.keys(b);
128
-
129
- if (keysA.length !== keysB.length) return false;
130
-
131
- for (const key of keysA) {
132
- if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
133
- if (!deepEqual(a[key], b[key])) return false;
134
- }
135
-
136
- return true;
137
- }
138
-
139
- export function diff<T>(
140
- prev: readonly T[],
141
- next: readonly T[],
142
- equals: (a: T, b: T) => boolean,
143
- filter?: (a: T) => boolean
144
- ) {
145
- const added: T[] = [];
146
- const removed: T[] = [];
147
- const unchanged: T[] = [];
148
-
149
- for (const n of next) {
150
- if (prev.some((p) => equals(p, n))) {
151
- if (filter?.(n) ?? true) unchanged.push(n);
152
- } else {
153
- if (filter?.(n) ?? true) added.push(n);
154
- }
155
- }
156
-
157
- for (const p of prev) {
158
- if (!next.some((n) => equals(p, n))) {
159
- if (filter?.(p) ?? true) removed.push(p);
160
- }
161
- }
162
-
163
- return { added, removed, unchanged };
164
- }
165
-
166
- export function parsePathFragments(path: string) {
167
- const result: Array<string | number> = [];
168
-
169
- let i = 0;
170
-
171
- while (i < path.length) {
172
- const char = path[i];
173
-
174
- // dot separator
175
- if (char === ".") {
176
- i++;
177
- continue;
178
- }
179
-
180
- // bracket index: [123]
181
- if (char === "[") {
182
- i++; // skip '['
183
- let num = "";
184
-
185
- while (i < path.length && path[i] !== "]") {
186
- num += path[i];
187
- i++;
188
- }
189
-
190
- i++; // skip ']'
191
-
192
- if (!num || !/^\d+$/.test(num)) {
193
- throw new Error(`Invalid array index in path: ${path}`);
194
- }
195
-
196
- result.push(Number(num));
197
- continue;
198
- }
199
-
200
- // identifier
201
- let key = "";
202
- while (i < path.length && /[^\.\[]/.test(path[i])) {
203
- key += path[i];
204
- i++;
205
- }
206
-
207
- if (key) {
208
- result.push(key);
209
- }
210
- }
211
-
212
- return result;
213
- }
214
-
215
- export function parsePath(
216
- fragments: readonly (PropertyKey | StandardSchemaV1.PathSegment)[]
217
- ) {
218
- let result = "";
219
-
220
- for (const fragment of fragments) {
221
- const pathFragment =
222
- typeof fragment === "object" && "key" in fragment
223
- ? fragment.key
224
- : fragment;
225
-
226
- if (typeof pathFragment === "number") {
227
- result += `[${pathFragment}]`;
228
- } else {
229
- if (result.length > 0) {
230
- result += ".";
231
- }
232
- result += pathFragment.toString();
233
- }
234
- }
235
-
236
- return result;
237
- }
238
-
239
- export function isDescendant(parentPath: string, childPath: string) {
240
- const parentFrags = parsePathFragments(parentPath);
241
- const childFrags = parsePathFragments(childPath);
242
-
243
- if (parentFrags.length >= childFrags.length) return false;
244
-
245
- for (let i = 0; i < parentFrags.length; i++) {
246
- if (parentFrags[i] !== childFrags[i]) {
247
- return false;
248
- }
249
- }
250
-
251
- return true;
252
- }
253
-
254
- export function getValue<
255
- TShape extends object,
256
- TPath extends Field.Paths<TShape>
257
- >(data: TShape, path: TPath): Field.GetValue<TShape, TPath> | undefined {
258
- if (data == null) return undefined;
259
-
260
- const pathFragments = parsePathFragments(path);
261
-
262
- let current: any = data;
263
-
264
- for (const pathFragment of pathFragments) {
265
- if (current == null) return undefined;
266
- current = current[pathFragment];
267
- }
268
-
269
- return current;
270
- }
271
-
272
- export function setValue<
273
- TShape extends object,
274
- TPath extends Field.Paths<TShape>
275
- >(data: TShape, key: TPath, value: Field.GetValue<TShape, TPath>) {
276
- return modifyValue(data, key, () => value);
277
- }
278
-
279
- export function modifyValue<
280
- TShape extends object,
281
- TPath extends Field.Paths<TShape>
282
- >(
283
- data: TShape,
284
- path: TPath,
285
- modifier: (
286
- currentValue: Field.GetValue<TShape, TPath>
287
- ) => Field.GetValue<TShape, TPath> | void
288
- ) {
289
- const pathFragments = parsePathFragments(path);
290
-
291
- let current: any = data;
292
-
293
- for (let i = 0; i < pathFragments.length - 1; i++) {
294
- const pathFragment = pathFragments[i];
295
- const nextFragment = pathFragments[i + 1];
296
-
297
- if (current[pathFragment] == null) {
298
- current[pathFragment] = typeof nextFragment === "number" ? [] : {};
299
- }
300
-
301
- current = current[pathFragment];
302
- }
303
-
304
- const lastFragment = pathFragments[pathFragments.length - 1];
305
-
306
- const oldValue = current[lastFragment];
307
- const newValue = modifier(oldValue);
308
-
309
- if (newValue !== undefined) {
310
- current[lastFragment] = newValue;
311
- }
312
- }
313
-
314
- export function deleteValue<
315
- TShape extends object,
316
- TPath extends Field.Paths<TShape>
317
- >(data: TShape, path: TPath) {
318
- const pathFragments = parsePathFragments(path);
319
-
320
- let current: any = data;
321
-
322
- for (let i = 0; i < pathFragments.length - 1; i++) {
323
- const pathFragment = pathFragments[i];
324
-
325
- if (current[pathFragment] == null) return;
326
-
327
- current = current[pathFragment];
328
- }
329
-
330
- const lastFragment = pathFragments[pathFragments.length - 1];
331
-
332
- delete current[lastFragment];
333
- }
334
- }