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({
|
sum: defineAction({
|
||||||
accept: 'form',
|
accept: 'search',
|
||||||
input: z.object({
|
input: z.object({
|
||||||
a: z.number(),
|
a: z.number(),
|
||||||
b: z.number(),
|
b: z.number(),
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
|
|
||||||
export * from './shared.js';
|
export * from './shared.js';
|
||||||
|
|
||||||
export type ActionAccept = 'form' | 'json';
|
export type ActionAccept = 'form' | 'json' | 'search';
|
||||||
|
|
||||||
export type ActionHandler<TInputSchema, TOutput> = TInputSchema extends z.ZodType
|
export type ActionHandler<TInputSchema, TOutput> = TInputSchema extends z.ZodType
|
||||||
? (input: z.infer<TInputSchema>, context: ActionAPIContext) => MaybePromise<TOutput>
|
? (input: z.infer<TInputSchema>, context: ActionAPIContext) => MaybePromise<TOutput>
|
||||||
|
@ -78,7 +78,9 @@ export function defineAction<
|
||||||
const serverHandler =
|
const serverHandler =
|
||||||
accept === 'form'
|
accept === 'form'
|
||||||
? getFormServerHandler(handler, inputSchema)
|
? getFormServerHandler(handler, inputSchema)
|
||||||
: getJsonServerHandler(handler, inputSchema);
|
: accept === 'search'
|
||||||
|
? getSearchServerHandler(handler, inputSchema)
|
||||||
|
: getJsonServerHandler(handler, inputSchema);
|
||||||
|
|
||||||
async function safeServerHandler(this: ActionAPIContext, unparsedInput: unknown) {
|
async function safeServerHandler(this: ActionAPIContext, unparsedInput: unknown) {
|
||||||
// The ActionAPIContext should always contain the `params` property
|
// 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. */
|
/** Transform form data to an object based on a Zod schema. */
|
||||||
export function formDataToObject<T extends z.AnyZodObject>(
|
export function formDataToObject<T extends z.AnyZodObject>(
|
||||||
formData: FormData,
|
formData: FormData | URLSearchParams,
|
||||||
schema: T,
|
schema: T,
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
const obj: Record<string, unknown> =
|
const obj: Record<string, unknown> =
|
||||||
|
@ -187,7 +216,7 @@ export function formDataToObject<T extends z.AnyZodObject>(
|
||||||
|
|
||||||
function handleFormDataGetAll(
|
function handleFormDataGetAll(
|
||||||
key: string,
|
key: string,
|
||||||
formData: FormData,
|
formData: FormData | URLSearchParams,
|
||||||
validator: z.ZodArray<z.ZodUnknown>,
|
validator: z.ZodArray<z.ZodUnknown>,
|
||||||
) {
|
) {
|
||||||
const entries = Array.from(formData.getAll(key));
|
const entries = Array.from(formData.getAll(key));
|
||||||
|
@ -202,7 +231,7 @@ function handleFormDataGetAll(
|
||||||
|
|
||||||
function handleFormDataGet(
|
function handleFormDataGet(
|
||||||
key: string,
|
key: string,
|
||||||
formData: FormData,
|
formData: FormData | URLSearchParams,
|
||||||
validator: unknown,
|
validator: unknown,
|
||||||
baseValidator: unknown,
|
baseValidator: unknown,
|
||||||
) {
|
) {
|
||||||
|
@ -213,7 +242,7 @@ function handleFormDataGet(
|
||||||
return validator instanceof z.ZodNumber ? Number(value) : value;
|
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) {
|
while (schema instanceof z.ZodEffects || schema instanceof z.ZodPipeline) {
|
||||||
if (schema instanceof z.ZodEffects) {
|
if (schema instanceof z.ZodEffects) {
|
||||||
schema = schema._def.schema;
|
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.
|
// When running client-side, make a fetch request to the action path.
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set('Accept', 'application/json');
|
headers.set('Accept', 'application/json');
|
||||||
let body = param;
|
let method = 'POST'
|
||||||
if (!(body instanceof FormData)) {
|
let search = ''
|
||||||
|
let body
|
||||||
|
if (param instanceof URLSearchParams) {
|
||||||
|
method = 'GET'
|
||||||
|
search = `?${param.toString()}`
|
||||||
|
} else if (param instanceof FormData) {
|
||||||
|
body = param
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
body = JSON.stringify(param);
|
body = JSON.stringify(param);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -83,8 +90,8 @@ async function handleAction(param, path, context) {
|
||||||
headers.set('Content-Length', '0');
|
headers.set('Content-Length', '0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rawResult = await fetch(`${import.meta.env.BASE_URL.replace(/\/$/, '')}/_actions/${path}`, {
|
const rawResult = await fetch(`${import.meta.env.BASE_URL.replace(/\/$/, '')}/_actions/${path}${search}`, {
|
||||||
method: 'POST',
|
method,
|
||||||
body,
|
body,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
|
@ -254,8 +254,22 @@ describe('Astro Actions', () => {
|
||||||
assert.equal($('#user').text(), 'Houston');
|
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 () => {
|
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',
|
method: 'POST',
|
||||||
body: new FormData(),
|
body: new FormData(),
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -75,6 +75,14 @@ export const server = {
|
||||||
return locals.user;
|
return locals.user;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
getUserProperty: defineAction({
|
||||||
|
accept: 'search',
|
||||||
|
input: z
|
||||||
|
.object({ property: z.string() }),
|
||||||
|
handler: async (input, { locals }) => {
|
||||||
|
return locals.user[input.property];
|
||||||
|
},
|
||||||
|
}),
|
||||||
validatePassword: defineAction({
|
validatePassword: defineAction({
|
||||||
accept: 'form',
|
accept: 'form',
|
||||||
input: z
|
input: z
|
||||||
|
|
|
@ -18,6 +18,7 @@ describe('defineAction accept', () => {
|
||||||
expectTypeOf(jsonAction).parameter(0).toBeAny();
|
expectTypeOf(jsonAction).parameter(0).toBeAny();
|
||||||
expectTypeOf(jsonAction).parameter(0).not.toEqualTypeOf<FormData>();
|
expectTypeOf(jsonAction).parameter(0).not.toEqualTypeOf<FormData>();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('accepts type `FormData` when input is omitted with accept form', async () => {
|
it('accepts type `FormData` when input is omitted with accept form', async () => {
|
||||||
const action = defineAction({
|
const action = defineAction({
|
||||||
accept: 'form',
|
accept: 'form',
|
||||||
|
@ -26,6 +27,14 @@ describe('defineAction accept', () => {
|
||||||
expectTypeOf(action).parameter(0).toEqualTypeOf<FormData>();
|
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 () => {
|
it('accept type safe values for input with accept json', async () => {
|
||||||
const action = defineAction({
|
const action = defineAction({
|
||||||
input: z.object({ name: z.string() }),
|
input: z.object({ name: z.string() }),
|
||||||
|
@ -42,4 +51,13 @@ describe('defineAction accept', () => {
|
||||||
});
|
});
|
||||||
expectTypeOf(action).parameter(0).toEqualTypeOf<FormData>();
|
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