diff --git a/.changeset/rude-islands-flow.md b/.changeset/rude-islands-flow.md new file mode 100644 index 000000000000..3e7813c69467 --- /dev/null +++ b/.changeset/rude-islands-flow.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: use validated args in batch resolver in both csr and ssr diff --git a/packages/kit/src/runtime/app/server/remote/command.js b/packages/kit/src/runtime/app/server/remote/command.js index 2ea88bfb02f0..0ce1575c08f1 100644 --- a/packages/kit/src/runtime/app/server/remote/command.js +++ b/packages/kit/src/runtime/app/server/remote/command.js @@ -78,7 +78,9 @@ export function command(validate_or_fn, maybe_fn) { state.refreshes ??= {}; - const promise = Promise.resolve(run_remote_function(event, state, true, arg, validate, fn)); + const promise = Promise.resolve( + run_remote_function(event, state, true, () => validate(arg), fn) + ); // @ts-expect-error promise.updates = () => { diff --git a/packages/kit/src/runtime/app/server/remote/form.js b/packages/kit/src/runtime/app/server/remote/form.js index 9f71d6e7e126..0c5824e19190 100644 --- a/packages/kit/src/runtime/app/server/remote/form.js +++ b/packages/kit/src/runtime/app/server/remote/form.js @@ -145,8 +145,7 @@ export function form(validate_or_fn, maybe_fn) { event, state, true, - data, - (d) => d, + () => data, (data) => (!maybe_fn ? fn() : fn(data, issue)) ); } catch (e) { diff --git a/packages/kit/src/runtime/app/server/remote/prerender.js b/packages/kit/src/runtime/app/server/remote/prerender.js index 8e9762463063..f9729f7c7752 100644 --- a/packages/kit/src/runtime/app/server/remote/prerender.js +++ b/packages/kit/src/runtime/app/server/remote/prerender.js @@ -131,7 +131,7 @@ export function prerender(validate_or_fn, fn_or_options, maybe_options) { } const promise = get_response(__, arg, state, () => - run_remote_function(event, state, false, arg, validate, fn) + run_remote_function(event, state, false, () => validate(arg), fn) ); if (state.prerendering) { diff --git a/packages/kit/src/runtime/app/server/remote/query.js b/packages/kit/src/runtime/app/server/remote/query.js index d8bb50ba7392..4b21603ae132 100644 --- a/packages/kit/src/runtime/app/server/remote/query.js +++ b/packages/kit/src/runtime/app/server/remote/query.js @@ -5,6 +5,8 @@ import { get_request_store } from '@sveltejs/kit/internal/server'; import { create_remote_key, stringify_remote_arg } from '../../../shared.js'; import { prerendering } from '__sveltekit/environment'; import { create_validator, get_cache, get_response, run_remote_function } from './shared.js'; +import { handle_error_and_jsonify } from '../../../server/utils.js'; +import { HttpError, SvelteKitError } from '@sveltejs/kit/internal'; /** * Creates a remote query. When called from the browser, the function will be invoked on the server via a `fetch` call. @@ -73,7 +75,7 @@ export function query(validate_or_fn, maybe_fn) { const { event, state } = get_request_store(); const get_remote_function_result = () => - run_remote_function(event, state, false, arg, validate, fn); + run_remote_function(event, state, false, () => validate(arg), fn); /** @type {Promise & Partial>} */ const promise = get_response(__, arg, state, get_remote_function_result); @@ -137,7 +139,7 @@ export function query(validate_or_fn, maybe_fn) { */ /*@__NO_SIDE_EFFECTS__*/ function batch(validate_or_fn, maybe_fn) { - /** @type {(args?: Input[]) => (arg: Input, idx: number) => Output} */ + /** @type {(args?: Input[]) => MaybePromise<(arg: Input, idx: number) => Output>} */ const fn = maybe_fn ?? validate_or_fn; /** @type {(arg?: any) => MaybePromise} */ @@ -148,16 +150,34 @@ function batch(validate_or_fn, maybe_fn) { type: 'query_batch', id: '', name: '', - run: (args) => { + run: async (args, options) => { const { event, state } = get_request_store(); return run_remote_function( event, state, false, - args, - (array) => Promise.all(array.map(validate)), - fn + async () => Promise.all(args.map(validate)), + async (/** @type {any[]} */ input) => { + const get_result = await fn(input); + + return Promise.all( + input.map(async (arg, i) => { + try { + return { type: 'result', data: get_result(arg, i) }; + } catch (error) { + return { + type: 'error', + error: await handle_error_and_jsonify(event, state, options, error), + status: + error instanceof HttpError || error instanceof SvelteKitError + ? error.status + : 500 + }; + } + }) + ); + } ); } }; @@ -190,22 +210,23 @@ function batch(validate_or_fn, maybe_fn) { batching = { args: [], resolvers: [] }; try { - const get_result = await run_remote_function( + return await run_remote_function( event, state, false, - batched.args, - (array) => Promise.all(array.map(validate)), - fn - ); - - for (let i = 0; i < batched.resolvers.length; i++) { - try { - batched.resolvers[i].resolve(get_result(batched.args[i], i)); - } catch (error) { - batched.resolvers[i].reject(error); + async () => Promise.all(batched.args.map(validate)), + async (input) => { + const get_result = await fn(input); + + for (let i = 0; i < batched.resolvers.length; i++) { + try { + batched.resolvers[i].resolve(get_result(input[i], i)); + } catch (error) { + batched.resolvers[i].reject(error); + } + } } - } + ); } catch (error) { for (const resolver of batched.resolvers) { resolver.reject(error); diff --git a/packages/kit/src/runtime/app/server/remote/shared.js b/packages/kit/src/runtime/app/server/remote/shared.js index 43698b76244d..a2e310649d1d 100644 --- a/packages/kit/src/runtime/app/server/remote/shared.js +++ b/packages/kit/src/runtime/app/server/remote/shared.js @@ -97,11 +97,10 @@ export function parse_remote_response(data, transport) { * @param {RequestEvent} event * @param {RequestState} state * @param {boolean} allow_cookies - * @param {any} arg - * @param {(arg: any) => any} validate + * @param {() => any} get_input * @param {(arg?: any) => T} fn */ -export async function run_remote_function(event, state, allow_cookies, arg, validate, fn) { +export async function run_remote_function(event, state, allow_cookies, get_input, fn) { /** @type {RequestStore} */ const store = { event: { @@ -142,8 +141,8 @@ export async function run_remote_function(event, state, allow_cookies, arg, vali }; // 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_request_store(store, () => validate(arg)); - return with_request_store(store, () => fn(validated)); + const input = await with_request_store(store, get_input); + return with_request_store(store, () => fn(input)); } /** diff --git a/packages/kit/src/runtime/server/remote.js b/packages/kit/src/runtime/server/remote.js index 27fd8c5a2515..b47a48210de4 100644 --- a/packages/kit/src/runtime/server/remote.js +++ b/packages/kit/src/runtime/server/remote.js @@ -73,23 +73,12 @@ async function handle_remote_call_internal(event, state, options, manifest, id) /** @type {{ payloads: string[] }} */ const { payloads } = await event.request.json(); - const args = payloads.map((payload) => parse_remote_arg(payload, transport)); - const get_result = await with_request_store({ event, state }, () => info.run(args)); - const results = await Promise.all( - args.map(async (arg, i) => { - try { - return { type: 'result', data: get_result(arg, i) }; - } catch (error) { - return { - type: 'error', - error: await handle_error_and_jsonify(event, state, options, error), - status: - error instanceof HttpError || error instanceof SvelteKitError ? error.status : 500 - }; - } - }) + const args = await Promise.all( + payloads.map((payload) => parse_remote_arg(payload, transport)) ); + const results = await with_request_store({ event, state }, () => info.run(args, options)); + return json( /** @type {RemoteFunctionResponse} */ ({ type: 'result', diff --git a/packages/kit/src/types/internal.d.ts b/packages/kit/src/types/internal.d.ts index 6384201af551..2e736bfc9dad 100644 --- a/packages/kit/src/types/internal.d.ts +++ b/packages/kit/src/types/internal.d.ts @@ -570,8 +570,8 @@ export type RemoteInfo = type: 'query_batch'; id: string; name: string; - /** Direct access to the function without batching etc logic, for remote functions called from the client */ - run: (args: any[]) => Promise<(arg: any, idx: number) => any>; + /** Direct access to the function, for remote functions called from the client */ + run: (args: any[], options: SSROptions) => Promise; } | { type: 'form'; diff --git a/packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte b/packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte new file mode 100644 index 000000000000..710c4f806c6a --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch-validation/+page.svelte @@ -0,0 +1,13 @@ + + +
+ {#each words as word, i} + {await reverse(word)}{i === words.length - 1 ? '' : ' '} + {/each} +
+ diff --git a/packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js b/packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js new file mode 100644 index 000000000000..95ff1e550aa8 --- /dev/null +++ b/packages/kit/test/apps/async/src/routes/remote/batch-validation/batch.remote.js @@ -0,0 +1,12 @@ +import { query } from '$app/server'; +import * as v from 'valibot'; + +export const reverse = query.batch( + v.pipe( + v.string(), + v.transform((val) => val.split('').reverse().join('')) + ), + () => { + return (x) => x; + } +); diff --git a/packages/kit/test/apps/async/test/client.test.js b/packages/kit/test/apps/async/test/client.test.js index 8266b5ac8338..d6d4a4da9717 100644 --- a/packages/kit/test/apps/async/test/client.test.js +++ b/packages/kit/test/apps/async/test/client.test.js @@ -301,6 +301,14 @@ test.describe('remote function mutations', () => { expect(request_count).toBe(1); // only the command request }); + test('query.batch resolver function always receives validated arguments', async ({ page }) => { + await page.goto('/remote/batch-validation'); + + await expect(page.locator('#phrase')).toHaveText('use the force'); + await page.locator('button').click(); + await expect(page.locator('#phrase')).toHaveText('i am your father'); + }); + // TODO ditto test('query works with transport', async ({ page }) => { await page.goto('/remote/transport');