UNPKG

next-safe-action

Version:

Type safe and validated Server Actions in your Next.js project.

544 lines (535 loc) 20.8 kB
import { t as FrameworkErrorHandler } from "./errors-BsNXg-5Q.mjs"; import { deepmerge } from "deepmerge-ts"; //#region src/standard-schema.ts function isStandardSchema(value) { if (value === null || (typeof value !== "object" && typeof value !== "function")) return false; const standardProps = value["~standard"]; if (standardProps === null || typeof standardProps !== "object") return false; return standardProps.version === 1 && typeof standardProps.validate === "function"; } async function standardParse(schema, value) { return schema["~standard"].validate(value); } //#endregion //#region src/utils.ts const DEFAULT_SERVER_ERROR_MESSAGE = "Something went wrong while executing the operation."; /** * Checks if passed argument is an instance of Error. */ const isError = (error) => error instanceof Error; /** * Checks what the winning boolean value is from a series of values, from lowest to highest priority. * `null` and `undefined` values are skipped. */ const winningBoolean = (...args) => { return args.reduce((acc, v) => (typeof v === "boolean" ? v : acc), false); }; //#endregion //#region src/validation-errors.ts const getKey = (segment) => (typeof segment === "object" ? segment.key : segment); const getIssueMessage = (issue) => { if (issue.unionErrors) return issue.unionErrors.map((u) => u.issues.map((i) => i.message)).flat(); return issue.message; }; const buildValidationErrors = (issues) => { const ve = {}; for (const issue of issues) { const { path, message, unionErrors } = issue; if (!path || path.length === 0) { ve._errors = ve._errors ? [...ve._errors, message] : [message]; continue; } let ref = ve; for (let i = 0; i < path.length - 1; i++) { const k = getKey(path[i]); if (!ref[k]) ref[k] = {}; ref = ref[k]; } const key = getKey(path[path.length - 1]); const issueMessage = getIssueMessage(issue); ref[key] = ref[key]?._errors ? { ...structuredClone(ref[key]), _errors: [...ref[key]._errors, issueMessage], } : { ...structuredClone(ref[key]), _errors: unionErrors ? issueMessage : [issueMessage], }; } return ve; }; var ActionServerValidationError = class extends Error { validationErrors; constructor(validationErrors) { super("Server Action server validation error(s) occurred"); this.validationErrors = validationErrors; } }; var ActionValidationError = class extends Error { validationErrors; constructor(validationErrors, overriddenErrorMessage) { super(overriddenErrorMessage ?? "Server Action validation error(s) occurred"); this.validationErrors = validationErrors; } }; var ActionBindArgsValidationError = class extends Error { validationErrors; constructor(validationErrors) { super("Server Action bind args validation error(s) occurred"); this.validationErrors = validationErrors; } }; /** * Return custom validation errors to the client from the action's server code function. * Code declared after this function invocation will not be executed. * @param schema Input schema * @param validationErrors Validation errors object * * {@link https://next-safe-action.dev/docs/define-actions/validation-errors#returnvalidationerrors See docs for more information} */ function returnValidationErrors(schema, validationErrors) { throw new ActionServerValidationError(validationErrors); } /** * Default validation errors format. * Emulation of `zod`'s [`format`](https://zod.dev/ERROR_HANDLING?id=formatting-errors) function. */ function formatValidationErrors(validationErrors) { return validationErrors; } /** * Transform default formatted validation errors into flattened structure. * `formErrors` contains global errors, and `fieldErrors` contains errors for each field, * one level deep. It discards errors for nested fields. * Emulation of `zod`'s [`flatten`](https://zod.dev/ERROR_HANDLING?id=flattening-errors) function. * @param {ValidationErrors} [validationErrors] Validation errors object * * {@link https://next-safe-action.dev/docs/define-actions/validation-errors#flattenvalidationerrorsutility-function See docs for more information} */ function flattenValidationErrors(validationErrors) { const flattened = { formErrors: [], fieldErrors: {}, }; for (const [key, value] of Object.entries(validationErrors ?? {})) if (key === "_errors" && Array.isArray(value)) flattened.formErrors = [...value]; else if ("_errors" in value) flattened.fieldErrors[key] = [...value._errors]; return flattened; } /** * This error is thrown when an action metadata is invalid, i.e. when there's a mismatch between the * type of the metadata schema returned from `defineMetadataSchema` and the actual data passed. */ var ActionMetadataValidationError = class extends Error { validationErrors; constructor(validationErrors) { super("Invalid metadata input. Please be sure to pass metadata via `metadata` method before defining the action."); this.name = "ActionMetadataError"; this.validationErrors = validationErrors; } }; /** * This error is thrown when an action's data (output) is invalid, i.e. when there's a mismatch between the * type of the data schema passed to `dataSchema` method and the actual return of the action. */ var ActionOutputDataValidationError = class extends Error { validationErrors; constructor(validationErrors) { super( "Invalid action data (output). Please be sure to return data following the shape of the schema passed to `dataSchema` method." ); this.name = "ActionOutputDataError"; this.validationErrors = validationErrors; } }; //#endregion //#region src/action-builder.ts function actionBuilder(args) { const bindArgsSchemas = args.bindArgsSchemas ?? []; function buildAction({ withState }) { return { action: (serverCodeFn, utils) => { return async (...clientInputs) => { let currentCtx = {}; const middlewareResult = { success: false }; let prevResult = {}; const parsedInputDatas = []; const frameworkErrorHandler = new FrameworkErrorHandler(); let serverErrorHandled = false; if (withState) prevResult = clientInputs.splice(bindArgsSchemas.length, 1)[0]; if (bindArgsSchemas.length + 1 > clientInputs.length) clientInputs.push(void 0); const executeMiddlewareStack = async (idx = 0) => { if (frameworkErrorHandler.error) return; const middlewareFn = args.middlewareFns[idx]; middlewareResult.ctx = currentCtx; try { if (idx === 0) { if (args.metadataSchema) { const parsedMd = await standardParse(args.metadataSchema, args.metadata); if (parsedMd.issues) throw new ActionMetadataValidationError(buildValidationErrors(parsedMd.issues)); } } if (middlewareFn) await middlewareFn({ clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], ctx: currentCtx, metadata: args.metadata, next: async (nextOpts) => { currentCtx = deepmerge(currentCtx, nextOpts?.ctx ?? {}); await executeMiddlewareStack(idx + 1); return middlewareResult; }, }).catch((e) => { frameworkErrorHandler.handleError(e); if (frameworkErrorHandler.error) { middlewareResult.success = false; middlewareResult.navigationKind = FrameworkErrorHandler.getNavigationKind( frameworkErrorHandler.error ); } }); else { const parsedInputs = await Promise.all( clientInputs.map(async (input, i) => { if (i === clientInputs.length - 1) { if (typeof args.inputSchemaFn === "undefined") return { value: void 0 }; return standardParse(await args.inputSchemaFn(input), input); } return standardParse(bindArgsSchemas[i], input); }) ); let hasBindValidationErrors = false; const bindArgsValidationErrors = Array(parsedInputs.length - 1).fill({}); for (let i = 0; i < parsedInputs.length; i++) { const parsedInput = parsedInputs[i]; if (!parsedInput.issues) parsedInputDatas.push(parsedInput.value); else if (i < parsedInputs.length - 1) { bindArgsValidationErrors[i] = buildValidationErrors(parsedInput.issues); hasBindValidationErrors = true; } else { const validationErrors = buildValidationErrors(parsedInput.issues); middlewareResult.validationErrors = await Promise.resolve( args.handleValidationErrorsShape(validationErrors, { clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], ctx: currentCtx, metadata: args.metadata, }) ); } } if (hasBindValidationErrors) throw new ActionBindArgsValidationError(bindArgsValidationErrors); if (middlewareResult.validationErrors) return; const scfArgs = []; scfArgs[0] = { parsedInput: parsedInputDatas.at(-1), bindArgsParsedInputs: parsedInputDatas.slice(0, -1), clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], ctx: currentCtx, metadata: args.metadata, }; if (withState) scfArgs[1] = { prevResult: structuredClone(prevResult) }; const data = await serverCodeFn(...scfArgs).catch((e) => frameworkErrorHandler.handleError(e)); if (typeof args.outputSchema !== "undefined" && !frameworkErrorHandler.error) { const parsedData = await standardParse(args.outputSchema, data); if (parsedData.issues) throw new ActionOutputDataValidationError(buildValidationErrors(parsedData.issues)); } if (frameworkErrorHandler.error) { middlewareResult.success = false; middlewareResult.navigationKind = FrameworkErrorHandler.getNavigationKind( frameworkErrorHandler.error ); } else { middlewareResult.success = true; middlewareResult.data = data; } middlewareResult.parsedInput = parsedInputDatas.at(-1); middlewareResult.bindArgsParsedInputs = parsedInputDatas.slice(0, -1); } } catch (e) { if (serverErrorHandled) throw e; if (e instanceof ActionServerValidationError) { const ve = e.validationErrors; middlewareResult.validationErrors = await Promise.resolve( args.handleValidationErrorsShape(ve, { clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], ctx: currentCtx, metadata: args.metadata, }) ); } else { serverErrorHandled = true; const error = isError(e) ? e : new Error(DEFAULT_SERVER_ERROR_MESSAGE); middlewareResult.serverError = await Promise.resolve( args.handleServerError(error, { clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], ctx: currentCtx, metadata: args.metadata, }) ); } } }; await executeMiddlewareStack(); const callbackPromises = []; if (frameworkErrorHandler.error) { callbackPromises.push( utils?.onNavigation?.({ metadata: args.metadata, ctx: currentCtx, clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], navigationKind: FrameworkErrorHandler.getNavigationKind(frameworkErrorHandler.error), }) ); callbackPromises.push( utils?.onSettled?.({ metadata: args.metadata, ctx: currentCtx, clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], result: {}, navigationKind: FrameworkErrorHandler.getNavigationKind(frameworkErrorHandler.error), }) ); await Promise.all(callbackPromises); throw frameworkErrorHandler.error; } const actionResult = {}; if (typeof middlewareResult.validationErrors !== "undefined") if ( winningBoolean( args.throwValidationErrors, typeof utils?.throwValidationErrors === "undefined" ? void 0 : Boolean(utils.throwValidationErrors) ) ) { const overrideErrorMessageFn = typeof utils?.throwValidationErrors === "object" && utils?.throwValidationErrors.overrideErrorMessage ? utils?.throwValidationErrors.overrideErrorMessage : void 0; throw new ActionValidationError( middlewareResult.validationErrors, await overrideErrorMessageFn?.(middlewareResult.validationErrors) ); } else actionResult.validationErrors = middlewareResult.validationErrors; if (typeof middlewareResult.serverError !== "undefined") if (utils?.throwServerError) throw middlewareResult.serverError; else actionResult.serverError = middlewareResult.serverError; if (middlewareResult.success) { if (typeof middlewareResult.data !== "undefined") actionResult.data = middlewareResult.data; callbackPromises.push( utils?.onSuccess?.({ metadata: args.metadata, ctx: currentCtx, data: actionResult.data, clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], parsedInput: parsedInputDatas.at(-1), bindArgsParsedInputs: parsedInputDatas.slice(0, -1), }) ); } else callbackPromises.push( utils?.onError?.({ metadata: args.metadata, ctx: currentCtx, clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], error: actionResult, }) ); callbackPromises.push( utils?.onSettled?.({ metadata: args.metadata, ctx: currentCtx, clientInput: clientInputs.at(-1), bindArgsClientInputs: bindArgsSchemas.length ? clientInputs.slice(0, -1) : [], result: actionResult, }) ); await Promise.all(callbackPromises); return actionResult; }; }, }; } return { action: buildAction({ withState: false }).action, stateAction: buildAction({ withState: true }).action, }; } //#endregion //#region src/safe-action-client.ts var SafeActionClient = class SafeActionClient { #args; constructor(args) { this.#args = args; } /** * Use a middleware function. * @param middlewareFn Middleware function * * {@link https://next-safe-action.dev/docs/define-actions/instance-methods#use See docs for more information} */ use(middlewareFn) { return new SafeActionClient({ ...this.#args, middlewareFns: [...this.#args.middlewareFns, middlewareFn], ctxType: {}, }); } /** * Define metadata for the action. * @param data Metadata with the same type as the return value of the [`defineMetadataSchema`](https://next-safe-action.dev/docs/define-actions/create-the-client#definemetadataschema) optional initialization function * * {@link https://next-safe-action.dev/docs/define-actions/instance-methods#metadata See docs for more information} */ metadata(data) { return new SafeActionClient({ ...this.#args, metadata: data, metadataProvided: true, }); } /** * Define the input validation schema for the action. * @param inputSchema Input validation schema * @param utils Optional utils object * * {@link https://next-safe-action.dev/docs/define-actions/create-the-client#inputschema See docs for more information} */ inputSchema(inputSchema, utils) { const isDirectStandardSchema = isStandardSchema(inputSchema); const isInputSchemaFactoryFn = !isDirectStandardSchema && typeof inputSchema === "function" && Object.prototype.toString.call(inputSchema) === "[object AsyncFunction]"; if (!isDirectStandardSchema && typeof inputSchema === "function" && !isInputSchemaFactoryFn) throw new TypeError( "`inputSchema()` received a function that is not a Standard Schema validator. Pass a Standard Schema validator (`~standard.validate`) directly, or use an async function to build/extend the schema." ); return new SafeActionClient({ ...this.#args, inputSchemaFn: isInputSchemaFactoryFn ? async (clientInput) => { return inputSchema(await this.#args.inputSchemaFn?.(clientInput), { clientInput }); } : async () => inputSchema, handleValidationErrorsShape: utils?.handleValidationErrorsShape ?? this.#args.handleValidationErrorsShape, }); } /** * @deprecated Alias for `inputSchema` method. Use that instead. */ schema = this.inputSchema; /** * Define the bind args input validation schema for the action. * @param bindArgsSchemas Bind args input validation schemas * * {@link https://next-safe-action.dev/docs/define-actions/instance-methods#bindargsschemas See docs for more information} */ bindArgsSchemas(bindArgsSchemas) { return new SafeActionClient({ ...this.#args, bindArgsSchemas, handleValidationErrorsShape: this.#args.handleValidationErrorsShape, }); } /** * Define the output data validation schema for the action. * @param schema Output data validation schema * * {@link https://next-safe-action.dev/docs/define-actions/create-the-client#outputschema See docs for more information} */ outputSchema(dataSchema) { return new SafeActionClient({ ...this.#args, outputSchema: dataSchema, }); } /** * Define the action. * @param serverCodeFn Code that will be executed on the **server side** * @param [cb] Optional callbacks that will be called after action execution, on the server. * * {@link https://next-safe-action.dev/docs/define-actions/instance-methods#action--stateaction See docs for more information} */ action(serverCodeFn, utils) { return actionBuilder(this.#args).action(serverCodeFn, utils); } /** * Define the stateful action. * To be used with the [`useStateAction`](https://next-safe-action.dev/docs/execute-actions/hooks/usestateaction) hook. * @param serverCodeFn Code that will be executed on the **server side** * @param [cb] Optional callbacks that will be called after action execution, on the server. * * {@link https://next-safe-action.dev/docs/define-actions/instance-methods#action--stateaction See docs for more information} */ stateAction(serverCodeFn, utils) { return actionBuilder(this.#args).stateAction(serverCodeFn, utils); } }; //#endregion //#region src/middleware.ts /** * Creates a standalone middleware function. It accepts a generic object with optional `serverError`, `ctx` and `metadata` * properties, if you need one or all of them to be typed. The type for each property that is passed as generic is the * **minimum** shape required to define the middleware function, but it can also be larger than that. * * {@link https://next-safe-action.dev/docs/define-actions/middleware#create-standalone-middleware See docs for more information} */ const createMiddleware = () => { return { define: (middlewareFn) => middlewareFn }; }; //#endregion //#region src/index.ts /** * Create a new safe action client. * Note: this client only works with Zod as the validation library. * @param createOpts Initialization options * * {@link https://next-safe-action.dev/docs/define-actions/create-the-client#initialization-options See docs for more information} */ const createSafeActionClient = (createOpts) => { return new SafeActionClient({ middlewareFns: [async ({ next }) => next({ ctx: {} })], handleServerError: createOpts?.handleServerError || ((e) => { console.error("Action error:", e.message); return DEFAULT_SERVER_ERROR_MESSAGE; }), inputSchemaFn: void 0, bindArgsSchemas: [], outputSchema: void 0, ctxType: {}, metadataSchema: createOpts?.defineMetadataSchema?.() ?? void 0, metadata: void 0, defaultValidationErrorsShape: createOpts?.defaultValidationErrorsShape ?? "formatted", throwValidationErrors: Boolean(createOpts?.throwValidationErrors), handleValidationErrorsShape: async (ve) => createOpts?.defaultValidationErrorsShape === "flattened" ? flattenValidationErrors(ve) : formatValidationErrors(ve), }); }; //#endregion export { ActionBindArgsValidationError, ActionMetadataValidationError, ActionOutputDataValidationError, ActionValidationError, DEFAULT_SERVER_ERROR_MESSAGE, createMiddleware, createSafeActionClient, flattenValidationErrors, formatValidationErrors, returnValidationErrors, }; //# sourceMappingURL=index.mjs.map