From 0ac139e39a9511f82b71d7ecd4ba6e3872431a81 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Wed, 5 Jul 2023 15:32:02 +0800 Subject: [PATCH] feat: allow to use custom fetch with generated hooks --- package.json | 2 +- packages/plugins/swr/res/helper.ts | 72 +++++++++++++------ packages/plugins/swr/src/generator.ts | 8 +-- .../tanstack-query/res/react/helper.ts | 50 ++++++++----- packages/plugins/tanstack-query/res/shared.ts | 18 ++++- .../tanstack-query/res/svelte/helper.ts | 49 ++++++++----- .../plugins/tanstack-query/src/generator.ts | 10 +-- 7 files changed, 142 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index 6b003ba61..4dc241e69 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test-ci": "ZENSTACK_TEST=1 pnpm -r run test --silent", "lint": "pnpm -r lint", "publish-all": "pnpm --filter \"./packages/**\" -r publish --access public", - "publish-preview": "pnpm --filter \"./packages/**\" -r publish --registry http://localhost:4873" + "publish-preview": "pnpm --filter \"./packages/**\" -r publish --force --registry http://localhost:4873" }, "keywords": [], "author": "", diff --git a/packages/plugins/swr/res/helper.ts b/packages/plugins/swr/res/helper.ts index 52159678e..8161c85e0 100644 --- a/packages/plugins/swr/res/helper.ts +++ b/packages/plugins/swr/res/helper.ts @@ -4,11 +4,24 @@ import { createContext } from 'react'; import type { MutatorCallback, MutatorOptions, SWRResponse } from 'swr'; import useSWR, { useSWRConfig } from 'swr'; +/** + * Function signature for `fetch`. + */ +export type FetchFn = (url: string, options?: RequestInit) => Promise; + /** * Context type for configuring react hooks. */ export type RequestHandlerContext = { + /** + * The endpoint to use for the queries. + */ endpoint: string; + + /** + * A custom fetch function for sending the HTTP requests. + */ + fetch?: FetchFn; }; /** @@ -16,6 +29,7 @@ export type RequestHandlerContext = { */ export const RequestHandlerContext = createContext({ endpoint: '/api/model', + fetch: undefined, }); /** @@ -43,10 +57,11 @@ export type RequestOptions = { export function get( url: string | null, args?: unknown, - options?: RequestOptions + options?: RequestOptions, + fetch?: FetchFn ): SWRResponse { const reqUrl = options?.disabled ? null : url ? makeUrl(url, args) : null; - return useSWR(reqUrl, fetcher, { + return useSWR(reqUrl, (url) => fetcher(url, undefined, fetch), { fallbackData: options?.initialData, }); } @@ -58,14 +73,18 @@ export function get( * @param data The request data. * @param mutate Mutator for invalidating cache. */ -export async function post(url: string, data: unknown, mutate: Mutator): Promise { - const r: Result = await fetcher(url, { - method: 'POST', - headers: { - 'content-type': 'application/json', +export async function post(url: string, data: unknown, mutate: Mutator, fetch?: FetchFn): Promise { + const r: Result = await fetcher( + url, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), }, - body: marshal(data), - }); + fetch + ); mutate(); return r; } @@ -77,14 +96,18 @@ export async function post(url: string, data: unknown, mutate: Mutator): * @param data The request data. * @param mutate Mutator for invalidating cache. */ -export async function put(url: string, data: unknown, mutate: Mutator): Promise { - const r: Result = await fetcher(url, { - method: 'PUT', - headers: { - 'content-type': 'application/json', +export async function put(url: string, data: unknown, mutate: Mutator, fetch?: FetchFn): Promise { + const r: Result = await fetcher( + url, + { + method: 'PUT', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), }, - body: marshal(data), - }); + fetch + ); mutate(); return r; } @@ -96,11 +119,15 @@ export async function put(url: string, data: unknown, mutate: Mutator): * @param args The request args object, which will be superjson-stringified and appended as "?q=" parameter * @param mutate Mutator for invalidating cache. */ -export async function del(url: string, args: unknown, mutate: Mutator): Promise { +export async function del(url: string, args: unknown, mutate: Mutator, fetch?: FetchFn): Promise { const reqUrl = makeUrl(url, args); - const r: Result = await fetcher(reqUrl, { - method: 'DELETE', - }); + const r: Result = await fetcher( + reqUrl, + { + method: 'DELETE', + }, + fetch + ); const path = url.split('/'); path.pop(); mutate(); @@ -128,8 +155,9 @@ export function getMutate(prefixes: string[]): Mutator { }; } -export async function fetcher(url: string, options?: RequestInit) { - const res = await fetch(url, options); +export async function fetcher(url: string, options?: RequestInit, fetch?: FetchFn) { + const _fetch = fetch ?? window.fetch; + const res = await _fetch(url, options); if (!res.ok) { const error: Error & { info?: unknown; status?: number } = new Error( 'An error occurred while fetching the data.' diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index 8cd3a3fac..f9b9f1da2 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -77,7 +77,7 @@ function generateModelHooks(project: Project, outDir: string, model: DataModel, name: `useMutate${model.name}`, isExported: true, statements: [ - 'const { endpoint } = useContext(RequestHandlerContext);', + 'const { endpoint, fetch } = useContext(RequestHandlerContext);', `const prefixesToMutate = [${prefixesToMutate .map((prefix) => '`${endpoint}/' + lowerCaseFirst(model.name) + '/' + prefix + '`') .join(', ')}];`, @@ -296,8 +296,8 @@ function generateQueryHook( }) .addBody() .addStatements([ - 'const { endpoint } = useContext(RequestHandlerContext);', - `return request.get<${returnType}>(\`\${endpoint}/${modelRouteName}/${operation}\`, args, options);`, + 'const { endpoint, fetch } = useContext(RequestHandlerContext);', + `return request.get<${returnType}>(\`\${endpoint}/${modelRouteName}/${operation}\`, args, options, fetch);`, ]); } @@ -327,7 +327,7 @@ function generateMutation( .addBody() .addStatements([ wrapReadbackErrorCheck( - `return await request.${fetcherFunc}<${returnType}>(\`\${endpoint}/${modelRouteName}/${operation}\`, args, mutate);` + `return await request.${fetcherFunc}<${returnType}>(\`\${endpoint}/${modelRouteName}/${operation}\`, args, mutate, fetch);` ), ]); return funcName; diff --git a/packages/plugins/tanstack-query/res/react/helper.ts b/packages/plugins/tanstack-query/res/react/helper.ts index 5ede0d694..5ead00257 100644 --- a/packages/plugins/tanstack-query/res/react/helper.ts +++ b/packages/plugins/tanstack-query/res/react/helper.ts @@ -16,6 +16,7 @@ import { createContext } from 'react'; */ export const RequestHandlerContext = createContext({ endpoint: DEFAULT_QUERY_ENDPOINT, + fetch: undefined, }); /** @@ -33,11 +34,11 @@ export const Provider = RequestHandlerContext.Provider; * @returns useQuery hook */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function query(model: string, url: string, args?: unknown, options?: UseQueryOptions) { +export function query(model: string, url: string, args?: unknown, options?: UseQueryOptions, fetch?: FetchFn) { const reqUrl = makeUrl(url, args); return useQuery({ queryKey: [QUERY_KEY_PREFIX + model, url, args], - queryFn: () => fetcher(reqUrl), + queryFn: () => fetcher(reqUrl, undefined, fetch), ...options, }); } @@ -55,17 +56,22 @@ export function postMutation( model: string, url: string, options?: Omit, 'mutationFn'>, + fetch?: FetchFn, invalidateQueries = true ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => - fetcher(url, { - method: 'POST', - headers: { - 'content-type': 'application/json', + fetcher( + url, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), }, - body: marshal(data), - }); + fetch + ); const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); const mutation = useMutation(finalOptions); @@ -85,17 +91,22 @@ export function putMutation( model: string, url: string, options?: Omit, 'mutationFn'>, + fetch?: FetchFn, invalidateQueries = true ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => - fetcher(url, { - method: 'PUT', - headers: { - 'content-type': 'application/json', + fetcher( + url, + { + method: 'PUT', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), }, - body: marshal(data), - }); + fetch + ); const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); const mutation = useMutation(finalOptions); @@ -115,13 +126,18 @@ export function deleteMutation( model: string, url: string, options?: Omit, 'mutationFn'>, + fetch?: FetchFn, invalidateQueries = true ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => - fetcher(makeUrl(url, data), { - method: 'DELETE', - }); + fetcher( + makeUrl(url, data), + { + method: 'DELETE', + }, + fetch + ); const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); const mutation = useMutation(finalOptions); diff --git a/packages/plugins/tanstack-query/res/shared.ts b/packages/plugins/tanstack-query/res/shared.ts index bfa88be9b..24caff7cb 100644 --- a/packages/plugins/tanstack-query/res/shared.ts +++ b/packages/plugins/tanstack-query/res/shared.ts @@ -8,15 +8,29 @@ export const DEFAULT_QUERY_ENDPOINT = '/api/model'; */ export const QUERY_KEY_PREFIX = 'zenstack:'; +/** + * Function signature for `fetch`. + */ +export type FetchFn = (url: string, options?: RequestInit) => Promise; + /** * Context type for configuring the hooks. */ export type RequestHandlerContext = { + /** + * The endpoint to use for the queries. + */ endpoint: string; + + /** + * A custom fetch function for sending the HTTP requests. + */ + fetch?: FetchFn; }; -async function fetcher(url: string, options?: RequestInit) { - const res = await fetch(url, options); +async function fetcher(url: string, options?: RequestInit, fetch?: FetchFn) { + const _fetch = fetch ?? window.fetch; + const res = await _fetch(url, options); if (!res.ok) { const error: Error & { info?: unknown; status?: number } = new Error( 'An error occurred while fetching the data.' diff --git a/packages/plugins/tanstack-query/res/svelte/helper.ts b/packages/plugins/tanstack-query/res/svelte/helper.ts index 508927fbf..d40d8cee5 100644 --- a/packages/plugins/tanstack-query/res/svelte/helper.ts +++ b/packages/plugins/tanstack-query/res/svelte/helper.ts @@ -25,11 +25,11 @@ export const SvelteQueryContextKey = 'zenstack-svelte-query-context'; * @returns useQuery hook */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function query(model: string, url: string, args?: unknown, options?: QueryOptions) { +export function query(model: string, url: string, args?: unknown, options?: QueryOptions, fetch?: FetchFn) { const reqUrl = makeUrl(url, args); return createQuery({ queryKey: [QUERY_KEY_PREFIX + model, url, args], - queryFn: () => fetcher(reqUrl), + queryFn: () => fetcher(reqUrl, undefined, fetch), ...options, }); } @@ -47,17 +47,22 @@ export function postMutation( model: string, url: string, options?: Omit, 'mutationFn'>, + fetch?: FetchFn, invalidateQueries = true ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => - fetcher(url, { - method: 'POST', - headers: { - 'content-type': 'application/json', + fetcher( + url, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), }, - body: marshal(data), - }); + fetch + ); const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); const mutation = createMutation(finalOptions); @@ -77,17 +82,22 @@ export function putMutation( model: string, url: string, options?: Omit, 'mutationFn'>, + fetch?: FetchFn, invalidateQueries = true ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => - fetcher(url, { - method: 'PUT', - headers: { - 'content-type': 'application/json', + fetcher( + url, + { + method: 'PUT', + headers: { + 'content-type': 'application/json', + }, + body: marshal(data), }, - body: marshal(data), - }); + fetch + ); const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); const mutation = createMutation(finalOptions); @@ -107,13 +117,18 @@ export function deleteMutation( model: string, url: string, options?: Omit, 'mutationFn'>, + fetch?: FetchFn, invalidateQueries = true ) { const queryClient = useQueryClient(); const mutationFn = (data: any) => - fetcher(makeUrl(url, data), { - method: 'DELETE', - }); + fetcher( + makeUrl(url, data), + { + method: 'DELETE', + }, + fetch + ); const finalOptions = mergeOptions(model, options, invalidateQueries, mutationFn, queryClient); const mutation = createMutation(finalOptions); diff --git a/packages/plugins/tanstack-query/src/generator.ts b/packages/plugins/tanstack-query/src/generator.ts index 5da93737d..53ebf0942 100644 --- a/packages/plugins/tanstack-query/src/generator.ts +++ b/packages/plugins/tanstack-query/src/generator.ts @@ -92,7 +92,7 @@ function generateQueryHook( makeGetContext(target), `return query<${returnType}>('${model}', \`\${endpoint}/${lowerCaseFirst( model - )}/${operation}\`, args, options);`, + )}/${operation}\`, args, options, fetch);`, ]); } @@ -143,7 +143,9 @@ function generateMutationHook( initializer: ` ${httpVerb}Mutation<${argsType}, ${ overrideReturnType ?? model - }>('${model}', \`\${endpoint}/${lowerCaseFirst(model)}/${operation}\`, options, invalidateQueries) + }>('${model}', \`\${endpoint}/${lowerCaseFirst( + model + )}/${operation}\`, options, fetch, invalidateQueries) `, }, ], @@ -418,9 +420,9 @@ function generateHelper(target: TargetFramework, project: Project, outDir: strin function makeGetContext(target: TargetFramework) { switch (target) { case 'react': - return 'const { endpoint } = useContext(RequestHandlerContext);'; + return 'const { endpoint, fetch } = useContext(RequestHandlerContext);'; case 'svelte': - return `const { endpoint } = getContext(SvelteQueryContextKey);`; + return `const { endpoint, fetch } = getContext(SvelteQueryContextKey);`; default: throw new PluginError(name, `Unsupported target "${target}"`); }