diff --git a/Readme.md b/Readme.md index 45d7c7e1..92926106 100644 --- a/Readme.md +++ b/Readme.md @@ -262,6 +262,60 @@ export class CustomUserResolver { } ``` +#### Additional decorators for Prisma schema resolvers + +When you need to apply some decorators like `@Authorized`, `@UseMiddleware` or `@Extensions` on the generated resolvers methods, you don't need to modify the generated source files. + +To support this, `typegraphql-prisma` generates two things: `applyResolversEnhanceMap` function and a `ResolversEnhanceMap` type. All you need to do is to create a config object, where you put the decorator functions (without `@`) in an array, and then call that function with that config, eg.: + +```ts +import { + ResolversEnhanceMap, + applyResolversEnhanceMap, +} from "@generated/type-graphql"; +import { Authorized } from "type-graphql"; + +const resolversEnhanceMap: ResolversEnhanceMap = { + Category: { + createCategory: [Authorized(Role.ADMIN)], + }, +}; + +applyResolversEnhanceMap(resolversEnhanceMap); +``` + +This way, when you call `createCategory` GraphQL mutation, it will trigger the `type-graphql` `authChecker` function, providing a `Role.ADMIN` role, just like you would put the `@Authorized` on top of the resolver method. + +Also, if you have a large schema and you need to provide plenty of decorators, you can split the config definition into multiple smaller objects placed in different files. +To accomplish this, just import the generic `ResolverActionsConfig` type and define the resolvers config separately for every Prisma schema model, e.g: + +```ts +import { + ResolversEnhanceMap, + ResolverActionsConfig, + applyResolversEnhanceMap, +} from "@generated/type-graphql"; +import { Authorized, Extensions } from "type-graphql"; + +// define the decorators config using generic ResolverActionsConfig type +const categoryActionsConfig: ResolverActionsConfig<"Category"> = { + deleteCategory: [ + Authorized(Role.ADMIN), + Extensions({ logMessage: "Danger zone", logLevel: LogLevel.WARN }), + ], +}; +const problemActionsConfig: ResolverActionsConfig<"Problem"> = { + createProblem: [Authorized()], +}; + +// join the actions config into a single resolvers enhance object +const resolversEnhanceMap: ResolversEnhanceMap = { + Category: categoryActionsConfig, + Problem: problemActionsConfig, +}; +applyResolversEnhanceMap(resolversEnhanceMap); +``` + #### Adding fields to model type If you want to add a field to the generated type like `User`, you have to add a proper `@FieldResolver` for that: diff --git a/experiments/generated-schema.graphql b/experiments/generated-schema.graphql index 39d10d65..96f61914 100644 --- a/experiments/generated-schema.graphql +++ b/experiments/generated-schema.graphql @@ -3,14 +3,6 @@ # !!! DO NOT MODIFY THIS FILE BY YOURSELF !!! # ----------------------------------------------- -type AggregateCategory { - avg: CategoryAvgAggregate - count: Int! - max: CategoryMaxAggregate - min: CategoryMinAggregate - sum: CategorySumAggregate -} - type AggregateClient { avg: ClientAvgAggregate count: Int! @@ -66,52 +58,18 @@ type Category { slug: String! } -type CategoryAvgAggregate { - number: Float! -} - -input CategoryCreateInput { - name: String! - number: Int! - slug: String! -} - enum CategoryDistinctFieldEnum { name number slug } -type CategoryMaxAggregate { - number: Int! -} - -type CategoryMinAggregate { - number: Int! -} - input CategoryOrderByInput { name: SortOrder number: SortOrder slug: SortOrder } -type CategorySumAggregate { - number: Int! -} - -input CategoryUpdateInput { - name: StringFieldUpdateOperationsInput - number: IntFieldUpdateOperationsInput - slug: StringFieldUpdateOperationsInput -} - -input CategoryUpdateManyMutationInput { - name: StringFieldUpdateOperationsInput - number: IntFieldUpdateOperationsInput - slug: StringFieldUpdateOperationsInput -} - input CategoryWhereInput { AND: [CategoryWhereInput!] name: StringFilter @@ -806,7 +764,6 @@ input MovieWhereUniqueInput { } type Mutation { - createCategory(data: CategoryCreateInput!): Category! createClient(data: ClientCreateInput!): Client! createCreator(data: CreatorCreateInput!): Creator! createDirector(data: DirectorCreateInput!): Director! @@ -815,11 +772,9 @@ type Mutation { createPost(data: PostCreateInput!): Post! createProblem(data: ProblemCreateInput!): Problem! customCreatePost(data: PostCreateInput!): Post! - deleteCategory(where: CategoryWhereUniqueInput!): Category deleteClient(where: ClientWhereUniqueInput!): Client deleteCreator(where: CreatorWhereUniqueInput!): Creator deleteDirector(where: DirectorWhereUniqueInput!): Director - deleteManyCategory(where: CategoryWhereInput): BatchPayload! deleteManyClient(where: ClientWhereInput): BatchPayload! deleteManyCreator(where: CreatorWhereInput): BatchPayload! deleteManyDirector(where: DirectorWhereInput): BatchPayload! @@ -829,11 +784,9 @@ type Mutation { deleteMovie(where: MovieWhereUniqueInput!): Movie deletePatient(where: PatientWhereUniqueInput!): Patient deleteProblem(where: ProblemWhereUniqueInput!): Problem - updateCategory(data: CategoryUpdateInput!, where: CategoryWhereUniqueInput!): Category updateClient(data: ClientUpdateInput!, where: ClientWhereUniqueInput!): Client updateCreator(data: CreatorUpdateInput!, where: CreatorWhereUniqueInput!): Creator updateDirector(data: DirectorUpdateInput!, where: DirectorWhereUniqueInput!): Director - updateManyCategory(data: CategoryUpdateManyMutationInput!, where: CategoryWhereInput): BatchPayload! updateManyClient(data: ClientUpdateManyMutationInput!, where: ClientWhereInput): BatchPayload! updateManyCreator(data: CreatorUpdateManyMutationInput!, where: CreatorWhereInput): BatchPayload! updateManyDirector(data: DirectorUpdateManyMutationInput!, where: DirectorWhereInput): BatchPayload! @@ -844,7 +797,6 @@ type Mutation { updateMovie(data: MovieUpdateInput!, where: MovieWhereUniqueInput!): Movie updatePatient(data: PatientUpdateInput!, where: PatientWhereUniqueInput!): Patient updateProblem(data: ProblemUpdateInput!, where: ProblemWhereUniqueInput!): Problem - upsertCategory(create: CategoryCreateInput!, update: CategoryUpdateInput!, where: CategoryWhereUniqueInput!): Category! upsertClient(create: ClientCreateInput!, update: ClientUpdateInput!, where: ClientWhereUniqueInput!): Client! upsertCreator(create: CreatorCreateInput!, update: CreatorUpdateInput!, where: CreatorWhereUniqueInput!): Creator! upsertDirector(create: DirectorCreateInput!, update: DirectorUpdateInput!, where: DirectorWhereUniqueInput!): Director! @@ -1434,7 +1386,6 @@ input ProblemWhereUniqueInput { } type Query { - aggregateCategory(cursor: CategoryWhereUniqueInput, distinct: [CategoryDistinctFieldEnum!], orderBy: [CategoryOrderByInput!], skip: Int, take: Int, where: CategoryWhereInput): AggregateCategory! aggregateClient(cursor: ClientWhereUniqueInput, distinct: [ClientDistinctFieldEnum!], orderBy: [ClientOrderByInput!], skip: Int, take: Int, where: ClientWhereInput): AggregateClient! aggregateCreator(cursor: CreatorWhereUniqueInput, distinct: [CreatorDistinctFieldEnum!], orderBy: [CreatorOrderByInput!], skip: Int, take: Int, where: CreatorWhereInput): AggregateCreator! aggregateDirector(cursor: DirectorWhereUniqueInput, distinct: [DirectorDistinctFieldEnum!], orderBy: [DirectorOrderByInput!], skip: Int, take: Int, where: DirectorWhereInput): AggregateDirector! @@ -1444,7 +1395,6 @@ type Query { allClients: [Client!]! allPosts: [Post!]! categories(cursor: CategoryWhereUniqueInput, distinct: [CategoryDistinctFieldEnum!], orderBy: [CategoryOrderByInput!], skip: Int, take: Int, where: CategoryWhereInput): [Category!]! - category(where: CategoryWhereUniqueInput!): Category client(where: ClientWhereUniqueInput!): Client clients(cursor: ClientWhereUniqueInput, distinct: [ClientDistinctFieldEnum!], orderBy: [ClientOrderByInput!], skip: Int, take: Int, where: ClientWhereInput): [Client!]! creator(where: CreatorWhereUniqueInput!): Creator @@ -1452,7 +1402,6 @@ type Query { customFindClientsWithArgs(cursor: ClientWhereUniqueInput, distinct: [ClientDistinctFieldEnum!], orderBy: [ClientOrderByInput!], skip: Int, take: Int, where: ClientWhereInput): [Client!]! director(where: DirectorWhereUniqueInput!): Director directors(cursor: DirectorWhereUniqueInput, distinct: [DirectorDistinctFieldEnum!], orderBy: [DirectorOrderByInput!], skip: Int, take: Int, where: DirectorWhereInput): [Director!]! - findFirstCategory(cursor: CategoryWhereUniqueInput, distinct: [CategoryDistinctFieldEnum!], orderBy: [CategoryOrderByInput!], skip: Int, take: Int, where: CategoryWhereInput): Category findFirstClient(cursor: ClientWhereUniqueInput, distinct: [ClientDistinctFieldEnum!], orderBy: [ClientOrderByInput!], skip: Int, take: Int, where: ClientWhereInput): Client findFirstCreator(cursor: CreatorWhereUniqueInput, distinct: [CreatorDistinctFieldEnum!], orderBy: [CreatorOrderByInput!], skip: Int, take: Int, where: CreatorWhereInput): Creator findFirstDirector(cursor: DirectorWhereUniqueInput, distinct: [DirectorDistinctFieldEnum!], orderBy: [DirectorOrderByInput!], skip: Int, take: Int, where: DirectorWhereInput): Director diff --git a/experiments/index.ts b/experiments/index.ts index db321272..6a8c65a3 100644 --- a/experiments/index.ts +++ b/experiments/index.ts @@ -8,6 +8,7 @@ import { Ctx, Args, Mutation, + Authorized, } from "type-graphql"; import { ApolloServer } from "apollo-server"; import path from "path"; @@ -22,7 +23,6 @@ import { CreatePostResolver, UpdateManyPostResolver, // Category, - CategoryCrudResolver, // Patient, PatientCrudResolver, FindManyPostResolver, @@ -34,12 +34,29 @@ import { ProblemRelationsResolver, CreatorRelationsResolver, CreatePostArgs, + ResolversEnhanceMap, + applyResolversEnhanceMap, + ResolverActionsConfig, + FindManyCategoryResolver, } from "./prisma/generated/type-graphql"; import { PrismaClient } from "./prisma/generated/client"; import * as Prisma from "./prisma/generated/client"; import { ProblemCrudResolver } from "./prisma/generated/type-graphql/resolvers/crud/Problem/ProblemCrudResolver"; import { CreatorCrudResolver } from "./prisma/generated/type-graphql/resolvers/crud/Creator/CreatorCrudResolver"; +const problemActionsConfig: ResolverActionsConfig<"Problem"> = { + createProblem: [Authorized()], +}; + +const resolversEnhanceMap: ResolversEnhanceMap = { + Category: { + categories: [Authorized()], + }, + Problem: problemActionsConfig, +}; + +applyResolversEnhanceMap(resolversEnhanceMap); + interface Context { prisma: PrismaClient; } @@ -92,7 +109,8 @@ async function main() { FindOnePostResolver, CreatePostResolver, UpdateManyPostResolver, - CategoryCrudResolver, + // CategoryCrudResolver, + FindManyCategoryResolver, PatientCrudResolver, FindManyPostResolver, MovieCrudResolver, @@ -106,6 +124,12 @@ async function main() { ], validate: false, emitSchemaFile: path.resolve(__dirname, "./generated-schema.graphql"), + authChecker: ({ info }) => { + console.log( + `${info.parentType.name}.${info.fieldName} requested, access prohibited`, + ); + return false; + }, }); const prisma = new PrismaClient({ @@ -113,6 +137,8 @@ async function main() { log: ["query"], }); + await prisma.$connect(); + const server = new ApolloServer({ schema, playground: true, diff --git a/experiments/prisma/generated/type-graphql/enhance.ts b/experiments/prisma/generated/type-graphql/enhance.ts new file mode 100644 index 00000000..7469e728 --- /dev/null +++ b/experiments/prisma/generated/type-graphql/enhance.ts @@ -0,0 +1,157 @@ +import * as crudResolvers from "./resolvers/crud/resolvers-crud.index"; +import * as actionResolvers from "./resolvers/crud/resolvers-actions.index"; + +const crudResolversMap = { + Client: crudResolvers.ClientCrudResolver, + Post: crudResolvers.PostCrudResolver, + Category: crudResolvers.CategoryCrudResolver, + Patient: crudResolvers.PatientCrudResolver, + Movie: crudResolvers.MovieCrudResolver, + Director: crudResolvers.DirectorCrudResolver, + Problem: crudResolvers.ProblemCrudResolver, + Creator: crudResolvers.CreatorCrudResolver +}; +const actionResolversMap = { + Client: { + client: actionResolvers.FindOneClientResolver, + findFirstClient: actionResolvers.FindFirstClientResolver, + clients: actionResolvers.FindManyClientResolver, + createClient: actionResolvers.CreateClientResolver, + deleteClient: actionResolvers.DeleteClientResolver, + updateClient: actionResolvers.UpdateClientResolver, + deleteManyClient: actionResolvers.DeleteManyClientResolver, + updateManyClient: actionResolvers.UpdateManyClientResolver, + upsertClient: actionResolvers.UpsertClientResolver, + aggregateClient: actionResolvers.AggregateClientResolver + }, + Post: { + post: actionResolvers.FindOnePostResolver, + findFirstPost: actionResolvers.FindFirstPostResolver, + posts: actionResolvers.FindManyPostResolver, + createPost: actionResolvers.CreatePostResolver, + deletePost: actionResolvers.DeletePostResolver, + updatePost: actionResolvers.UpdatePostResolver, + deleteManyPost: actionResolvers.DeleteManyPostResolver, + updateManyPost: actionResolvers.UpdateManyPostResolver, + upsertPost: actionResolvers.UpsertPostResolver, + aggregatePost: actionResolvers.AggregatePostResolver + }, + Category: { + category: actionResolvers.FindOneCategoryResolver, + findFirstCategory: actionResolvers.FindFirstCategoryResolver, + categories: actionResolvers.FindManyCategoryResolver, + createCategory: actionResolvers.CreateCategoryResolver, + deleteCategory: actionResolvers.DeleteCategoryResolver, + updateCategory: actionResolvers.UpdateCategoryResolver, + deleteManyCategory: actionResolvers.DeleteManyCategoryResolver, + updateManyCategory: actionResolvers.UpdateManyCategoryResolver, + upsertCategory: actionResolvers.UpsertCategoryResolver, + aggregateCategory: actionResolvers.AggregateCategoryResolver + }, + Patient: { + patient: actionResolvers.FindOnePatientResolver, + findFirstPatient: actionResolvers.FindFirstPatientResolver, + patients: actionResolvers.FindManyPatientResolver, + createPatient: actionResolvers.CreatePatientResolver, + deletePatient: actionResolvers.DeletePatientResolver, + updatePatient: actionResolvers.UpdatePatientResolver, + deleteManyPatient: actionResolvers.DeleteManyPatientResolver, + updateManyPatient: actionResolvers.UpdateManyPatientResolver, + upsertPatient: actionResolvers.UpsertPatientResolver, + aggregatePatient: actionResolvers.AggregatePatientResolver + }, + Movie: { + movie: actionResolvers.FindOneMovieResolver, + findFirstMovie: actionResolvers.FindFirstMovieResolver, + movies: actionResolvers.FindManyMovieResolver, + createMovie: actionResolvers.CreateMovieResolver, + deleteMovie: actionResolvers.DeleteMovieResolver, + updateMovie: actionResolvers.UpdateMovieResolver, + deleteManyMovie: actionResolvers.DeleteManyMovieResolver, + updateManyMovie: actionResolvers.UpdateManyMovieResolver, + upsertMovie: actionResolvers.UpsertMovieResolver, + aggregateMovie: actionResolvers.AggregateMovieResolver + }, + Director: { + director: actionResolvers.FindOneDirectorResolver, + findFirstDirector: actionResolvers.FindFirstDirectorResolver, + directors: actionResolvers.FindManyDirectorResolver, + createDirector: actionResolvers.CreateDirectorResolver, + deleteDirector: actionResolvers.DeleteDirectorResolver, + updateDirector: actionResolvers.UpdateDirectorResolver, + deleteManyDirector: actionResolvers.DeleteManyDirectorResolver, + updateManyDirector: actionResolvers.UpdateManyDirectorResolver, + upsertDirector: actionResolvers.UpsertDirectorResolver, + aggregateDirector: actionResolvers.AggregateDirectorResolver + }, + Problem: { + problem: actionResolvers.FindOneProblemResolver, + findFirstProblem: actionResolvers.FindFirstProblemResolver, + problems: actionResolvers.FindManyProblemResolver, + createProblem: actionResolvers.CreateProblemResolver, + deleteProblem: actionResolvers.DeleteProblemResolver, + updateProblem: actionResolvers.UpdateProblemResolver, + deleteManyProblem: actionResolvers.DeleteManyProblemResolver, + updateManyProblem: actionResolvers.UpdateManyProblemResolver, + upsertProblem: actionResolvers.UpsertProblemResolver, + aggregateProblem: actionResolvers.AggregateProblemResolver + }, + Creator: { + creator: actionResolvers.FindOneCreatorResolver, + findFirstCreator: actionResolvers.FindFirstCreatorResolver, + creators: actionResolvers.FindManyCreatorResolver, + createCreator: actionResolvers.CreateCreatorResolver, + deleteCreator: actionResolvers.DeleteCreatorResolver, + updateCreator: actionResolvers.UpdateCreatorResolver, + deleteManyCreator: actionResolvers.DeleteManyCreatorResolver, + updateManyCreator: actionResolvers.UpdateManyCreatorResolver, + upsertCreator: actionResolvers.UpsertCreatorResolver, + aggregateCreator: actionResolvers.AggregateCreatorResolver + } +}; + +type ModelNames = keyof typeof crudResolversMap; + +type ModelResolverActionNames< + TModel extends ModelNames + > = keyof typeof crudResolversMap[TModel]["prototype"]; + +export type ResolverActionsConfig = { + [TActionName in ModelResolverActionNames]?: MethodDecorator[]; +}; + +export type ResolversEnhanceMap = { + [TModel in ModelNames]?: ResolverActionsConfig; +}; + +export function applyResolversEnhanceMap( + resolversEnhanceMap: ResolversEnhanceMap, +) { + for (const resolversEnhanceMapKey of Object.keys(resolversEnhanceMap)) { + const modelName = resolversEnhanceMapKey as keyof typeof resolversEnhanceMap; + const resolverActionsConfig = resolversEnhanceMap[modelName]!; + for (const modelResolverActionName of Object.keys(resolverActionsConfig)) { + const decorators = resolverActionsConfig[ + modelResolverActionName as keyof typeof resolverActionsConfig + ] as MethodDecorator[]; + const crudTarget = crudResolversMap[modelName].prototype; + const actionResolversConfig = actionResolversMap[modelName]; + const actionTarget = (actionResolversConfig[ + modelResolverActionName as keyof typeof actionResolversConfig + ] as Function).prototype; + for (const decorator of decorators) { + decorator( + crudTarget, + modelResolverActionName, + Object.getOwnPropertyDescriptor(crudTarget, modelResolverActionName)!, + ); + decorator( + actionTarget, + modelResolverActionName, + Object.getOwnPropertyDescriptor(actionTarget, modelResolverActionName)!, + ); + } + } + } +} + diff --git a/experiments/prisma/generated/type-graphql/index.ts b/experiments/prisma/generated/type-graphql/index.ts index 39ed8ea5..2f9e8ef5 100644 --- a/experiments/prisma/generated/type-graphql/index.ts +++ b/experiments/prisma/generated/type-graphql/index.ts @@ -1,6 +1,6 @@ import { NonEmptyArray } from "type-graphql"; -import * as crudResolvers from "./resolvers/crud/resolvers-crud.index"; -import * as relationResolvers from "./resolvers/relations/resolvers.index"; +import * as crudResolversImport from "./resolvers/crud/resolvers-crud.index"; +import * as relationResolversImport from "./resolvers/relations/resolvers.index"; export * from "./enums"; export * from "./models"; @@ -8,7 +8,8 @@ export * from "./resolvers/crud"; export * from "./resolvers/relations"; export * from "./resolvers/inputs"; export * from "./resolvers/outputs"; +export * from "./enhance"; -export const resolvers = [...Object.values(crudResolvers), ...Object.values(relationResolvers)] as unknown as NonEmptyArray; - -export { crudResolvers, relationResolvers }; +export const crudResolvers = Object.values(crudResolversImport) as unknown as NonEmptyArray; +export const relationResolvers = Object.values(relationResolversImport) as unknown as NonEmptyArray; +export const resolvers = [...crudResolvers, ...relationResolvers] as unknown as NonEmptyArray; diff --git a/experiments/query.graphql b/experiments/query.graphql index a3f30980..704a1542 100644 --- a/experiments/query.graphql +++ b/experiments/query.graphql @@ -224,3 +224,11 @@ mutation AtomicNumberOperations { ...ClientData } } + +query ForbiddenCategories { + categories { + name + slug + number + } +} diff --git a/src/generator/enhance.ts b/src/generator/enhance.ts new file mode 100644 index 00000000..ccaf80e2 --- /dev/null +++ b/src/generator/enhance.ts @@ -0,0 +1,107 @@ +import { SourceFile, VariableDeclarationKind, Writers } from "ts-morph"; +import { crudResolversFolderName, resolversFolderName } from "./config"; +import { DMMF } from "./dmmf/types"; + +export function generateEnhanceMap( + sourceFile: SourceFile, + modelMappings: DMMF.ModelMapping[], +) { + sourceFile.addImportDeclaration({ + moduleSpecifier: `./${resolversFolderName}/${crudResolversFolderName}/resolvers-crud.index`, + namespaceImport: "crudResolvers", + }); + + sourceFile.addImportDeclaration({ + moduleSpecifier: `./${resolversFolderName}/${crudResolversFolderName}/resolvers-actions.index`, + namespaceImport: "actionResolvers", + }); + + sourceFile.addVariableStatement({ + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: "crudResolversMap", + initializer: Writers.object( + Object.fromEntries( + modelMappings.map(mapping => [ + mapping.modelTypeName, + `crudResolvers.${mapping.resolverName}`, + ]), + ), + ), + }, + ], + trailingTrivia: "\r\n", + }); + + sourceFile.addVariableStatement({ + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: "actionResolversMap", + initializer: Writers.object( + Object.fromEntries( + modelMappings.map(mapping => [ + mapping.modelTypeName, + Writers.object( + Object.fromEntries( + mapping.actions.map(action => [ + action.name, + `actionResolvers.${action.actionResolverName}`, + ]), + ), + ), + ]), + ), + ), + }, + ], + }); + + sourceFile.addStatements(/* ts */ ` + type ModelNames = keyof typeof crudResolversMap; + + type ModelResolverActionNames< + TModel extends ModelNames + > = keyof typeof crudResolversMap[TModel]["prototype"]; + + export type ResolverActionsConfig = { + [TActionName in ModelResolverActionNames]?: MethodDecorator[]; + }; + + export type ResolversEnhanceMap = { + [TModel in ModelNames]?: ResolverActionsConfig; + }; + + export function applyResolversEnhanceMap( + resolversEnhanceMap: ResolversEnhanceMap, + ) { + for (const resolversEnhanceMapKey of Object.keys(resolversEnhanceMap)) { + const modelName = resolversEnhanceMapKey as keyof typeof resolversEnhanceMap; + const resolverActionsConfig = resolversEnhanceMap[modelName]!; + for (const modelResolverActionName of Object.keys(resolverActionsConfig)) { + const decorators = resolverActionsConfig[ + modelResolverActionName as keyof typeof resolverActionsConfig + ] as MethodDecorator[]; + const crudTarget = crudResolversMap[modelName].prototype; + const actionResolversConfig = actionResolversMap[modelName]; + const actionTarget = (actionResolversConfig[ + modelResolverActionName as keyof typeof actionResolversConfig + ] as Function).prototype; + for (const decorator of decorators) { + decorator( + crudTarget, + modelResolverActionName, + Object.getOwnPropertyDescriptor(crudTarget, modelResolverActionName)!, + ); + decorator( + actionTarget, + modelResolverActionName, + Object.getOwnPropertyDescriptor(actionTarget, modelResolverActionName)!, + ); + } + } + } + } + `); +} diff --git a/src/generator/generate-code.ts b/src/generator/generate-code.ts index 60f7e98d..132361b7 100644 --- a/src/generator/generate-code.ts +++ b/src/generator/generate-code.ts @@ -39,6 +39,7 @@ import generateArgsTypeClassFromArgs from "./args-class"; import generateActionResolverClass from "./resolvers/separate-action"; import { ensureInstalledCorrectPrismaPackage } from "../utils/prisma-version"; import { GenerateMappingData } from "./types"; +import { generateEnhanceMap } from "./enhance"; const baseCompilerOptions: CompilerOptions = { target: ScriptTarget.ES2019, @@ -428,6 +429,14 @@ export default async function generateCode( .map(mapping => mapping.modelTypeName), ); + log("Generate enhance map"); + const enhanceSourceFile = project.createSourceFile( + baseDirPath + "/enhance.ts", + undefined, + { overwrite: true }, + ); + generateEnhanceMap(enhanceSourceFile, dmmfDocument.modelMappings); + log("Generating index file"); const indexSourceFile = project.createSourceFile( baseDirPath + "/index.ts", diff --git a/src/generator/imports.ts b/src/generator/imports.ts index 881b5c95..f3d9dc9e 100644 --- a/src/generator/imports.ts +++ b/src/generator/imports.ts @@ -165,6 +165,7 @@ export function generateIndexFile( : []), { moduleSpecifier: `./${resolversFolderName}/${inputsFolderName}` }, { moduleSpecifier: `./${resolversFolderName}/${outputsFolderName}` }, + { moduleSpecifier: `./enhance` }, ]); sourceFile.addImportDeclarations([ @@ -174,13 +175,13 @@ export function generateIndexFile( }, { moduleSpecifier: `./${resolversFolderName}/${crudResolversFolderName}/resolvers-crud.index`, - namespaceImport: "crudResolvers", + namespaceImport: "crudResolversImport", }, ...(hasSomeRelations ? [ { moduleSpecifier: `./${resolversFolderName}/${relationsResolversFolderName}/resolvers.index`, - namespaceImport: "relationResolvers", + namespaceImport: "relationResolversImport", }, ] : []), @@ -191,18 +192,35 @@ export function generateIndexFile( declarationKind: VariableDeclarationKind.Const, declarations: [ { - name: "resolvers", - initializer: `[...Object.values(crudResolvers)${ - hasSomeRelations ? ", ...Object.values(relationResolvers)" : "" - }] as unknown as NonEmptyArray`, + name: "crudResolvers", + initializer: `Object.values(crudResolversImport) as unknown as NonEmptyArray`, }, ], }); - sourceFile.addExportDeclaration({ - namedExports: [ - "crudResolvers", - ...(hasSomeRelations ? ["relationResolvers"] : []), + if (hasSomeRelations) { + sourceFile.addVariableStatement({ + isExported: true, + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: "relationResolvers", + initializer: `Object.values(relationResolversImport) as unknown as NonEmptyArray`, + }, + ], + }); + } + + sourceFile.addVariableStatement({ + isExported: true, + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: "resolvers", + initializer: `[...crudResolvers${ + hasSomeRelations ? ", ...relationResolvers" : "" + }] as unknown as NonEmptyArray`, + }, ], }); } diff --git a/tests/functional/__snapshots__/integration.ts.snap b/tests/functional/__snapshots__/integration.ts.snap index d0675e24..ec50e147 100644 --- a/tests/functional/__snapshots__/integration.ts.snap +++ b/tests/functional/__snapshots__/integration.ts.snap @@ -408,6 +408,7 @@ input UserWhereUniqueInput { exports[`generator integration should generates TypeGraphQL classes files to output folder by running \`prisma generate\`: files structure 1`] = ` " [type-graphql] + enhance.ts [enums] Color.ts PostDistinctFieldEnum.ts diff --git a/tests/functional/enhance.ts b/tests/functional/enhance.ts new file mode 100644 index 00000000..f4e666ba --- /dev/null +++ b/tests/functional/enhance.ts @@ -0,0 +1,81 @@ +import "reflect-metadata"; +import { promises as fs } from "fs"; +import { buildSchema, Authorized } from "type-graphql"; +import { graphql } from "graphql"; + +import generateArtifactsDirPath from "../helpers/artifacts-dir"; +import { generateCodeFromSchema } from "../helpers/generate-code"; + +describe("custom resolvers execution", () => { + let outputDirPath: string; + + beforeAll(async () => { + outputDirPath = generateArtifactsDirPath("functional-enhance"); + await fs.mkdir(outputDirPath, { recursive: true }); + const prismaSchema = /* prisma */ ` + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + enum Color { + RED + GREEN + BLUE + } + + model Post { + uuid String @id @default(cuid()) + content String + color Color + } + `; + await generateCodeFromSchema(prismaSchema, { outputDirPath }); + }); + + it("should properly apply decorators in enhance map", async () => { + const { + applyResolversEnhanceMap, + PostCrudResolver, + } = require(outputDirPath); + + applyResolversEnhanceMap({ + Post: { + posts: [Authorized()], + }, + }); + + const document = /* graphql */ ` + query { + posts( + take: 1 + skip: 1 + where: { + content: { startsWith: "Test" } + } + orderBy: { + color: desc + } + ) { + uuid + color + } + } + `; + const graphQLSchema = await buildSchema({ + resolvers: [PostCrudResolver], + validate: false, + authChecker: () => false, + emitSchemaFile: outputDirPath + "/schema.graphql", + }); + const { errors } = await graphql(graphQLSchema, document, null, { + prisma: {}, + }); + + expect(errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: Access denied! You need to be authorized to perform this action!], + ] + `); + }); +}); diff --git a/tests/regression/__snapshots__/crud.ts.snap b/tests/regression/__snapshots__/crud.ts.snap index b80bc453..698eb4b1 100644 --- a/tests/regression/__snapshots__/crud.ts.snap +++ b/tests/regression/__snapshots__/crud.ts.snap @@ -561,17 +561,17 @@ exports[`crud should properly generate resolver class for single prisma model: c exports[`crud should properly generate resolver class for single prisma model: mainIndex 1`] = ` "import { NonEmptyArray } from \\"type-graphql\\"; -import * as crudResolvers from \\"./resolvers/crud/resolvers-crud.index\\"; +import * as crudResolversImport from \\"./resolvers/crud/resolvers-crud.index\\"; export * from \\"./enums\\"; export * from \\"./models\\"; export * from \\"./resolvers/crud\\"; export * from \\"./resolvers/inputs\\"; export * from \\"./resolvers/outputs\\"; +export * from \\"./enhance\\"; -export const resolvers = [...Object.values(crudResolvers)] as unknown as NonEmptyArray; - -export { crudResolvers }; +export const crudResolvers = Object.values(crudResolversImport) as unknown as NonEmptyArray; +export const resolvers = [...crudResolvers] as unknown as NonEmptyArray; " `; diff --git a/tests/regression/__snapshots__/enhance.ts.snap b/tests/regression/__snapshots__/enhance.ts.snap new file mode 100644 index 00000000..968c9b7b --- /dev/null +++ b/tests/regression/__snapshots__/enhance.ts.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`resolvers enhance should emit resolvers model config map with types: enhance 1`] = ` +"import * as crudResolvers from \\"./resolvers/crud/resolvers-crud.index\\"; +import * as actionResolvers from \\"./resolvers/crud/resolvers-actions.index\\"; + +const crudResolversMap = { + Client: crudResolvers.ClientCrudResolver, + Post: crudResolvers.PostCrudResolver +}; +const actionResolversMap = { + Client: { + client: actionResolvers.FindOneClientResolver, + findFirstClient: actionResolvers.FindFirstClientResolver, + clients: actionResolvers.FindManyClientResolver, + createClient: actionResolvers.CreateClientResolver, + deleteClient: actionResolvers.DeleteClientResolver, + updateClient: actionResolvers.UpdateClientResolver, + deleteManyClient: actionResolvers.DeleteManyClientResolver, + updateManyClient: actionResolvers.UpdateManyClientResolver, + upsertClient: actionResolvers.UpsertClientResolver, + aggregateClient: actionResolvers.AggregateClientResolver + }, + Post: { + post: actionResolvers.FindOnePostResolver, + findFirstPost: actionResolvers.FindFirstPostResolver, + posts: actionResolvers.FindManyPostResolver, + createPost: actionResolvers.CreatePostResolver, + deletePost: actionResolvers.DeletePostResolver, + updatePost: actionResolvers.UpdatePostResolver, + deleteManyPost: actionResolvers.DeleteManyPostResolver, + updateManyPost: actionResolvers.UpdateManyPostResolver, + upsertPost: actionResolvers.UpsertPostResolver, + aggregatePost: actionResolvers.AggregatePostResolver + } +}; + +type ModelNames = keyof typeof crudResolversMap; + +type ModelResolverActionNames< + TModel extends ModelNames + > = keyof typeof crudResolversMap[TModel][\\"prototype\\"]; + +export type ResolverActionsConfig = { + [TActionName in ModelResolverActionNames]?: MethodDecorator[]; +}; + +export type ResolversEnhanceMap = { + [TModel in ModelNames]?: ResolverActionsConfig; +}; + +export function applyResolversEnhanceMap( + resolversEnhanceMap: ResolversEnhanceMap, +) { + for (const resolversEnhanceMapKey of Object.keys(resolversEnhanceMap)) { + const modelName = resolversEnhanceMapKey as keyof typeof resolversEnhanceMap; + const resolverActionsConfig = resolversEnhanceMap[modelName]!; + for (const modelResolverActionName of Object.keys(resolverActionsConfig)) { + const decorators = resolverActionsConfig[ + modelResolverActionName as keyof typeof resolverActionsConfig + ] as MethodDecorator[]; + const crudTarget = crudResolversMap[modelName].prototype; + const actionResolversConfig = actionResolversMap[modelName]; + const actionTarget = (actionResolversConfig[ + modelResolverActionName as keyof typeof actionResolversConfig + ] as Function).prototype; + for (const decorator of decorators) { + decorator( + crudTarget, + modelResolverActionName, + Object.getOwnPropertyDescriptor(crudTarget, modelResolverActionName)!, + ); + decorator( + actionTarget, + modelResolverActionName, + Object.getOwnPropertyDescriptor(actionTarget, modelResolverActionName)!, + ); + } + } + } +} + +" +`; diff --git a/tests/regression/__snapshots__/relations.ts.snap b/tests/regression/__snapshots__/relations.ts.snap index 87399519..d2f103ee 100644 --- a/tests/regression/__snapshots__/relations.ts.snap +++ b/tests/regression/__snapshots__/relations.ts.snap @@ -13,8 +13,8 @@ export * from \\"./args.index\\"; exports[`relations resolvers generation should properly generate index files for relation resolvers: mainIndex 1`] = ` "import { NonEmptyArray } from \\"type-graphql\\"; -import * as crudResolvers from \\"./resolvers/crud/resolvers-crud.index\\"; -import * as relationResolvers from \\"./resolvers/relations/resolvers.index\\"; +import * as crudResolversImport from \\"./resolvers/crud/resolvers-crud.index\\"; +import * as relationResolversImport from \\"./resolvers/relations/resolvers.index\\"; export * from \\"./enums\\"; export * from \\"./models\\"; @@ -22,10 +22,11 @@ export * from \\"./resolvers/crud\\"; export * from \\"./resolvers/relations\\"; export * from \\"./resolvers/inputs\\"; export * from \\"./resolvers/outputs\\"; +export * from \\"./enhance\\"; -export const resolvers = [...Object.values(crudResolvers), ...Object.values(relationResolvers)] as unknown as NonEmptyArray; - -export { crudResolvers, relationResolvers }; +export const crudResolvers = Object.values(crudResolversImport) as unknown as NonEmptyArray; +export const relationResolvers = Object.values(relationResolversImport) as unknown as NonEmptyArray; +export const resolvers = [...crudResolvers, ...relationResolvers] as unknown as NonEmptyArray; " `; diff --git a/tests/regression/__snapshots__/structure.ts.snap b/tests/regression/__snapshots__/structure.ts.snap index a9bfea19..65bf94c9 100644 --- a/tests/regression/__snapshots__/structure.ts.snap +++ b/tests/regression/__snapshots__/structure.ts.snap @@ -3,6 +3,8 @@ exports[`structure should generate *.js and *.d.ts files when emitTranspiledCode is set to true: structure 1`] = ` " [type-graphql] + enhance.d.ts + enhance.js [enums] Color.d.ts Color.js @@ -257,6 +259,7 @@ exports[`structure should generate *.js and *.d.ts files when emitTranspiledCode exports[`structure should generate proper folders and file names when model is renamed: structure 1`] = ` " [type-graphql] + enhance.ts [enums] Color.ts QueryMode.ts @@ -394,6 +397,7 @@ exports[`structure should generate proper folders and file names when model is r exports[`structure should generate proper folders structure and file names for complex datamodel: structure 1`] = ` " [type-graphql] + enhance.ts [enums] Color.ts PostDistinctFieldEnum.ts diff --git a/tests/regression/enhance.ts b/tests/regression/enhance.ts new file mode 100644 index 00000000..7d815d43 --- /dev/null +++ b/tests/regression/enhance.ts @@ -0,0 +1,53 @@ +import { promises as fs } from "fs"; +import generateArtifactsDirPath from "../helpers/artifacts-dir"; +import { generateCodeFromSchema } from "../helpers/generate-code"; +import createReadGeneratedFile, { + ReadGeneratedFile, +} from "../helpers/read-file"; + +describe("resolvers enhance", () => { + let outputDirPath: string; + let readGeneratedFile: ReadGeneratedFile; + + beforeEach(async () => { + outputDirPath = generateArtifactsDirPath("enhance-regression"); + await fs.mkdir(outputDirPath, { recursive: true }); + readGeneratedFile = createReadGeneratedFile(outputDirPath); + }); + + it("should emit resolvers model config map with types", async () => { + const schema = /* prisma */ ` + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + /// @@TypeGraphQL.type(name: "Client") + model User { + id Int @id @default(autoincrement()) + email String @unique + posts Post[] @relation("posts") + } + + model Post { + uuid String @id @default(uuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + published Boolean + title String + content String? + author User @relation(fields: [authorId], references: [id], name: "posts") + authorId Int + metadata Json + } + `; + + await generateCodeFromSchema(schema, { + outputDirPath, + simpleResolvers: true, + }); + const enhanceTSFile = await readGeneratedFile("/enhance.ts"); + + expect(enhanceTSFile).toMatchSnapshot("enhance"); + }); +});