--- /dev/null
+---
+'@sveltejs/kit': patch
+---
+
+breaking: `invalid` now must be imported from `@sveltejs/kit`
### Programmatic validation
-In addition to declarative schema validation, you can programmatically mark fields as invalid inside the form handler using the `invalid` function. This is useful for cases where you can't know if something is valid until you try to perform some action:
+In addition to declarative schema validation, you can programmatically mark fields as invalid inside the form handler using the `invalid` function. This is useful for cases where you can't know if something is valid until you try to perform some action. Just like `redirect` or `error`, `invalid` throws. It expects a list of strings (for issues relating to the form as a whole) or standard-schema-compliant issues (for those relating to a specific field). Use the `issue` parameter for type-safe creation of such issues:
```js
/// file: src/routes/shop/data.remote.js
import * as v from 'valibot';
+import { invalid } from '@sveltejs/kit';
import { form } from '$app/server';
import * as db from '$lib/server/database';
v.minValue(1, 'you must buy at least one hotcake')
)
}),
- async (data, invalid) => {
+ async (data, issue) => {
try {
await db.buy(data.qty);
} catch (e) {
if (e.code === 'OUT_OF_STOCK') {
invalid(
- invalid.qty(`we don't have enough hotcakes`)
+ issue.qty(`we don't have enough hotcakes`)
);
}
}
-import { HttpError, Redirect, ActionFailure } from './internal/index.js';
+/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
+
+import { HttpError, Redirect, ActionFailure, ValidationError } from './internal/index.js';
import { BROWSER, DEV } from 'esm-env';
import {
add_data_suffix,
return e instanceof ActionFailure;
}
+/**
+ * Use this to throw a validation error to imperatively fail form validation.
+ * Can be used in combination with `issue` passed to form actions to create field-specific issues.
+ *
+ * @example
+ * ```ts
+ * import { invalid } from '@sveltejs/kit';
+ * import { form } from '$app/server';
+ * import { tryLogin } from '$lib/server/auth';
+ * import * as v from 'valibot';
+ *
+ * export const login = form(
+ * v.object({ name: v.string(), _password: v.string() }),
+ * async ({ name, _password }) => {
+ * const success = tryLogin(name, _password);
+ * if (!success) {
+ * invalid('Incorrect username or password');
+ * }
+ *
+ * // ...
+ * }
+ * );
+ * ```
+ * @param {...(StandardSchemaV1.Issue | string)} issues
+ * @returns {never}
+ * @since 2.47.3
+ */
+export function invalid(...issues) {
+ throw new ValidationError(
+ issues.map((issue) => (typeof issue === 'string' ? { message: issue } : issue))
+ );
+}
+
+/**
+ * Checks whether this is an validation error thrown by {@link invalid}.
+ * @param {unknown} e The object to check.
+ * @return {e is import('./public.js').ActionFailure}
+ * @since 2.47.3
+ */
+export function isValidationError(e) {
+ return e instanceof ValidationError;
+}
+
/**
* Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
* Returns the normalized URL as well as a method for adding the potential suffix back
+/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
+
export class HttpError {
/**
* @param {number} status
}
}
+/**
+ * Error thrown when form validation fails imperatively
+ */
+export class ValidationError extends Error {
+ /**
+ * @param {StandardSchemaV1.Issue[]} issues
+ */
+ constructor(issues) {
+ super('Validation failed');
+ this.name = 'ValidationError';
+ this.issues = issues;
+ }
+}
+
export { init_remote_functions } from './remote-functions.js';
: string | number;
/**
- * Recursively maps an input type to a structure where each field can create a validation issue.
- * This mirrors the runtime behavior of the `invalid` proxy passed to form handlers.
+ * A function and proxy object used to imperatively create validation errors in form handlers.
+ *
+ * Access properties to create field-specific issues: `issue.fieldName('message')`.
+ * The type structure mirrors the input data structure for type-safe field access.
+ * Call `invalid(issue.foo(...), issue.nested.bar(...))` to throw a validation error.
*/
-type InvalidField<T> =
+export type InvalidField<T> =
WillRecurseIndefinitely<T> extends true
? Record<string | number, any>
: NonNullable<T> extends string | number | boolean | File
: Record<string, never>;
/**
- * A function and proxy object used to imperatively create validation errors in form handlers.
- *
- * Call `invalid(issue1, issue2, ...issueN)` to throw a validation error.
- * If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`)
- * Access properties to create field-specific issues: `invalid.fieldName('message')`.
- * The type structure mirrors the input data structure for type-safe field access.
+ * A validation error thrown by `invalid`.
*/
-export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) &
- InvalidField<Input>;
+export interface ValidationError {
+ /** The validation issues */
+ issues: StandardSchemaV1.Issue[];
+}
/**
* The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
-/** @import { RemoteFormInput, RemoteForm } from '@sveltejs/kit' */
+/** @import { RemoteFormInput, RemoteForm, InvalidField } from '@sveltejs/kit' */
/** @import { InternalRemoteFormIssue, MaybePromise, RemoteInfo } from 'types' */
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
import { get_request_store } from '@sveltejs/kit/internal/server';
flatten_issues
} from '../../../form-utils.js';
import { get_cache, run_remote_function } from './shared.js';
+import { ValidationError } from '@sveltejs/kit/internal';
/**
* Creates a form object that can be spread onto a `<form>` element.
*
* @template Output
* @overload
- * @param {(invalid: import('@sveltejs/kit').Invalid<void>) => MaybePromise<Output>} fn
+ * @param {() => MaybePromise<Output>} fn
* @returns {RemoteForm<void, Output>}
* @since 2.27
*/
* @template Output
* @overload
* @param {'unchecked'} validate
- * @param {(data: Input, invalid: import('@sveltejs/kit').Invalid<Input>) => MaybePromise<Output>} fn
+ * @param {(data: Input, issue: InvalidField<Input>) => MaybePromise<Output>} fn
* @returns {RemoteForm<Input, Output>}
* @since 2.27
*/
* @template Output
* @overload
* @param {Schema} validate
- * @param {(data: StandardSchemaV1.InferOutput<Schema>, invalid: import('@sveltejs/kit').Invalid<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>} fn
+ * @param {(data: StandardSchemaV1.InferOutput<Schema>, issue: InvalidField<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>} fn
* @returns {RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>}
* @since 2.27
*/
* @template {RemoteFormInput} Input
* @template Output
* @param {any} validate_or_fn
- * @param {(data_or_invalid: any, invalid?: any) => MaybePromise<Output>} [maybe_fn]
+ * @param {(data_or_issue: any, issue?: any) => MaybePromise<Output>} [maybe_fn]
* @returns {RemoteForm<Input, Output>}
* @since 2.27
*/
state.refreshes ??= {};
- const invalid = create_invalid();
+ const issue = create_issues();
try {
output.result = await run_remote_function(
true,
data,
(d) => d,
- (data) => (!maybe_fn ? fn(invalid) : fn(data, invalid))
+ (data) => (!maybe_fn ? fn() : fn(data, issue))
);
} catch (e) {
if (e instanceof ValidationError) {
/**
* Creates an invalid function that can be used to imperatively mark form fields as invalid
- * @returns {import('@sveltejs/kit').Invalid}
+ * @returns {InvalidField<any>}
*/
-function create_invalid() {
- /**
- * @param {...(string | StandardSchemaV1.Issue)} issues
- * @returns {never}
- */
- function invalid(...issues) {
- throw new ValidationError(
- issues.map((issue) => {
- if (typeof issue === 'string') {
- return {
- path: [],
- message: issue
- };
+function create_issues() {
+ return /** @type {InvalidField<any>} */ (
+ new Proxy(
+ /** @param {string} message */
+ (message) => {
+ // TODO 3.0 remove
+ if (typeof message !== 'string') {
+ throw new Error(
+ '`invalid` should now be imported from `@sveltejs/kit` to throw validation issues. ' +
+ "The second parameter provided to the form function (renamed to `issue`) is still used to construct issues, e.g. `invalid(issue.field('message'))`. " +
+ 'For more info see https://github.com/sveltejs/kit/pulls/14768'
+ );
}
- return issue;
- })
- );
- }
-
- return /** @type {import('@sveltejs/kit').Invalid} */ (
- new Proxy(invalid, {
- get(target, prop) {
- if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
+ return create_issue(message);
+ },
+ {
+ get(target, prop) {
+ if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
- /**
- * @param {string} message
- * @param {(string | number)[]} path
- * @returns {StandardSchemaV1.Issue}
- */
- const create_issue = (message, path = []) => ({
- message,
- path
- });
-
- return create_issue_proxy(prop, create_issue, []);
+ return create_issue_proxy(prop, []);
+ }
}
- })
+ )
);
-}
-/**
- * Error thrown when form validation fails imperatively
- */
-class ValidationError extends Error {
/**
- * @param {StandardSchemaV1.Issue[]} issues
+ * @param {string} message
+ * @param {(string | number)[]} path
+ * @returns {StandardSchemaV1.Issue}
*/
- constructor(issues) {
- super('Validation failed');
- this.name = 'ValidationError';
- this.issues = issues;
+ function create_issue(message, path = []) {
+ return {
+ message,
+ path
+ };
}
-}
-
-/**
- * Creates a proxy that builds up a path and returns a function to create an issue
- * @param {string | number} key
- * @param {(message: string, path: (string | number)[]) => StandardSchemaV1.Issue} create_issue
- * @param {(string | number)[]} path
- */
-function create_issue_proxy(key, create_issue, path) {
- const new_path = [...path, key];
/**
- * @param {string} message
- * @returns {StandardSchemaV1.Issue}
+ * Creates a proxy that builds up a path and returns a function to create an issue
+ * @param {string | number} key
+ * @param {(string | number)[]} path
*/
- const issue_func = (message) => create_issue(message, new_path);
+ function create_issue_proxy(key, path) {
+ const new_path = [...path, key];
- return new Proxy(issue_func, {
- get(target, prop) {
- if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
+ /**
+ * @param {string} message
+ * @returns {StandardSchemaV1.Issue}
+ */
+ const issue_func = (message) => create_issue(message, new_path);
- // Handle array access like invalid.items[0]
- if (/^\d+$/.test(prop)) {
- return create_issue_proxy(parseInt(prop, 10), create_issue, new_path);
- }
+ return new Proxy(issue_func, {
+ get(target, prop) {
+ if (typeof prop === 'symbol') return /** @type {any} */ (target)[prop];
- // Handle property access like invalid.field.nested
- return create_issue_proxy(prop, create_issue, new_path);
- }
- });
+ // Handle array access like invalid.items[0]
+ if (/^\d+$/.test(prop)) {
+ return create_issue_proxy(parseInt(prop, 10), new_path);
+ }
+
+ // Handle property access like invalid.field.nested
+ return create_issue_proxy(prop, new_path);
+ }
+ });
+ }
}
import { form } from '$app/server';
-import { error } from '@sveltejs/kit';
+import { error, invalid } from '@sveltejs/kit';
import * as v from 'valibot';
export const my_form = form(
bar: v.picklist(['d', 'e', 'f']),
button: v.optional(v.literal('submitter'))
}),
- async (data, invalid) => {
+ async (data, issue) => {
// Test imperative validation
if (data.foo === 'c') {
- invalid(invalid.foo('Imperative: foo cannot be c'));
+ invalid(issue.foo('Imperative: foo cannot be c'));
}
console.log(data);
import { query, prerender, command, form } from '$app/server';
import { StandardSchemaV1 } from '@standard-schema/spec';
-import { RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit';
+import { RemotePrerenderFunction, RemoteQueryFunction, invalid } from '@sveltejs/kit';
const schema: StandardSchemaV1<string> = null as any;
const schema2: StandardSchemaV1<string, number> = null as any;
function form_tests() {
const q = query(() => '');
- const f = form('unchecked', (data: { input: string }, invalid) => {
+ const f = form('unchecked', (data: { input: string }, issue) => {
data.input;
- invalid(
- 'foo',
- invalid.input('bar'),
- // @ts-expect-error
- invalid.nonexistent.prop('baz')
- );
+ if (Math.random() > 0.5) {
+ invalid(
+ 'foo',
+ issue.input('bar'),
+ // @ts-expect-error
+ issue.nonexistent.prop('baz')
+ );
+ }
return { success: true };
});
const f2 = form(
null as any as StandardSchemaV1<{ a: string; nested: { prop: string } }>,
- (data, invalid) => {
+ (data, issue) => {
data.a === '';
data.nested.prop === '';
// @ts-expect-error
data.nonexistent;
// @ts-expect-error
data.a === 123;
- invalid(
- 'foo',
- invalid.nested.prop('bar'),
- // @ts-expect-error
- invalid.nonexistent.prop('baz')
- );
+ if (Math.random() > 0.5) {
+ invalid(
+ 'foo',
+ issue.nested.prop('bar'),
+ // @ts-expect-error
+ issue.nonexistent.prop('baz')
+ );
+ }
return { success: true };
}
);
// @ts-expect-error
- f2.fields.name();
+ f2.fields.as('text');
f2.fields.a.issues();
f2.fields.nested.prop.issues();
// @ts-expect-error
// @ts-expect-error
f2.fields.nonexistent.value();
// @ts-expect-error
- f2.fields.array[0].array.name();
+ f2.fields.array[0].array.as('text');
// all schema properties optional
const f3 = form(
null as any as StandardSchemaV1<{ a?: string; nested?: { prop?: string } }>,
- (data, invalid) => {
+ (data, issue) => {
data.a === '';
data.nested?.prop === '';
// @ts-expect-error
data.nonexistent;
// @ts-expect-error
data.a === 123;
- invalid(
- 'foo',
- invalid.nested.prop('bar'),
- // @ts-expect-error
- invalid.nonexistent.prop('baz')
- );
+ if (Math.random() > 0.5) {
+ invalid(
+ 'foo',
+ issue.nested.prop('bar'),
+ // @ts-expect-error
+ issue.nonexistent.prop('baz')
+ );
+ }
return { success: true };
}
);
// @ts-expect-error
- f3.fields.name();
+ f3.fields.as('text');
f3.fields.a.issues();
f3.fields.a.value();
f3.fields.nested.prop.issues();
f3.fields.nested.prop.value();
// @ts-expect-error
- f3.fields.nonexistent.name();
+ f3.fields.nonexistent.as('text');
// index signature schema
const f4 = form(null as any as StandardSchemaV1<Record<string, any>>, (data) => {
return { success: true };
});
// @ts-expect-error
- f4.fields.name();
+ f4.fields.as('text');
f4.fields.a.issues();
f4.fields.a.value();
f4.fields.nested.prop.issues();
// schema with union types
const f5 = form(
null as any as StandardSchemaV1<{ foo: 'a' | 'b'; bar: 'c' | 'd' }>,
- (data, invalid) => {
+ (data, issue) => {
data.foo === 'a';
data.bar === 'c';
// @ts-expect-error
data.foo === 'e';
- invalid(
- 'foo',
- invalid.bar('bar'),
- // @ts-expect-error
- invalid.nonexistent.prop('baz')
- );
+ if (Math.random() > 0.5) {
+ invalid(
+ 'foo',
+ issue.bar('bar'),
+ // @ts-expect-error
+ issue.nonexistent.prop('baz')
+ );
+ }
return { success: true };
}
);
// @ts-expect-error
- f5.fields.name();
+ f5.fields.as('text');
f5.fields.foo.issues();
f5.fields.bar.issues();
f5.fields.foo.value();
// @ts-expect-error
f5.fields.foo.value() === 'e';
// @ts-expect-error
- f5.fields.nonexistent.name();
+ f5.fields.nonexistent.as('text');
// schema with arrays
const f6 = form(
null as any as StandardSchemaV1<{ array: Array<{ array: string[]; prop: string }> }>,
- (data, invalid) => {
+ (data, issue) => {
data.array[0].prop === 'a';
data.array[0].array[0] === 'a';
// @ts-expect-error
data.array[0].array[0] === 1;
- invalid(
- 'foo',
- invalid.array[0].prop('bar'),
- // @ts-expect-error
- invalid.nonexistent.prop('baz')
- );
+ if (Math.random() > 0.5) {
+ invalid(
+ 'foo',
+ issue.array[0].prop('bar'),
+ // @ts-expect-error
+ issue.nonexistent.prop('baz')
+ );
+ }
return { success: true };
}
);
// @ts-expect-error
- f6.fields.name();
+ f6.fields.as('text');
// @ts-expect-error
f6.field('array[0].array');
f6.fields.array.issues();
f6.fields.array[0].prop.value();
f6.fields.array[0].array.value();
// @ts-expect-error
- f6.fields.array[0].array.name();
+ f6.fields.array[0].array.as('text');
// any
- const f7 = form(null as any, (data, invalid) => {
+ const f7 = form(null as any, (data, issue) => {
data.a === '';
data.nested?.prop === '';
- invalid('foo', invalid.nested.prop('bar'));
+ if (Math.random() > 0.5) {
+ invalid('foo', issue.nested.prop('bar'));
+ }
return { success: true };
});
// @ts-expect-error
- f7.fields.name();
+ f7.fields.as('text');
f7.fields.a.issues();
f7.fields.a.value();
f7.fields.nested.prop.issues();
f7.fields.nested.prop.value();
// no schema
- const f8 = form((invalid) => {
- invalid(
- 'foo',
- // @ts-expect-error
- invalid.x('bar')
- );
+ const f8 = form(() => {
+ invalid('foo');
});
f8.fields.issues();
f8.fields.allIssues();
: string | number;
/**
- * Recursively maps an input type to a structure where each field can create a validation issue.
- * This mirrors the runtime behavior of the `invalid` proxy passed to form handlers.
+ * A function and proxy object used to imperatively create validation errors in form handlers.
+ *
+ * Access properties to create field-specific issues: `issue.fieldName('message')`.
+ * The type structure mirrors the input data structure for type-safe field access.
+ * Call `invalid(issue.foo(...), issue.nested.bar(...))` to throw a validation error.
*/
- type InvalidField<T> =
+ export type InvalidField<T> =
WillRecurseIndefinitely<T> extends true
? Record<string | number, any>
: NonNullable<T> extends string | number | boolean | File
: Record<string, never>;
/**
- * A function and proxy object used to imperatively create validation errors in form handlers.
- *
- * Call `invalid(issue1, issue2, ...issueN)` to throw a validation error.
- * If an issue is a `string`, it applies to the form as a whole (and will show up in `fields.allIssues()`)
- * Access properties to create field-specific issues: `invalid.fieldName('message')`.
- * The type structure mirrors the input data structure for type-safe field access.
+ * A validation error thrown by `invalid`.
*/
- export type Invalid<Input = any> = ((...issues: Array<string | StandardSchemaV1.Issue>) => never) &
- InvalidField<Input>;
+ export interface ValidationError {
+ /** The validation issues */
+ issues: StandardSchemaV1.Issue[];
+ }
/**
* The return value of a remote `form` function. See [Remote functions](https://svelte.dev/docs/kit/remote-functions#form) for full documentation.
* @param e The object to check.
* */
export function isActionFailure(e: unknown): e is ActionFailure;
+ /**
+ * Use this to throw a validation error to imperatively fail form validation.
+ * Can be used in combination with `issue` passed to form actions to create field-specific issues.
+ *
+ * @example
+ * ```ts
+ * import { invalid } from '@sveltejs/kit';
+ * import { form } from '$app/server';
+ * import { tryLogin } from '$lib/server/auth';
+ * import * as v from 'valibot';
+ *
+ * export const login = form(
+ * v.object({ name: v.string(), _password: v.string() }),
+ * async ({ name, _password }) => {
+ * const success = tryLogin(name, _password);
+ * if (!success) {
+ * invalid('Incorrect username or password');
+ * }
+ *
+ * // ...
+ * }
+ * );
+ * ```
+ * @since 2.47.3
+ */
+ export function invalid(...issues: (StandardSchemaV1.Issue | string)[]): never;
+ /**
+ * Checks whether this is an validation error thrown by {@link invalid}.
+ * @param e The object to check.
+ * @since 2.47.3
+ */
+ export function isValidationError(e: unknown): e is ActionFailure;
/**
* Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
* Returns the normalized URL as well as a method for adding the potential suffix back
}
declare module '$app/server' {
- import type { RequestEvent, RemoteCommand, RemoteForm, RemoteFormInput, RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit';
+ import type { RequestEvent, RemoteCommand, RemoteForm, RemoteFormInput, InvalidField, RemotePrerenderFunction, RemoteQueryFunction } from '@sveltejs/kit';
import type { StandardSchemaV1 } from '@standard-schema/spec';
/**
* Read the contents of an imported asset from the filesystem
*
* @since 2.27
*/
- export function form<Output>(fn: (invalid: import("@sveltejs/kit").Invalid<void>) => MaybePromise<Output>): RemoteForm<void, Output>;
+ export function form<Output>(fn: () => MaybePromise<Output>): RemoteForm<void, Output>;
/**
* Creates a form object that can be spread onto a `<form>` element.
*
*
* @since 2.27
*/
- export function form<Input extends RemoteFormInput, Output>(validate: "unchecked", fn: (data: Input, invalid: import("@sveltejs/kit").Invalid<Input>) => MaybePromise<Output>): RemoteForm<Input, Output>;
+ export function form<Input extends RemoteFormInput, Output>(validate: "unchecked", fn: (data: Input, issue: InvalidField<Input>) => MaybePromise<Output>): RemoteForm<Input, Output>;
/**
* Creates a form object that can be spread onto a `<form>` element.
*
*
* @since 2.27
*/
- export function form<Schema extends StandardSchemaV1<RemoteFormInput, Record<string, any>>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput<Schema>, invalid: import("@sveltejs/kit").Invalid<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>): RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>;
+ export function form<Schema extends StandardSchemaV1<RemoteFormInput, Record<string, any>>, Output>(validate: Schema, fn: (data: StandardSchemaV1.InferOutput<Schema>, issue: InvalidField<StandardSchemaV1.InferInput<Schema>>) => MaybePromise<Output>): RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>;
/**
* Creates a remote prerender function. When called from the browser, the function will be invoked on the server via a `fetch` call.
*