@sveltejs/kit
Version:
SvelteKit is the fastest way to build Svelte apps
154 lines (135 loc) • 4.74 kB
JavaScript
/** @import { RequestEvent } from '@sveltejs/kit' */
/** @import { ServerHooks, MaybePromise } from 'types' */
import { parse } from 'devalue';
import { error } from '@sveltejs/kit';
import { getRequestEvent, with_event } from '../event.js';
import { create_remote_cache_key, stringify_remote_arg } from '../../../shared.js';
import { EVENT_STATE, get_event_state } from '../../../server/event-state.js';
/**
* @param {any} validate_or_fn
* @param {(arg?: any) => any} [maybe_fn]
* @returns {(arg?: any) => MaybePromise<any>}
*/
export function create_validator(validate_or_fn, maybe_fn) {
// prevent functions without validators being called with arguments
if (!maybe_fn) {
return (arg) => {
if (arg !== undefined) {
error(400, 'Bad Request');
}
};
}
// if 'unchecked', pass input through without validating
if (validate_or_fn === 'unchecked') {
return (arg) => arg;
}
// use https://standardschema.dev validator if provided
if ('~standard' in validate_or_fn) {
return async (arg) => {
// Get event before async validation to ensure it's available in server environments without AsyncLocalStorage, too
const event = getRequestEvent();
const state = get_event_state(event);
const validate = validate_or_fn['~standard'].validate;
const result = await validate(arg);
// if the `issues` field exists, the validation failed
if (result.issues) {
error(
400,
await state.handleValidationError({
...result,
event
})
);
}
return result.value;
};
}
throw new Error(
'Invalid validator passed to remote function. Expected "unchecked" or a Standard Schema (https://standardschema.dev)'
);
}
/**
* In case of a single remote function call, just returns the result.
*
* In case of a full page reload, returns the response for a remote function call,
* either from the cache or by invoking the function.
* Also saves an uneval'ed version of the result for later HTML inlining for hydration.
*
* @template {MaybePromise<any>} T
* @param {string} id
* @param {any} arg
* @param {RequestEvent} event
* @param {() => Promise<T>} get_result
* @returns {Promise<T>}
*/
export function get_response(id, arg, event, get_result) {
const state = get_event_state(event);
const cache_key = create_remote_cache_key(id, stringify_remote_arg(arg, state.transport));
return ((state.remote_data ??= {})[cache_key] ??= get_result());
}
/** @param {string} feature */
export function check_experimental(feature) {
if (!__SVELTEKIT_EXPERIMENTAL__REMOTE_FUNCTIONS__) {
throw new Error(
`Cannot use \`${feature}\` from \`$app/server\` without the experimental flag set to true. Please set kit.experimental.remoteFunctions to \`true\` in your config.`
);
}
}
/**
* @param {any} data
* @param {ServerHooks['transport']} transport
*/
export function parse_remote_response(data, transport) {
/** @type {Record<string, any>} */
const revivers = {};
for (const key in transport) {
revivers[key] = transport[key].decode;
}
return parse(data, revivers);
}
/**
* Like `with_event` but removes things from `event` you cannot see/call in remote functions, such as `setHeaders`.
* @template T
* @param {RequestEvent} event
* @param {boolean} allow_cookies
* @param {any} arg
* @param {(arg: any) => any} validate
* @param {(arg?: any) => T} fn
*/
export async function run_remote_function(event, allow_cookies, arg, validate, fn) {
/** @type {RequestEvent} */
const cleansed = {
...event,
// @ts-expect-error this isn't part of the public `RequestEvent` type
[EVENT_STATE]: event[EVENT_STATE],
setHeaders: () => {
throw new Error('setHeaders is not allowed in remote functions');
},
cookies: {
...event.cookies,
set: (name, value, opts) => {
if (!allow_cookies) {
throw new Error('Cannot set cookies in `query` or `prerender` functions');
}
if (opts.path && !opts.path.startsWith('/')) {
throw new Error('Cookies set in remote functions must have an absolute path');
}
return event.cookies.set(name, value, opts);
},
delete: (name, opts) => {
if (!allow_cookies) {
throw new Error('Cannot delete cookies in `query` or `prerender` functions');
}
if (opts.path && !opts.path.startsWith('/')) {
throw new Error('Cookies deleted in remote functions must have an absolute path');
}
return event.cookies.delete(name, opts);
}
},
route: { id: null },
url: new URL(event.url.origin)
};
// In two parts, each with_event, so that runtimes without async local storage can still get the event at the start of the function
const validated = await with_event(cleansed, () => validate(arg));
return with_event(cleansed, () => fn(validated));
}