mirror of
https://github.com/withastro/astro.git
synced 2024-12-30 22:03:56 -05:00
feat(actions): add support for accept: 'search' for GET requests
This commit is contained in:
parent
74ee2e45ec
commit
e76bc82909
6 changed files with 88 additions and 12 deletions
|
@ -76,7 +76,7 @@ export const server = {
|
|||
}),
|
||||
},
|
||||
sum: defineAction({
|
||||
accept: 'form',
|
||||
accept: 'search',
|
||||
input: z.object({
|
||||
a: z.number(),
|
||||
b: z.number(),
|
||||
|
|
|
@ -27,7 +27,7 @@ import {
|
|||
|
||||
export * from './shared.js';
|
||||
|
||||
export type ActionAccept = 'form' | 'json';
|
||||
export type ActionAccept = 'form' | 'json' | 'search';
|
||||
|
||||
export type ActionHandler<TInputSchema, TOutput> = TInputSchema extends z.ZodType
|
||||
? (input: z.infer<TInputSchema>, context: ActionAPIContext) => MaybePromise<TOutput>
|
||||
|
@ -78,7 +78,9 @@ export function defineAction<
|
|||
const serverHandler =
|
||||
accept === 'form'
|
||||
? getFormServerHandler(handler, inputSchema)
|
||||
: getJsonServerHandler(handler, inputSchema);
|
||||
: accept === 'search'
|
||||
? getSearchServerHandler(handler, inputSchema)
|
||||
: getJsonServerHandler(handler, inputSchema);
|
||||
|
||||
async function safeServerHandler(this: ActionAPIContext, unparsedInput: unknown) {
|
||||
// The ActionAPIContext should always contain the `params` property
|
||||
|
@ -148,9 +150,36 @@ function getJsonServerHandler<TOutput, TInputSchema extends z.ZodType>(
|
|||
};
|
||||
}
|
||||
|
||||
function getSearchServerHandler<TOutput, TInputSchema extends z.ZodType>(
|
||||
handler: ActionHandler<TInputSchema, TOutput>,
|
||||
inputSchema?: TInputSchema,
|
||||
) {
|
||||
return async (unparsedInput: unknown, context: ActionAPIContext): Promise<Awaited<TOutput>> => {
|
||||
if (!(unparsedInput instanceof URLSearchParams)) {
|
||||
throw new ActionError({
|
||||
code: 'UNSUPPORTED_MEDIA_TYPE',
|
||||
message: 'This action only accepts URLSearchParams.',
|
||||
});
|
||||
}
|
||||
|
||||
if (!inputSchema) return await handler(unparsedInput, context);
|
||||
|
||||
const baseSchema = unwrapBaseObjectSchema(inputSchema, unparsedInput);
|
||||
const parsed = await inputSchema.safeParseAsync(
|
||||
baseSchema instanceof z.ZodObject
|
||||
? formDataToObject(unparsedInput, baseSchema)
|
||||
: unparsedInput,
|
||||
);
|
||||
if (!parsed.success) {
|
||||
throw new ActionInputError(parsed.error.issues);
|
||||
}
|
||||
return await handler(parsed.data, context);
|
||||
};
|
||||
}
|
||||
|
||||
/** Transform form data to an object based on a Zod schema. */
|
||||
export function formDataToObject<T extends z.AnyZodObject>(
|
||||
formData: FormData,
|
||||
formData: FormData | URLSearchParams,
|
||||
schema: T,
|
||||
): Record<string, unknown> {
|
||||
const obj: Record<string, unknown> =
|
||||
|
@ -187,7 +216,7 @@ export function formDataToObject<T extends z.AnyZodObject>(
|
|||
|
||||
function handleFormDataGetAll(
|
||||
key: string,
|
||||
formData: FormData,
|
||||
formData: FormData | URLSearchParams,
|
||||
validator: z.ZodArray<z.ZodUnknown>,
|
||||
) {
|
||||
const entries = Array.from(formData.getAll(key));
|
||||
|
@ -202,7 +231,7 @@ function handleFormDataGetAll(
|
|||
|
||||
function handleFormDataGet(
|
||||
key: string,
|
||||
formData: FormData,
|
||||
formData: FormData | URLSearchParams,
|
||||
validator: unknown,
|
||||
baseValidator: unknown,
|
||||
) {
|
||||
|
@ -213,7 +242,7 @@ function handleFormDataGet(
|
|||
return validator instanceof z.ZodNumber ? Number(value) : value;
|
||||
}
|
||||
|
||||
function unwrapBaseObjectSchema(schema: z.ZodType, unparsedInput: FormData) {
|
||||
function unwrapBaseObjectSchema(schema: z.ZodType, unparsedInput: FormData | URLSearchParams) {
|
||||
while (schema instanceof z.ZodEffects || schema instanceof z.ZodPipeline) {
|
||||
if (schema instanceof z.ZodEffects) {
|
||||
schema = schema._def.schema;
|
||||
|
|
|
@ -67,8 +67,15 @@ async function handleAction(param, path, context) {
|
|||
// 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)) {
|
||||
let method = 'POST'
|
||||
let search = ''
|
||||
let body
|
||||
if (param instanceof URLSearchParams) {
|
||||
method = 'GET'
|
||||
search = `?${param.toString()}`
|
||||
} else if (param instanceof FormData) {
|
||||
body = param
|
||||
} else {
|
||||
try {
|
||||
body = JSON.stringify(param);
|
||||
} catch (e) {
|
||||
|
@ -83,8 +90,8 @@ async function handleAction(param, path, context) {
|
|||
headers.set('Content-Length', '0');
|
||||
}
|
||||
}
|
||||
const rawResult = await fetch(`${import.meta.env.BASE_URL.replace(/\/$/, '')}/_actions/${path}`, {
|
||||
method: 'POST',
|
||||
const rawResult = await fetch(`${import.meta.env.BASE_URL.replace(/\/$/, '')}/_actions/${path}${search}`, {
|
||||
method,
|
||||
body,
|
||||
headers,
|
||||
});
|
||||
|
|
|
@ -254,8 +254,22 @@ describe('Astro Actions', () => {
|
|||
assert.equal($('#user').text(), 'Houston');
|
||||
});
|
||||
|
||||
it('Supports URLSearchParams', async () => {
|
||||
const req = new Request('http://example.com/_actions/getUserProperty?property=name', {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
const res = await app.render(req);
|
||||
|
||||
assert.equal(res.ok, true);
|
||||
assert.equal(res.headers.get('Content-Type'), 'application/json+devalue');
|
||||
|
||||
const data = devalue.parse(await res.text());
|
||||
assert.deepEqual(data, 'Houston');
|
||||
});
|
||||
|
||||
it('Respects custom errors - POST', async () => {
|
||||
const req = new Request('http://example.com/user-or-throw?_action=getUserOrThrow', {
|
||||
const req = new Request('http://example.com/user-or-throw?_astroAction=getUserOrThrow', {
|
||||
method: 'POST',
|
||||
body: new FormData(),
|
||||
headers: {
|
||||
|
|
|
@ -75,6 +75,14 @@ export const server = {
|
|||
return locals.user;
|
||||
},
|
||||
}),
|
||||
getUserProperty: defineAction({
|
||||
accept: 'search',
|
||||
input: z
|
||||
.object({ property: z.string() }),
|
||||
handler: async (input, { locals }) => {
|
||||
return locals.user[input.property];
|
||||
},
|
||||
}),
|
||||
validatePassword: defineAction({
|
||||
accept: 'form',
|
||||
input: z
|
||||
|
|
|
@ -18,6 +18,7 @@ describe('defineAction accept', () => {
|
|||
expectTypeOf(jsonAction).parameter(0).toBeAny();
|
||||
expectTypeOf(jsonAction).parameter(0).not.toEqualTypeOf<FormData>();
|
||||
});
|
||||
|
||||
it('accepts type `FormData` when input is omitted with accept form', async () => {
|
||||
const action = defineAction({
|
||||
accept: 'form',
|
||||
|
@ -26,6 +27,14 @@ describe('defineAction accept', () => {
|
|||
expectTypeOf(action).parameter(0).toEqualTypeOf<FormData>();
|
||||
});
|
||||
|
||||
it('accepts type `URLSearchParams` when input is omitted with accept form', async () => {
|
||||
const action = defineAction({
|
||||
accept: 'search',
|
||||
handler: () => {},
|
||||
});
|
||||
expectTypeOf(action).parameter(0).toEqualTypeOf<URLSearchParams>();
|
||||
});
|
||||
|
||||
it('accept type safe values for input with accept json', async () => {
|
||||
const action = defineAction({
|
||||
input: z.object({ name: z.string() }),
|
||||
|
@ -42,4 +51,13 @@ describe('defineAction accept', () => {
|
|||
});
|
||||
expectTypeOf(action).parameter(0).toEqualTypeOf<FormData>();
|
||||
});
|
||||
|
||||
it('accepts type `URLSearchParams` for all inputs with accept form', async () => {
|
||||
const action = defineAction({
|
||||
accept: 'form',
|
||||
input: z.object({ name: z.string() }),
|
||||
handler: () => {},
|
||||
});
|
||||
expectTypeOf(action).parameter(0).toEqualTypeOf<URLSearchParams>();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue