valixa 1.0.0-rc.1 → 1.0.0-rc.2
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 +31 -29
- package/dist/standard.d.ts +44 -0
- package/dist/standard.d.ts.map +1 -0
- package/dist/standard.js +85 -0
- package/dist/standard.js.map +1 -0
- package/package.json +5 -1
- package/src/standard.ts +97 -0
package/README.md
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
# valixa
|
|
2
|
-
|
|
3
|
-
Framework-independent form state, validation, subscriptions and submission.
|
|
4
|
-
ESM and TypeScript declarations, with no runtime dependencies.
|
|
5
|
-
|
|
6
|
-
```js
|
|
7
|
-
import { createValixa } from 'valixa';
|
|
8
|
-
|
|
9
|
-
const form = createValixa({ email: ['', 'required | email'] });
|
|
10
|
-
form.setValue('email', 'user@example.com');
|
|
11
|
-
const result = await form.validate();
|
|
12
|
-
if (result.valid) await form.submit(async data => data.email);
|
|
13
|
-
form.dispose();
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Core API: `blueprint`, `createValixa`, `nested`, `collection`, `registerLocale`, `parseRules`, `RuleSyntaxError`,
|
|
17
|
-
`defaultMessages`, `Blueprint`, `ValixaForm`, plus public TypeScript contracts.
|
|
18
|
-
Use `form.state` to read immutable snapshots, `subscribe` / `subscribeField` to
|
|
19
|
-
observe them, and `handleSubmit` for an event callback.
|
|
20
|
-
|
|
21
|
-
Supported rules: required, email, url, min/max length, minValue/maxValue, same.
|
|
22
|
-
Custom and asynchronous validators are supported with abort signals and stale-result
|
|
23
|
-
protection. Values are acyclic plain data. Existing object/array fields stay atomic;
|
|
24
|
-
explicit `nested` and `collection` constructors opt into typed paths and stable rows.
|
|
25
|
-
Dependency graphs, field scheduling, conditional behaviors, form validation and
|
|
26
|
-
English/Arabic localization are included. Framework bindings remain separate packages.
|
|
27
|
-
See [API.md](API.md) and the [advanced guide](ADVANCED.md) for semantics and examples.
|
|
28
|
-
|
|
29
|
-
MIT license. This
|
|
1
|
+
# valixa
|
|
2
|
+
|
|
3
|
+
Framework-independent form state, validation, subscriptions and submission.
|
|
4
|
+
ESM and TypeScript declarations, with no runtime dependencies.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
import { createValixa } from 'valixa';
|
|
8
|
+
|
|
9
|
+
const form = createValixa({ email: ['', 'required | email'] });
|
|
10
|
+
form.setValue('email', 'user@example.com');
|
|
11
|
+
const result = await form.validate();
|
|
12
|
+
if (result.valid) await form.submit(async data => data.email);
|
|
13
|
+
form.dispose();
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Core API: `blueprint`, `createValixa`, `nested`, `collection`, `registerLocale`, `parseRules`, `RuleSyntaxError`,
|
|
17
|
+
`defaultMessages`, `Blueprint`, `ValixaForm`, plus public TypeScript contracts.
|
|
18
|
+
Use `form.state` to read immutable snapshots, `subscribe` / `subscribeField` to
|
|
19
|
+
observe them, and `handleSubmit` for an event callback.
|
|
20
|
+
|
|
21
|
+
Supported rules: required, email, url, min/max length, minValue/maxValue, same.
|
|
22
|
+
Custom and asynchronous validators are supported with abort signals and stale-result
|
|
23
|
+
protection. Values are acyclic plain data. Existing object/array fields stay atomic;
|
|
24
|
+
explicit `nested` and `collection` constructors opt into typed paths and stable rows.
|
|
25
|
+
Dependency graphs, field scheduling, conditional behaviors, form validation and
|
|
26
|
+
English/Arabic localization are included. Framework bindings remain separate packages.
|
|
27
|
+
See [API.md](API.md) and the [advanced guide](ADVANCED.md) for semantics and examples.
|
|
28
|
+
|
|
29
|
+
MIT license. This 1.0.0-rc.2 candidate includes optional Standard Schema integration
|
|
30
|
+
at `valixa/standard`, supporting Zod, Yup and Valibot without bundled providers.
|
|
31
|
+
Version 1.0.0-rc.1 does not include this subpath. See docs/standard-schema.md in the repository.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Blueprint } from './blueprint.js';
|
|
2
|
+
import type { BlueprintInput, FormOptions, InferValues, Validator, ValueInput } from './types.js';
|
|
3
|
+
/** Structural Standard Schema v1 contract. No schema vendor is bundled. */
|
|
4
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
5
|
+
readonly '~standard': {
|
|
6
|
+
readonly version: 1;
|
|
7
|
+
readonly vendor: string;
|
|
8
|
+
readonly types?: {
|
|
9
|
+
readonly input: Input;
|
|
10
|
+
readonly output: Output;
|
|
11
|
+
} | undefined;
|
|
12
|
+
readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface StandardIssue {
|
|
16
|
+
readonly message: string;
|
|
17
|
+
readonly path?: readonly (PropertyKey | {
|
|
18
|
+
readonly key: PropertyKey;
|
|
19
|
+
})[] | undefined;
|
|
20
|
+
}
|
|
21
|
+
export type StandardResult<T> = {
|
|
22
|
+
readonly value: T;
|
|
23
|
+
readonly issues?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
readonly issues: readonly StandardIssue[];
|
|
26
|
+
};
|
|
27
|
+
export type StandardInput<S extends StandardSchemaV1> = NonNullable<S['~standard']['types']>['input'];
|
|
28
|
+
export type StandardOutput<S extends StandardSchemaV1> = NonNullable<S['~standard']['types']>['output'];
|
|
29
|
+
/** Field validation preserves the input value; schema transformations are not stored. */
|
|
30
|
+
export declare function standardValidator<S extends StandardSchemaV1>(schema: S): Validator<StandardInput<S>>;
|
|
31
|
+
/** Map provider paths through the actual blueprint, including literal dotted keys.
|
|
32
|
+
* Issues inside a collection are aggregated on its collection field. Row forms
|
|
33
|
+
* can use their own adapter for individual row-field messages.
|
|
34
|
+
*/
|
|
35
|
+
export declare function standardFormValidator<S extends StandardSchemaV1, const B extends BlueprintInput>(schema: S, input: (B | Blueprint<B>) & (ValueInput<InferValues<NoInfer<B>>> extends StandardInput<NoInfer<S>> ? unknown : never)): NonNullable<FormOptions<InferValues<B>>['validateForm']>;
|
|
36
|
+
export declare class StandardSchemaError extends Error {
|
|
37
|
+
readonly issues: readonly StandardIssue[];
|
|
38
|
+
constructor(issues: readonly StandardIssue[]);
|
|
39
|
+
}
|
|
40
|
+
/** Explicit typed output parsing for submit/API boundaries, not a form-state mutation. */
|
|
41
|
+
export declare function parseStandard<S extends StandardSchemaV1>(schema: S, value: StandardInput<S>, options?: {
|
|
42
|
+
readonly signal?: AbortSignal;
|
|
43
|
+
}): Promise<StandardOutput<S>>;
|
|
44
|
+
//# sourceMappingURL=standard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standard.d.ts","sourceRoot":"","sources":["../src/standard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAElG,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK;IAC/D,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,KAAK,CAAC,EAAE;YAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;YAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;SAAE,GAAG,SAAS,CAAC;QAChF,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,cAAc,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;KACjG,CAAC;CACH;AACD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG;QAAE,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;KAAE,CAAC,EAAE,GAAG,SAAS,CAAC;CACtF;AACD,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAA;CAAE,CAAC;AACnI,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,gBAAgB,IAAI,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AACtG,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,gBAAgB,IAAI,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;AA6BxG,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,gBAAgB,EAAE,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAMpG;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,gBAAgB,EAAE,KAAK,CAAC,CAAC,SAAS,cAAc,EAC9F,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,KAAK,CAAC,GACpH,WAAW,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAiB1D;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;gBAC9B,MAAM,EAAE,SAAS,aAAa,EAAE;CAI7C;AACD,0FAA0F;AAC1F,wBAAsB,aAAa,CAAC,CAAC,SAAS,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE;IAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAK/K"}
|
package/dist/standard.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { blueprint, Blueprint } from './blueprint.js';
|
|
2
|
+
import { copy } from './data.js';
|
|
3
|
+
function assertSchema(schema) {
|
|
4
|
+
if (schema?.['~standard']?.version !== 1 || typeof schema['~standard'].validate !== 'function')
|
|
5
|
+
throw new TypeError('Expected a Standard Schema v1 validator');
|
|
6
|
+
}
|
|
7
|
+
function aborted() { const error = new Error('Schema validation aborted'); error.name = 'AbortError'; return error; }
|
|
8
|
+
async function run(schema, value, signal) {
|
|
9
|
+
if (signal?.aborted)
|
|
10
|
+
throw aborted();
|
|
11
|
+
// Providers may cast/mutate input. Never expose a form snapshot or caller-owned data.
|
|
12
|
+
const input = copy(value);
|
|
13
|
+
let off = () => { };
|
|
14
|
+
const cancelled = new Promise((_, reject) => {
|
|
15
|
+
const abort = () => reject(aborted());
|
|
16
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
17
|
+
off = () => signal?.removeEventListener('abort', abort);
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
const work = Promise.resolve().then(() => {
|
|
21
|
+
if (signal?.aborted)
|
|
22
|
+
throw aborted();
|
|
23
|
+
return schema['~standard'].validate(input);
|
|
24
|
+
});
|
|
25
|
+
const result = await Promise.race([work, cancelled]);
|
|
26
|
+
if (signal?.aborted)
|
|
27
|
+
throw aborted();
|
|
28
|
+
if (!result || typeof result !== 'object' || (result.issues !== undefined && (!Array.isArray(result.issues) || result.issues.length === 0)) || (result.issues === undefined && !Object.hasOwn(result, 'value')))
|
|
29
|
+
throw new TypeError('Invalid Standard Schema result');
|
|
30
|
+
if (result.issues?.some(issue => !issue || typeof issue.message !== 'string'))
|
|
31
|
+
throw new TypeError('Invalid Standard Schema issue');
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
off();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Field validation preserves the input value; schema transformations are not stored. */
|
|
39
|
+
export function standardValidator(schema) {
|
|
40
|
+
assertSchema(schema);
|
|
41
|
+
return async (value, { signal }) => {
|
|
42
|
+
const result = await run(schema, value, signal);
|
|
43
|
+
return result.issues?.[0]?.message ?? true;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Map provider paths through the actual blueprint, including literal dotted keys.
|
|
47
|
+
* Issues inside a collection are aggregated on its collection field. Row forms
|
|
48
|
+
* can use their own adapter for individual row-field messages.
|
|
49
|
+
*/
|
|
50
|
+
export function standardFormValidator(schema, input) {
|
|
51
|
+
assertSchema(schema);
|
|
52
|
+
const definition = input instanceof Blueprint ? input : blueprint(input);
|
|
53
|
+
const fields = Object.entries(definition.description.fields);
|
|
54
|
+
if (fields.some(([name]) => name === '$form'))
|
|
55
|
+
throw new TypeError('Standard form validation reserves the $form field name');
|
|
56
|
+
return async (values, { signal }) => {
|
|
57
|
+
const result = await run(schema, values, signal);
|
|
58
|
+
const errors = Object.create(null);
|
|
59
|
+
for (const issue of result.issues ?? []) {
|
|
60
|
+
const path = issue.path?.map(segment => typeof segment === 'object' && segment !== null ? segment.key : segment);
|
|
61
|
+
const exact = path && fields.find(([, field]) => field.path.length === path.length && field.path.every((key, i) => key === path[i]));
|
|
62
|
+
const collection = !exact && path ? fields.find(([name, field]) => Object.hasOwn(definition.collections, name) && field.path.every((key, i) => key === path[i])) : undefined;
|
|
63
|
+
const name = (exact || collection)?.[0] ?? '$form';
|
|
64
|
+
(errors[name] ??= []).push(issue.message);
|
|
65
|
+
}
|
|
66
|
+
return errors;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export class StandardSchemaError extends Error {
|
|
70
|
+
issues;
|
|
71
|
+
constructor(issues) {
|
|
72
|
+
super('Schema validation failed');
|
|
73
|
+
this.name = 'StandardSchemaError';
|
|
74
|
+
this.issues = Object.freeze(issues.map(issue => Object.freeze({ message: issue.message, ...(issue.path ? { path: Object.freeze(issue.path.map(part => typeof part === 'object' && part !== null ? Object.freeze({ key: part.key }) : part)) } : {}) })));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Explicit typed output parsing for submit/API boundaries, not a form-state mutation. */
|
|
78
|
+
export async function parseStandard(schema, value, options = {}) {
|
|
79
|
+
assertSchema(schema);
|
|
80
|
+
const result = await run(schema, value, options.signal);
|
|
81
|
+
if (result.issues)
|
|
82
|
+
throw new StandardSchemaError(result.issues);
|
|
83
|
+
return result.value;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=standard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standard.js","sourceRoot":"","sources":["../src/standard.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "valixa",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
4
4
|
"description": "Headless, framework-independent form state and validation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
".": {
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./standard": {
|
|
24
|
+
"types": "./dist/standard.d.ts",
|
|
25
|
+
"import": "./dist/standard.js"
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"engines": {
|
package/src/standard.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { blueprint, Blueprint } from './blueprint.js';
|
|
2
|
+
import { copy } from './data.js';
|
|
3
|
+
import type { BlueprintInput, FormOptions, InferValues, Validator, ValueInput } from './types.js';
|
|
4
|
+
|
|
5
|
+
/** Structural Standard Schema v1 contract. No schema vendor is bundled. */
|
|
6
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
7
|
+
readonly '~standard': {
|
|
8
|
+
readonly version: 1;
|
|
9
|
+
readonly vendor: string;
|
|
10
|
+
readonly types?: { readonly input: Input; readonly output: Output } | undefined;
|
|
11
|
+
readonly validate: (value: unknown) => StandardResult<Output> | Promise<StandardResult<Output>>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface StandardIssue {
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly path?: readonly (PropertyKey | { readonly key: PropertyKey })[] | undefined;
|
|
17
|
+
}
|
|
18
|
+
export type StandardResult<T> = { readonly value: T; readonly issues?: undefined } | { readonly issues: readonly StandardIssue[] };
|
|
19
|
+
export type StandardInput<S extends StandardSchemaV1> = NonNullable<S['~standard']['types']>['input'];
|
|
20
|
+
export type StandardOutput<S extends StandardSchemaV1> = NonNullable<S['~standard']['types']>['output'];
|
|
21
|
+
|
|
22
|
+
function assertSchema(schema: StandardSchemaV1): void {
|
|
23
|
+
if (schema?.['~standard']?.version !== 1 || typeof schema['~standard'].validate !== 'function') throw new TypeError('Expected a Standard Schema v1 validator');
|
|
24
|
+
}
|
|
25
|
+
function aborted(): Error { const error = new Error('Schema validation aborted'); error.name = 'AbortError'; return error; }
|
|
26
|
+
async function run<S extends StandardSchemaV1>(schema: S, value: unknown, signal?: AbortSignal): Promise<StandardResult<StandardOutput<S>>> {
|
|
27
|
+
if (signal?.aborted) throw aborted();
|
|
28
|
+
// Providers may cast/mutate input. Never expose a form snapshot or caller-owned data.
|
|
29
|
+
const input = copy(value);
|
|
30
|
+
let off = () => {};
|
|
31
|
+
const cancelled = new Promise<never>((_, reject) => {
|
|
32
|
+
const abort = () => reject(aborted());
|
|
33
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
34
|
+
off = () => signal?.removeEventListener('abort', abort);
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
const work = Promise.resolve().then(() => {
|
|
38
|
+
if (signal?.aborted) throw aborted();
|
|
39
|
+
return schema['~standard'].validate(input);
|
|
40
|
+
});
|
|
41
|
+
const result = await Promise.race([work, cancelled]);
|
|
42
|
+
if (signal?.aborted) throw aborted();
|
|
43
|
+
if (!result || typeof result !== 'object' || (result.issues !== undefined && (!Array.isArray(result.issues) || result.issues.length === 0)) || (result.issues === undefined && !Object.hasOwn(result, 'value'))) throw new TypeError('Invalid Standard Schema result');
|
|
44
|
+
if (result.issues?.some(issue => !issue || typeof issue.message !== 'string')) throw new TypeError('Invalid Standard Schema issue');
|
|
45
|
+
return result as StandardResult<StandardOutput<S>>;
|
|
46
|
+
} finally { off(); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Field validation preserves the input value; schema transformations are not stored. */
|
|
50
|
+
export function standardValidator<S extends StandardSchemaV1>(schema: S): Validator<StandardInput<S>> {
|
|
51
|
+
assertSchema(schema);
|
|
52
|
+
return async (value, { signal }) => {
|
|
53
|
+
const result = await run(schema, value, signal);
|
|
54
|
+
return result.issues?.[0]?.message ?? true;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Map provider paths through the actual blueprint, including literal dotted keys.
|
|
59
|
+
* Issues inside a collection are aggregated on its collection field. Row forms
|
|
60
|
+
* can use their own adapter for individual row-field messages.
|
|
61
|
+
*/
|
|
62
|
+
export function standardFormValidator<S extends StandardSchemaV1, const B extends BlueprintInput>(
|
|
63
|
+
schema: S,
|
|
64
|
+
input: (B | Blueprint<B>) & (ValueInput<InferValues<NoInfer<B>>> extends StandardInput<NoInfer<S>> ? unknown : never)
|
|
65
|
+
): NonNullable<FormOptions<InferValues<B>>['validateForm']> {
|
|
66
|
+
assertSchema(schema);
|
|
67
|
+
const definition = input instanceof Blueprint ? input : blueprint(input);
|
|
68
|
+
const fields = Object.entries(definition.description.fields);
|
|
69
|
+
if (fields.some(([name]) => name === '$form')) throw new TypeError('Standard form validation reserves the $form field name');
|
|
70
|
+
return async (values, { signal }) => {
|
|
71
|
+
const result = await run(schema, values, signal);
|
|
72
|
+
const errors: Record<string, string[]> = Object.create(null);
|
|
73
|
+
for (const issue of result.issues ?? []) {
|
|
74
|
+
const path = issue.path?.map(segment => typeof segment === 'object' && segment !== null ? segment.key : segment);
|
|
75
|
+
const exact = path && fields.find(([, field]) => field.path.length === path.length && field.path.every((key, i) => key === path[i]));
|
|
76
|
+
const collection = !exact && path ? fields.find(([name, field]) => Object.hasOwn(definition.collections, name) && field.path.every((key, i) => key === path[i])) : undefined;
|
|
77
|
+
const name = (exact || collection)?.[0] ?? '$form';
|
|
78
|
+
(errors[name] ??= []).push(issue.message);
|
|
79
|
+
}
|
|
80
|
+
return errors;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class StandardSchemaError extends Error {
|
|
85
|
+
readonly issues: readonly StandardIssue[];
|
|
86
|
+
constructor(issues: readonly StandardIssue[]) {
|
|
87
|
+
super('Schema validation failed'); this.name = 'StandardSchemaError';
|
|
88
|
+
this.issues = Object.freeze(issues.map(issue => Object.freeze({ message: issue.message, ...(issue.path ? { path: Object.freeze(issue.path.map(part => typeof part === 'object' && part !== null ? Object.freeze({ key: part.key }) : part)) } : {}) })));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Explicit typed output parsing for submit/API boundaries, not a form-state mutation. */
|
|
92
|
+
export async function parseStandard<S extends StandardSchemaV1>(schema: S, value: StandardInput<S>, options: { readonly signal?: AbortSignal } = {}): Promise<StandardOutput<S>> {
|
|
93
|
+
assertSchema(schema);
|
|
94
|
+
const result = await run(schema, value, options.signal);
|
|
95
|
+
if (result.issues) throw new StandardSchemaError(result.issues);
|
|
96
|
+
return result.value;
|
|
97
|
+
}
|