0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-24 23:21:57 -05:00
astro/packages/astro/templates/actions.mjs
Magomed Chabaev 14dffcc3af
fix(actions): support trailing slash (#12657)
* fix(actions): support trailing slash

* refactoring
2024-12-11 16:06:57 +00:00

114 lines
3.5 KiB
JavaScript

import {
ActionError,
deserializeActionResult,
getActionQueryString,
appendForwardSlash,
} from 'astro:actions';
const ENCODED_DOT = '%2E';
function toActionProxy(actionCallback = {}, aggregatedPath = '') {
return new Proxy(actionCallback, {
get(target, objKey) {
if (objKey in target || typeof objKey === 'symbol') {
return target[objKey];
}
// Add the key, encoding dots so they're not interpreted as nested properties.
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,
// 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;
},
});
// recurse to construct queries for nested object paths
// ex. actions.user.admins.auth()
return toActionProxy(action, path + '.');
},
});
}
/**
* @param {*} param argument passed to the action when called server or client-side.
* @param {string} path Built path to call action by path name.
* @param {import('../dist/types/public/context.js').APIContext | undefined} context Injected API context when calling actions from the server.
* Usage: `actions.[name](param)`.
* @returns {Promise<import('../dist/actions/runtime/virtual/shared.js').SafeResult<any, any>>}
*/
async function handleAction(param, path, context) {
// When running server-side, import the action and call it.
if (import.meta.env.SSR) {
const { getAction } = await import('astro/actions/runtime/virtual/get-action.js');
const action = await getAction(path);
if (!action) throw new Error(`Action not found: ${path}`);
return action.bind(context)(param);
}
// When running client-side, make a fetch request to the action path.
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 shouldAppendTrailingSlash = '/** @TRAILING_SLASH@ **/';
let actionPath = import.meta.env.BASE_URL.replace(/\/$/, '') + '/_actions/' + path;
if (shouldAppendTrailingSlash) {
actionPath = appendForwardSlash(actionPath);
}
const rawResult = await fetch(actionPath, {
method: 'POST',
body,
headers,
});
if (rawResult.status === 204) {
return deserializeActionResult({ type: 'empty', status: 204 });
}
return deserializeActionResult({
type: rawResult.ok ? 'data' : 'error',
body: await rawResult.text(),
});
}
export const actions = toActionProxy();