Files
moreminimore-new/node_modules/astro/dist/actions/runtime/server.js

380 lines
13 KiB
JavaScript

import { stringify as devalueStringify } from "devalue";
import * as z from "zod/v4/core";
import { shouldAppendForwardSlash } from "../../core/build/util.js";
import { pipelineSymbol, REDIRECT_STATUS_CODES } from "../../core/constants.js";
import {
ActionCalledFromServerError,
ActionNotFoundError,
ActionsReturnedInvalidDataError
} from "../../core/errors/errors-data.js";
import { AstroError } from "../../core/errors/errors.js";
import { removeTrailingForwardSlash } from "../../core/path.js";
import { BodySizeLimitError, readBodyWithLimit } from "../../core/request-body.js";
import { ACTION_QUERY_PARAMS, ACTION_RPC_ROUTE_PATTERN } from "../consts.js";
import {
ActionError,
ActionInputError,
actionResultErrorStack,
deserializeActionResult
} from "./client.js";
function defineAction({
accept,
input: inputSchema,
handler
}) {
const serverHandler = accept === "form" ? getFormServerHandler(handler, inputSchema) : getJsonServerHandler(handler, inputSchema);
async function safeServerHandler(unparsedInput) {
if (typeof this === "function" || !isActionAPIContext(this)) {
throw new AstroError(ActionCalledFromServerError);
}
return callSafely(() => serverHandler(unparsedInput, this));
}
Object.assign(safeServerHandler, {
orThrow(unparsedInput) {
if (typeof this === "function") {
throw new AstroError(ActionCalledFromServerError);
}
return serverHandler(unparsedInput, this);
}
});
return safeServerHandler;
}
function getFormServerHandler(handler, inputSchema) {
return async (unparsedInput, context) => {
if (!(unparsedInput instanceof FormData)) {
throw new ActionError({
code: "UNSUPPORTED_MEDIA_TYPE",
message: "This action only accepts FormData."
});
}
if (!inputSchema) return await handler(unparsedInput, context);
const parsed = await parseFormInput(inputSchema, unparsedInput);
if (!parsed.success) {
throw new ActionInputError(parsed.error.issues);
}
return await handler(parsed.data, context);
};
}
async function parseFormInput(inputSchema, unparsedInput) {
const baseSchema = unwrapBaseZ4ObjectSchema(inputSchema, unparsedInput);
const input = baseSchema instanceof z.$ZodObject ? formDataToObject(unparsedInput, baseSchema) : unparsedInput;
const parsed = await z.safeParseAsync(inputSchema, input);
return parsed;
}
function getJsonServerHandler(handler, inputSchema) {
return async (unparsedInput, context) => {
if (unparsedInput instanceof FormData) {
throw new ActionError({
code: "UNSUPPORTED_MEDIA_TYPE",
message: "This action only accepts JSON."
});
}
if (!inputSchema) return await handler(unparsedInput, context);
const parsed = await z.safeParseAsync(inputSchema, unparsedInput);
if (!parsed.success) {
throw new ActionInputError(parsed.error.issues);
}
return await handler(parsed.data, context);
};
}
function getActionContext(context) {
const callerInfo = getCallerInfo(context);
const actionResultAlreadySet = Boolean(context.locals._actionPayload);
let action = void 0;
if (callerInfo && context.request.method === "POST" && !actionResultAlreadySet) {
action = {
calledFrom: callerInfo.from,
name: callerInfo.name,
handler: async () => {
const pipeline = Reflect.get(context, pipelineSymbol);
const callerInfoName = shouldAppendForwardSlash(
pipeline.manifest.trailingSlash,
pipeline.manifest.buildFormat
) ? removeTrailingForwardSlash(callerInfo.name) : callerInfo.name;
let baseAction;
try {
baseAction = await pipeline.getAction(callerInfoName);
} catch (error) {
if (error instanceof Error && "name" in error && typeof error.name === "string" && error.name === ActionNotFoundError.name) {
return { data: void 0, error: new ActionError({ code: "NOT_FOUND" }) };
}
throw error;
}
const bodySizeLimit = pipeline.manifest.actionBodySizeLimit;
let input;
try {
input = await parseRequestBody(context.request, bodySizeLimit);
} catch (e) {
if (e instanceof ActionError) {
return { data: void 0, error: e };
}
if (e instanceof TypeError) {
return { data: void 0, error: new ActionError({ code: "UNSUPPORTED_MEDIA_TYPE" }) };
}
throw e;
}
const omitKeys = ["props", "getActionResult", "callAction", "redirect"];
const actionAPIContext = Object.create(
Object.getPrototypeOf(context),
Object.fromEntries(
Object.entries(Object.getOwnPropertyDescriptors(context)).filter(
([key]) => !omitKeys.includes(key)
)
)
);
Reflect.set(actionAPIContext, ACTION_API_CONTEXT_SYMBOL, true);
const handler = baseAction.bind(actionAPIContext);
return handler(input);
}
};
}
function setActionResult(actionName, actionResult) {
context.locals._actionPayload = {
actionResult,
actionName
};
}
return {
action,
setActionResult,
serializeActionResult,
deserializeActionResult
};
}
function getCallerInfo(ctx) {
if (ctx.routePattern === ACTION_RPC_ROUTE_PATTERN) {
return { from: "rpc", name: ctx.url.pathname.replace(/^.*\/_actions\//, "") };
}
const queryParam = ctx.url.searchParams.get(ACTION_QUERY_PARAMS.actionName);
if (queryParam) {
return { from: "form", name: queryParam };
}
return void 0;
}
async function parseRequestBody(request, bodySizeLimit) {
const contentType = request.headers.get("content-type");
const contentLengthHeader = request.headers.get("content-length");
const contentLength = contentLengthHeader ? Number.parseInt(contentLengthHeader, 10) : void 0;
const hasContentLength = typeof contentLength === "number" && Number.isFinite(contentLength);
if (!contentType) return void 0;
if (hasContentLength && contentLength > bodySizeLimit) {
throw new ActionError({
code: "CONTENT_TOO_LARGE",
message: `Request body exceeds ${bodySizeLimit} bytes`
});
}
try {
if (hasContentType(contentType, formContentTypes)) {
if (!hasContentLength) {
const body = await readBodyWithLimit(request.clone(), bodySizeLimit);
const formRequest = new Request(request.url, {
method: request.method,
headers: request.headers,
body: toArrayBuffer(body)
});
return await formRequest.formData();
}
return await request.clone().formData();
}
if (hasContentType(contentType, ["application/json"])) {
if (contentLength === 0) return void 0;
if (!hasContentLength) {
const body = await readBodyWithLimit(request.clone(), bodySizeLimit);
if (body.byteLength === 0) return void 0;
return JSON.parse(new TextDecoder().decode(body));
}
return await request.clone().json();
}
} catch (e) {
if (e instanceof BodySizeLimitError) {
throw new ActionError({
code: "CONTENT_TOO_LARGE",
message: `Request body exceeds ${bodySizeLimit} bytes`
});
}
throw e;
}
throw new TypeError("Unsupported content type");
}
const ACTION_API_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("astro.actionAPIContext");
const formContentTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
function hasContentType(contentType, expected) {
const type = contentType.split(";")[0].toLowerCase();
return expected.some((t) => type === t);
}
function isActionAPIContext(ctx) {
const symbol = Reflect.get(ctx, ACTION_API_CONTEXT_SYMBOL);
return symbol === true;
}
function formDataToObject(formData, schema, prefix = "") {
const formKeys = [...formData.keys()];
const obj = schema._zod.def.catchall ? Object.fromEntries(
[...formData.entries()].filter(([k]) => k.startsWith(prefix)).map(([k, v]) => [k.slice(prefix.length), v])
) : {};
for (const [key, baseValidator] of Object.entries(schema._zod.def.shape)) {
const prefixedKey = prefix + key;
let validator = baseValidator;
while (validator instanceof z.$ZodOptional || validator instanceof z.$ZodNullable || validator instanceof z.$ZodDefault) {
if (validator instanceof z.$ZodDefault && !formDataHasKeyOrPrefix(formKeys, prefixedKey)) {
obj[key] = validator._zod.def.defaultValue instanceof Function ? validator._zod.def.defaultValue() : validator._zod.def.defaultValue;
}
validator = validator._zod.def.innerType;
}
while (validator instanceof z.$ZodPipe) {
validator = validator._zod.def.in;
}
if (validator instanceof z.$ZodDiscriminatedUnion) {
const typeKey = validator._zod.def.discriminator;
const typeValue = formData.get(prefixedKey + "." + typeKey);
if (typeof typeValue === "string") {
const match = validator._zod.def.options.find(
(option) => option.def.shape[typeKey].values.has(typeValue)
);
if (match) {
validator = match;
}
}
}
if (validator instanceof z.$ZodObject) {
const nestedPrefix = prefixedKey + ".";
const hasNestedKeys = formKeys.some((k) => k.startsWith(nestedPrefix));
if (hasNestedKeys) {
obj[key] = formDataToObject(formData, validator, nestedPrefix);
} else if (!(key in obj)) {
obj[key] = baseValidator instanceof z.$ZodNullable ? null : void 0;
}
} else if (!formData.has(prefixedKey) && key in obj) {
continue;
} else if (validator instanceof z.$ZodBoolean) {
const val = formData.get(prefixedKey);
obj[key] = val === "true" ? true : val === "false" ? false : formData.has(prefixedKey);
} else if (validator instanceof z.$ZodArray) {
obj[key] = handleFormDataGetAll(prefixedKey, formData, validator);
} else {
obj[key] = handleFormDataGet(prefixedKey, formData, validator, baseValidator);
}
}
return obj;
}
function formDataHasKeyOrPrefix(formKeys, key) {
const prefix = key + ".";
return formKeys.some((k) => k === key || k.startsWith(prefix));
}
function handleFormDataGetAll(key, formData, validator) {
const entries = Array.from(formData.getAll(key));
const elementValidator = validator._zod.def.element;
if (elementValidator instanceof z.$ZodNumber) {
return entries.map(Number);
} else if (elementValidator instanceof z.$ZodBoolean) {
return entries.map(Boolean);
}
return entries;
}
function handleFormDataGet(key, formData, validator, baseValidator) {
const value = formData.get(key);
if (!value) {
return baseValidator instanceof z.$ZodOptional ? void 0 : null;
}
return validator instanceof z.$ZodNumber ? Number(value) : value;
}
function unwrapBaseZ4ObjectSchema(schema, unparsedInput) {
if (schema instanceof z.$ZodPipe) {
return unwrapBaseZ4ObjectSchema(schema._zod.def.in, unparsedInput);
}
if (schema instanceof z.$ZodDiscriminatedUnion) {
const typeKey = schema._zod.def.discriminator;
const typeValue = unparsedInput.get(typeKey);
if (typeof typeValue !== "string") return schema;
const objSchema = schema._zod.def.options.find(
(option) => option.def.shape[typeKey].values.has(typeValue)
);
if (!objSchema) return schema;
return objSchema;
}
return schema;
}
async function callSafely(handler) {
try {
const data = await handler();
return { data, error: void 0 };
} catch (e) {
if (e instanceof ActionError) {
return { data: void 0, error: e };
}
return {
data: void 0,
error: new ActionError({
message: e instanceof Error ? e.message : "Unknown error",
code: "INTERNAL_SERVER_ERROR"
})
};
}
}
function serializeActionResult(res) {
if (res.error) {
if (import.meta.env?.DEV) {
actionResultErrorStack.set(res.error.stack);
}
let body2;
if (res.error instanceof ActionInputError) {
body2 = {
type: res.error.type,
issues: res.error.issues,
fields: res.error.fields
};
} else {
body2 = {
...res.error,
message: res.error.message
};
}
return {
type: "error",
status: res.error.status,
contentType: "application/json",
body: JSON.stringify(body2)
};
}
if (res.data === void 0) {
return {
type: "empty",
status: 204
};
}
let body;
try {
body = devalueStringify(res.data, {
// Add support for URL objects
URL: (value) => value instanceof URL && value.href
});
} catch (e) {
let hint = ActionsReturnedInvalidDataError.hint;
if (res.data instanceof Response) {
hint = REDIRECT_STATUS_CODES.includes(res.data.status) ? "If you need to redirect when the action succeeds, trigger a redirect where the action is called. See the Actions guide for server and client redirect examples: https://docs.astro.build/en/guides/actions." : "If you need to return a Response object, try using a server endpoint instead. See https://docs.astro.build/en/guides/endpoints/#server-endpoints-api-routes";
}
throw new AstroError({
...ActionsReturnedInvalidDataError,
message: ActionsReturnedInvalidDataError.message(String(e)),
hint
});
}
return {
type: "data",
status: 200,
contentType: "application/json+devalue",
body
};
}
function toArrayBuffer(buffer) {
const copy = new Uint8Array(buffer.byteLength);
copy.set(buffer);
return copy.buffer;
}
export {
ACTION_API_CONTEXT_SYMBOL,
defineAction,
formDataToObject,
getActionContext,
serializeActionResult
};