From 0779587b32b8c38f7d4d245de97fbac470eb2828 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sat, 28 Jun 2025 17:58:13 +0200 Subject: [PATCH 01/12] copy original pagination plugin and rename it --- apps/graphql/src/pagination/plugin.ts | 39 ++++ apps/graphql/src/pagination/schemaBuilder.ts | 151 +++++++++++++++ apps/graphql/src/pagination/types.ts | 177 ++++++++++++++++++ .../src/pagination/usePaginationBuilder.ts | 79 ++++++++ 4 files changed, 446 insertions(+) create mode 100644 apps/graphql/src/pagination/plugin.ts create mode 100644 apps/graphql/src/pagination/schemaBuilder.ts create mode 100644 apps/graphql/src/pagination/types.ts create mode 100644 apps/graphql/src/pagination/usePaginationBuilder.ts diff --git a/apps/graphql/src/pagination/plugin.ts b/apps/graphql/src/pagination/plugin.ts new file mode 100644 index 0000000..63b40d7 --- /dev/null +++ b/apps/graphql/src/pagination/plugin.ts @@ -0,0 +1,39 @@ +// import './types'; +// import './schemaBuilder'; + +// import { paginate } from '@fleek-platform/prisma'; +// import SchemaBuilder, { BasePlugin, SchemaTypes } from '@pothos/core'; +// import { GraphQLFieldResolver } from 'graphql'; + +// const pluginName = 'inputGroup' as const; + +// // eslint-disable-next-line import/no-default-export +// export default pluginName; + +// export class PothosWithInputPlugin extends BasePlugin { +// wrapResolve(resolver: GraphQLFieldResolver | string>>): GraphQLFieldResolver< +// unknown, +// Types['Context'], +// { +// filter?: { sortField: string; sortOrder: string; take: number; page?: number }; +// } +// > { +// return async (parent, args, context, info) => { +// const result = await resolver(parent, args, context, info); + +// if (typeof result === 'object' && result !== null && 'withPages' in result && typeof result.withPages === 'function') { +// const [data, paginationMetadata] = await result.withPages({ +// limit: args.filter?.take ?? 100, +// page: args.filter?.page ?? 1, +// includePageCount: true, +// }); + +// return { data, ...paginationMetadata }; +// } + +// return result; +// }; +// } +// } + +// SchemaBuilder.registerPlugin(pluginName, PothosWithInputPlugin); diff --git a/apps/graphql/src/pagination/schemaBuilder.ts b/apps/graphql/src/pagination/schemaBuilder.ts new file mode 100644 index 0000000..32985d2 --- /dev/null +++ b/apps/graphql/src/pagination/schemaBuilder.ts @@ -0,0 +1,151 @@ +// import { InputFieldBuilder, RootFieldBuilder, SchemaTypes } from '@pothos/core'; +// import { upperCaseFirst } from 'upper-case-first'; + +// import { usePaginationBuilder } from './usePaginationBuilder'; + +// const rootBuilderProto = RootFieldBuilder.prototype as PothosSchemaTypes.RootFieldBuilder; + +// rootBuilderProto.fieldWithInputGroup = function ({ +// args: { where, whereRequired, data, dataRequired, pagination, sortable }, +// ...fieldOptions +// }) { +// const whereInputRef = where ? this.builder.inputRef(upperCaseFirst(`${this.typename}WhereInput`)) : null; +// const dataInputRef = data ? this.builder.inputRef(upperCaseFirst(`${this.typename}DataInput`)) : null; +// const paginationInputRef = pagination ? this.builder.inputRef(upperCaseFirst(`${this.typename}PaginationInput`)) : null; +// const typeWithAggregationRef = pagination ? this.builder.objectRef(upperCaseFirst(`${this.typename}WithAggregation`)) : null; + +// const whereArgsGroup = whereInputRef +// ? { +// where: this.arg({ +// required: whereRequired ?? true, +// type: whereInputRef, +// }), +// } +// : {}; + +// const dataArgsGroup = dataInputRef +// ? { +// data: this.arg({ +// required: dataRequired ?? true, +// type: dataInputRef, +// }), +// } +// : {}; + +// const paginationArgsGroup = +// pagination && paginationInputRef +// ? { +// filter: this.arg({ +// required: false, +// type: paginationInputRef, +// defaultValue: { +// sortField: sortable?.defaultField, +// sortOrder: sortable?.defaultOrder, +// }, +// }), +// } +// : {}; + +// const fieldRef = this.field({ +// ...fieldOptions, +// type: typeWithAggregationRef ?? fieldOptions.type, +// args: { ...whereArgsGroup, ...dataArgsGroup, ...paginationArgsGroup }, +// } as never); + +// this.builder.configStore.onFieldUse(fieldRef, (config) => { +// if (where && whereInputRef) { +// const name = upperCaseFirst(`${config.name}WhereInput`); + +// this.builder.inputType(name, { +// fields: () => where, +// }); + +// this.builder.configStore.associateRefWithName(whereInputRef, name); +// } + +// if (data && dataInputRef) { +// const name = upperCaseFirst(`${config.name}DataInput`); + +// this.builder.inputType(name, { +// fields: () => data, +// }); + +// this.builder.configStore.associateRefWithName(dataInputRef, name); +// } + +// if (pagination && typeWithAggregationRef && paginationInputRef) { +// const namePaginationInput = upperCaseFirst(`${config.name}PaginationInput`); + +// let sortOrder = this.builder.configStore.getInputTypeRef('SortOrder'); + +// if (typeof sortOrder === 'string') { +// sortOrder = this.builder.enumType('SortOrder', { +// values: ['asc', 'desc'] as const, +// }); +// } + +// if (sortable?.fields && Array.isArray(sortable?.fields)) { +// const sortableFieldsType = this.builder.enumType(`${config.name}SortableFields`, { +// values: sortable.fields, +// }); + +// this.builder.inputType(namePaginationInput, { +// fields: (t) => ({ +// take: t.int({ required: false }), +// page: t.int({ required: false }), +// sortField: t.field({ required: false, type: sortableFieldsType, defaultValue: sortable?.defaultField }), +// sortOrder: t.field({ required: false, type: sortOrder, defaultValue: sortable?.defaultOrder }), +// match: t.string({ required: false }), +// }), +// }); +// } else { +// this.builder.inputType(namePaginationInput, { +// fields: (t) => ({ +// take: t.int({ required: false }), +// page: t.int({ required: false }), +// match: t.string({ required: false }), +// }), +// }); +// } + +// this.builder.configStore.associateRefWithName(paginationInputRef, namePaginationInput); + +// const nameWithAggregation = upperCaseFirst(`${config.name}WithAggregation`); + +// this.builder.simpleObject(nameWithAggregation, { +// fields: (t) => ({ +// currentPage: t.field({ type: 'Int' }), +// isFirstPage: t.field({ type: 'Boolean' }), +// isLastPage: t.field({ type: 'Boolean' }), +// previousPage: t.field({ type: 'Int', nullable: true }), +// nextPage: t.field({ type: 'Int', nullable: true }), +// pageCount: t.field({ type: 'Int' }), +// totalCount: t.field({ type: 'Int' }), +// data: t.field({ +// type: fieldOptions.type, +// }), +// }), +// }); + +// this.builder.configStore.associateRefWithName(typeWithAggregationRef, nameWithAggregation); +// } +// }); + +// return fieldRef; +// }; + +// rootBuilderProto.fieldWithPagination = function (fieldOptions) { +// const { typeWithAggregationRef, paginationArgsGroup } = usePaginationBuilder({ +// builder: this.builder, +// arg: this.arg, +// type: fieldOptions.type, +// }); + +// return this.field({ ...fieldOptions, type: typeWithAggregationRef, args: { ...paginationArgsGroup } } as never); +// }; + +// Object.defineProperty(rootBuilderProto, 'input', { +// get: function getInputBuilder(this: RootFieldBuilder) { +// return new InputFieldBuilder(this.builder, 'InputObject', `UnnamedWithInputOn${this.typename}`); +// }, +// }); diff --git a/apps/graphql/src/pagination/types.ts b/apps/graphql/src/pagination/types.ts new file mode 100644 index 0000000..e475f55 --- /dev/null +++ b/apps/graphql/src/pagination/types.ts @@ -0,0 +1,177 @@ +// /* eslint-disable @typescript-eslint/no-explicit-any */ +// /* eslint-disable fleek-custom/no-interface */ +// import { paginate } from '@fleek-platform/prisma'; +// import { +// FieldKind, +// FieldNullability, +// FieldRef, +// InputFieldMap, +// InputFieldRef, +// InputShapeFromFields, +// MaybePromise, +// SchemaTypes, +// ShapeFromTypeParam, +// TypeParam, +// } from '@pothos/core'; +// import { GraphQLResolveInfo } from 'graphql'; +// import type { PothosWithInputPlugin } from './plugin'; + +// type Sortable = { +// fields: string[]; +// defaultField: string; +// defaultOrder: 'asc' | 'desc'; +// }; + +// type PartialArgs = Args extends undefined +// ? object +// : { +// [key in Key]: InputFieldRef> | (true extends ArgRequired ? never : null | undefined)>; +// }; + +// type PaginationArgs = PaginationFlag extends true +// ? { +// filter?: InputFieldRef<{ sortField: string; sortOrder: string; take: number; page?: number; match: string }>; +// } +// : NonNullable; + +// type FieldWithPaginationOptionsFromKind< +// Types extends SchemaTypes, +// ParentShape, +// Type extends TypeParam, +// Nullable extends FieldNullability, +// Args extends InputFieldMap, +// Kind extends 'Query' | 'Mutation' | 'Object', +// _ResolveShape, +// ResolveReturnShape, +// PaginationFlag, +// > = FieldWithPaginationOptionsByKind[Kind]; + +// interface FieldWithPaginationOptionsByKind< +// Types extends SchemaTypes, +// ParentShape, +// Type extends TypeParam, +// Nullable extends FieldNullability, +// Args extends InputFieldMap, +// ResolveReturnShape, +// PaginationFlag, +// > { +// Query: QueryFieldOptions; +// Mutation: MutationFieldOptions; +// Object: ObjectFieldOptions; +// } + +// interface QueryFieldOptions< +// Types extends SchemaTypes, +// Type extends TypeParam, +// Nullable extends FieldNullability, +// Args extends InputFieldMap, +// ResolveReturnShape, +// PaginationFlag, +// > extends Omit, 'resolve'> { +// resolve: Resolver, Types['Context'], ShapeFromTypeParam, PaginationFlag, Nullable>; +// } + +// interface MutationFieldOptions< +// Types extends SchemaTypes, +// Type extends TypeParam, +// Nullable extends FieldNullability, +// Args extends InputFieldMap, +// ResolveReturnShape, +// PaginationFlag, +// > extends Omit, 'resolve'> { +// resolve: Resolver, Types['Context'], ShapeFromTypeParam, PaginationFlag, Nullable>; +// } + +// interface ObjectFieldOptions< +// Types extends SchemaTypes, +// ParentShape, +// Type extends TypeParam, +// Nullable extends FieldNullability, +// Args extends InputFieldMap, +// ResolveReturnShape, +// PaginationFlag, +// > extends Omit, 'resolve'> { +// resolve: Resolver, Types['Context'], ShapeFromTypeParam, PaginationFlag, Nullable>; +// } + +// type Resolver = ( +// parent: Parent, +// args: Args, +// context: Context, +// info: GraphQLResolveInfo, +// type: Type, +// ) => PaginationFlag extends true +// ? Type extends readonly any[] +// ? Nullable extends true +// ? MaybePromise> | null | undefined> +// : MaybePromise>> +// : never +// : MaybePromise; + +// declare global { +// // eslint-disable-next-line @typescript-eslint/no-namespace +// export namespace PothosSchemaTypes { +// export interface Plugins { +// inputGroup: PothosWithInputPlugin; +// } + +// export interface RootFieldBuilder { +// input: InputFieldBuilder; +// fieldWithInputGroup: < +// WhereArgs extends Record> | undefined, +// DataArgs extends Record> | undefined, +// PaginationFlag extends boolean, +// Type extends TypeParam, +// ResolveShape, +// ResolveReturnShape, +// ArgRequired extends boolean, +// Nullable extends FieldNullability = Types['DefaultFieldNullability'], +// >( +// options: Omit< +// FieldWithPaginationOptionsFromKind< +// Types, +// ParentShape, +// Type, +// Nullable, +// PartialArgs & PartialArgs & PaginationArgs, +// Extract, +// ResolveShape, +// ResolveReturnShape, +// PaginationFlag +// >, +// 'args' +// > & { +// args: { +// pagination: PaginationFlag; +// sortable?: Sortable; +// where?: WhereArgs; +// whereRequired?: boolean; +// data?: DataArgs; +// dataRequired?: boolean; +// }; +// }, +// ) => FieldRef>; +// fieldWithPagination: < +// Type extends TypeParam, +// ResolveShape, +// ResolveReturnShape, +// Nullable extends FieldNullability = Types['DefaultFieldNullability'], +// >( +// options: Omit< +// FieldWithPaginationOptionsFromKind< +// Types, +// ParentShape, +// Type, +// Nullable, +// PaginationArgs, +// Kind extends 'Object' ? 'Object' : never, +// ResolveShape, +// ResolveReturnShape, +// true +// >, +// 'args' +// >, +// ) => FieldRef>; +// } +// } +// } diff --git a/apps/graphql/src/pagination/usePaginationBuilder.ts b/apps/graphql/src/pagination/usePaginationBuilder.ts new file mode 100644 index 0000000..138932f --- /dev/null +++ b/apps/graphql/src/pagination/usePaginationBuilder.ts @@ -0,0 +1,79 @@ +import { ArgBuilder, OutputType, SchemaTypes, TypeParam } from '@pothos/core'; +import { upperCaseFirst } from 'upper-case-first'; + +type UsePaginationBuilderArgs = { + builder: PothosSchemaTypes.SchemaBuilder; + arg: ArgBuilder; + type: TypeParam; +}; + +export const usePaginationBuilder = ({ builder, arg, type }: UsePaginationBuilderArgs) => { + if (!Array.isArray(type) || typeof type[0] !== 'object' || !('name' in type[0])) { + // eslint-disable-next-line fleek-custom/no-default-error + throw new Error(`Type of field with pagination must be list e.g. type: [SomeType]`); + } + + const paginationInputTypeRef = getOrCreatePaginationInputTypeRef({ builder, name: 'PaginationInput' }); + + const typeWithAggregationRef = getOrCreateTypeWithAggregationRef({ + builder, + name: upperCaseFirst(`${type[0].name}sWithNestedAggregation`), + type, + }); + + const paginationArgsGroup = { filter: arg({ required: false, type: paginationInputTypeRef }) }; + + return { + typeWithAggregationRef, + paginationArgsGroup, + }; +}; + +type GetOrCreatePaginationInputTypeRefArgs = { + builder: PothosSchemaTypes.SchemaBuilder; + name: string; +}; + +const getOrCreatePaginationInputTypeRef = ({ builder, name }: GetOrCreatePaginationInputTypeRefArgs) => { + const originalRef = builder.configStore.getInputTypeRef(name); + + if (typeof originalRef !== 'string') { + return originalRef; + } + + return builder.inputType(name, { + fields: (t) => ({ + take: t.int(), + page: t.int({ required: false }), + }), + }); +}; + +type GetOrCreateTypeWithAggregationRefArgs = { + builder: PothosSchemaTypes.SchemaBuilder; + name: string; + type: TypeParam; +}; + +const getOrCreateTypeWithAggregationRef = ({ builder, name, type }: GetOrCreateTypeWithAggregationRefArgs) => { + const originalRef = builder.configStore.getOutputTypeRef(name); + + if (typeof originalRef !== 'string') { + return originalRef as OutputType; + } + + return builder.simpleObject(name, { + fields: (t) => ({ + currentPage: t.field({ type: 'Int' }), + isFirstPage: t.field({ type: 'Boolean' }), + isLastPage: t.field({ type: 'Boolean' }), + previousPage: t.field({ type: 'Int', nullable: true }), + nextPage: t.field({ type: 'Int', nullable: true }), + pageCount: t.field({ type: 'Int' }), + totalCount: t.field({ type: 'Int' }), + data: t.field({ + type, + }), + }), + }); +}; From cfdffe1c4a6dd7dc3d93bfa462a2d8602d72b9f1 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sat, 28 Jun 2025 17:58:40 +0200 Subject: [PATCH 02/12] implement simple pagination as Prisma extension --- apps/graphql/src/builder.ts | 3 +- apps/graphql/src/context.ts | 5 +-- apps/graphql/src/prisma.ts | 43 ++++++++++++++++++- .../src/utils/context/findOrCreateUser.ts | 4 +- .../utils/context/resolveUserFromContext.ts | 5 ++- .../context/resolveWorkspaceFromContext.ts | 5 ++- 6 files changed, 53 insertions(+), 12 deletions(-) diff --git a/apps/graphql/src/builder.ts b/apps/graphql/src/builder.ts index d231ca4..cb7d98c 100644 --- a/apps/graphql/src/builder.ts +++ b/apps/graphql/src/builder.ts @@ -3,6 +3,7 @@ import DataloaderPlugin from '@pothos/plugin-dataloader'; import ScopeAuthPlugin from '@pothos/plugin-scope-auth'; import SimpleObjectsPlugin from '@pothos/plugin-simple-objects'; import ZodPlugin from '@pothos/plugin-zod'; +// import PaginationPlugin from './pagination/plugin'; import { accessPolicyAuthScope, AccessPolicyAuthScopeArgs } from './authScopes/accessPolicyAuthScope'; import { forbiddenAuthScope, ForbiddenAuthScopeArgs } from './authScopes/forbiddenAuthScope'; import { modelItemsBelongToWorkspaceScope, ModelItemsBelongToWorkspaceScopeArgs } from './authScopes/modelItemsBelongToWorkspace'; @@ -49,4 +50,4 @@ export const builder = new SchemaBuilder<{ builder.queryType({}); -builder.mutationType({}); \ No newline at end of file +builder.mutationType({}); diff --git a/apps/graphql/src/context.ts b/apps/graphql/src/context.ts index f259460..5809067 100644 --- a/apps/graphql/src/context.ts +++ b/apps/graphql/src/context.ts @@ -1,7 +1,6 @@ import { createClerkClient } from '@clerk/backend'; -import { PrismaClient } from '@steadystart/prisma'; import { parseSecrets } from '@steadystart/secrets'; -import { prisma as productionPrisma } from './prisma'; +import { PaginatedPrisma, prisma as productionPrisma } from './prisma'; import { Request } from './types/Request'; import { resolveUserFromContext } from './utils/context/resolveUserFromContext'; import { resolveWorkspaceFromContext } from './utils/context/resolveWorkspaceFromContext'; @@ -9,7 +8,7 @@ import { resolveWorkspaceFromContext } from './utils/context/resolveWorkspaceFro export type ContextProps = { request: Request | undefined; test?: { - prisma: PrismaClient; + prisma: PaginatedPrisma; userId: string | undefined; workspaceId: string | undefined; }; diff --git a/apps/graphql/src/prisma.ts b/apps/graphql/src/prisma.ts index 7fe3b6a..da3b602 100644 --- a/apps/graphql/src/prisma.ts +++ b/apps/graphql/src/prisma.ts @@ -1,3 +1,42 @@ -import { PrismaClient } from '@steadystart/prisma'; +import { Prisma, PrismaClient } from '@steadystart/prisma'; -export const prisma = new PrismaClient(); +export type PaginationArgs = { + /** An index of page you want to get starting at 1. */ + page: number; + /** A number of rows on each page. */ + size: number; +}; + +export type PaginationResult = { + /** A requested index of a page you wanted to get starting at 1. */ + page: number; + /** A requested number of rows on each page */ + size: number; + rows: R[]; + /** A total number of rows that match all `where` conditions. It equals to a table size if no `where` conditions are specified. */ + totalSize: number; +}; + +export const prisma = new PrismaClient().$extends({ + model: { + $allModels: { + paginate, R = Prisma.Result>( + this: T, + args: A, + ): (paginationArgs: PaginationArgs) => Promise> { + const context: any = Prisma.getExtensionContext(this); + + return async ({ page, size }: PaginationArgs) => { + const take = Math.max(0, size); + const skip = (1 - Math.max(1, page)) * size; + + const [rows, totalSize] = await prisma.$transaction([context.findMany({ ...args, take, skip }), context.count(args)]); + + return { page, size, rows, totalSize }; + }; + }, + }, + }, +}); + +export type PaginatedPrisma = typeof prisma; diff --git a/apps/graphql/src/utils/context/findOrCreateUser.ts b/apps/graphql/src/utils/context/findOrCreateUser.ts index f933e24..3b9ab24 100644 --- a/apps/graphql/src/utils/context/findOrCreateUser.ts +++ b/apps/graphql/src/utils/context/findOrCreateUser.ts @@ -1,9 +1,9 @@ -import { PrismaClient } from '@steadystart/prisma'; +import { PaginatedPrisma } from '../../prisma'; type FindOrCreateUserArgs = { clerkUserId: string; emailAddress: string; - prisma: PrismaClient; + prisma: PaginatedPrisma; }; export const findOrCreateUser = async ({ clerkUserId, emailAddress, prisma }: FindOrCreateUserArgs) => { const user = await prisma.user.findFirst({ where: { clerkId: clerkUserId } }); diff --git a/apps/graphql/src/utils/context/resolveUserFromContext.ts b/apps/graphql/src/utils/context/resolveUserFromContext.ts index 5381e88..f2fd1d4 100644 --- a/apps/graphql/src/utils/context/resolveUserFromContext.ts +++ b/apps/graphql/src/utils/context/resolveUserFromContext.ts @@ -1,13 +1,14 @@ import { ClerkClient } from '@clerk/backend'; -import { PrismaClient, User } from '@steadystart/prisma'; +import { User } from '@steadystart/prisma'; import { Secrets } from '@steadystart/secrets'; import { match } from 'ts-pattern'; import { findOrCreateUser } from './findOrCreateUser'; import { getClerkSessionData } from './getClerkSessionData'; import { ContextProps } from '../../context'; +import { PaginatedPrisma } from '../../prisma'; type ResolveUserFromContextArgs = ContextProps & { - prisma: PrismaClient; + prisma: PaginatedPrisma; clerk: ClerkClient; secrets: Secrets; }; diff --git a/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts b/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts index 33d795d..3feba65 100644 --- a/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts +++ b/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts @@ -1,10 +1,11 @@ -import { PrismaClient, User, Workspace } from '@steadystart/prisma'; +import { User, Workspace } from '@steadystart/prisma'; import { match } from 'ts-pattern'; import { findAndValidateWorkspaceFromRequestHeaders } from './findAndValidateWorkspaceFromRequestHeaders'; import { ContextProps } from '../../context'; +import { PaginatedPrisma } from '../../prisma'; type ResolveWorkspaceFromContextArgs = ContextProps & { - prisma: PrismaClient; + prisma: PaginatedPrisma; user: User | null; }; From bcb00848caa6ca02c405266c7d5c37e8fe1227d3 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sat, 28 Jun 2025 17:59:10 +0200 Subject: [PATCH 03/12] comment out usePaginationBuilder --- .../src/pagination/usePaginationBuilder.ts | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/apps/graphql/src/pagination/usePaginationBuilder.ts b/apps/graphql/src/pagination/usePaginationBuilder.ts index 138932f..c9be5c1 100644 --- a/apps/graphql/src/pagination/usePaginationBuilder.ts +++ b/apps/graphql/src/pagination/usePaginationBuilder.ts @@ -1,79 +1,79 @@ -import { ArgBuilder, OutputType, SchemaTypes, TypeParam } from '@pothos/core'; -import { upperCaseFirst } from 'upper-case-first'; +// import { ArgBuilder, OutputType, SchemaTypes, TypeParam } from '@pothos/core'; +// import { upperCaseFirst } from 'upper-case-first'; -type UsePaginationBuilderArgs = { - builder: PothosSchemaTypes.SchemaBuilder; - arg: ArgBuilder; - type: TypeParam; -}; +// type UsePaginationBuilderArgs = { +// builder: PothosSchemaTypes.SchemaBuilder; +// arg: ArgBuilder; +// type: TypeParam; +// }; -export const usePaginationBuilder = ({ builder, arg, type }: UsePaginationBuilderArgs) => { - if (!Array.isArray(type) || typeof type[0] !== 'object' || !('name' in type[0])) { - // eslint-disable-next-line fleek-custom/no-default-error - throw new Error(`Type of field with pagination must be list e.g. type: [SomeType]`); - } +// export const usePaginationBuilder = ({ builder, arg, type }: UsePaginationBuilderArgs) => { +// if (!Array.isArray(type) || typeof type[0] !== 'object' || !('name' in type[0])) { +// // eslint-disable-next-line fleek-custom/no-default-error +// throw new Error(`Type of field with pagination must be list e.g. type: [SomeType]`); +// } - const paginationInputTypeRef = getOrCreatePaginationInputTypeRef({ builder, name: 'PaginationInput' }); +// const paginationInputTypeRef = getOrCreatePaginationInputTypeRef({ builder, name: 'PaginationInput' }); - const typeWithAggregationRef = getOrCreateTypeWithAggregationRef({ - builder, - name: upperCaseFirst(`${type[0].name}sWithNestedAggregation`), - type, - }); +// const typeWithAggregationRef = getOrCreateTypeWithAggregationRef({ +// builder, +// name: upperCaseFirst(`${type[0].name}sWithNestedAggregation`), +// type, +// }); - const paginationArgsGroup = { filter: arg({ required: false, type: paginationInputTypeRef }) }; +// const paginationArgsGroup = { filter: arg({ required: false, type: paginationInputTypeRef }) }; - return { - typeWithAggregationRef, - paginationArgsGroup, - }; -}; +// return { +// typeWithAggregationRef, +// paginationArgsGroup, +// }; +// }; -type GetOrCreatePaginationInputTypeRefArgs = { - builder: PothosSchemaTypes.SchemaBuilder; - name: string; -}; +// type GetOrCreatePaginationInputTypeRefArgs = { +// builder: PothosSchemaTypes.SchemaBuilder; +// name: string; +// }; -const getOrCreatePaginationInputTypeRef = ({ builder, name }: GetOrCreatePaginationInputTypeRefArgs) => { - const originalRef = builder.configStore.getInputTypeRef(name); +// const getOrCreatePaginationInputTypeRef = ({ builder, name }: GetOrCreatePaginationInputTypeRefArgs) => { +// const originalRef = builder.configStore.getInputTypeRef(name); - if (typeof originalRef !== 'string') { - return originalRef; - } +// if (typeof originalRef !== 'string') { +// return originalRef; +// } - return builder.inputType(name, { - fields: (t) => ({ - take: t.int(), - page: t.int({ required: false }), - }), - }); -}; +// return builder.inputType(name, { +// fields: (t) => ({ +// take: t.int(), +// page: t.int({ required: false }), +// }), +// }); +// }; -type GetOrCreateTypeWithAggregationRefArgs = { - builder: PothosSchemaTypes.SchemaBuilder; - name: string; - type: TypeParam; -}; +// type GetOrCreateTypeWithAggregationRefArgs = { +// builder: PothosSchemaTypes.SchemaBuilder; +// name: string; +// type: TypeParam; +// }; -const getOrCreateTypeWithAggregationRef = ({ builder, name, type }: GetOrCreateTypeWithAggregationRefArgs) => { - const originalRef = builder.configStore.getOutputTypeRef(name); +// const getOrCreateTypeWithAggregationRef = ({ builder, name, type }: GetOrCreateTypeWithAggregationRefArgs) => { +// const originalRef = builder.configStore.getOutputTypeRef(name); - if (typeof originalRef !== 'string') { - return originalRef as OutputType; - } +// if (typeof originalRef !== 'string') { +// return originalRef as OutputType; +// } - return builder.simpleObject(name, { - fields: (t) => ({ - currentPage: t.field({ type: 'Int' }), - isFirstPage: t.field({ type: 'Boolean' }), - isLastPage: t.field({ type: 'Boolean' }), - previousPage: t.field({ type: 'Int', nullable: true }), - nextPage: t.field({ type: 'Int', nullable: true }), - pageCount: t.field({ type: 'Int' }), - totalCount: t.field({ type: 'Int' }), - data: t.field({ - type, - }), - }), - }); -}; +// return builder.simpleObject(name, { +// fields: (t) => ({ +// currentPage: t.field({ type: 'Int' }), +// isFirstPage: t.field({ type: 'Boolean' }), +// isLastPage: t.field({ type: 'Boolean' }), +// previousPage: t.field({ type: 'Int', nullable: true }), +// nextPage: t.field({ type: 'Int', nullable: true }), +// pageCount: t.field({ type: 'Int' }), +// totalCount: t.field({ type: 'Int' }), +// data: t.field({ +// type, +// }), +// }), +// }); +// }; From 71500c02f2d74e5f0e60c8a79d42bf911707f872 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sat, 28 Jun 2025 18:01:58 +0200 Subject: [PATCH 04/12] move to plugins folder --- apps/graphql/src/{ => plugins}/pagination/plugin.ts | 0 apps/graphql/src/{ => plugins}/pagination/schemaBuilder.ts | 0 apps/graphql/src/{ => plugins}/pagination/types.ts | 0 apps/graphql/src/{ => plugins}/pagination/usePaginationBuilder.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename apps/graphql/src/{ => plugins}/pagination/plugin.ts (100%) rename apps/graphql/src/{ => plugins}/pagination/schemaBuilder.ts (100%) rename apps/graphql/src/{ => plugins}/pagination/types.ts (100%) rename apps/graphql/src/{ => plugins}/pagination/usePaginationBuilder.ts (100%) diff --git a/apps/graphql/src/pagination/plugin.ts b/apps/graphql/src/plugins/pagination/plugin.ts similarity index 100% rename from apps/graphql/src/pagination/plugin.ts rename to apps/graphql/src/plugins/pagination/plugin.ts diff --git a/apps/graphql/src/pagination/schemaBuilder.ts b/apps/graphql/src/plugins/pagination/schemaBuilder.ts similarity index 100% rename from apps/graphql/src/pagination/schemaBuilder.ts rename to apps/graphql/src/plugins/pagination/schemaBuilder.ts diff --git a/apps/graphql/src/pagination/types.ts b/apps/graphql/src/plugins/pagination/types.ts similarity index 100% rename from apps/graphql/src/pagination/types.ts rename to apps/graphql/src/plugins/pagination/types.ts diff --git a/apps/graphql/src/pagination/usePaginationBuilder.ts b/apps/graphql/src/plugins/pagination/usePaginationBuilder.ts similarity index 100% rename from apps/graphql/src/pagination/usePaginationBuilder.ts rename to apps/graphql/src/plugins/pagination/usePaginationBuilder.ts From 6eeb8191b03fed4608a097ec828f446714730b77 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sat, 28 Jun 2025 23:18:36 +0200 Subject: [PATCH 05/12] prisma changes --- libs/prisma/package.json | 3 ++- libs/prisma/src/index.ts | 45 +++++++++++++++++++++++++++++++++++ libs/prisma/src/schema.prisma | 2 +- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 libs/prisma/src/index.ts diff --git a/libs/prisma/package.json b/libs/prisma/package.json index 9312b6c..984c502 100644 --- a/libs/prisma/package.json +++ b/libs/prisma/package.json @@ -5,7 +5,8 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "build": "pnpm prisma generate", + "generate": "pnpm prisma generate", + "build": "pnpm generate && tsc ./src/index.ts --declaration --outDir ./dist", "watch": "tsc --build --watch --preserveWatchOutput --incremental", "migrate:new": "env-cmd -f ../../.env -- pnpm prisma migrate dev --create-only --skip-generate", "migrate:up": "env-cmd -f ../../.env -- pnpm prisma migrate dev --skip-generate", diff --git a/libs/prisma/src/index.ts b/libs/prisma/src/index.ts new file mode 100644 index 0000000..075e422 --- /dev/null +++ b/libs/prisma/src/index.ts @@ -0,0 +1,45 @@ +import { Prisma, PrismaClient as OriginalPrismaClient } from '../generated'; + +export * from '../generated'; + +export type PaginationArgs = { + /** An index of page you want to get starting at 1. */ + page: number; + /** A number of rows on each page. */ + size: number; +}; + +export type PaginationResult = { + /** A requested index of a page you wanted to get starting at 1. */ + page: number; + /** A requested number of rows on each page */ + size: number; + rows: R; + /** A total number of rows that match all `where` conditions. It equals to a table size if no `where` conditions are specified. */ + totalSize: number; +}; + +export const createPrismaClient = () => + new OriginalPrismaClient().$extends({ + model: { + $allModels: { + paginate, R = Prisma.Result>( + this: T, + args: A, + ): (paginationArgs: PaginationArgs) => Promise> { + const context: any = Prisma.getExtensionContext(this); + + return async ({ page, size }: PaginationArgs) => { + const take = Math.max(0, size); + const skip = (1 - Math.max(1, page)) * size; + + const [rows, totalSize] = await context.$transaction([context.findMany({ ...args, take, skip }), context.count(args)]); + + return { page, size, rows, totalSize }; + }; + }, + }, + }, + }); + +export type PrismaClient = ReturnType; diff --git a/libs/prisma/src/schema.prisma b/libs/prisma/src/schema.prisma index cbbffc9..9214fe2 100644 --- a/libs/prisma/src/schema.prisma +++ b/libs/prisma/src/schema.prisma @@ -6,6 +6,6 @@ datasource db { generator client { provider = "prisma-client-js" - output = "../dist" + output = "../generated" previewFeatures = ["prismaSchemaFolder"] } From 3f5fde3892b849669c22967bd1bc369a4c4b3cf5 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sat, 28 Jun 2025 23:18:45 +0200 Subject: [PATCH 06/12] graphql changes --- apps/graphql/src/context.ts | 4 +- apps/graphql/src/prisma.ts | 44 ++----------------- .../src/resolvers/post/queries/posts.ts | 6 +-- apps/graphql/src/tests/genqlCall.ts | 1 + .../src/utils/context/findOrCreateUser.ts | 4 +- .../utils/context/resolveUserFromContext.ts | 5 +-- .../context/resolveWorkspaceFromContext.ts | 4 +- .../utils/createContextForAuthScopeTest.ts | 2 +- 8 files changed, 16 insertions(+), 54 deletions(-) diff --git a/apps/graphql/src/context.ts b/apps/graphql/src/context.ts index 5809067..83d1bb4 100644 --- a/apps/graphql/src/context.ts +++ b/apps/graphql/src/context.ts @@ -1,6 +1,6 @@ import { createClerkClient } from '@clerk/backend'; import { parseSecrets } from '@steadystart/secrets'; -import { PaginatedPrisma, prisma as productionPrisma } from './prisma'; +import { PrismaClient, prisma as productionPrisma } from './prisma'; import { Request } from './types/Request'; import { resolveUserFromContext } from './utils/context/resolveUserFromContext'; import { resolveWorkspaceFromContext } from './utils/context/resolveWorkspaceFromContext'; @@ -8,7 +8,7 @@ import { resolveWorkspaceFromContext } from './utils/context/resolveWorkspaceFro export type ContextProps = { request: Request | undefined; test?: { - prisma: PaginatedPrisma; + prisma: PrismaClient; userId: string | undefined; workspaceId: string | undefined; }; diff --git a/apps/graphql/src/prisma.ts b/apps/graphql/src/prisma.ts index da3b602..5479c4c 100644 --- a/apps/graphql/src/prisma.ts +++ b/apps/graphql/src/prisma.ts @@ -1,42 +1,4 @@ -import { Prisma, PrismaClient } from '@steadystart/prisma'; +import { createPrismaClient } from '@steadystart/prisma'; +export type { PrismaClient } from '@steadystart/prisma'; -export type PaginationArgs = { - /** An index of page you want to get starting at 1. */ - page: number; - /** A number of rows on each page. */ - size: number; -}; - -export type PaginationResult = { - /** A requested index of a page you wanted to get starting at 1. */ - page: number; - /** A requested number of rows on each page */ - size: number; - rows: R[]; - /** A total number of rows that match all `where` conditions. It equals to a table size if no `where` conditions are specified. */ - totalSize: number; -}; - -export const prisma = new PrismaClient().$extends({ - model: { - $allModels: { - paginate, R = Prisma.Result>( - this: T, - args: A, - ): (paginationArgs: PaginationArgs) => Promise> { - const context: any = Prisma.getExtensionContext(this); - - return async ({ page, size }: PaginationArgs) => { - const take = Math.max(0, size); - const skip = (1 - Math.max(1, page)) * size; - - const [rows, totalSize] = await prisma.$transaction([context.findMany({ ...args, take, skip }), context.count(args)]); - - return { page, size, rows, totalSize }; - }; - }, - }, - }, -}); - -export type PaginatedPrisma = typeof prisma; +export const prisma = createPrismaClient(); diff --git a/apps/graphql/src/resolvers/post/queries/posts.ts b/apps/graphql/src/resolvers/post/queries/posts.ts index b2d054a..294a06f 100644 --- a/apps/graphql/src/resolvers/post/queries/posts.ts +++ b/apps/graphql/src/resolvers/post/queries/posts.ts @@ -9,13 +9,13 @@ builder.queryField('posts', (t) => userHasAccessOnWorkspace: true, }, resolve: async (_parent, _args, ctx) => { - const posts = await ctx.prisma.post.findMany({ + const posts = await ctx.prisma.post.paginate({ where: { workspaceId: ctx.workspace!.id, }, - }); + })({ size: 1, page: 1 }); - return posts; + return posts.rows; }, }), ); diff --git a/apps/graphql/src/tests/genqlCall.ts b/apps/graphql/src/tests/genqlCall.ts index 19e5b66..9828850 100644 --- a/apps/graphql/src/tests/genqlCall.ts +++ b/apps/graphql/src/tests/genqlCall.ts @@ -4,6 +4,7 @@ import { createContext } from '../context'; import { createClient, QueryRequest, QueryResult, MutationRequest, MutationResult } from '../generated/genql'; import { schema } from '../schema'; import { handleGraphQLError } from '../utils/handleGraphQLError'; + export type CreateClientWithContextArgs = { userId?: string; workspaceId?: string; diff --git a/apps/graphql/src/utils/context/findOrCreateUser.ts b/apps/graphql/src/utils/context/findOrCreateUser.ts index 3b9ab24..f933e24 100644 --- a/apps/graphql/src/utils/context/findOrCreateUser.ts +++ b/apps/graphql/src/utils/context/findOrCreateUser.ts @@ -1,9 +1,9 @@ -import { PaginatedPrisma } from '../../prisma'; +import { PrismaClient } from '@steadystart/prisma'; type FindOrCreateUserArgs = { clerkUserId: string; emailAddress: string; - prisma: PaginatedPrisma; + prisma: PrismaClient; }; export const findOrCreateUser = async ({ clerkUserId, emailAddress, prisma }: FindOrCreateUserArgs) => { const user = await prisma.user.findFirst({ where: { clerkId: clerkUserId } }); diff --git a/apps/graphql/src/utils/context/resolveUserFromContext.ts b/apps/graphql/src/utils/context/resolveUserFromContext.ts index f2fd1d4..5381e88 100644 --- a/apps/graphql/src/utils/context/resolveUserFromContext.ts +++ b/apps/graphql/src/utils/context/resolveUserFromContext.ts @@ -1,14 +1,13 @@ import { ClerkClient } from '@clerk/backend'; -import { User } from '@steadystart/prisma'; +import { PrismaClient, User } from '@steadystart/prisma'; import { Secrets } from '@steadystart/secrets'; import { match } from 'ts-pattern'; import { findOrCreateUser } from './findOrCreateUser'; import { getClerkSessionData } from './getClerkSessionData'; import { ContextProps } from '../../context'; -import { PaginatedPrisma } from '../../prisma'; type ResolveUserFromContextArgs = ContextProps & { - prisma: PaginatedPrisma; + prisma: PrismaClient; clerk: ClerkClient; secrets: Secrets; }; diff --git a/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts b/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts index 3feba65..56678cf 100644 --- a/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts +++ b/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts @@ -2,10 +2,10 @@ import { User, Workspace } from '@steadystart/prisma'; import { match } from 'ts-pattern'; import { findAndValidateWorkspaceFromRequestHeaders } from './findAndValidateWorkspaceFromRequestHeaders'; import { ContextProps } from '../../context'; -import { PaginatedPrisma } from '../../prisma'; +import { PrismaClient } from '../../prisma'; type ResolveWorkspaceFromContextArgs = ContextProps & { - prisma: PaginatedPrisma; + prisma: PrismaClient; user: User | null; }; diff --git a/apps/graphql/src/utils/createContextForAuthScopeTest.ts b/apps/graphql/src/utils/createContextForAuthScopeTest.ts index f663ba0..3f14078 100644 --- a/apps/graphql/src/utils/createContextForAuthScopeTest.ts +++ b/apps/graphql/src/utils/createContextForAuthScopeTest.ts @@ -1,5 +1,5 @@ -import { PrismaClient } from '@steadystart/prisma'; import { Context } from '../context'; +import { PrismaClient } from '../prisma'; type CreateContextForAuthScopeTestArgs = { user: Context['user']; From 7c1026a9da3a6d4dabd39dc0c2779f54b13fe016 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sat, 28 Jun 2025 23:18:50 +0200 Subject: [PATCH 07/12] tester changes --- libs/tester/src/lib/TestDatabaseOrchestrator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/tester/src/lib/TestDatabaseOrchestrator.ts b/libs/tester/src/lib/TestDatabaseOrchestrator.ts index b143f61..7934232 100644 --- a/libs/tester/src/lib/TestDatabaseOrchestrator.ts +++ b/libs/tester/src/lib/TestDatabaseOrchestrator.ts @@ -1,4 +1,3 @@ -import { PrismaClient } from '@steadystart/prisma'; import * as path from 'path'; import { v1, v4 } from 'uuid'; import { exec } from '../utils/exec'; @@ -51,7 +50,7 @@ export class TestDatabaseOrchestrator { return this.getConnectionString(); } - public async getTestDatabaseWithPrismaClient(): Promise { + public async getTestDatabaseWithPrismaClient(): Promise { // Check if the docker container exists const { stdout } = await exec(`docker ps -a --filter name=${this.serviceName} --format '{{.Names}}'`); From 2b631cce75832b5b9188fc3305425f2d3cdc7c02 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sat, 28 Jun 2025 23:36:31 +0200 Subject: [PATCH 08/12] prisma --- apps/graphql/src/prisma.ts | 2 +- libs/prisma/src/index.ts | 4 ++-- .../src/generators/db-dump-with-seeded-data-generator.ts | 6 +++--- libs/tester/src/lib/TestDatabaseOrchestrator.ts | 7 ++++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/graphql/src/prisma.ts b/apps/graphql/src/prisma.ts index 5479c4c..dd89b10 100644 --- a/apps/graphql/src/prisma.ts +++ b/apps/graphql/src/prisma.ts @@ -1,4 +1,4 @@ import { createPrismaClient } from '@steadystart/prisma'; export type { PrismaClient } from '@steadystart/prisma'; -export const prisma = createPrismaClient(); +export const prisma = createPrismaClient({}); diff --git a/libs/prisma/src/index.ts b/libs/prisma/src/index.ts index 075e422..e71e859 100644 --- a/libs/prisma/src/index.ts +++ b/libs/prisma/src/index.ts @@ -19,8 +19,8 @@ export type PaginationResult = { totalSize: number; }; -export const createPrismaClient = () => - new OriginalPrismaClient().$extends({ +export const createPrismaClient = (prismaClientOptions: Prisma.PrismaClientOptions) => + new OriginalPrismaClient(prismaClientOptions).$extends({ model: { $allModels: { paginate, R = Prisma.Result>( diff --git a/libs/tester/src/generators/db-dump-with-seeded-data-generator.ts b/libs/tester/src/generators/db-dump-with-seeded-data-generator.ts index 3ce67a7..8e40577 100644 --- a/libs/tester/src/generators/db-dump-with-seeded-data-generator.ts +++ b/libs/tester/src/generators/db-dump-with-seeded-data-generator.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from '@steadystart/prisma'; +import { createPrismaClient } from '@steadystart/prisma'; import * as path from 'path'; import { seedFullDatabase } from '../lib/seed/seedFullDatabase'; import { TestDatabaseOrchestrator } from '../lib/TestDatabaseOrchestrator'; @@ -13,10 +13,10 @@ import { exec } from '../utils/exec'; const prismaBinary = path.join(__dirname, '..', '..', '..', '..', 'libs', 'prisma', 'node_modules', '.bin', 'prisma'); await exec( - `pnpm cross-env DATABASE_URL="${databaseUrl}" DATABASE_DIRECT_URL="${databaseUrl}" ${prismaBinary} db push --accept-data-loss --schema ./../../libs/prisma/dist/schema.prisma`, + `pnpm cross-env DATABASE_URL="${databaseUrl}" DATABASE_DIRECT_URL="${databaseUrl}" ${prismaBinary} db push --accept-data-loss --schema ./../../libs/prisma/generated/schema.prisma`, ); - const client = new PrismaClient({ datasources: { db: { url: databaseUrl } } }); + const client = createPrismaClient({ datasources: { db: { url: databaseUrl } } }); await seedFullDatabase({ prisma: client }); diff --git a/libs/tester/src/lib/TestDatabaseOrchestrator.ts b/libs/tester/src/lib/TestDatabaseOrchestrator.ts index 7934232..b9d1df7 100644 --- a/libs/tester/src/lib/TestDatabaseOrchestrator.ts +++ b/libs/tester/src/lib/TestDatabaseOrchestrator.ts @@ -1,3 +1,4 @@ +import { createPrismaClient, PrismaClient } from '@steadystart/prisma'; import * as path from 'path'; import { v1, v4 } from 'uuid'; import { exec } from '../utils/exec'; @@ -50,7 +51,7 @@ export class TestDatabaseOrchestrator { return this.getConnectionString(); } - public async getTestDatabaseWithPrismaClient(): Promise { + public async getTestDatabaseWithPrismaClient(): Promise { // Check if the docker container exists const { stdout } = await exec(`docker ps -a --filter name=${this.serviceName} --format '{{.Names}}'`); @@ -76,7 +77,7 @@ export class TestDatabaseOrchestrator { await this.loadDatabaseDump(); const databaseUrl = this.getConnectionString(); - const prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } } }); + const prisma = createPrismaClient({ datasources: { db: { url: databaseUrl } } }); return prisma; } @@ -106,7 +107,7 @@ export class TestDatabaseOrchestrator { public async waitForDatabase(): Promise { const baseConnection = `postgresql://${this.user}:${this.password}@${this.host}:${this.port}`; - const client = new PrismaClient({ + const client = createPrismaClient({ datasources: { db: { url: baseConnection, From 44f8bf3d2ab9f6bfa43d32ad8964f1e4036e0170 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sun, 29 Jun 2025 00:30:50 +0200 Subject: [PATCH 09/12] WIP --- apps/graphql/src/builder.ts | 4 +- apps/graphql/src/plugins/pagination/plugin.ts | 77 +++-- .../src/plugins/pagination/schemaBuilder.ts | 160 +--------- apps/graphql/src/plugins/pagination/types.ts | 279 +++++++----------- 4 files changed, 163 insertions(+), 357 deletions(-) diff --git a/apps/graphql/src/builder.ts b/apps/graphql/src/builder.ts index cb7d98c..00e552e 100644 --- a/apps/graphql/src/builder.ts +++ b/apps/graphql/src/builder.ts @@ -3,7 +3,7 @@ import DataloaderPlugin from '@pothos/plugin-dataloader'; import ScopeAuthPlugin from '@pothos/plugin-scope-auth'; import SimpleObjectsPlugin from '@pothos/plugin-simple-objects'; import ZodPlugin from '@pothos/plugin-zod'; -// import PaginationPlugin from './pagination/plugin'; +import PaginationPlugin from './pagination/plugin'; import { accessPolicyAuthScope, AccessPolicyAuthScopeArgs } from './authScopes/accessPolicyAuthScope'; import { forbiddenAuthScope, ForbiddenAuthScopeArgs } from './authScopes/forbiddenAuthScope'; import { modelItemsBelongToWorkspaceScope, ModelItemsBelongToWorkspaceScopeArgs } from './authScopes/modelItemsBelongToWorkspace'; @@ -34,7 +34,7 @@ export const builder = new SchemaBuilder<{ }>({ defaultFieldNullability: false, defaultInputFieldRequiredness: true, - plugins: [ZodPlugin, ScopeAuthPlugin, SimpleObjectsPlugin, DataloaderPlugin], + plugins: [ZodPlugin, ScopeAuthPlugin, SimpleObjectsPlugin, DataloaderPlugin, PaginationPlugin], scopeAuth: { defaultStrategy: 'all', authScopes: async (ctx) => { diff --git a/apps/graphql/src/plugins/pagination/plugin.ts b/apps/graphql/src/plugins/pagination/plugin.ts index 63b40d7..9f04241 100644 --- a/apps/graphql/src/plugins/pagination/plugin.ts +++ b/apps/graphql/src/plugins/pagination/plugin.ts @@ -1,39 +1,38 @@ -// import './types'; -// import './schemaBuilder'; - -// import { paginate } from '@fleek-platform/prisma'; -// import SchemaBuilder, { BasePlugin, SchemaTypes } from '@pothos/core'; -// import { GraphQLFieldResolver } from 'graphql'; - -// const pluginName = 'inputGroup' as const; - -// // eslint-disable-next-line import/no-default-export -// export default pluginName; - -// export class PothosWithInputPlugin extends BasePlugin { -// wrapResolve(resolver: GraphQLFieldResolver | string>>): GraphQLFieldResolver< -// unknown, -// Types['Context'], -// { -// filter?: { sortField: string; sortOrder: string; take: number; page?: number }; -// } -// > { -// return async (parent, args, context, info) => { -// const result = await resolver(parent, args, context, info); - -// if (typeof result === 'object' && result !== null && 'withPages' in result && typeof result.withPages === 'function') { -// const [data, paginationMetadata] = await result.withPages({ -// limit: args.filter?.take ?? 100, -// page: args.filter?.page ?? 1, -// includePageCount: true, -// }); - -// return { data, ...paginationMetadata }; -// } - -// return result; -// }; -// } -// } - -// SchemaBuilder.registerPlugin(pluginName, PothosWithInputPlugin); +import './types'; +import './schemaBuilder'; + +import SchemaBuilder, { BasePlugin, SchemaTypes } from '@pothos/core'; +import { PaginationArgs, PaginationResult } from '@steadystart/prisma'; +import { GraphQLFieldResolver } from 'graphql'; + +const pluginName = 'pagination' as const; + +function isPaginationResult(result: unknown): result is (args: PaginationArgs) => Promise> { + return typeof result === 'function'; +} + +// eslint-disable-next-line import/no-default-export +export default class PaginationPlugin extends BasePlugin { + wrapResolve(resolver: GraphQLFieldResolver>): GraphQLFieldResolver< + unknown, + Types['Context'], + { + filter?: { size: number; page?: number }; + } + > { + return async (parent, args, context, info) => { + const result = await resolver(parent, args, context, info); + + if (isPaginationResult(result)) { + return result({ + size: args.filter?.size ?? 100, + page: args.filter?.page ?? 1, + }); + } + + return result; + }; + } +} + +SchemaBuilder.registerPlugin(pluginName, PaginationPlugin); diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder.ts b/apps/graphql/src/plugins/pagination/schemaBuilder.ts index 32985d2..4574035 100644 --- a/apps/graphql/src/plugins/pagination/schemaBuilder.ts +++ b/apps/graphql/src/plugins/pagination/schemaBuilder.ts @@ -1,151 +1,15 @@ -// import { InputFieldBuilder, RootFieldBuilder, SchemaTypes } from '@pothos/core'; -// import { upperCaseFirst } from 'upper-case-first'; +import { InputFieldBuilder, RootFieldBuilder, SchemaTypes } from '@pothos/core'; +import { upperCaseFirst } from 'upper-case-first'; +import { usePaginationBuilder } from './usePaginationBuilder'; -// import { usePaginationBuilder } from './usePaginationBuilder'; +const rootBuilderProto = RootFieldBuilder.prototype as PothosSchemaTypes.RootFieldBuilder; -// const rootBuilderProto = RootFieldBuilder.prototype as PothosSchemaTypes.RootFieldBuilder; +rootBuilderProto.fieldWithPagination = function (fieldOptions: { type: any }) { + const { typeWithAggregationRef, paginationArgsGroup } = usePaginationBuilder({ + builder: this.builder, + arg: this.arg, + type: fieldOptions.type, + }); -// rootBuilderProto.fieldWithInputGroup = function ({ -// args: { where, whereRequired, data, dataRequired, pagination, sortable }, -// ...fieldOptions -// }) { -// const whereInputRef = where ? this.builder.inputRef(upperCaseFirst(`${this.typename}WhereInput`)) : null; -// const dataInputRef = data ? this.builder.inputRef(upperCaseFirst(`${this.typename}DataInput`)) : null; -// const paginationInputRef = pagination ? this.builder.inputRef(upperCaseFirst(`${this.typename}PaginationInput`)) : null; -// const typeWithAggregationRef = pagination ? this.builder.objectRef(upperCaseFirst(`${this.typename}WithAggregation`)) : null; - -// const whereArgsGroup = whereInputRef -// ? { -// where: this.arg({ -// required: whereRequired ?? true, -// type: whereInputRef, -// }), -// } -// : {}; - -// const dataArgsGroup = dataInputRef -// ? { -// data: this.arg({ -// required: dataRequired ?? true, -// type: dataInputRef, -// }), -// } -// : {}; - -// const paginationArgsGroup = -// pagination && paginationInputRef -// ? { -// filter: this.arg({ -// required: false, -// type: paginationInputRef, -// defaultValue: { -// sortField: sortable?.defaultField, -// sortOrder: sortable?.defaultOrder, -// }, -// }), -// } -// : {}; - -// const fieldRef = this.field({ -// ...fieldOptions, -// type: typeWithAggregationRef ?? fieldOptions.type, -// args: { ...whereArgsGroup, ...dataArgsGroup, ...paginationArgsGroup }, -// } as never); - -// this.builder.configStore.onFieldUse(fieldRef, (config) => { -// if (where && whereInputRef) { -// const name = upperCaseFirst(`${config.name}WhereInput`); - -// this.builder.inputType(name, { -// fields: () => where, -// }); - -// this.builder.configStore.associateRefWithName(whereInputRef, name); -// } - -// if (data && dataInputRef) { -// const name = upperCaseFirst(`${config.name}DataInput`); - -// this.builder.inputType(name, { -// fields: () => data, -// }); - -// this.builder.configStore.associateRefWithName(dataInputRef, name); -// } - -// if (pagination && typeWithAggregationRef && paginationInputRef) { -// const namePaginationInput = upperCaseFirst(`${config.name}PaginationInput`); - -// let sortOrder = this.builder.configStore.getInputTypeRef('SortOrder'); - -// if (typeof sortOrder === 'string') { -// sortOrder = this.builder.enumType('SortOrder', { -// values: ['asc', 'desc'] as const, -// }); -// } - -// if (sortable?.fields && Array.isArray(sortable?.fields)) { -// const sortableFieldsType = this.builder.enumType(`${config.name}SortableFields`, { -// values: sortable.fields, -// }); - -// this.builder.inputType(namePaginationInput, { -// fields: (t) => ({ -// take: t.int({ required: false }), -// page: t.int({ required: false }), -// sortField: t.field({ required: false, type: sortableFieldsType, defaultValue: sortable?.defaultField }), -// sortOrder: t.field({ required: false, type: sortOrder, defaultValue: sortable?.defaultOrder }), -// match: t.string({ required: false }), -// }), -// }); -// } else { -// this.builder.inputType(namePaginationInput, { -// fields: (t) => ({ -// take: t.int({ required: false }), -// page: t.int({ required: false }), -// match: t.string({ required: false }), -// }), -// }); -// } - -// this.builder.configStore.associateRefWithName(paginationInputRef, namePaginationInput); - -// const nameWithAggregation = upperCaseFirst(`${config.name}WithAggregation`); - -// this.builder.simpleObject(nameWithAggregation, { -// fields: (t) => ({ -// currentPage: t.field({ type: 'Int' }), -// isFirstPage: t.field({ type: 'Boolean' }), -// isLastPage: t.field({ type: 'Boolean' }), -// previousPage: t.field({ type: 'Int', nullable: true }), -// nextPage: t.field({ type: 'Int', nullable: true }), -// pageCount: t.field({ type: 'Int' }), -// totalCount: t.field({ type: 'Int' }), -// data: t.field({ -// type: fieldOptions.type, -// }), -// }), -// }); - -// this.builder.configStore.associateRefWithName(typeWithAggregationRef, nameWithAggregation); -// } -// }); - -// return fieldRef; -// }; - -// rootBuilderProto.fieldWithPagination = function (fieldOptions) { -// const { typeWithAggregationRef, paginationArgsGroup } = usePaginationBuilder({ -// builder: this.builder, -// arg: this.arg, -// type: fieldOptions.type, -// }); - -// return this.field({ ...fieldOptions, type: typeWithAggregationRef, args: { ...paginationArgsGroup } } as never); -// }; - -// Object.defineProperty(rootBuilderProto, 'input', { -// get: function getInputBuilder(this: RootFieldBuilder) { -// return new InputFieldBuilder(this.builder, 'InputObject', `UnnamedWithInputOn${this.typename}`); -// }, -// }); + return this.field({ ...fieldOptions, type: typeWithAggregationRef, args: { ...paginationArgsGroup } } as never); +}; diff --git a/apps/graphql/src/plugins/pagination/types.ts b/apps/graphql/src/plugins/pagination/types.ts index e475f55..954479b 100644 --- a/apps/graphql/src/plugins/pagination/types.ts +++ b/apps/graphql/src/plugins/pagination/types.ts @@ -1,177 +1,120 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// /* eslint-disable fleek-custom/no-interface */ -// import { paginate } from '@fleek-platform/prisma'; -// import { -// FieldKind, -// FieldNullability, -// FieldRef, -// InputFieldMap, -// InputFieldRef, -// InputShapeFromFields, -// MaybePromise, -// SchemaTypes, -// ShapeFromTypeParam, -// TypeParam, -// } from '@pothos/core'; -// import { GraphQLResolveInfo } from 'graphql'; -// import type { PothosWithInputPlugin } from './plugin'; +import { + FieldKind, + FieldNullability, + FieldRef, + InputFieldMap, + InputFieldRef, + InputShapeFromFields, + MaybePromise, + SchemaTypes, + ShapeFromTypeParam, + TypeParam, +} from '@pothos/core'; +import { GraphQLResolveInfo } from 'graphql'; +import PaginationPlugin from './plugin'; -// type Sortable = { -// fields: string[]; -// defaultField: string; -// defaultOrder: 'asc' | 'desc'; -// }; +type PartialArgs = Args extends undefined + ? object + : { + [key in Key]: InputFieldRef> | undefined>; + }; -// type PartialArgs = Args extends undefined -// ? object -// : { -// [key in Key]: InputFieldRef> | (true extends ArgRequired ? never : null | undefined)>; -// }; +type FieldWithPaginationOptionsFromKind< + Types extends SchemaTypes, + ParentShape, + Type extends TypeParam, + Nullable extends FieldNullability, + Args extends InputFieldMap, + Kind extends 'Query' | 'Mutation' | 'Object', + _ResolveShape, + ResolveReturnShape, +> = FieldWithPaginationOptionsByKind[Kind]; -// type PaginationArgs = PaginationFlag extends true -// ? { -// filter?: InputFieldRef<{ sortField: string; sortOrder: string; take: number; page?: number; match: string }>; -// } -// : NonNullable; +interface FieldWithPaginationOptionsByKind< + Types extends SchemaTypes, + ParentShape, + Type extends TypeParam, + Nullable extends FieldNullability, + Args extends InputFieldMap, + ResolveReturnShape, +> { + Query: QueryFieldOptions; + Mutation: MutationFieldOptions; + Object: ObjectFieldOptions; +} -// type FieldWithPaginationOptionsFromKind< -// Types extends SchemaTypes, -// ParentShape, -// Type extends TypeParam, -// Nullable extends FieldNullability, -// Args extends InputFieldMap, -// Kind extends 'Query' | 'Mutation' | 'Object', -// _ResolveShape, -// ResolveReturnShape, -// PaginationFlag, -// > = FieldWithPaginationOptionsByKind[Kind]; +interface QueryFieldOptions< + Types extends SchemaTypes, + Type extends TypeParam, + Nullable extends FieldNullability, + Args extends InputFieldMap, + ResolveReturnShape, +> extends Omit, 'resolve'> { + resolve: Resolver, Types['Context'], ShapeFromTypeParam, Nullable>; +} -// interface FieldWithPaginationOptionsByKind< -// Types extends SchemaTypes, -// ParentShape, -// Type extends TypeParam, -// Nullable extends FieldNullability, -// Args extends InputFieldMap, -// ResolveReturnShape, -// PaginationFlag, -// > { -// Query: QueryFieldOptions; -// Mutation: MutationFieldOptions; -// Object: ObjectFieldOptions; -// } +interface MutationFieldOptions< + Types extends SchemaTypes, + Type extends TypeParam, + Nullable extends FieldNullability, + Args extends InputFieldMap, + ResolveReturnShape, +> extends Omit, 'resolve'> { + resolve: Resolver, Types['Context'], ShapeFromTypeParam, Nullable>; +} -// interface QueryFieldOptions< -// Types extends SchemaTypes, -// Type extends TypeParam, -// Nullable extends FieldNullability, -// Args extends InputFieldMap, -// ResolveReturnShape, -// PaginationFlag, -// > extends Omit, 'resolve'> { -// resolve: Resolver, Types['Context'], ShapeFromTypeParam, PaginationFlag, Nullable>; -// } +interface ObjectFieldOptions< + Types extends SchemaTypes, + ParentShape, + Type extends TypeParam, + Nullable extends FieldNullability, + Args extends InputFieldMap, + ResolveReturnShape, +> extends Omit, 'resolve'> { + resolve: Resolver, Types['Context'], ShapeFromTypeParam, Nullable>; +} -// interface MutationFieldOptions< -// Types extends SchemaTypes, -// Type extends TypeParam, -// Nullable extends FieldNullability, -// Args extends InputFieldMap, -// ResolveReturnShape, -// PaginationFlag, -// > extends Omit, 'resolve'> { -// resolve: Resolver, Types['Context'], ShapeFromTypeParam, PaginationFlag, Nullable>; -// } +type Resolver = ( + parent: Parent, + args: Args, + context: Context, + info: GraphQLResolveInfo, + type: Type, +) => Type extends readonly any[] + ? Nullable extends true + ? MaybePromise Promise> | null | undefined> + : MaybePromise Promise>> + : never; -// interface ObjectFieldOptions< -// Types extends SchemaTypes, -// ParentShape, -// Type extends TypeParam, -// Nullable extends FieldNullability, -// Args extends InputFieldMap, -// ResolveReturnShape, -// PaginationFlag, -// > extends Omit, 'resolve'> { -// resolve: Resolver, Types['Context'], ShapeFromTypeParam, PaginationFlag, Nullable>; -// } +declare global { + export namespace PothosSchemaTypes { + export interface Plugins { + pagination: PaginationPlugin; + } -// type Resolver = ( -// parent: Parent, -// args: Args, -// context: Context, -// info: GraphQLResolveInfo, -// type: Type, -// ) => PaginationFlag extends true -// ? Type extends readonly any[] -// ? Nullable extends true -// ? MaybePromise> | null | undefined> -// : MaybePromise>> -// : never -// : MaybePromise; - -// declare global { -// // eslint-disable-next-line @typescript-eslint/no-namespace -// export namespace PothosSchemaTypes { -// export interface Plugins { -// inputGroup: PothosWithInputPlugin; -// } - -// export interface RootFieldBuilder { -// input: InputFieldBuilder; -// fieldWithInputGroup: < -// WhereArgs extends Record> | undefined, -// DataArgs extends Record> | undefined, -// PaginationFlag extends boolean, -// Type extends TypeParam, -// ResolveShape, -// ResolveReturnShape, -// ArgRequired extends boolean, -// Nullable extends FieldNullability = Types['DefaultFieldNullability'], -// >( -// options: Omit< -// FieldWithPaginationOptionsFromKind< -// Types, -// ParentShape, -// Type, -// Nullable, -// PartialArgs & PartialArgs & PaginationArgs, -// Extract, -// ResolveShape, -// ResolveReturnShape, -// PaginationFlag -// >, -// 'args' -// > & { -// args: { -// pagination: PaginationFlag; -// sortable?: Sortable; -// where?: WhereArgs; -// whereRequired?: boolean; -// data?: DataArgs; -// dataRequired?: boolean; -// }; -// }, -// ) => FieldRef>; -// fieldWithPagination: < -// Type extends TypeParam, -// ResolveShape, -// ResolveReturnShape, -// Nullable extends FieldNullability = Types['DefaultFieldNullability'], -// >( -// options: Omit< -// FieldWithPaginationOptionsFromKind< -// Types, -// ParentShape, -// Type, -// Nullable, -// PaginationArgs, -// Kind extends 'Object' ? 'Object' : never, -// ResolveShape, -// ResolveReturnShape, -// true -// >, -// 'args' -// >, -// ) => FieldRef>; -// } -// } -// } + export interface RootFieldBuilder { + paginatedField: < + Type extends TypeParam, + ResolveShape, + ResolveReturnShape, + Nullable extends FieldNullability = Types['DefaultFieldNullability'], + >( + options: Omit< + FieldWithPaginationOptionsFromKind< + Types, + ParentShape, + Type, + Nullable, + { + filter?: InputFieldRef<{ size: number; page?: number }>; + }, + Extract, + ResolveShape, + ResolveReturnShape + >, + 'args' + >, + ) => FieldRef>; + } + } +} From 3529c4fa50f187b2d648f84ecc5652604718e1c8 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Thu, 3 Jul 2025 20:31:57 +0200 Subject: [PATCH 10/12] WIP --- apps/graphql/package.json | 5 +- .../src/plugins/pagination/global-types.ts | 22 ++++ apps/graphql/src/plugins/pagination/plugin.ts | 2 +- .../src/plugins/pagination/schemaBuilder.ts | 15 --- .../getOrCreatePaginationInputTypeRef.ts | 21 +++ .../getOrCreateTypeWithAggregationRef.ts | 30 +++++ .../pagination/schemaBuilder/schemaBuilder.ts | 12 ++ .../schemaBuilder/usePaginationBuilder.ts | 48 +++++++ apps/graphql/src/plugins/pagination/types.ts | 122 ++++++------------ .../pagination/usePaginationBuilder.ts | 79 ------------ pnpm-lock.yaml | 19 ++- 11 files changed, 188 insertions(+), 187 deletions(-) create mode 100644 apps/graphql/src/plugins/pagination/global-types.ts delete mode 100644 apps/graphql/src/plugins/pagination/schemaBuilder.ts create mode 100644 apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreatePaginationInputTypeRef.ts create mode 100644 apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts create mode 100644 apps/graphql/src/plugins/pagination/schemaBuilder/schemaBuilder.ts create mode 100644 apps/graphql/src/plugins/pagination/schemaBuilder/usePaginationBuilder.ts delete mode 100644 apps/graphql/src/plugins/pagination/usePaginationBuilder.ts diff --git a/apps/graphql/package.json b/apps/graphql/package.json index 24724a7..39d6862 100644 --- a/apps/graphql/package.json +++ b/apps/graphql/package.json @@ -38,22 +38,23 @@ "graphql-yoga": "^5.6.2", "ts-pattern": "^5.6.2", "type-fest": "^4.35.0", + "upper-case-first": "^3.0.0", "zod": "3.22.4" }, "devDependencies": { - "@genql/runtime": "2.6.0", "@eslint/eslintrc": "^3.3.0", "@genql/cli": "2.6.0", + "@genql/runtime": "2.6.0", "@jest/types": "^29.6.3", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/jest": "^29.5.14", "cross-env": "^7.0.3", + "dotenv-cli": "^8.0.0", "eslint": "^9.1.0", "jest": "^29.7.0", "nodemon": "^3.1.4", "ts-jest": "^29.3.1", - "dotenv-cli": "^8.0.0", "ts-node": "^10.9.2", "typescript": "^5" } diff --git a/apps/graphql/src/plugins/pagination/global-types.ts b/apps/graphql/src/plugins/pagination/global-types.ts new file mode 100644 index 0000000..5f745fb --- /dev/null +++ b/apps/graphql/src/plugins/pagination/global-types.ts @@ -0,0 +1,22 @@ +import { FieldKind, FieldNullability, FieldRef, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; +import PaginationPlugin from './plugin'; +import { PaginatedField } from './types'; + +declare global { + export namespace PothosSchemaTypes { + export interface Plugins { + pagination: PaginationPlugin; + } + + export interface RootFieldBuilder { + paginatedField: < + Type extends TypeParam, + ResolveShape, + ReturnResolveShape, + Nullable extends FieldNullability = Types['DefaultFieldNullability'], + >( + options: PaginatedField, + ) => FieldRef>; + } + } +} diff --git a/apps/graphql/src/plugins/pagination/plugin.ts b/apps/graphql/src/plugins/pagination/plugin.ts index 9f04241..3f2c78e 100644 --- a/apps/graphql/src/plugins/pagination/plugin.ts +++ b/apps/graphql/src/plugins/pagination/plugin.ts @@ -1,4 +1,4 @@ -import './types'; +import './global-types'; import './schemaBuilder'; import SchemaBuilder, { BasePlugin, SchemaTypes } from '@pothos/core'; diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder.ts b/apps/graphql/src/plugins/pagination/schemaBuilder.ts deleted file mode 100644 index 4574035..0000000 --- a/apps/graphql/src/plugins/pagination/schemaBuilder.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { InputFieldBuilder, RootFieldBuilder, SchemaTypes } from '@pothos/core'; -import { upperCaseFirst } from 'upper-case-first'; -import { usePaginationBuilder } from './usePaginationBuilder'; - -const rootBuilderProto = RootFieldBuilder.prototype as PothosSchemaTypes.RootFieldBuilder; - -rootBuilderProto.fieldWithPagination = function (fieldOptions: { type: any }) { - const { typeWithAggregationRef, paginationArgsGroup } = usePaginationBuilder({ - builder: this.builder, - arg: this.arg, - type: fieldOptions.type, - }); - - return this.field({ ...fieldOptions, type: typeWithAggregationRef, args: { ...paginationArgsGroup } } as never); -}; diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreatePaginationInputTypeRef.ts b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreatePaginationInputTypeRef.ts new file mode 100644 index 0000000..2152066 --- /dev/null +++ b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreatePaginationInputTypeRef.ts @@ -0,0 +1,21 @@ +import { ArgBuilder, OutputType, SchemaTypes, TypeParam } from '@pothos/core'; + +export type GetOrCreatePaginationInputTypeRefArgs = { + builder: PothosSchemaTypes.SchemaBuilder; + name: string; +}; + +export const getOrCreatePaginationInputTypeRef = ({ builder, name }: GetOrCreatePaginationInputTypeRefArgs) => { + const originalRef = builder.configStore.getInputTypeRef(name); + + if (typeof originalRef !== 'string') { + return originalRef; + } + + return builder.inputType(name, { + fields: (t) => ({ + take: t.int(), + page: t.int({ required: false }), + }), + }); +}; diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts new file mode 100644 index 0000000..81adf30 --- /dev/null +++ b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts @@ -0,0 +1,30 @@ +import { ArgBuilder, OutputType, SchemaTypes, TypeParam } from '@pothos/core'; + +export type GetOrCreateTypeWithAggregationRefArgs = { + builder: PothosSchemaTypes.SchemaBuilder; + name: string; + type: TypeParam; +}; + +export const getOrCreateTypeWithAggregationRef = ({ builder, name, type }: GetOrCreateTypeWithAggregationRefArgs) => { + const originalRef = builder.configStore.getOutputTypeRef(name); + + if (typeof originalRef !== 'string') { + return originalRef as OutputType; + } + + return builder.simpleObject(name, { + fields: (t) => ({ + currentPage: t.field({ type: 'Int' }), + isFirstPage: t.field({ type: 'Boolean' }), + isLastPage: t.field({ type: 'Boolean' }), + previousPage: t.field({ type: 'Int', nullable: true }), + nextPage: t.field({ type: 'Int', nullable: true }), + pageCount: t.field({ type: 'Int' }), + totalCount: t.field({ type: 'Int' }), + data: t.field({ + type, + }), + }), + }); +}; diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/schemaBuilder.ts b/apps/graphql/src/plugins/pagination/schemaBuilder/schemaBuilder.ts new file mode 100644 index 0000000..754778b --- /dev/null +++ b/apps/graphql/src/plugins/pagination/schemaBuilder/schemaBuilder.ts @@ -0,0 +1,12 @@ +import { RootFieldBuilder } from '@pothos/core'; +import { usePaginationBuilder } from './usePaginationBuilder'; + +RootFieldBuilder.prototype.fieldWithPagination = function (fieldOptions: { type: any }) { + const { typeWithAggregationRef, paginationArgsGroup } = usePaginationBuilder({ + builder: this.builder, + arg: this.arg, + type: fieldOptions.type, + }); + + return this.field({ ...fieldOptions, type: typeWithAggregationRef, args: { ...paginationArgsGroup } }); +}; diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/usePaginationBuilder.ts b/apps/graphql/src/plugins/pagination/schemaBuilder/usePaginationBuilder.ts new file mode 100644 index 0000000..e90f112 --- /dev/null +++ b/apps/graphql/src/plugins/pagination/schemaBuilder/usePaginationBuilder.ts @@ -0,0 +1,48 @@ +import { ArgBuilder, SchemaTypes, TypeParam } from '@pothos/core'; +import { upperCaseFirst } from 'upper-case-first'; +import { getOrCreatePaginationInputTypeRef } from './getOrCreatePaginationInputTypeRef'; +import { getOrCreateTypeWithAggregationRef } from './getOrCreateTypeWithAggregationRef'; + +export type UsePaginationBuilderArgs = { + builder: PothosSchemaTypes.SchemaBuilder; + arg: ArgBuilder; + type: TypeParam; +}; + +export const usePaginationBuilder = ({ builder, arg, type }: UsePaginationBuilderArgs) => { + if (!Array.isArray(type) || typeof type[0] !== 'object' || !('name' in type[0])) { + // eslint-disable-next-line no-restricted-syntax + throw new Error(`Type of field with pagination must be list e.g. type: [SomeType]`); + } + + const paginationInputTypeRef = getOrCreatePaginationInputTypeRef({ builder, name: 'PaginationInput' }); + + const typeWithAggregationRef = getOrCreateTypeWithAggregationRef({ + builder, + name: upperCaseFirst(`${type[0].name}sWithNestedAggregation`), + type, + }); + + const paginationArgsGroup = { filter: arg({ required: false, type: paginationInputTypeRef }) }; + + return { + typeWithAggregationRef, + paginationArgsGroup, + }; +}; + +export type PaginationArgs = { + /** An index of page you want to get starting at 1. */ + page: number; + /** A number of rows on each page. */ + size: number; +}; +export type PaginationResult = { + /** A requested index of a page you wanted to get starting at 1. */ + page: number; + /** A requested number of rows on each page */ + size: number; + rows: R; + /** A total number of rows that match all `where` conditions. It equals to a table size if no `where` conditions are specified. */ + totalSize: number; +}; diff --git a/apps/graphql/src/plugins/pagination/types.ts b/apps/graphql/src/plugins/pagination/types.ts index 954479b..4bcf6bf 100644 --- a/apps/graphql/src/plugins/pagination/types.ts +++ b/apps/graphql/src/plugins/pagination/types.ts @@ -1,120 +1,72 @@ import { FieldKind, FieldNullability, - FieldRef, + FieldOptionsFromKind, + InferredFieldOptionsByKind, InputFieldMap, InputFieldRef, InputShapeFromFields, MaybePromise, SchemaTypes, - ShapeFromTypeParam, TypeParam, } from '@pothos/core'; -import { GraphQLResolveInfo } from 'graphql'; -import PaginationPlugin from './plugin'; -type PartialArgs = Args extends undefined - ? object - : { - [key in Key]: InputFieldRef> | undefined>; - }; - -type FieldWithPaginationOptionsFromKind< - Types extends SchemaTypes, - ParentShape, - Type extends TypeParam, - Nullable extends FieldNullability, - Args extends InputFieldMap, - Kind extends 'Query' | 'Mutation' | 'Object', - _ResolveShape, - ResolveReturnShape, -> = FieldWithPaginationOptionsByKind[Kind]; - -interface FieldWithPaginationOptionsByKind< +interface ObjectFieldOptions< Types extends SchemaTypes, ParentShape, Type extends TypeParam, Nullable extends FieldNullability, Args extends InputFieldMap, ResolveReturnShape, -> { - Query: QueryFieldOptions; - Mutation: MutationFieldOptions; - Object: ObjectFieldOptions; -} +> extends FieldOptions {} -interface QueryFieldOptions< +export interface QueryFieldOptions< Types extends SchemaTypes, Type extends TypeParam, Nullable extends FieldNullability, Args extends InputFieldMap, ResolveReturnShape, -> extends Omit, 'resolve'> { - resolve: Resolver, Types['Context'], ShapeFromTypeParam, Nullable>; -} +> extends FieldOptions {} -interface MutationFieldOptions< +type FieldOptionsByKind< Types extends SchemaTypes, + ParentShape, Type extends TypeParam, Nullable extends FieldNullability, Args extends InputFieldMap, ResolveReturnShape, -> extends Omit, 'resolve'> { - resolve: Resolver, Types['Context'], ShapeFromTypeParam, Nullable>; -} +> = { + Query: QueryFieldOptions & + InferredFieldOptionsByKind; + Object: ObjectFieldOptions & + InferredFieldOptionsByKind; +}; -interface ObjectFieldOptions< +export type PaginatedField< Types extends SchemaTypes, ParentShape, + Kind extends FieldKind, Type extends TypeParam, Nullable extends FieldNullability, - Args extends InputFieldMap, - ResolveReturnShape, -> extends Omit, 'resolve'> { - resolve: Resolver, Types['Context'], ShapeFromTypeParam, Nullable>; -} - -type Resolver = ( - parent: Parent, - args: Args, - context: Context, - info: GraphQLResolveInfo, - type: Type, -) => Type extends readonly any[] - ? Nullable extends true - ? MaybePromise Promise> | null | undefined> - : MaybePromise Promise>> - : never; - -declare global { - export namespace PothosSchemaTypes { - export interface Plugins { - pagination: PaginationPlugin; - } + ResolveShape, + ReturnResolveShape, + PaginationEnabled extends boolean = false, +> = FieldOptionsByKind[Kind] & { + pagination: PaginationEnabled; +}; - export interface RootFieldBuilder { - paginatedField: < - Type extends TypeParam, - ResolveShape, - ResolveReturnShape, - Nullable extends FieldNullability = Types['DefaultFieldNullability'], - >( - options: Omit< - FieldWithPaginationOptionsFromKind< - Types, - ParentShape, - Type, - Nullable, - { - filter?: InputFieldRef<{ size: number; page?: number }>; - }, - Extract, - ResolveShape, - ResolveReturnShape - >, - 'args' - >, - ) => FieldRef>; - } - } -} +export type PaginationArgs = { + /** An index of page you want to get starting at 1. */ + page: number; + /** A number of rows on each page. */ + size: number; +}; +export type PaginationResult = { + /** A requested index of a page you wanted to get starting at 1. */ + page: number; + /** A requested number of rows on each page */ + size: number; + rows: R; + /** A total number of rows that match all `where` conditions. It equals to a table size if no `where` conditions are specified. */ + totalSize: number; +}; diff --git a/apps/graphql/src/plugins/pagination/usePaginationBuilder.ts b/apps/graphql/src/plugins/pagination/usePaginationBuilder.ts deleted file mode 100644 index c9be5c1..0000000 --- a/apps/graphql/src/plugins/pagination/usePaginationBuilder.ts +++ /dev/null @@ -1,79 +0,0 @@ -// import { ArgBuilder, OutputType, SchemaTypes, TypeParam } from '@pothos/core'; -// import { upperCaseFirst } from 'upper-case-first'; - -// type UsePaginationBuilderArgs = { -// builder: PothosSchemaTypes.SchemaBuilder; -// arg: ArgBuilder; -// type: TypeParam; -// }; - -// export const usePaginationBuilder = ({ builder, arg, type }: UsePaginationBuilderArgs) => { -// if (!Array.isArray(type) || typeof type[0] !== 'object' || !('name' in type[0])) { -// // eslint-disable-next-line fleek-custom/no-default-error -// throw new Error(`Type of field with pagination must be list e.g. type: [SomeType]`); -// } - -// const paginationInputTypeRef = getOrCreatePaginationInputTypeRef({ builder, name: 'PaginationInput' }); - -// const typeWithAggregationRef = getOrCreateTypeWithAggregationRef({ -// builder, -// name: upperCaseFirst(`${type[0].name}sWithNestedAggregation`), -// type, -// }); - -// const paginationArgsGroup = { filter: arg({ required: false, type: paginationInputTypeRef }) }; - -// return { -// typeWithAggregationRef, -// paginationArgsGroup, -// }; -// }; - -// type GetOrCreatePaginationInputTypeRefArgs = { -// builder: PothosSchemaTypes.SchemaBuilder; -// name: string; -// }; - -// const getOrCreatePaginationInputTypeRef = ({ builder, name }: GetOrCreatePaginationInputTypeRefArgs) => { -// const originalRef = builder.configStore.getInputTypeRef(name); - -// if (typeof originalRef !== 'string') { -// return originalRef; -// } - -// return builder.inputType(name, { -// fields: (t) => ({ -// take: t.int(), -// page: t.int({ required: false }), -// }), -// }); -// }; - -// type GetOrCreateTypeWithAggregationRefArgs = { -// builder: PothosSchemaTypes.SchemaBuilder; -// name: string; -// type: TypeParam; -// }; - -// const getOrCreateTypeWithAggregationRef = ({ builder, name, type }: GetOrCreateTypeWithAggregationRefArgs) => { -// const originalRef = builder.configStore.getOutputTypeRef(name); - -// if (typeof originalRef !== 'string') { -// return originalRef as OutputType; -// } - -// return builder.simpleObject(name, { -// fields: (t) => ({ -// currentPage: t.field({ type: 'Int' }), -// isFirstPage: t.field({ type: 'Boolean' }), -// isLastPage: t.field({ type: 'Boolean' }), -// previousPage: t.field({ type: 'Int', nullable: true }), -// nextPage: t.field({ type: 'Int', nullable: true }), -// pageCount: t.field({ type: 'Int' }), -// totalCount: t.field({ type: 'Int' }), -// data: t.field({ -// type, -// }), -// }), -// }); -// }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43cfcf8..9f45eac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,6 +217,9 @@ importers: type-fest: specifier: ^4.35.0 version: 4.35.0 + upper-case-first: + specifier: ^3.0.0 + version: 3.0.0 zod: specifier: 3.22.4 version: 3.22.4 @@ -5803,6 +5806,10 @@ packages: upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} + upper-case-first@3.0.0: + resolution: {integrity: sha512-YCgaS8dIParorDq1chrJBoFwQhnTW2/MaL3+cVyFjatwDUQIdlzoMneSearU9YAvc4bBtjgMyGtdkWUYvuN25Q==} + deprecated: Use `input.charAt(0).toUpperCase() + input.slice(1)` + upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} @@ -9092,7 +9099,7 @@ snapshots: '@typescript-eslint/parser': 7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) eslint: 9.21.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.8.3)(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.21.0(jiti@2.4.2)) eslint-plugin-react: 7.37.4(eslint@9.21.0(jiti@2.4.2)) @@ -9112,7 +9119,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0(supports-color@5.5.0) @@ -9127,14 +9134,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3) eslint: 9.21.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)) transitivePeerDependencies: - supports-color @@ -9149,7 +9156,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.21.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.18.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@9.21.0(jiti@2.4.2)))(eslint@9.21.0(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -12476,6 +12483,8 @@ snapshots: dependencies: tslib: 2.8.1 + upper-case-first@3.0.0: {} + upper-case@2.0.2: dependencies: tslib: 2.8.1 From d26f688ab7d57e1ee2d7ce5d3095e5c30ffb079c Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Sat, 5 Jul 2025 19:05:49 +0200 Subject: [PATCH 11/12] WIP --- apps/graphql/package.json | 1 - apps/graphql/src/builder.ts | 2 +- .../src/plugins/pagination/global-types.ts | 11 +-- .../pagination/{plugin.ts => index.ts} | 6 +- .../pagination/schemaBuilder/.DS_Store | Bin 0 -> 6148 bytes .../getOrCreatePaginationInputTypeRef.ts | 4 +- .../getOrCreateTypeWithAggregationRef.ts | 18 ++-- .../plugins/pagination/schemaBuilder/index.ts | 26 ++++++ .../pagination/schemaBuilder/schemaBuilder.ts | 12 --- .../schemaBuilder/usePaginationBuilder.ts | 48 ---------- apps/graphql/src/plugins/pagination/types.ts | 88 +++++++++++------- .../resolvers/workspace/queries/workspaces.ts | 6 +- libs/prisma/src/index.ts | 21 +++-- pnpm-lock.yaml | 9 -- 14 files changed, 114 insertions(+), 138 deletions(-) rename apps/graphql/src/plugins/pagination/{plugin.ts => index.ts} (90%) create mode 100644 apps/graphql/src/plugins/pagination/schemaBuilder/.DS_Store create mode 100644 apps/graphql/src/plugins/pagination/schemaBuilder/index.ts delete mode 100644 apps/graphql/src/plugins/pagination/schemaBuilder/schemaBuilder.ts delete mode 100644 apps/graphql/src/plugins/pagination/schemaBuilder/usePaginationBuilder.ts diff --git a/apps/graphql/package.json b/apps/graphql/package.json index 39d6862..25423f2 100644 --- a/apps/graphql/package.json +++ b/apps/graphql/package.json @@ -38,7 +38,6 @@ "graphql-yoga": "^5.6.2", "ts-pattern": "^5.6.2", "type-fest": "^4.35.0", - "upper-case-first": "^3.0.0", "zod": "3.22.4" }, "devDependencies": { diff --git a/apps/graphql/src/builder.ts b/apps/graphql/src/builder.ts index 00e552e..8874dfd 100644 --- a/apps/graphql/src/builder.ts +++ b/apps/graphql/src/builder.ts @@ -3,12 +3,12 @@ import DataloaderPlugin from '@pothos/plugin-dataloader'; import ScopeAuthPlugin from '@pothos/plugin-scope-auth'; import SimpleObjectsPlugin from '@pothos/plugin-simple-objects'; import ZodPlugin from '@pothos/plugin-zod'; -import PaginationPlugin from './pagination/plugin'; import { accessPolicyAuthScope, AccessPolicyAuthScopeArgs } from './authScopes/accessPolicyAuthScope'; import { forbiddenAuthScope, ForbiddenAuthScopeArgs } from './authScopes/forbiddenAuthScope'; import { modelItemsBelongToWorkspaceScope, ModelItemsBelongToWorkspaceScopeArgs } from './authScopes/modelItemsBelongToWorkspace'; import { userHasAccessOnWorkspaceScope, UserHasAccessOnWorkspaceScopeArgs } from './authScopes/userHasAccessOnWorkspaceScope'; import { Context } from './context'; +import PaginationPlugin from './plugins/pagination'; import { Date } from './schema/Date'; import { DateTime } from './schema/DateTime'; import { ID } from './schema/ID'; diff --git a/apps/graphql/src/plugins/pagination/global-types.ts b/apps/graphql/src/plugins/pagination/global-types.ts index 5f745fb..20fb7b6 100644 --- a/apps/graphql/src/plugins/pagination/global-types.ts +++ b/apps/graphql/src/plugins/pagination/global-types.ts @@ -1,5 +1,5 @@ import { FieldKind, FieldNullability, FieldRef, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; -import PaginationPlugin from './plugin'; +import PaginationPlugin from './index'; import { PaginatedField } from './types'; declare global { @@ -9,13 +9,8 @@ declare global { } export interface RootFieldBuilder { - paginatedField: < - Type extends TypeParam, - ResolveShape, - ReturnResolveShape, - Nullable extends FieldNullability = Types['DefaultFieldNullability'], - >( - options: PaginatedField, + paginatedField: , ResolveReturnShape, Nullable extends FieldNullability = Types['DefaultFieldNullability']>( + options: PaginatedField, ) => FieldRef>; } } diff --git a/apps/graphql/src/plugins/pagination/plugin.ts b/apps/graphql/src/plugins/pagination/index.ts similarity index 90% rename from apps/graphql/src/plugins/pagination/plugin.ts rename to apps/graphql/src/plugins/pagination/index.ts index 3f2c78e..97166ba 100644 --- a/apps/graphql/src/plugins/pagination/plugin.ts +++ b/apps/graphql/src/plugins/pagination/index.ts @@ -11,8 +11,7 @@ function isPaginationResult(result: unknown): result is (args: PaginationArgs) = return typeof result === 'function'; } -// eslint-disable-next-line import/no-default-export -export default class PaginationPlugin extends BasePlugin { +export class PaginationPlugin extends BasePlugin { wrapResolve(resolver: GraphQLFieldResolver>): GraphQLFieldResolver< unknown, Types['Context'], @@ -36,3 +35,6 @@ export default class PaginationPlugin extends BasePlu } SchemaBuilder.registerPlugin(pluginName, PaginationPlugin); + +// eslint-disable-next-line import/no-default-export +export default pluginName; diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/.DS_Store b/apps/graphql/src/plugins/pagination/schemaBuilder/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0; @@ -14,7 +14,7 @@ export const getOrCreatePaginationInputTypeRef = ({ builder, name }: GetOrCreate return builder.inputType(name, { fields: (t) => ({ - take: t.int(), + size: t.int({ required: false }), page: t.int({ required: false }), }), }); diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts index 81adf30..aee8b00 100644 --- a/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts +++ b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts @@ -1,4 +1,4 @@ -import { ArgBuilder, OutputType, SchemaTypes, TypeParam } from '@pothos/core'; +import { OutputType, SchemaTypes, TypeParam } from '@pothos/core'; export type GetOrCreateTypeWithAggregationRefArgs = { builder: PothosSchemaTypes.SchemaBuilder; @@ -6,23 +6,19 @@ export type GetOrCreateTypeWithAggregationRefArgs = { type: TypeParam; }; -export const getOrCreateTypeWithAggregationRef = ({ builder, name, type }: GetOrCreateTypeWithAggregationRefArgs) => { +export const getOrCreateTypeWithAggregationRef = ({ builder, name, type }: GetOrCreateTypeWithAggregationRefArgs): OutputType => { const originalRef = builder.configStore.getOutputTypeRef(name); if (typeof originalRef !== 'string') { - return originalRef as OutputType; + return originalRef; } return builder.simpleObject(name, { fields: (t) => ({ - currentPage: t.field({ type: 'Int' }), - isFirstPage: t.field({ type: 'Boolean' }), - isLastPage: t.field({ type: 'Boolean' }), - previousPage: t.field({ type: 'Int', nullable: true }), - nextPage: t.field({ type: 'Int', nullable: true }), - pageCount: t.field({ type: 'Int' }), - totalCount: t.field({ type: 'Int' }), - data: t.field({ + page: t.field({ type: 'Int' }), + size: t.field({ type: 'Int' }), + totalSize: t.field({ type: 'Int' }), + rows: t.field({ type, }), }), diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/index.ts b/apps/graphql/src/plugins/pagination/schemaBuilder/index.ts new file mode 100644 index 0000000..a270433 --- /dev/null +++ b/apps/graphql/src/plugins/pagination/schemaBuilder/index.ts @@ -0,0 +1,26 @@ +import { RootFieldBuilder, SchemaTypes } from '@pothos/core'; +import { getOrCreatePaginationInputTypeRef } from './getOrCreatePaginationInputTypeRef'; +import { getOrCreateTypeWithAggregationRef } from './getOrCreateTypeWithAggregationRef'; + +const rootBuilderProto = RootFieldBuilder.prototype as unknown as PothosSchemaTypes.RootFieldBuilder; + +const upperCaseFirst = (input: string) => input.charAt(0).toUpperCase() + input.slice(1); + +rootBuilderProto.paginatedField = function (fieldOptions) { + if (!Array.isArray(fieldOptions.type) || typeof fieldOptions.type[0] !== 'object' || !('name' in fieldOptions.type[0])) { + // eslint-disable-next-line no-restricted-syntax + throw new Error(`Type of field with pagination must be list e.g. type: [SomeType]`); + } + + const paginationInputTypeRef = getOrCreatePaginationInputTypeRef({ builder: this.builder, name: 'PaginationInput' }); + + const typeWithAggregationRef = getOrCreateTypeWithAggregationRef({ + builder: this.builder, + name: upperCaseFirst(`Paginated${fieldOptions.type[0].name}s`), + type: fieldOptions.type, + }); + + const paginationArgsGroup = { filter: this.arg({ type: paginationInputTypeRef, required: false }) }; + + return this.field({ ...fieldOptions, type: typeWithAggregationRef, args: { ...fieldOptions.args, ...paginationArgsGroup } } as never); +}; diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/schemaBuilder.ts b/apps/graphql/src/plugins/pagination/schemaBuilder/schemaBuilder.ts deleted file mode 100644 index 754778b..0000000 --- a/apps/graphql/src/plugins/pagination/schemaBuilder/schemaBuilder.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { RootFieldBuilder } from '@pothos/core'; -import { usePaginationBuilder } from './usePaginationBuilder'; - -RootFieldBuilder.prototype.fieldWithPagination = function (fieldOptions: { type: any }) { - const { typeWithAggregationRef, paginationArgsGroup } = usePaginationBuilder({ - builder: this.builder, - arg: this.arg, - type: fieldOptions.type, - }); - - return this.field({ ...fieldOptions, type: typeWithAggregationRef, args: { ...paginationArgsGroup } }); -}; diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/usePaginationBuilder.ts b/apps/graphql/src/plugins/pagination/schemaBuilder/usePaginationBuilder.ts deleted file mode 100644 index e90f112..0000000 --- a/apps/graphql/src/plugins/pagination/schemaBuilder/usePaginationBuilder.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ArgBuilder, SchemaTypes, TypeParam } from '@pothos/core'; -import { upperCaseFirst } from 'upper-case-first'; -import { getOrCreatePaginationInputTypeRef } from './getOrCreatePaginationInputTypeRef'; -import { getOrCreateTypeWithAggregationRef } from './getOrCreateTypeWithAggregationRef'; - -export type UsePaginationBuilderArgs = { - builder: PothosSchemaTypes.SchemaBuilder; - arg: ArgBuilder; - type: TypeParam; -}; - -export const usePaginationBuilder = ({ builder, arg, type }: UsePaginationBuilderArgs) => { - if (!Array.isArray(type) || typeof type[0] !== 'object' || !('name' in type[0])) { - // eslint-disable-next-line no-restricted-syntax - throw new Error(`Type of field with pagination must be list e.g. type: [SomeType]`); - } - - const paginationInputTypeRef = getOrCreatePaginationInputTypeRef({ builder, name: 'PaginationInput' }); - - const typeWithAggregationRef = getOrCreateTypeWithAggregationRef({ - builder, - name: upperCaseFirst(`${type[0].name}sWithNestedAggregation`), - type, - }); - - const paginationArgsGroup = { filter: arg({ required: false, type: paginationInputTypeRef }) }; - - return { - typeWithAggregationRef, - paginationArgsGroup, - }; -}; - -export type PaginationArgs = { - /** An index of page you want to get starting at 1. */ - page: number; - /** A number of rows on each page. */ - size: number; -}; -export type PaginationResult = { - /** A requested index of a page you wanted to get starting at 1. */ - page: number; - /** A requested number of rows on each page */ - size: number; - rows: R; - /** A total number of rows that match all `where` conditions. It equals to a table size if no `where` conditions are specified. */ - totalSize: number; -}; diff --git a/apps/graphql/src/plugins/pagination/types.ts b/apps/graphql/src/plugins/pagination/types.ts index 4bcf6bf..0b4a0f7 100644 --- a/apps/graphql/src/plugins/pagination/types.ts +++ b/apps/graphql/src/plugins/pagination/types.ts @@ -1,24 +1,24 @@ import { FieldKind, FieldNullability, - FieldOptionsFromKind, - InferredFieldOptionsByKind, InputFieldMap, - InputFieldRef, InputShapeFromFields, MaybePromise, SchemaTypes, + ShapeFromTypeParam, TypeParam, } from '@pothos/core'; +import { PrismaPaginationFn } from '@steadystart/prisma'; +import { GraphQLResolveInfo } from 'graphql'; -interface ObjectFieldOptions< +export interface ObjectFieldOptions< Types extends SchemaTypes, ParentShape, Type extends TypeParam, Nullable extends FieldNullability, Args extends InputFieldMap, ResolveReturnShape, -> extends FieldOptions {} +> extends PothosSchemaTypes.FieldOptions {} export interface QueryFieldOptions< Types extends SchemaTypes, @@ -26,21 +26,60 @@ export interface QueryFieldOptions< Nullable extends FieldNullability, Args extends InputFieldMap, ResolveReturnShape, -> extends FieldOptions {} +> extends PothosSchemaTypes.FieldOptions {} -type FieldOptionsByKind< +type Resolver = ( + parent: Parent, + args: Args, + context: Context, + info: GraphQLResolveInfo, +) => [Type] extends [readonly (infer Item)[] | null | undefined] + ? MaybePromise> + : { __error: 'Pagination needs to be enable for lists only.' }; + +interface InferredFieldOptions< + Types extends SchemaTypes, + ResolveShape = unknown, + Type extends TypeParam = TypeParam, + Nullable extends FieldNullability = FieldNullability, + Args extends InputFieldMap = InputFieldMap, +> { + Resolve: { + /** + * Resolver function for this field + * @param parent - The parent object for the current type + * @param {object} args - args object based on the args defined for this field + * @param {object} context - the context object for the current query, based on `Context` type provided to the SchemaBuilder + * @param {GraphQLResolveInfo} info - info about how this field was queried + */ + resolve: Resolver, Types['Context'], ShapeFromTypeParam>; + }; +} + +export type InferredFieldOptionsKind = keyof InferredFieldOptions; + +export type InferredFieldOptionsByKind< + Types extends SchemaTypes, + Kind extends InferredFieldOptionsKind, + ResolveShape = unknown, + Type extends TypeParam = TypeParam, + Nullable extends FieldNullability = FieldNullability, + Args extends InputFieldMap = InputFieldMap, +> = InferredFieldOptions[Kind]; + +export interface FieldOptionsByKind< Types extends SchemaTypes, ParentShape, Type extends TypeParam, Nullable extends FieldNullability, Args extends InputFieldMap, ResolveReturnShape, -> = { +> { Query: QueryFieldOptions & - InferredFieldOptionsByKind; + InferredFieldOptionsByKind; Object: ObjectFieldOptions & - InferredFieldOptionsByKind; -}; + InferredFieldOptionsByKind; +} export type PaginatedField< Types extends SchemaTypes, @@ -48,25 +87,8 @@ export type PaginatedField< Kind extends FieldKind, Type extends TypeParam, Nullable extends FieldNullability, - ResolveShape, - ReturnResolveShape, - PaginationEnabled extends boolean = false, -> = FieldOptionsByKind[Kind] & { - pagination: PaginationEnabled; -}; - -export type PaginationArgs = { - /** An index of page you want to get starting at 1. */ - page: number; - /** A number of rows on each page. */ - size: number; -}; -export type PaginationResult = { - /** A requested index of a page you wanted to get starting at 1. */ - page: number; - /** A requested number of rows on each page */ - size: number; - rows: R; - /** A total number of rows that match all `where` conditions. It equals to a table size if no `where` conditions are specified. */ - totalSize: number; -}; + ResolveReturnShape, + Args extends InputFieldMap = InputFieldMap, +> = Kind extends 'Query' | 'Object' + ? FieldOptionsByKind[Kind] + : { __error: 'Pagination needs to be used with Query or Object.' }; diff --git a/apps/graphql/src/resolvers/workspace/queries/workspaces.ts b/apps/graphql/src/resolvers/workspace/queries/workspaces.ts index b337839..6bb7daf 100644 --- a/apps/graphql/src/resolvers/workspace/queries/workspaces.ts +++ b/apps/graphql/src/resolvers/workspace/queries/workspaces.ts @@ -2,19 +2,17 @@ import { builder } from '../../../builder'; import { Workspace } from '../../../schema/Workspace'; builder.queryField('workspaces', (t) => - t.field({ + t.paginatedField({ type: [Workspace], authScopes: { accessPolicy: 'authenticated', }, resolve: async (_parent, _args, ctx) => { - const workspaces = await ctx.prisma.workspace.findMany({ + return ctx.prisma.workspace.paginate({ where: { memberships: { some: { userId: ctx.user!.id } }, }, }); - - return workspaces; }, }), ); diff --git a/libs/prisma/src/index.ts b/libs/prisma/src/index.ts index e71e859..ad887d9 100644 --- a/libs/prisma/src/index.ts +++ b/libs/prisma/src/index.ts @@ -14,29 +14,36 @@ export type PaginationResult = { page: number; /** A requested number of rows on each page */ size: number; - rows: R; + rows: R[]; /** A total number of rows that match all `where` conditions. It equals to a table size if no `where` conditions are specified. */ totalSize: number; }; +type PrismaPaginationFnBrandedType = { __type: 'PrismaPaginationFn' }; + +export type PrismaPaginationFn = (paginationArgs: PaginationArgs) => Promise> & PrismaPaginationFnBrandedType; + +type PrismaPaginationFnInternal< + TModel, + TArgs = Prisma.Args, + TResult = Prisma.Result, +> = PrismaPaginationFn; + export const createPrismaClient = (prismaClientOptions: Prisma.PrismaClientOptions) => new OriginalPrismaClient(prismaClientOptions).$extends({ model: { $allModels: { - paginate, R = Prisma.Result>( - this: T, - args: A, - ): (paginationArgs: PaginationArgs) => Promise> { + paginate>(this: T, args: A): PrismaPaginationFnInternal { const context: any = Prisma.getExtensionContext(this); - return async ({ page, size }: PaginationArgs) => { + return (async ({ page, size }: PaginationArgs) => { const take = Math.max(0, size); const skip = (1 - Math.max(1, page)) * size; const [rows, totalSize] = await context.$transaction([context.findMany({ ...args, take, skip }), context.count(args)]); return { page, size, rows, totalSize }; - }; + }) as PrismaPaginationFnInternal; }, }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f45eac..a06ca02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,9 +217,6 @@ importers: type-fest: specifier: ^4.35.0 version: 4.35.0 - upper-case-first: - specifier: ^3.0.0 - version: 3.0.0 zod: specifier: 3.22.4 version: 3.22.4 @@ -5806,10 +5803,6 @@ packages: upper-case-first@2.0.2: resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} - upper-case-first@3.0.0: - resolution: {integrity: sha512-YCgaS8dIParorDq1chrJBoFwQhnTW2/MaL3+cVyFjatwDUQIdlzoMneSearU9YAvc4bBtjgMyGtdkWUYvuN25Q==} - deprecated: Use `input.charAt(0).toUpperCase() + input.slice(1)` - upper-case@2.0.2: resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} @@ -12483,8 +12476,6 @@ snapshots: dependencies: tslib: 2.8.1 - upper-case-first@3.0.0: {} - upper-case@2.0.2: dependencies: tslib: 2.8.1 From 10fba56f5712361f69a8dba9592d5f7b04289743 Mon Sep 17 00:00:00 2001 From: RobertPechaCZ Date: Mon, 14 Jul 2025 16:20:29 +0200 Subject: [PATCH 12/12] feat: add options for pagination, prepare cursor-based pagination --- apps/graphql/CLAUDE.md | 3 +- apps/graphql/src/builder.ts | 1 + .../src/plugins/pagination/global-types.ts | 12 ++- apps/graphql/src/plugins/pagination/index.ts | 14 +-- .../getOrCreatePaginationInputTypeRef.ts | 20 ++--- .../getOrCreateTypeWithAggregationRef.ts | 28 +++--- apps/graphql/src/plugins/pagination/types.ts | 10 ++- .../src/resolvers/post/queries/posts.ts | 6 +- .../workspace/queries/workspaces.test.ts | 90 ++++++++++++++----- .../resolvers/workspace/queries/workspaces.ts | 1 + libs/prisma/src/index.ts | 48 ++++++---- 11 files changed, 153 insertions(+), 80 deletions(-) diff --git a/apps/graphql/CLAUDE.md b/apps/graphql/CLAUDE.md index 756d93f..28c97b1 100644 --- a/apps/graphql/CLAUDE.md +++ b/apps/graphql/CLAUDE.md @@ -4,5 +4,6 @@ - When adding a new resolver, always add it to `/workspaces/steadystart/apps/graphql/src/schema.ts` - Follow the existing pattern for organizing resolvers by model +- Use `paginatedField` helper for all queries and nested fields where a list is returned - Write tests for all resolvers -- Use auth scopes to handle permissions \ No newline at end of file +- Use auth scopes to handle permissions diff --git a/apps/graphql/src/builder.ts b/apps/graphql/src/builder.ts index 8874dfd..0a86695 100644 --- a/apps/graphql/src/builder.ts +++ b/apps/graphql/src/builder.ts @@ -35,6 +35,7 @@ export const builder = new SchemaBuilder<{ defaultFieldNullability: false, defaultInputFieldRequiredness: true, plugins: [ZodPlugin, ScopeAuthPlugin, SimpleObjectsPlugin, DataloaderPlugin, PaginationPlugin], + pagination: { defaultPageSize: 100 }, scopeAuth: { defaultStrategy: 'all', authScopes: async (ctx) => { diff --git a/apps/graphql/src/plugins/pagination/global-types.ts b/apps/graphql/src/plugins/pagination/global-types.ts index 20fb7b6..ec2521d 100644 --- a/apps/graphql/src/plugins/pagination/global-types.ts +++ b/apps/graphql/src/plugins/pagination/global-types.ts @@ -1,6 +1,6 @@ import { FieldKind, FieldNullability, FieldRef, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; -import PaginationPlugin from './index'; -import { PaginatedField } from './types'; +import { PaginationPlugin } from './index'; +import { PaginatedField, PaginationFieldOptions, PaginationPluginOptions } from './types'; declare global { export namespace PothosSchemaTypes { @@ -8,6 +8,14 @@ declare global { pagination: PaginationPlugin; } + export interface SchemaBuilderOptions { + pagination?: PaginationPluginOptions; + } + + export interface FieldOptions { + pagination?: PaginationFieldOptions; + } + export interface RootFieldBuilder { paginatedField: , ResolveReturnShape, Nullable extends FieldNullability = Types['DefaultFieldNullability']>( options: PaginatedField, diff --git a/apps/graphql/src/plugins/pagination/index.ts b/apps/graphql/src/plugins/pagination/index.ts index 97166ba..c2ab848 100644 --- a/apps/graphql/src/plugins/pagination/index.ts +++ b/apps/graphql/src/plugins/pagination/index.ts @@ -1,18 +1,21 @@ import './global-types'; import './schemaBuilder'; -import SchemaBuilder, { BasePlugin, SchemaTypes } from '@pothos/core'; -import { PaginationArgs, PaginationResult } from '@steadystart/prisma'; +import SchemaBuilder, { BasePlugin, PothosOutputFieldConfig, SchemaTypes } from '@pothos/core'; +import { PaginationArgs, PaginationResult, PrismaPaginationFn } from '@steadystart/prisma'; import { GraphQLFieldResolver } from 'graphql'; const pluginName = 'pagination' as const; -function isPaginationResult(result: unknown): result is (args: PaginationArgs) => Promise> { +function isPaginationResult(result: unknown): result is (args: PaginationArgs>) => Promise> { return typeof result === 'function'; } export class PaginationPlugin extends BasePlugin { - wrapResolve(resolver: GraphQLFieldResolver>): GraphQLFieldResolver< + wrapResolve( + resolver: GraphQLFieldResolver>>, + fieldConfig: PothosOutputFieldConfig, + ): GraphQLFieldResolver< unknown, Types['Context'], { @@ -24,8 +27,9 @@ export class PaginationPlugin extends BasePlugin { - const originalRef = builder.configStore.getInputTypeRef(name); - - if (typeof originalRef !== 'string') { - return originalRef; + try { + return builder.configStore.getInputTypeRef(name); + } catch { + return builder.inputType(name, { + fields: (t) => ({ + size: t.int({ required: false }), + page: t.int({ required: false }), + }), + }); } - - return builder.inputType(name, { - fields: (t) => ({ - size: t.int({ required: false }), - page: t.int({ required: false }), - }), - }); }; diff --git a/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts index aee8b00..156d3f7 100644 --- a/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts +++ b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts @@ -7,20 +7,18 @@ export type GetOrCreateTypeWithAggregationRefArgs = { }; export const getOrCreateTypeWithAggregationRef = ({ builder, name, type }: GetOrCreateTypeWithAggregationRefArgs): OutputType => { - const originalRef = builder.configStore.getOutputTypeRef(name); - - if (typeof originalRef !== 'string') { - return originalRef; - } - - return builder.simpleObject(name, { - fields: (t) => ({ - page: t.field({ type: 'Int' }), - size: t.field({ type: 'Int' }), - totalSize: t.field({ type: 'Int' }), - rows: t.field({ - type, + try { + return builder.configStore.getOutputTypeRef(name); + } catch { + return builder.simpleObject(name, { + fields: (t) => ({ + page: t.field({ type: 'Int' }), + size: t.field({ type: 'Int' }), + totalSize: t.field({ type: 'Int' }), + rows: t.field({ + type, + }), }), - }), - }); + }); + } }; diff --git a/apps/graphql/src/plugins/pagination/types.ts b/apps/graphql/src/plugins/pagination/types.ts index 0b4a0f7..80a125c 100644 --- a/apps/graphql/src/plugins/pagination/types.ts +++ b/apps/graphql/src/plugins/pagination/types.ts @@ -11,6 +11,14 @@ import { import { PrismaPaginationFn } from '@steadystart/prisma'; import { GraphQLResolveInfo } from 'graphql'; +export type PaginationPluginOptions = { + defaultPageSize?: number; +}; + +export type PaginationFieldOptions = { + defaultPageSize?: number; +}; + export interface ObjectFieldOptions< Types extends SchemaTypes, ParentShape, @@ -34,7 +42,7 @@ type Resolver = ( context: Context, info: GraphQLResolveInfo, ) => [Type] extends [readonly (infer Item)[] | null | undefined] - ? MaybePromise> + ? MaybePromise> : { __error: 'Pagination needs to be enable for lists only.' }; interface InferredFieldOptions< diff --git a/apps/graphql/src/resolvers/post/queries/posts.ts b/apps/graphql/src/resolvers/post/queries/posts.ts index 294a06f..bc14a18 100644 --- a/apps/graphql/src/resolvers/post/queries/posts.ts +++ b/apps/graphql/src/resolvers/post/queries/posts.ts @@ -9,13 +9,11 @@ builder.queryField('posts', (t) => userHasAccessOnWorkspace: true, }, resolve: async (_parent, _args, ctx) => { - const posts = await ctx.prisma.post.paginate({ + return ctx.prisma.post.findMany({ where: { workspaceId: ctx.workspace!.id, }, - })({ size: 1, page: 1 }); - - return posts.rows; + }); }, }), ); diff --git a/apps/graphql/src/resolvers/workspace/queries/workspaces.test.ts b/apps/graphql/src/resolvers/workspace/queries/workspaces.test.ts index cd319cd..4e7d8c1 100644 --- a/apps/graphql/src/resolvers/workspace/queries/workspaces.test.ts +++ b/apps/graphql/src/resolvers/workspace/queries/workspaces.test.ts @@ -4,28 +4,64 @@ import { genqlQuery } from '../../../tests/genqlCall'; test('Should return workspaces for authenticated user', async () => { const prisma = await new TestDatabaseOrchestrator().getTestDatabaseWithPrismaClient(); - const response = await genqlQuery({ + const firstPage = await genqlQuery({ prisma, source: { - workspaces: { __scalar: true, id: true, title: true }, + workspaces: [{ filter: { page: 1, size: 1 } }, { __scalar: true, rows: { __scalar: true } }], }, userId: seededData.users[0].id, }); - expect(response).toMatchInlineSnapshot(` + expect(firstPage).toMatchInlineSnapshot(` { - "workspaces": [ - { - "__typename": "Workspace", - "id": "56bff15d207f41f9b20d", - "title": "Test Workspace 1", - }, - { - "__typename": "Workspace", - "id": "c4ff8234d86d4f2b9321", - "title": "Test Workspace 2", - }, - ], + "workspaces": { + "__typename": "PaginatedWorkspaces", + "page": 1, + "rows": [ + { + "__typename": "Workspace", + "id": "56bff15d207f41f9b20d", + "title": "Test Workspace 1", + }, + ], + "size": 1, + "totalSize": 2, + }, + } + `); +}); + +test('Should return all workspaces for authenticated user', async () => { + const prisma = await new TestDatabaseOrchestrator().getTestDatabaseWithPrismaClient(); + + const firstPage = await genqlQuery({ + prisma, + source: { + workspaces: [{ filter: { page: 1 } }, { __scalar: true, rows: { __scalar: true } }], + }, + userId: seededData.users[0].id, + }); + + expect(firstPage).toMatchInlineSnapshot(` + { + "workspaces": { + "__typename": "PaginatedWorkspaces", + "page": 1, + "rows": [ + { + "__typename": "Workspace", + "id": "56bff15d207f41f9b20d", + "title": "Test Workspace 1", + }, + { + "__typename": "Workspace", + "id": "c4ff8234d86d4f2b9321", + "title": "Test Workspace 2", + }, + ], + "size": 100, + "totalSize": 2, + }, } `); }); @@ -36,20 +72,26 @@ test('Should return only workspaces where user has membership', async () => { const response = await genqlQuery({ prisma, source: { - workspaces: { __scalar: true, id: true, title: true }, + workspaces: [{}, { __scalar: true, rows: { __scalar: true } }], }, userId: seededData.users[1].id, }); expect(response).toMatchInlineSnapshot(` { - "workspaces": [ - { - "__typename": "Workspace", - "id": "56bff15d207f41f9b20d", - "title": "Test Workspace 1", - }, - ], + "workspaces": { + "__typename": "PaginatedWorkspaces", + "page": 1, + "rows": [ + { + "__typename": "Workspace", + "id": "56bff15d207f41f9b20d", + "title": "Test Workspace 1", + }, + ], + "size": 100, + "totalSize": 1, + }, } `); }); @@ -60,7 +102,7 @@ test('Should not return workspaces when user is not authenticated', async () => const response = await genqlQuery({ prisma, source: { - workspaces: { __scalar: true, id: true, title: true }, + workspaces: [{}, { __scalar: true, rows: { __scalar: true } }], }, }); diff --git a/apps/graphql/src/resolvers/workspace/queries/workspaces.ts b/apps/graphql/src/resolvers/workspace/queries/workspaces.ts index 6bb7daf..847afeb 100644 --- a/apps/graphql/src/resolvers/workspace/queries/workspaces.ts +++ b/apps/graphql/src/resolvers/workspace/queries/workspaces.ts @@ -7,6 +7,7 @@ builder.queryField('workspaces', (t) => authScopes: { accessPolicy: 'authenticated', }, + pagination: { defaultPageSize: 20 }, resolve: async (_parent, _args, ctx) => { return ctx.prisma.workspace.paginate({ where: { diff --git a/libs/prisma/src/index.ts b/libs/prisma/src/index.ts index ad887d9..55d5884 100644 --- a/libs/prisma/src/index.ts +++ b/libs/prisma/src/index.ts @@ -2,11 +2,13 @@ import { Prisma, PrismaClient as OriginalPrismaClient } from '../generated'; export * from '../generated'; -export type PaginationArgs = { +export type PaginationArgs = { /** An index of page you want to get starting at 1. */ page: number; /** A number of rows on each page. */ size: number; + /** Optional `cursor` object where key must be an unique, sequential column. */ + cursor?: C; }; export type PaginationResult = { @@ -21,32 +23,44 @@ export type PaginationResult = { type PrismaPaginationFnBrandedType = { __type: 'PrismaPaginationFn' }; -export type PrismaPaginationFn = (paginationArgs: PaginationArgs) => Promise> & PrismaPaginationFnBrandedType; +export type PrismaPaginationFn = ( + paginationArgs: PaginationArgs, +) => Promise> & PrismaPaginationFnBrandedType; type PrismaPaginationFnInternal< TModel, TArgs = Prisma.Args, TResult = Prisma.Result, -> = PrismaPaginationFn; +> = PrismaPaginationFn['cursor'], TResult>; -export const createPrismaClient = (prismaClientOptions: Prisma.PrismaClientOptions) => - new OriginalPrismaClient(prismaClientOptions).$extends({ - model: { - $allModels: { - paginate>(this: T, args: A): PrismaPaginationFnInternal { - const context: any = Prisma.getExtensionContext(this); +export const createPrismaClient = (prismaClientOptions: Prisma.PrismaClientOptions) => { + const paginationExtension = Prisma.defineExtension((prisma) => + prisma.$extends({ + model: { + $allModels: { + paginate, 'take' | 'skip' | 'cursor'>>(this: T, args: A): PrismaPaginationFnInternal { + const context: any = Prisma.getExtensionContext(this); - return (async ({ page, size }: PaginationArgs) => { - const take = Math.max(0, size); - const skip = (1 - Math.max(1, page)) * size; + return (async ({ page, size, cursor }: PaginationArgs['cursor']>) => { + const take = Math.max(0, size); + let skip = (1 - Math.max(1, page)) * size; - const [rows, totalSize] = await context.$transaction([context.findMany({ ...args, take, skip }), context.count(args)]); + // We skip cursor itself that is the first returned row + if (cursor) { + skip += 1; + } - return { page, size, rows, totalSize }; - }) as PrismaPaginationFnInternal; + const [rows, totalSize] = await prisma.$transaction([context.findMany({ ...args, take, skip, cursor }), context.count(args)]); + + return { page, size, rows, totalSize }; + }) as PrismaPaginationFnInternal; + }, }, }, - }, - }); + }), + ); + + return new OriginalPrismaClient(prismaClientOptions).$extends(paginationExtension); +}; export type PrismaClient = ReturnType;