241 lines
6.8 KiB
JavaScript
241 lines
6.8 KiB
JavaScript
import { parse as devalueParse } from "devalue";
|
|
import { ACTION_QUERY_PARAMS } from "../consts.js";
|
|
import { appendForwardSlash } from "../../core/path.js";
|
|
const codeToStatusMap = {
|
|
// Implemented from IANA HTTP Status Code Registry
|
|
// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
|
BAD_REQUEST: 400,
|
|
UNAUTHORIZED: 401,
|
|
PAYMENT_REQUIRED: 402,
|
|
FORBIDDEN: 403,
|
|
NOT_FOUND: 404,
|
|
METHOD_NOT_ALLOWED: 405,
|
|
NOT_ACCEPTABLE: 406,
|
|
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
REQUEST_TIMEOUT: 408,
|
|
CONFLICT: 409,
|
|
GONE: 410,
|
|
LENGTH_REQUIRED: 411,
|
|
PRECONDITION_FAILED: 412,
|
|
CONTENT_TOO_LARGE: 413,
|
|
URI_TOO_LONG: 414,
|
|
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
RANGE_NOT_SATISFIABLE: 416,
|
|
EXPECTATION_FAILED: 417,
|
|
MISDIRECTED_REQUEST: 421,
|
|
UNPROCESSABLE_CONTENT: 422,
|
|
LOCKED: 423,
|
|
FAILED_DEPENDENCY: 424,
|
|
TOO_EARLY: 425,
|
|
UPGRADE_REQUIRED: 426,
|
|
PRECONDITION_REQUIRED: 428,
|
|
TOO_MANY_REQUESTS: 429,
|
|
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
|
|
INTERNAL_SERVER_ERROR: 500,
|
|
NOT_IMPLEMENTED: 501,
|
|
BAD_GATEWAY: 502,
|
|
SERVICE_UNAVAILABLE: 503,
|
|
GATEWAY_TIMEOUT: 504,
|
|
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
VARIANT_ALSO_NEGOTIATES: 506,
|
|
INSUFFICIENT_STORAGE: 507,
|
|
LOOP_DETECTED: 508,
|
|
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
};
|
|
const statusToCodeMap = Object.fromEntries(
|
|
Object.entries(codeToStatusMap).map(([key, value]) => [value, key])
|
|
);
|
|
class ActionError extends Error {
|
|
type = "AstroActionError";
|
|
code = "INTERNAL_SERVER_ERROR";
|
|
status = 500;
|
|
constructor(params) {
|
|
super(params.message);
|
|
this.code = params.code;
|
|
this.status = ActionError.codeToStatus(params.code);
|
|
if (params.stack) {
|
|
this.stack = params.stack;
|
|
}
|
|
}
|
|
static codeToStatus(code) {
|
|
return codeToStatusMap[code];
|
|
}
|
|
static statusToCode(status) {
|
|
return statusToCodeMap[status] ?? "INTERNAL_SERVER_ERROR";
|
|
}
|
|
static fromJson(body) {
|
|
if (isInputError(body)) {
|
|
return new ActionInputError(body.issues);
|
|
}
|
|
if (isActionError(body)) {
|
|
return new ActionError(body);
|
|
}
|
|
return new ActionError({
|
|
code: "INTERNAL_SERVER_ERROR"
|
|
});
|
|
}
|
|
}
|
|
function isActionError(error) {
|
|
return typeof error === "object" && error != null && "type" in error && error.type === "AstroActionError";
|
|
}
|
|
function isInputError(error) {
|
|
return typeof error === "object" && error != null && "type" in error && error.type === "AstroActionInputError" && "issues" in error && Array.isArray(error.issues);
|
|
}
|
|
class ActionInputError extends ActionError {
|
|
type = "AstroActionInputError";
|
|
// We don't expose all ZodError properties.
|
|
// Not all properties will serialize from server to client,
|
|
// and we don't want to import the full ZodError object into the client.
|
|
issues;
|
|
fields;
|
|
constructor(issues) {
|
|
super({
|
|
message: `Failed to validate: ${JSON.stringify(issues, null, 2)}`,
|
|
code: "BAD_REQUEST"
|
|
});
|
|
this.issues = issues;
|
|
this.fields = {};
|
|
for (const issue of issues) {
|
|
if (issue.path.length > 0) {
|
|
const key = issue.path[0].toString();
|
|
this.fields[key] ??= [];
|
|
this.fields[key]?.push(issue.message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function deserializeActionResult(res) {
|
|
if (res.type === "error") {
|
|
let json;
|
|
try {
|
|
json = JSON.parse(res.body);
|
|
} catch {
|
|
return {
|
|
data: void 0,
|
|
error: new ActionError({
|
|
message: res.body,
|
|
code: "INTERNAL_SERVER_ERROR"
|
|
})
|
|
};
|
|
}
|
|
if (import.meta.env?.PROD) {
|
|
return { error: ActionError.fromJson(json), data: void 0 };
|
|
} else {
|
|
const error = ActionError.fromJson(json);
|
|
error.stack = actionResultErrorStack.get();
|
|
return {
|
|
error,
|
|
data: void 0
|
|
};
|
|
}
|
|
}
|
|
if (res.type === "empty") {
|
|
return { data: void 0, error: void 0 };
|
|
}
|
|
return {
|
|
data: devalueParse(res.body, {
|
|
URL: (href) => new URL(href)
|
|
}),
|
|
error: void 0
|
|
};
|
|
}
|
|
const actionResultErrorStack = /* @__PURE__ */ (function actionResultErrorStackFn() {
|
|
let errorStack;
|
|
return {
|
|
set(stack) {
|
|
errorStack = stack;
|
|
},
|
|
get() {
|
|
return errorStack;
|
|
}
|
|
};
|
|
})();
|
|
function getActionQueryString(name) {
|
|
const searchParams = new URLSearchParams({ [ACTION_QUERY_PARAMS.actionName]: name });
|
|
return `?${searchParams.toString()}`;
|
|
}
|
|
function getActionPathFromString({
|
|
baseUrl,
|
|
shouldAppendTrailingSlash,
|
|
path: input
|
|
}) {
|
|
let path = `${baseUrl.replace(/\/$/, "")}/_actions/${new URLSearchParams(input).get(ACTION_QUERY_PARAMS.actionName)}`;
|
|
if (shouldAppendTrailingSlash) {
|
|
path = appendForwardSlash(path);
|
|
}
|
|
return path;
|
|
}
|
|
function createGetActionPath(options) {
|
|
return function getActionPath(action) {
|
|
return getActionPathFromString({
|
|
baseUrl: options.baseUrl,
|
|
shouldAppendTrailingSlash: options.shouldAppendTrailingSlash,
|
|
path: action.toString()
|
|
});
|
|
};
|
|
}
|
|
const ENCODED_DOT = "%2E";
|
|
function createActionsProxy({
|
|
actionCallback = {},
|
|
aggregatedPath = "",
|
|
handleAction
|
|
}) {
|
|
return new Proxy(actionCallback, {
|
|
get(target, objKey) {
|
|
if (target.hasOwnProperty(objKey) || typeof objKey === "symbol") {
|
|
return target[objKey];
|
|
}
|
|
const path = aggregatedPath + encodeURIComponent(objKey.toString()).replaceAll(".", ENCODED_DOT);
|
|
function action(param) {
|
|
return handleAction(param, path, this);
|
|
}
|
|
Object.assign(action, {
|
|
queryString: getActionQueryString(path),
|
|
toString: () => action.queryString,
|
|
// redefine prototype methods as the object's own property, not the prototype's
|
|
bind: action.bind,
|
|
valueOf: () => action.valueOf,
|
|
// Progressive enhancement info for React.
|
|
$$FORM_ACTION: function() {
|
|
const searchParams = new URLSearchParams(action.toString());
|
|
return {
|
|
method: "POST",
|
|
// `name` creates a hidden input.
|
|
// It's unused by Astro, but we can't turn this off.
|
|
// At least use a name that won't conflict with a user's formData.
|
|
name: "_astroAction",
|
|
action: "?" + searchParams.toString()
|
|
};
|
|
},
|
|
// Note: `orThrow` does not have progressive enhancement info.
|
|
// If you want to throw exceptions,
|
|
// you must handle those exceptions with client JS.
|
|
async orThrow(param) {
|
|
const { data, error } = await handleAction(param, path, this);
|
|
if (error) throw error;
|
|
return data;
|
|
}
|
|
});
|
|
return createActionsProxy({
|
|
actionCallback: action,
|
|
aggregatedPath: path + ".",
|
|
handleAction
|
|
});
|
|
}
|
|
});
|
|
}
|
|
export {
|
|
ActionError,
|
|
ActionInputError,
|
|
actionResultErrorStack,
|
|
codeToStatusMap,
|
|
createActionsProxy,
|
|
createGetActionPath,
|
|
deserializeActionResult,
|
|
getActionPathFromString,
|
|
getActionQueryString,
|
|
isActionError,
|
|
isInputError
|
|
};
|