UNPKG

next-action-forge

Version:

A simple, type-safe toolkit for Next.js server actions with Zod validation

422 lines (416 loc) 15.1 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __typeError = (msg) => { throw TypeError(msg); }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value); // src/index.ts var src_exports = {}; __export(src_exports, { ServerActionClient: () => ServerActionClient, createActionClient: () => createActionClient, handleServerActionError: () => handleServerActionError, isErrorResponse: () => isErrorResponse, isHTTPAccessFallbackError: () => isHTTPAccessFallbackError, isNextNavigationError: () => isNextNavigationError, isNotFoundError: () => isNotFoundError, isRedirectError: () => isRedirectError }); module.exports = __toCommonJS(src_exports); // src/core/error-handler.ts var import_zod = require("zod"); function convertZodError(error) { const { fieldErrors, formErrors } = import_zod.z.flattenError(error); const [field] = Object.keys(fieldErrors); const message = (field && fieldErrors[field]?.[0]) ?? formErrors[0] ?? "Validation failed"; return { message, code: "VALIDATION_ERROR", field, fields: fieldErrors }; } function convertGenericError(error) { const message = error instanceof Error ? error.message : "An unexpected error occurred"; return { message: process.env.NODE_ENV === "production" ? "An unexpected error occurred" : message, code: "INTERNAL_ERROR", statusCode: 500 }; } function handleServerActionError(error, customHandler) { if (process.env.NODE_ENV === "development") { console.error("[Server Action Error]", error?.constructor?.name || "Unknown", error instanceof Error ? error.message : error); } if (customHandler) { try { const customError = customHandler(error); if (customError) { return { success: false, error: customError }; } } catch (handlerError) { console.error("[Server Action Error] Custom handler threw an error:", handlerError); } } if (error instanceof import_zod.ZodError) { return { success: false, error: convertZodError(error) }; } if (error instanceof Error && "toServerActionError" in error && typeof error.toServerActionError === "function") { const serverError = error.toServerActionError(); if (serverError.code === "AUTHENTICATION_ERROR" || error.type === "AUTHENTICATION_ERROR") { serverError.shouldRedirect = true; serverError.redirectTo = "/login"; } return { success: false, error: serverError }; } return { success: false, error: convertGenericError(error) }; } function isErrorResponse(response) { return !response.success; } // src/next/errors/redirect.ts var RedirectStatusCode = /* @__PURE__ */ ((RedirectStatusCode2) => { RedirectStatusCode2[RedirectStatusCode2["SeeOther"] = 303] = "SeeOther"; RedirectStatusCode2[RedirectStatusCode2["TemporaryRedirect"] = 307] = "TemporaryRedirect"; RedirectStatusCode2[RedirectStatusCode2["PermanentRedirect"] = 308] = "PermanentRedirect"; return RedirectStatusCode2; })(RedirectStatusCode || {}); var REDIRECT_ERROR_CODE = "NEXT_REDIRECT"; function isRedirectError(error) { if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") { return false; } const digest = error.digest.split(";"); const [errorCode, type] = digest; const destination = digest.slice(2, -2).join(";"); const status = digest.at(-2); const statusCode = Number(status); return errorCode === REDIRECT_ERROR_CODE && (type === "replace" || type === "push") && typeof destination === "string" && !isNaN(statusCode) && statusCode in RedirectStatusCode; } // src/next/errors/http-access-fallback.ts var HTTPAccessErrorStatus = { NOT_FOUND: 404, FORBIDDEN: 403, UNAUTHORIZED: 401 }; var ALLOWED_CODES = new Set(Object.values(HTTPAccessErrorStatus)); var HTTP_ERROR_FALLBACK_ERROR_CODE = "NEXT_HTTP_ERROR_FALLBACK"; function isHTTPAccessFallbackError(error) { if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") { return false; } const [prefix, httpStatus] = error.digest.split(";"); return prefix === HTTP_ERROR_FALLBACK_ERROR_CODE && ALLOWED_CODES.has(Number(httpStatus)); } function getAccessFallbackHTTPStatus(error) { const httpStatus = error.digest.split(";")[1]; return Number(httpStatus); } // src/next/errors/index.ts function isNextNavigationError(error) { return isRedirectError(error) || isHTTPAccessFallbackError(error); } function isNotFoundError(error) { return isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 404; } // src/core/server-action-client.ts var _args; var _ServerActionClient = class _ServerActionClient { constructor(args = { middlewareFns: [] }) { __privateAdd(this, _args); __privateSet(this, _args, args); } /** * Use a middleware function. * @param middlewareFn Middleware function */ use(middlewareFn) { return new _ServerActionClient({ ...__privateGet(this, _args), middlewareFns: [...__privateGet(this, _args).middlewareFns, middlewareFn], ctxType: {} }); } /** * Define the input validation schema for the action. * @param schema Input validation schema */ inputSchema(schema) { return new _ServerActionClient({ ...__privateGet(this, _args), inputSchema: schema }); } /** * Define the output validation schema for the action. * @param schema Output validation schema */ outputSchema(schema) { return new _ServerActionClient({ ...__privateGet(this, _args), outputSchema: schema }); } /** * Define a custom error handler for the action. * @param handler Error handler function */ onError(handler) { return new _ServerActionClient({ ...__privateGet(this, _args), onError: handler }); } /** * Define a redirect configuration for successful actions. * @param config Redirect URL, config object, or function that returns redirect config based on result */ redirect(config) { return new _ServerActionClient({ ...__privateGet(this, _args), redirectConfig: config }); } /** * Define the action. * @param serverCodeFn Code that will be executed on the server side */ action(serverCodeFn) { return createServerAction({ inputSchema: __privateGet(this, _args).inputSchema, outputSchema: __privateGet(this, _args).outputSchema, middlewareFns: __privateGet(this, _args).middlewareFns, onError: __privateGet(this, _args).onError, serverCodeFn, redirectConfig: __privateGet(this, _args).redirectConfig }); } /** * Define a form action that accepts FormData. * @param serverCodeFn Code that will be executed on the server side */ formAction(serverCodeFn) { return createFormServerAction({ inputSchema: __privateGet(this, _args).inputSchema, outputSchema: __privateGet(this, _args).outputSchema, middlewareFns: __privateGet(this, _args).middlewareFns, onError: __privateGet(this, _args).onError, serverCodeFn, redirectConfig: __privateGet(this, _args).redirectConfig }); } }; _args = new WeakMap(); var ServerActionClient = _ServerActionClient; function createServerAction(config) { const actionFn = async (...args) => { const context = {}; const hasInputSchema = config.inputSchema !== void 0 && config.inputSchema !== null; const input = hasInputSchema ? args[0] : void 0; try { let parsedInput = void 0; if (hasInputSchema && config.inputSchema) { const parseResult = config.inputSchema.safeParse(input); if (!parseResult.success) { return handleServerActionError(parseResult.error, config.onError ? (err) => config.onError(err, { parsedInput: input }) : void 0); } parsedInput = parseResult.data; context.parsedInput = parsedInput; } let middlewareContext = {}; for (const middleware of config.middlewareFns) { const middlewareInput = hasInputSchema ? parsedInput : void 0; const result2 = await middleware({ context: middlewareContext, input: middlewareInput }); if ("error" in result2) { return { success: false, error: result2.error }; } middlewareContext = { ...middlewareContext, ...result2.context }; } const result = hasInputSchema ? await config.serverCodeFn(parsedInput, middlewareContext) : await config.serverCodeFn(middlewareContext); let validatedOutput = result; if (config.outputSchema) { const parseResult = config.outputSchema.safeParse(result); if (!parseResult.success) { return handleServerActionError(parseResult.error); } validatedOutput = parseResult.data; } let redirectValue; if (config.redirectConfig) { if (typeof config.redirectConfig === "function") { redirectValue = config.redirectConfig(validatedOutput); } else { redirectValue = config.redirectConfig; } } return { success: true, data: validatedOutput, ...redirectValue && { redirect: redirectValue } }; } catch (error) { if (isNextNavigationError(error)) { throw error; } if (process.env.NODE_ENV === "development") { console.log("[executeAction] Error:", error?.constructor?.name, error instanceof Error ? error.message : error); } const errorHandler = config.onError ? (err) => { return config.onError(err, { parsedInput: context.parsedInput }); } : void 0; return handleServerActionError(error, errorHandler); } }; actionFn.__hasInputSchema = config.inputSchema !== void 0 && config.inputSchema !== null; return actionFn; } function tryParseJSON(value) { if (typeof value !== "string") return value; const trimmed = value.trim(); if (!trimmed) return value; if (trimmed === "true") return true; if (trimmed === "false") return false; if (trimmed === "null") return null; if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) { try { const parsed = JSON.parse(trimmed); if (typeof parsed === "object" && parsed !== null) { return parsed; } } catch { } } return value; } function parseFormData(formData) { const result = {}; const keys = /* @__PURE__ */ new Set(); for (const [key] of formData.entries()) { keys.add(key); } for (const key of keys) { const values = formData.getAll(key); if (key.endsWith("[]")) { const cleanKey = key.slice(0, -2); result[cleanKey] = values.map(tryParseJSON); } else if (values.length === 1) { result[key] = tryParseJSON(values[0]); } else { result[key] = values.map(tryParseJSON); } } return result; } function createFormServerAction(config) { return async (_prev, formData) => { const context = {}; const hasInputSchema = config.inputSchema !== void 0 && config.inputSchema !== null; try { let parsedInput = parseFormData(formData); if (config.inputSchema) { const parseResult = config.inputSchema.safeParse(parsedInput); if (!parseResult.success) { return handleServerActionError(parseResult.error, config.onError ? (err) => config.onError(err, { parsedInput }) : void 0); } parsedInput = parseResult.data; context.parsedInput = parsedInput; } let middlewareContext = {}; for (const middleware of config.middlewareFns) { const middlewareInput = hasInputSchema ? parsedInput : void 0; const result2 = await middleware({ context: middlewareContext, input: middlewareInput }); if ("error" in result2) { return { success: false, error: result2.error }; } middlewareContext = { ...middlewareContext, ...result2.context }; } const result = hasInputSchema ? await config.serverCodeFn(parsedInput, middlewareContext) : await config.serverCodeFn(middlewareContext); let validatedOutput = result; if (config.outputSchema) { const parseResult = config.outputSchema.safeParse(result); if (!parseResult.success) { return handleServerActionError(parseResult.error); } validatedOutput = parseResult.data; } let redirectValue; if (config.redirectConfig) { if (typeof config.redirectConfig === "function") { redirectValue = config.redirectConfig(validatedOutput); } else { redirectValue = config.redirectConfig; } } return { success: true, data: validatedOutput, ...redirectValue && { redirect: redirectValue } }; } catch (error) { if (isNextNavigationError(error)) { throw error; } if (process.env.NODE_ENV === "development") { console.log("[ServerActionClient] Error:", error?.constructor?.name, error instanceof Error ? error.message : error); } const errorHandler = config.onError ? (err) => { return config.onError(err, { parsedInput: context.parsedInput }); } : void 0; return handleServerActionError(error, errorHandler); } }; } function createActionClient() { return new ServerActionClient(); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ServerActionClient, createActionClient, handleServerActionError, isErrorResponse, isHTTPAccessFallbackError, isNextNavigationError, isNotFoundError, isRedirectError }); //# sourceMappingURL=index.js.map