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/package.json b/apps/graphql/package.json index 24724a7..25423f2 100644 --- a/apps/graphql/package.json +++ b/apps/graphql/package.json @@ -41,19 +41,19 @@ "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/builder.ts b/apps/graphql/src/builder.ts index d231ca4..0a86695 100644 --- a/apps/graphql/src/builder.ts +++ b/apps/graphql/src/builder.ts @@ -8,6 +8,7 @@ import { forbiddenAuthScope, ForbiddenAuthScopeArgs } from './authScopes/forbidd 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'; @@ -33,7 +34,8 @@ export const builder = new SchemaBuilder<{ }>({ defaultFieldNullability: false, defaultInputFieldRequiredness: true, - plugins: [ZodPlugin, ScopeAuthPlugin, SimpleObjectsPlugin, DataloaderPlugin], + plugins: [ZodPlugin, ScopeAuthPlugin, SimpleObjectsPlugin, DataloaderPlugin, PaginationPlugin], + pagination: { defaultPageSize: 100 }, scopeAuth: { defaultStrategy: 'all', authScopes: async (ctx) => { @@ -49,4 +51,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..83d1bb4 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 { PrismaClient, prisma as productionPrisma } from './prisma'; import { Request } from './types/Request'; import { resolveUserFromContext } from './utils/context/resolveUserFromContext'; import { resolveWorkspaceFromContext } from './utils/context/resolveWorkspaceFromContext'; 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..ec2521d --- /dev/null +++ b/apps/graphql/src/plugins/pagination/global-types.ts @@ -0,0 +1,25 @@ +import { FieldKind, FieldNullability, FieldRef, SchemaTypes, ShapeFromTypeParam, TypeParam } from '@pothos/core'; +import { PaginationPlugin } from './index'; +import { PaginatedField, PaginationFieldOptions, PaginationPluginOptions } from './types'; + +declare global { + export namespace PothosSchemaTypes { + export interface Plugins { + pagination: PaginationPlugin; + } + + export interface SchemaBuilderOptions { + pagination?: PaginationPluginOptions; + } + + export interface FieldOptions { + pagination?: PaginationFieldOptions; + } + + export interface RootFieldBuilder { + paginatedField: , ResolveReturnShape, Nullable extends FieldNullability = Types['DefaultFieldNullability']>( + options: PaginatedField, + ) => FieldRef>; + } + } +} diff --git a/apps/graphql/src/plugins/pagination/index.ts b/apps/graphql/src/plugins/pagination/index.ts new file mode 100644 index 0000000..c2ab848 --- /dev/null +++ b/apps/graphql/src/plugins/pagination/index.ts @@ -0,0 +1,44 @@ +import './global-types'; +import './schemaBuilder'; + +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> { + return typeof result === 'function'; +} + +export class PaginationPlugin extends BasePlugin { + wrapResolve( + resolver: GraphQLFieldResolver>>, + fieldConfig: PothosOutputFieldConfig, + ): 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 ?? this.builder.options.pagination?.defaultPageSize ?? fieldConfig.pothosOptions.pagination?.defaultPageSize ?? 100, + page: args.filter?.page ?? 1, + cursor: undefined, + }); + } + + return result; + }; + } +} + +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 0000000..5008ddf Binary files /dev/null and b/apps/graphql/src/plugins/pagination/schemaBuilder/.DS_Store differ 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..bb30428 --- /dev/null +++ b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreatePaginationInputTypeRef.ts @@ -0,0 +1,19 @@ +import { SchemaTypes } from '@pothos/core'; + +export type GetOrCreatePaginationInputTypeRefArgs = { + builder: PothosSchemaTypes.SchemaBuilder; + name: string; +}; + +export const getOrCreatePaginationInputTypeRef = ({ builder, name }: GetOrCreatePaginationInputTypeRefArgs) => { + try { + return builder.configStore.getInputTypeRef(name); + } catch { + 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 new file mode 100644 index 0000000..156d3f7 --- /dev/null +++ b/apps/graphql/src/plugins/pagination/schemaBuilder/getOrCreateTypeWithAggregationRef.ts @@ -0,0 +1,24 @@ +import { OutputType, SchemaTypes, TypeParam } from '@pothos/core'; + +export type GetOrCreateTypeWithAggregationRefArgs = { + builder: PothosSchemaTypes.SchemaBuilder; + name: string; + type: TypeParam; +}; + +export const getOrCreateTypeWithAggregationRef = ({ builder, name, type }: GetOrCreateTypeWithAggregationRefArgs): OutputType => { + 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/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/types.ts b/apps/graphql/src/plugins/pagination/types.ts new file mode 100644 index 0000000..80a125c --- /dev/null +++ b/apps/graphql/src/plugins/pagination/types.ts @@ -0,0 +1,102 @@ +import { + FieldKind, + FieldNullability, + InputFieldMap, + InputShapeFromFields, + MaybePromise, + SchemaTypes, + ShapeFromTypeParam, + TypeParam, +} from '@pothos/core'; +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, + Type extends TypeParam, + Nullable extends FieldNullability, + Args extends InputFieldMap, + ResolveReturnShape, +> extends PothosSchemaTypes.FieldOptions {} + +export interface QueryFieldOptions< + Types extends SchemaTypes, + Type extends TypeParam, + Nullable extends FieldNullability, + Args extends InputFieldMap, + ResolveReturnShape, +> extends PothosSchemaTypes.FieldOptions {} + +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; + Object: ObjectFieldOptions & + InferredFieldOptionsByKind; +} + +export type PaginatedField< + Types extends SchemaTypes, + ParentShape, + Kind extends FieldKind, + Type extends TypeParam, + Nullable extends FieldNullability, + 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/prisma.ts b/apps/graphql/src/prisma.ts index 7fe3b6a..dd89b10 100644 --- a/apps/graphql/src/prisma.ts +++ b/apps/graphql/src/prisma.ts @@ -1,3 +1,4 @@ -import { PrismaClient } from '@steadystart/prisma'; +import { createPrismaClient } from '@steadystart/prisma'; +export type { PrismaClient } from '@steadystart/prisma'; -export const prisma = new PrismaClient(); +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..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.findMany({ + return ctx.prisma.post.findMany({ where: { workspaceId: ctx.workspace!.id, }, }); - - return posts; }, }), ); 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 b337839..847afeb 100644 --- a/apps/graphql/src/resolvers/workspace/queries/workspaces.ts +++ b/apps/graphql/src/resolvers/workspace/queries/workspaces.ts @@ -2,19 +2,18 @@ import { builder } from '../../../builder'; import { Workspace } from '../../../schema/Workspace'; builder.queryField('workspaces', (t) => - t.field({ + t.paginatedField({ type: [Workspace], authScopes: { accessPolicy: 'authenticated', }, + pagination: { defaultPageSize: 20 }, 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/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/resolveWorkspaceFromContext.ts b/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts index 33d795d..56678cf 100644 --- a/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts +++ b/apps/graphql/src/utils/context/resolveWorkspaceFromContext.ts @@ -1,7 +1,8 @@ -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 { PrismaClient } from '../../prisma'; type ResolveWorkspaceFromContextArgs = ContextProps & { prisma: PrismaClient; 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']; 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..55d5884 --- /dev/null +++ b/libs/prisma/src/index.ts @@ -0,0 +1,66 @@ +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; + /** Optional `cursor` object where key must be an unique, sequential column. */ + cursor?: C; +}; + +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; +}; + +type PrismaPaginationFnBrandedType = { __type: 'PrismaPaginationFn' }; + +export type PrismaPaginationFn = ( + paginationArgs: PaginationArgs, +) => Promise> & PrismaPaginationFnBrandedType; + +type PrismaPaginationFnInternal< + TModel, + TArgs = Prisma.Args, + TResult = Prisma.Result, +> = PrismaPaginationFn['cursor'], TResult>; + +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, cursor }: PaginationArgs['cursor']>) => { + const take = Math.max(0, size); + let skip = (1 - Math.max(1, page)) * size; + + // We skip cursor itself that is the first returned row + if (cursor) { + skip += 1; + } + + 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; 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"] } 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 b143f61..b9d1df7 100644 --- a/libs/tester/src/lib/TestDatabaseOrchestrator.ts +++ b/libs/tester/src/lib/TestDatabaseOrchestrator.ts @@ -1,4 +1,4 @@ -import { PrismaClient } from '@steadystart/prisma'; +import { createPrismaClient, PrismaClient } from '@steadystart/prisma'; import * as path from 'path'; import { v1, v4 } from 'uuid'; import { exec } from '../utils/exec'; @@ -77,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; } @@ -107,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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43cfcf8..a06ca02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9092,7 +9092,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 +9112,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 +9127,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 +9149,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