import { ACTION_QUERY_PARAMS, ActionError, deserializeActionResult, getActionQueryString, } 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()); // Astro will redirect with a GET request by default. // Disable this behavior to preserve form state // for React's progressive enhancement. searchParams.set(ACTION_QUERY_PARAMS.actionRedirect, 'false'); 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/astro.d.ts').APIContext | undefined} context Injected API context when calling actions from the server. * Usage: `actions.[name](param)`. * @returns {Promise>} */ 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 rawResult = await fetch(`${import.meta.env.BASE_URL.replace(/\/$/, '')}/_actions/${path}`, { 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();