next-safe-action
Version:
Type safe and validated Server Actions in your Next.js project.
544 lines (535 loc) • 20.8 kB
JavaScript
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