astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
217 lines (216 loc) • 7.62 kB
JavaScript
import { z } from "zod";
import { shouldAppendForwardSlash } from "../../../core/build/util.js";
import { ActionCalledFromServerError } from "../../../core/errors/errors-data.js";
import { AstroError } from "../../../core/errors/errors.js";
import { removeTrailingForwardSlash } from "../../../core/path.js";
import { apiContextRoutesSymbol } from "../../../core/render-context.js";
import { ACTION_RPC_ROUTE_PATTERN } from "../../consts.js";
import {
ACTION_API_CONTEXT_SYMBOL,
formContentTypes,
hasContentType,
isActionAPIContext
} from "../utils.js";
import { getAction } from "./get-action.js";
import {
ACTION_QUERY_PARAMS,
ActionError,
ActionInputError,
callSafely,
deserializeActionResult,
serializeActionResult
} from "./shared.js";
export * from "./shared.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 baseSchema = unwrapBaseObjectSchema(inputSchema, unparsedInput);
const parsed = await inputSchema.safeParseAsync(
baseSchema instanceof z.ZodObject ? formDataToObject(unparsedInput, baseSchema) : unparsedInput
);
if (!parsed.success) {
throw new ActionInputError(parsed.error.issues);
}
return await handler(parsed.data, context);
};
}
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 inputSchema.safeParseAsync(unparsedInput);
if (!parsed.success) {
throw new ActionInputError(parsed.error.issues);
}
return await handler(parsed.data, context);
};
}
function formDataToObject(formData, schema) {
const obj = schema._def.unknownKeys === "passthrough" ? Object.fromEntries(formData.entries()) : {};
for (const [key, baseValidator] of Object.entries(schema.shape)) {
let validator = baseValidator;
while (validator instanceof z.ZodOptional || validator instanceof z.ZodNullable || validator instanceof z.ZodDefault) {
if (validator instanceof z.ZodDefault && !formData.has(key)) {
obj[key] = validator._def.defaultValue();
}
validator = validator._def.innerType;
}
if (!formData.has(key) && key in obj) {
continue;
} else if (validator instanceof z.ZodBoolean) {
const val = formData.get(key);
obj[key] = val === "true" ? true : val === "false" ? false : formData.has(key);
} else if (validator instanceof z.ZodArray) {
obj[key] = handleFormDataGetAll(key, formData, validator);
} else {
obj[key] = handleFormDataGet(key, formData, validator, baseValidator);
}
}
return obj;
}
function handleFormDataGetAll(key, formData, validator) {
const entries = Array.from(formData.getAll(key));
const elementValidator = validator._def.type;
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 unwrapBaseObjectSchema(schema, unparsedInput) {
while (schema instanceof z.ZodEffects || schema instanceof z.ZodPipeline) {
if (schema instanceof z.ZodEffects) {
schema = schema._def.schema;
}
if (schema instanceof z.ZodPipeline) {
schema = schema._def.in;
}
}
if (schema instanceof z.ZodDiscriminatedUnion) {
const typeKey = schema._def.discriminator;
const typeValue = unparsedInput.get(typeKey);
if (typeof typeValue !== "string") return schema;
const objSchema = schema._def.optionsMap.get(typeValue);
if (!objSchema) return schema;
return objSchema;
}
return schema;
}
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, apiContextRoutesSymbol);
const callerInfoName = shouldAppendForwardSlash(
pipeline.manifest.trailingSlash,
pipeline.manifest.buildFormat
) ? removeTrailingForwardSlash(callerInfo.name) : callerInfo.name;
const baseAction = await getAction(callerInfoName);
let input;
try {
input = await parseRequestBody(context.request);
} catch (e) {
if (e instanceof TypeError) {
return { data: void 0, error: new ActionError({ code: "UNSUPPORTED_MEDIA_TYPE" }) };
}
throw e;
}
const {
props: _props,
getActionResult: _getActionResult,
callAction: _callAction,
redirect: _redirect,
...actionAPIContext
} = context;
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) {
const contentType = request.headers.get("content-type");
const contentLength = request.headers.get("Content-Length");
if (!contentType) return void 0;
if (hasContentType(contentType, formContentTypes)) {
return await request.clone().formData();
}
if (hasContentType(contentType, ["application/json"])) {
return contentLength === "0" ? void 0 : await request.clone().json();
}
throw new TypeError("Unsupported content type");
}
export {
defineAction,
formDataToObject,
getActionContext
};