124 lines
3.9 KiB
JavaScript
124 lines
3.9 KiB
JavaScript
import { shouldAppendTrailingSlash } from "virtual:astro:actions/options";
|
|
import {
|
|
ACTION_QUERY_PARAMS,
|
|
ActionError,
|
|
appendForwardSlash,
|
|
astroCalledServerError,
|
|
deserializeActionResult,
|
|
getActionQueryString
|
|
} from "../actions/runtime/shared.js";
|
|
export * from "virtual:astro:actions/runtime";
|
|
const apiContextRoutesSymbol = Symbol.for("context.routes");
|
|
const ENCODED_DOT = "%2E";
|
|
function toActionProxy(actionCallback = {}, aggregatedPath = "") {
|
|
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 toActionProxy(action, path + ".");
|
|
}
|
|
});
|
|
}
|
|
function _getActionPath(toString) {
|
|
let path = `${import.meta.env.BASE_URL.replace(/\/$/, "")}/_actions/${new URLSearchParams(toString()).get(ACTION_QUERY_PARAMS.actionName)}`;
|
|
if (shouldAppendTrailingSlash) {
|
|
path = appendForwardSlash(path);
|
|
}
|
|
return path;
|
|
}
|
|
function getActionPath(action) {
|
|
return _getActionPath(action.toString);
|
|
}
|
|
async function handleAction(param, path, context) {
|
|
if (import.meta.env.SSR && context) {
|
|
const pipeline = Reflect.get(context, apiContextRoutesSymbol);
|
|
if (!pipeline) {
|
|
throw astroCalledServerError();
|
|
}
|
|
const action = await pipeline.getAction(path);
|
|
if (!action) throw new Error(`Action not found: ${path}`);
|
|
return action.bind(context)(param);
|
|
}
|
|
const headers = new Headers();
|
|
headers.set("Accept", "application/json");
|
|
let body = param;
|
|
if (!(body instanceof FormData)) {
|
|
try {
|
|
body = JSON.stringify(param);
|
|
} catch (e) {
|
|
throw new ActionError({
|
|
code: "BAD_REQUEST",
|
|
message: `Failed to serialize request body to JSON. Full error: ${e.message}`
|
|
});
|
|
}
|
|
if (body) {
|
|
headers.set("Content-Type", "application/json");
|
|
} else {
|
|
headers.set("Content-Length", "0");
|
|
}
|
|
}
|
|
const rawResult = await fetch(
|
|
_getActionPath(() => getActionQueryString(path)),
|
|
{
|
|
method: "POST",
|
|
body,
|
|
headers
|
|
}
|
|
);
|
|
if (rawResult.status === 204) {
|
|
return deserializeActionResult({ type: "empty", status: 204 });
|
|
}
|
|
const bodyText = await rawResult.text();
|
|
if (rawResult.ok) {
|
|
return deserializeActionResult({
|
|
type: "data",
|
|
body: bodyText,
|
|
status: 200,
|
|
contentType: "application/json+devalue"
|
|
});
|
|
}
|
|
return deserializeActionResult({
|
|
type: "error",
|
|
body: bodyText,
|
|
status: rawResult.status,
|
|
contentType: "application/json"
|
|
});
|
|
}
|
|
const actions = toActionProxy();
|
|
export {
|
|
actions,
|
|
getActionPath
|
|
};
|