Skip to content

feat: infinite query for swr plugin #680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions packages/plugins/swr/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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';`,
]);

Expand Down Expand Up @@ -108,7 +108,12 @@ function generateModelHooks(project: Project, outDir: string, model: DataModel,
const argsType = `Prisma.${model.name}FindManyArgs`;
const inputType = `Prisma.SelectSubset<T, ${argsType}>`;
const returnType = `Array<Prisma.${model.name}GetPayload<T>>`;

// regular findMany
generateQueryHook(sf, model, 'findMany', argsType, inputType, returnType);

// infinite findMany
generateQueryHook(sf, model, 'findMany', argsType, inputType, returnType, undefined, true);
}

// findUnique
Expand Down Expand Up @@ -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<ParameterDeclarationStructure>[] = [];
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);`,
]);
}

Expand Down
78 changes: 70 additions & 8 deletions packages/plugins/swr/src/runtime/index.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -39,31 +40,92 @@ export const RequestHandlerContext = createContext<RequestHandlerContext>({
export const Provider = RequestHandlerContext.Provider;

/**
* Client request options
* Client request options for regular query.
*/
export type RequestOptions<T> = {
// disable data fetching
export type RequestOptions<Result, Error = any> = {
/**
* Disable data fetching
*/
disabled?: boolean;

/**
* Equivalent to @see SWRConfiguration.fallbackData
*/
initialData?: Result;
} & SWRConfiguration<Result, Error, Fetcher<Result>>;

/**
* Client request options for infinite query.
*/
export type InfiniteRequestOptions<Result, Error = any> = {
/**
* Disable data fetching
*/
disabled?: boolean;
initialData?: T;
};

/**
* Equivalent to @see SWRInfiniteConfiguration.fallbackData
*/
initialData?: Result[];
} & SWRInfiniteConfiguration<Result, Error, SWRInfiniteFetcher<Result>>;

/**
* 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<Result, Error = any>(
url: string | null,
args?: unknown,
options?: RequestOptions<Result>,
options?: RequestOptions<Result, Error>,
fetch?: FetchFn
): SWRResponse<Result, Error> {
const reqUrl = options?.disabled ? null : url ? makeUrl(url, args) : null;
return useSWR<Result, Error>(reqUrl, (url) => fetcher<Result, false>(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<Args, Result> = (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<Args, Result, Error = any>(
url: string | null,
getNextArgs: GetNextArgs<Args, any>,
options?: InfiniteRequestOptions<Result, Error>,
fetch?: FetchFn
): SWRInfiniteResponse<Result, Error> {
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<Result, Error>(getKey, (url) => fetcher<Result, false>(url, undefined, fetch, false), {
...options,
fallbackData: options?.initialData ?? options?.fallbackData,
});
}

Expand Down