From ce2c2112f6c6bf7b13bdba742e0922834c33a5c8 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sun, 10 Sep 2023 10:54:50 +0800 Subject: [PATCH] feat: infinite query for swr plugin --- packages/plugins/swr/src/generator.ts | 54 +++++++++++----- packages/plugins/swr/src/runtime/index.ts | 78 ++++++++++++++++++++--- 2 files changed, 108 insertions(+), 24 deletions(-) diff --git a/packages/plugins/swr/src/generator.ts b/packages/plugins/swr/src/generator.ts index 31431e74e..0ad8177f8 100644 --- a/packages/plugins/swr/src/generator.ts +++ b/packages/plugins/swr/src/generator.ts @@ -14,7 +14,7 @@ import { paramCase } from 'change-case'; import { lowerCaseFirst } from 'lower-case-first'; import path from 'path'; import semver from 'semver'; -import { FunctionDeclaration, Project, SourceFile } from 'ts-morph'; +import { FunctionDeclaration, OptionalKind, ParameterDeclarationStructure, Project, SourceFile } from 'ts-morph'; import { upperCaseFirst } from 'upper-case-first'; export async function generate(model: Model, options: PluginOptions, dmmf: DMMF.Document) { @@ -61,7 +61,7 @@ function generateModelHooks(project: Project, outDir: string, model: DataModel, }); sf.addStatements([ `import { useContext } from 'react';`, - `import { RequestHandlerContext, type RequestOptions, type PickEnumerable, type CheckSelect } from '@zenstackhq/swr/runtime';`, + `import { RequestHandlerContext, type GetNextArgs, type RequestOptions, type InfiniteRequestOptions, type PickEnumerable, type CheckSelect } from '@zenstackhq/swr/runtime';`, `import * as request from '@zenstackhq/swr/runtime';`, ]); @@ -108,7 +108,12 @@ function generateModelHooks(project: Project, outDir: string, model: DataModel, const argsType = `Prisma.${model.name}FindManyArgs`; const inputType = `Prisma.SelectSubset`; const returnType = `Array>`; + + // regular findMany generateQueryHook(sf, model, 'findMany', argsType, inputType, returnType); + + // infinite findMany + generateQueryHook(sf, model, 'findMany', argsType, inputType, returnType, undefined, true); } // findUnique @@ -289,28 +294,45 @@ function generateQueryHook( argsType: string, inputType: string, returnType: string, - typeParameters?: string[] + typeParameters?: string[], + infinite = false ) { const modelRouteName = lowerCaseFirst(model.name); + + const typeParams = typeParameters ? [...typeParameters] : [`T extends ${argsType}`]; + if (infinite) { + typeParams.push(`R extends ${returnType}`); + } + + const parameters: OptionalKind[] = []; + if (!infinite) { + parameters.push({ + name: 'args?', + type: inputType, + }); + } else { + parameters.push({ + name: 'getNextArgs', + type: `GetNextArgs<${inputType} | undefined, R>`, + }); + } + parameters.push({ + name: 'options?', + type: infinite ? `InfiniteRequestOptions<${returnType}>` : `RequestOptions<${returnType}>`, + }); + sf.addFunction({ - name: `use${upperCaseFirst(operation)}${model.name}`, - typeParameters: typeParameters ?? [`T extends ${argsType}`], + name: `use${infinite ? 'Infinite' : ''}${upperCaseFirst(operation)}${model.name}`, + typeParameters: typeParams, isExported: true, - parameters: [ - { - name: 'args?', - type: inputType, - }, - { - name: 'options?', - type: `RequestOptions<${returnType}>`, - }, - ], + parameters, }) .addBody() .addStatements([ 'const { endpoint, fetch } = useContext(RequestHandlerContext);', - `return request.get<${returnType}>(\`\${endpoint}/${modelRouteName}/${operation}\`, args, options, fetch);`, + !infinite + ? `return request.get<${returnType}>(\`\${endpoint}/${modelRouteName}/${operation}\`, args, options, fetch);` + : `return request.infiniteGet<${inputType} | undefined, ${returnType}>(\`\${endpoint}/${modelRouteName}/${operation}\`, getNextArgs, options, fetch);`, ]); } diff --git a/packages/plugins/swr/src/runtime/index.ts b/packages/plugins/swr/src/runtime/index.ts index 78b17242f..3b82af812 100644 --- a/packages/plugins/swr/src/runtime/index.ts +++ b/packages/plugins/swr/src/runtime/index.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { deserialize, serialize } from '@zenstackhq/runtime/browser'; import { createContext } from 'react'; -import type { MutatorCallback, MutatorOptions, SWRResponse } from 'swr'; +import type { Fetcher, MutatorCallback, MutatorOptions, SWRConfiguration, SWRResponse } from 'swr'; import useSWR, { useSWRConfig } from 'swr'; +import useSWRInfinite, { SWRInfiniteConfiguration, SWRInfiniteFetcher, SWRInfiniteResponse } from 'swr/infinite'; export * from './prisma-types'; /** @@ -39,31 +40,92 @@ export const RequestHandlerContext = createContext({ export const Provider = RequestHandlerContext.Provider; /** - * Client request options + * Client request options for regular query. */ -export type RequestOptions = { - // disable data fetching +export type RequestOptions = { + /** + * Disable data fetching + */ + disabled?: boolean; + + /** + * Equivalent to @see SWRConfiguration.fallbackData + */ + initialData?: Result; +} & SWRConfiguration>; + +/** + * Client request options for infinite query. + */ +export type InfiniteRequestOptions = { + /** + * Disable data fetching + */ disabled?: boolean; - initialData?: T; -}; + + /** + * Equivalent to @see SWRInfiniteConfiguration.fallbackData + */ + initialData?: Result[]; +} & SWRInfiniteConfiguration>; /** * Makes a GET request with SWR. * * @param url The request URL. * @param args The request args object, which will be superjson-stringified and appended as "?q=" parameter + * @param options Query options + * @param fetch Custom fetch function * @returns SWR response */ // eslint-disable-next-line @typescript-eslint/no-explicit-any 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, (url) => fetcher(url, undefined, fetch, false), { - fallbackData: options?.initialData, + ...options, + fallbackData: options?.initialData ?? options?.fallbackData, + }); +} + +/** + * Function for computing the query args for fetching a page during an infinite query. + */ +export type GetNextArgs = (pageIndex: number, previousPageData: Result | null) => Args | null; + +/** + * Makes an infinite GET request with SWR. + * + * @param url The request URL. + * @param getNextArgs Function for computing the query args for a page. + * @param options Query options + * @param fetch Custom fetch function + * @returns SWR infinite query response + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function infiniteGet( + url: string | null, + getNextArgs: GetNextArgs, + options?: InfiniteRequestOptions, + fetch?: FetchFn +): SWRInfiniteResponse { + const getKey = (pageIndex: number, previousPageData: Result | null) => { + if (options?.disabled || !url) { + return null; + } + const nextArgs = getNextArgs(pageIndex, previousPageData); + return nextArgs !== null // null means reached the end + ? makeUrl(url, nextArgs) + : null; + }; + + return useSWRInfinite(getKey, (url) => fetcher(url, undefined, fetch, false), { + ...options, + fallbackData: options?.initialData ?? options?.fallbackData, }); }