import { defaultFieldResolver , GraphQLFieldResolver , GraphQLScalarType } from 'graphql' ; import * as Nexus from 'nexus' ; import { DynamicOutputPropertyDef } from 'nexus/dist/dynamicProperty' ; import * as path from 'path' ; import * as Constraints from './constraints' ; import { addComputedInputs , DmmfDocument , getTransformedDmmf , InternalDMMF } from './dmmf' ; import * as GraphQL from './graphql' ; import { OnUnknownArgName , OnUnknownFieldName , OnUnknownFieldType , OnUnknownPrismaModelName , raiseErrorOrTriggerHook , } from './hooks' ; import { getCrudMappedFields } from './mapping' ; import { ArgsNamingStrategy , defaultArgsNamingStrategy , defaultFieldNamingStrategy , FieldNamingStrategy , OperationName , } from './naming-strategies' ; import { transformNullsToUndefined } from './null' ; import { paginationStrategies , PaginationStrategyTypes } from './pagination' ; import { proxifyModelFunction , proxifyPublishers } from './proxifier' ; import { getNexusTypesCompositionForOutput , Publisher } from './publisher' ; import * as Typegen from './typegen' ; import { apply , assertPrismaClientInContext , GlobalComputedInputs , Index , indexBy , isEmptyObject , LocalComputedInputs , lowerFirst , } from './utils' ; interface FieldPublisherConfig { alias ? : string ; type ? : string ; pagination ? : boolean | Record < string , boolean > ; filtering ? : boolean | Record < string , boolean > ; ordering ? : boolean | Record < string , boolean > ; computedInputs ? : LocalComputedInputs < any > ; resolve ? : ( root : object , args : object , ctx : object , info : object , originalResolve : GraphQLFieldResolver < any , any , any > ) => Promise < any > ; } type WithRequiredKeys < T , K extends keyof T > = Omit < T , K > & Required < Pick < T , K > > ; type ResolvedFieldPublisherConfig = Omit < WithRequiredKeys < FieldPublisherConfig , 'alias' | 'type' > , 'computedInputs' > & { locallyComputedInputs : LocalComputedInputs < any > ; } ; type FieldPublisher = ( opts ? : FieldPublisherConfig ) => PublisherMethods ; type PublisherMethods = Record < string , FieldPublisher > ; type PublisherConfigData = { field : InternalDMMF . SchemaField ; givenConfig ? : FieldPublisherConfig ; } ; type FieldConfigData = { field : InternalDMMF . SchemaField ; publisherConfig : ResolvedFieldPublisherConfig ; typeName : string ; operation ? : OperationName | null ; resolve ? : Nexus . FieldResolver < any , string > ; } ; type PrismaClientFetcher = ( ctx : Nexus . core . GetGen < 'context' > ) => any ; export interface Options { /**
     * nexus-prisma will call this to get a reference to an instance of the Prisma Client.
     * The function is passed the context object. Typically a Prisma Client instance will
     * be available on the context to support your custom resolvers. Therefore the
     * default getter returns `ctx.prisma`.
     */ prismaClient ? : PrismaClientFetcher ; /**
     * Same purpose as for that used in `Nexus.makeSchema`. Follows the same rules
     * and permits the same environment variables. This configuration will completely
     * go away once Nexus has typeGen plugin support.
     */ shouldGenerateArtifacts ? : boolean ; inputs ? : { /**
         * Where can nexus-prisma find the Prisma Client JS package? By default looks in
         * `node_modules/@prisma/client`. This is needed because nexus-prisma
         * gets your Prisma schema AST and Prisma Client JS crud info from the generated
         * Prisma Client JS package.
         */ prismaClient ? : string ; } ; outputs ? : { default remarks typegen ? : string ; } ; default paginationStrategy ? : 'relay' | 'prisma' ; default experimentalCRUD ? : boolean ; default scalars ? : Partial < Record < Typegen . GetGen < 'scalars' , string > , GraphQLScalarType > > ; computedInputs ? : GlobalComputedInputs ; remarks default atomicOperations ? : boolean ; } export interface InternalOptions extends Options { dmmf ? : DmmfDocument ; nexusBuilder : Nexus . PluginBuilderLens ; nexusPrismaImportId ? : string ; onUnknownFieldName ? : OnUnknownFieldName ; onUnknownFieldType ? : OnUnknownFieldType ; onUnknownArgName ? : OnUnknownArgName ; onUnknownPrismaModelName ? : OnUnknownPrismaModelName ; } export interface InternalPublicOptions extends Omit < InternalOptions , 'nexusBuilder' > { } export function build ( options ) { const builder = new SchemaBuilder ( options ) ; return { types : builder . build ( ) , wasCrudUsedButDisabled ( ) { return builder . wasCrudUsedButDisabled ; } , } ; } let defaultTypegenPath ; if ( process . env . NEXUS_PRISMA_TYPEGEN_PATH ) { defaultTypegenPath = process . env . NEXUS_PRISMA_TYPEGEN_PATH ; } else if ( process . env . LINK ) { defaultTypegenPath = path . join ( process . cwd ( ) , 'node_modules/@types/typegen-nexus-plugin-prisma/index.d.ts' ) ; } else { defaultTypegenPath = path . join ( __dirname , '../../@types/typegen-nexus-plugin-prisma/index.d.ts' ) ; } let defaultClientPath ; if ( process . env . NEXUS_PRISMA_CLIENT_PATH ) { defaultClientPath = process . env . NEXUS_PRISMA_CLIENT_PATH ; } else if ( process . env . LINK ) { defaultClientPath = eval ( "path.join(process.cwd(), '/node_modules/@prisma/client')" ) ; } else { defaultClientPath = '@prisma/client' ; } const shouldGenerateArtifacts = process . env . NEXUS_SHOULD_GENERATE_ARTIFACTS === 'true' ? true : process . env . NEXUS_SHOULD_GENERATE_ARTIFACTS === 'false' ? false : Boolean ( ! process . env . NODE_ENV || process . env . NODE_ENV === 'development' ) ; const defaultOptions = { shouldGenerateArtifacts , prismaClient : ( ctx ) => ctx . prisma , paginationStrategy : 'relay' as const , atomicOperations : true , inputs : { prismaClient : defaultClientPath , } , outputs : { typegen : defaultTypegenPath , } , computedInputs : { } , } ; export interface CustomInputArg { arg : InternalDMMF . SchemaArg ; type : InternalDMMF . InputType | InternalDMMF . SchemaEnum | { name : string ; } ; } export class SchemaBuilder { readonly dmmf : DmmfDocument ; protected argsNamingStrategy : ArgsNamingStrategy ; protected fieldNamingStrategy : FieldNamingStrategy ; protected paginationStrategy : PaginationStrategyTypes ; protected getPrismaClient : PrismaClientFetcher ; protected publisher : Publisher ; protected scalars : Record < string , GraphQLScalarType > ; protected globallyComputedInputs : GlobalComputedInputs ; protected unknownFieldsByModel : Index < string [ ] > ; public wasCrudUsedButDisabled : boolean ; constructor ( public options ) { const config = { ... defaultOptions , ... options , inputs : { ... defaultOptions . inputs , ... options . inputs } , outputs : { ... defaultOptions . outputs , ... options . outputs } , } ; this . globallyComputedInputs = config . computedInputs ? config . computedInputs : { } ; this . paginationStrategy = paginationStrategies [ config . paginationStrategy ] ; this . dmmf = options . dmmf || getTransformedDmmf ( config . inputs . prismaClient , { globallyComputedInputs : this . globallyComputedInputs , paginationStrategy : this . paginationStrategy , atomicOperations : config . atomicOperations , } ) ; this . scalars = ( options . scalars as any ) ?? { } ; this . publisher = new Publisher ( this . dmmf , config . nexusBuilder , this . scalars ) ; this . unknownFieldsByModel = { } ; this . argsNamingStrategy = defaultArgsNamingStrategy ; this . fieldNamingStrategy = defaultFieldNamingStrategy ; this . wasCrudUsedButDisabled = false ; this . getPrismaClient = ( ctx ) => { const prismaClient = config . prismaClient ( ctx ) ; assertPrismaClientInContext ( prismaClient ) ; return prismaClient ; } ; if ( config . shouldGenerateArtifacts ) { Typegen . generateSync ( { prismaClientPath : config . inputs . prismaClient , typegenPath : config . outputs . typegen , paginationStrategy : this . paginationStrategy , nexusPrismaImportId : options . nexusPrismaImportId , } ) ; } } /**
     * The build entrypoint, bringing together sub-builders.
     */ build ( ) { if ( this . options . experimentalCRUD === true ) { return [ this . buildCRUD ( ) , this . buildModel ( ) ] ; } return [ this . buildModel ( ) , this . buildDisabledCRUD ( ) ] ; } /**
     * Build `t.crud` dynamic output property
     */ protected buildCRUD ( ) { return Nexus . dynamicOutputProperty ( { name : 'crud' , typeDefinition : `: NexusPrisma<TypeName, 'crud'>` , factory : ( { typeDef : t , typeName , stage } ) => { if ( typeName === GraphQL . rootNames . Subscription ) { throw new Error ( `t.crud is not yet supported on the 'Subscription' type.` ) ; } if ( typeName !== GraphQL . rootNames . Query && typeName !== GraphQL . rootNames . Mutation ) { throw new Error ( `t.crud can only be used on GraphQL root types 'Query' & 'Mutation' but was used on '${ typeName }'. Please use 't.model' instead` ) ; } const publishers = getCrudMappedFields ( typeName , this . dmmf ) . reduce ( ( crud , mappedField ) => { const fieldPublisher = ( givenConfig ) => { const inputType = this . dmmf . getInputType ( mappedField . field . args [ 0 ] . inputType . type ) ; const publisherConfig = this . buildPublisherConfig ( { field : mappedField . field , givenConfig : givenConfig ? givenConfig : { } , } ) ; const schemaArgsIndex = indexBy ( mappedField . field . args , 'name' ) ; const originalResolve = async ( _root , args , ctx , info ) => { const prismaClient = this . getPrismaClient ( ctx ) ; args = transformNullsToUndefined ( args , schemaArgsIndex , this . dmmf ) ; if ( typeName === 'Mutation' && ( ! isEmptyObject ( publisherConfig . locallyComputedInputs ) || ! isEmptyObject ( this . globallyComputedInputs ) ) ) { args = await addComputedInputs ( { inputType , dmmf : this . dmmf , params : { info , args , ctx , } , locallyComputedInputs : publisherConfig . locallyComputedInputs , } ) ; } args = this . paginationStrategy . resolve ( args ) ; return prismaClient [ mappedField . prismaClientAccessor ] [ mappedField . operation ] ( args ) ; } ; const fieldConfig = this . buildFieldConfig ( { field : mappedField . field , publisherConfig , typeName , operation : mappedField . operation , resolve : ( root , args , ctx , info ) => { return givenConfig ?. resolve ? givenConfig . resolve ( root , args , ctx , info , originalResolve ) : originalResolve ( root , args , ctx , info ) ; } , } ) ; if ( this . assertOutputTypeIsDefined ( typeName , mappedField . field . name , publisherConfig . type , stage ) ) { t . field ( publisherConfig . alias , fieldConfig ) ; } this . assertFilteringOrOrderingArgNameExists ( typeName , mappedField . field . outputType . type , mappedField . field . name , publisherConfig , stage ) ; return crud ; } ; crud [ mappedField . field . name ] = fieldPublisher ; return crud ; } , { } as PublisherMethods ) ; return proxifyPublishers ( publishers , typeName , stage , this . options . onUnknownFieldName ) ; } , } ) ; } protected buildDisabledCRUD ( ) { return Nexus . dynamicOutputProperty ( { name : 'crud' , factory : ( ) => { this . wasCrudUsedButDisabled = true ; return new Proxy ( { } , { get ( ) { return ( ) => { } ; } , } ) ; } , } ) ; } /**
     * Build the `t.model` dynamic output property.
     */ protected buildModel ( ) { return Nexus . dynamicOutputProperty ( { name : 'model' , typeDefinition : `: NexusPrisma<TypeName, 'model'>` , /**
             * This factory implements what .model will actually be.
             *
             * If the user's GQL typedef name matches a PSL model name,
             * then we infer that the user is trying to create a mapping
             * between them. This is the implicit mapping case.
             *
             * Otherwise we need the user to specify what PSL model
             * their GQL object maps to. This is the explicit mapping case.
             *
             * In the implicit case we eagerly do the .model implementation,
             * but in the explicit case we return a function in order that the
             * user may specify the mapping.
             *
             * Examples:
             *
             *    // Given PSL that contains:
             *
             *    model User {
             *      id    String @unique @id @default(uuid())
             *      email String @unique
             *    }
             *
             *    // Example of implicit mapping
             *
             *    objectType({
             *      name: 'User',
             *      definition(t) {
             *        t.model.id()
             *        t.model.email()
             *      }
             *    })
             *
             *    // Example of explicit mapping
             *
             *    objectType({
             *      name: 'Customer',
             *      definition(t) {
             *        t.model('User').id()
             *        t.model('User').email()
             *      }
             *    })
             *
             */ factory : ( { typeDef , typeName , stage } ) => { const hasPrismaModel = this . dmmf . hasModel ( typeName ) ; if ( hasPrismaModel ) { return this . internalBuildModel ( typeName , typeDef , stage ) ; } else { const accessor = ( modelName ) => this . internalBuildModel ( modelName , typeDef , stage ) ; return proxifyModelFunction ( accessor , typeName , stage , this . options . onUnknownPrismaModelName , this . unknownFieldsByModel ) ; } } , } ) ; } protected internalBuildModel ( typeName , t , stage ) { const model = this . dmmf . getModelOrThrow ( typeName ) ; const outputType = this . dmmf . getOutputType ( model . name ) ; const publishers = outputType . fields . reduce ( ( acc , field ) => { const fieldPublisher = ( givenConfig ) => { const publisherConfig = this . buildPublisherConfig ( { field , givenConfig : givenConfig ?? { } , } ) ; if ( ! this . assertOutputTypeIsDefined ( typeName , publisherConfig . alias , publisherConfig . type , stage ) ) { return acc ; } this . assertFilteringOrOrderingArgNameExists ( typeName , field . outputType . type , publisherConfig . alias , publisherConfig , stage ) ; const mapping = this . dmmf . getMapping ( typeName ) ; const uniqueIdentifiers = Constraints . resolveUniqueIdentifiers ( typeName , this . dmmf ) ; const schemaArgsIndex = indexBy ( field . args , 'name' ) ; const originalResolve = field . outputType . kind === 'object' ? ( root , args , ctx ) => { const missingIdentifiers = Constraints . findMissingUniqueIdentifiers ( root , uniqueIdentifiers ) ; if ( missingIdentifiers !== null ) { throw new Error ( `Resolver ${ typeName }.${ publisherConfig . alias } is missing the following unique identifiers: ${ missingIdentifiers . join ( ', ' ) }` ) ; } const prismaClient = this . getPrismaClient ( ctx ) ; args = transformNullsToUndefined ( args , schemaArgsIndex , this . dmmf ) ; args = this . paginationStrategy . resolve ( args ) ; return prismaClient [ lowerFirst ( mapping . model ) ] . findUnique ( { where : Constraints . buildWhereUniqueInput ( root , uniqueIdentifiers ) , } ) [ field . name ] ( args ) ; } : publisherConfig . alias != field . name ? ( root ) => root [ field . name ] : undefined ; const fieldConfig = this . buildFieldConfig ( { field , publisherConfig , typeName , resolve : givenConfig ?. resolve ? ( root , args , ctx , info ) => { return givenConfig . resolve ! ( root , args , ctx , info , originalResolve ?? defaultFieldResolver ) ; } : originalResolve , } ) ; t . field ( publisherConfig . alias , fieldConfig ) ; return publishers ; } ; acc [ field . name ] = fieldPublisher ; return acc ; } , { } as PublisherMethods ) ; return proxifyPublishers ( publishers , typeName , stage , this . options . onUnknownFieldName ) ; } buildPublisherConfig ( { field , givenConfig : { computedInputs , ... otherConfig } , } ) { return { pagination : true , type : field . outputType . type , alias : field . name , locallyComputedInputs : computedInputs ? computedInputs : { } , ... otherConfig , } ; } buildFieldConfig ( config ) { const { alias , locallyComputedInputs , type , filtering , ordering , pagination , ... additionalExternalPropsSuchAsPlugins } = config . publisherConfig ; return { ... additionalExternalPropsSuchAsPlugins , args : this . buildArgsFromField ( config ) , type : getNexusTypesCompositionForOutput ( config . field . outputType ) . reduceRight ( apply , this . publisher . outputType ( config . publisherConfig . type , config . field ) ) , resolve : config . resolve , } ; } buildArgsFromField ( config ) { return this . determineArgs ( config ) . reduce ( ( acc , customArg ) => ( { ... acc , [ customArg . arg . name ] : this . publisher . inputType ( customArg ) as any , } ) , { } as Nexus . core . ArgsRecord ) ; } determineArgs ( config ) { if ( config . typeName === 'Mutation' ) { return this . argsFromMutationField ( config ) ; } else if ( config . operation === 'findUnique' ) { return config . field . args . map ( ( arg ) => ( { arg , type : this . dmmf . getInputType ( arg . inputType . type ) , } ) ) ; } else { return this . argsFromQueryOrModelField ( config ) ; } } argsFromMutationField ( { publisherConfig , field } ) { return field . args . map ( ( arg ) => { const prismaClientInputType = this . dmmf . getInputType ( arg . inputType . type ) ; return { arg , type : { ... prismaClientInputType , fields : publisherConfig . locallyComputedInputs ? prismaClientInputType . fields . filter ( ( field ) => ! ( field . name in publisherConfig . locallyComputedInputs ) ) : prismaClientInputType . fields , } , } ; } ) ; } protected argsFromQueryOrModelField ( { typeName , field , publisherConfig } ) { let args = [ ] ; if ( publisherConfig . filtering ) { const inputObjectTypeDefName = `${ field . outputType . type }WhereInput` ; const whereArg = field . args . find ( ( arg ) => arg . inputType . type === inputObjectTypeDefName && arg . name === 'where' ) ; if ( ! whereArg ) { throw new Error ( `Could not find filtering argument for ${ typeName }.${ field . name }` ) ; } const inputType = this . handleInputObjectCustomization ( { fieldWhitelist : publisherConfig . filtering , inputTypeName : inputObjectTypeDefName , fieldName : field . name , graphQLTypeName : typeName , isWhereType : true , } ) ; if ( inputType . fields . length > 0 ) { args . push ( { arg : whereArg , type : inputType , } ) ; } } if ( publisherConfig . ordering ) { const orderByTypeName = `${ field . outputType . type }OrderByInput` ; const orderByTypeNamePreviewFeature = `${ field . outputType . type }OrderByWithRelationInput` ; const orderByArg = field . args . find ( ( arg ) => ( arg . inputType . type === orderByTypeName || arg . inputType . type === orderByTypeNamePreviewFeature ) && arg . name === 'orderBy' ) ; if ( ! orderByArg ) { throw new Error ( `Could not find ordering argument for ${ typeName }.${ field . name }` ) ; } const inputType = this . handleInputObjectCustomization ( { fieldWhitelist : publisherConfig . ordering , inputTypeName : orderByArg . inputType . type , fieldName : field . name , graphQLTypeName : typeName , isWhereType : false , } ) ; if ( inputType . fields . length > 0 ) { args . push ( { arg : orderByArg , type : inputType , } ) ; } } if ( publisherConfig . pagination ) { const paginationsArgs = publisherConfig . pagination === true ? field . args . filter ( ( a ) => { const argNames = this . paginationStrategy . paginationArgNames as string [ ] ; return argNames . includes ( a . name ) ; } ) : field . args . filter ( ( arg ) => { return ( publisherConfig . pagination as any ) [ arg . name ] === true ; } ) ; args . push ( ... paginationsArgs . map ( ( a ) => { if ( a . inputType . kind === 'scalar' || a . inputType . kind === 'enum' ) { return { arg : a , type : { name : a . inputType . type } , } ; } else { return { arg : a , type : this . dmmf . inputTypesIndex [ a . inputType . type ] , } ; } } ) ) ; } return args ; } /**
     * This handles "tailored field feature publishing".
     *
     * With tailored field feature publishing, users can specify that only some
     * fields of the PSL model are exposed under the given field feature. For
     * example, in the following...
     *
     * ```ts
     * t.model.friends({ filtering: { firstName: true, location: true } })
     * ```
     *
     * ...the field feature is "filtering" and the user has tailored it so that
     * only "firstName" and "location" of the field's type (e.g. "User") are
     * exposed to filtering on this field. So the resulting GQL TypeDef would look
     * something like:
     *
     * ```ts
     * ...
     * friends(where: { firstName: ..., location: ..., }): [User]
     * ...
     * ```
     */ protected handleInputObjectCustomization ( params ) { const prismaClientObject = this . dmmf . getInputType ( params . inputTypeName ) ; if ( params . fieldWhitelist === true ) { return prismaClientObject ; } const whitelistedFieldNames = Object . keys ( params . fieldWhitelist ) ; const userExposedObjectFields = prismaClientObject . fields . filter ( ( field ) => whitelistedFieldNames . includes ( field . name ) ) ; const uniqueName = params . isWhereType ? this . argsNamingStrategy . whereInput ( params . graphQLTypeName , params . fieldName ) : this . argsNamingStrategy . orderByInput ( params . graphQLTypeName , params . fieldName ) ; return { ... prismaClientObject , name : uniqueName , fields : userExposedObjectFields , } ; } protected assertOutputTypeIsDefined ( typeName , fieldName , outputType , stage ) { if ( this . options . nexusBuilder . hasType ( outputType ) || GraphQL . isScalarType ( outputType ) || ! this . dmmf . hasModel ( outputType ) ) { return true ; } const message = `Your GraphQL \`${ typeName }\` object definition is projecting a field \`${ fieldName }\` with \`${ outputType }\` as output type, but \`${ outputType }\` is not defined in your GraphQL Schema` ; raiseErrorOrTriggerHook ( this . options . onUnknownFieldType , { unknownFieldType : outputType , typeName , fieldName , error : new Error ( message ) , } , message , stage ) ; return false ; } protected assertArgNameExists ( parentTypeName , prismaOutputTypeName , fieldName , config , stage , configProperty ) { if ( ! config [ configProperty ] || config [ configProperty ] === true ) { return true ; } const argNames = Object . keys ( config [ configProperty ] as Record < string , boolean > ) ; const typeNameFieldNames = this . dmmf . getModelOrThrow ( prismaOutputTypeName ) . fields . map ( ( f ) => f . name ) ; const wrongArgNames = argNames . filter ( ( filteringFieldName ) => ! typeNameFieldNames . includes ( filteringFieldName ) ) ; if ( wrongArgNames . length === 0 ) { return true ; } const actionWord = configProperty === 'filtering' ? 'filter' : 'order' ; const renderMessage = ( argName ) => `Your GraphQL \`${ parentTypeName }\` object definition is projecting a relational field \`${ fieldName }\`. On it, you are declaring that clients be able to ${ actionWord } by Prisma \`${ prismaOutputTypeName }\` model field \`${ argName }\`. However, your Prisma model \`${ prismaOutputTypeName }\` model has no such field \`${ argName }\`` ; const message = wrongArgNames . map ( ( argName ) => renderMessage ( argName ) ) . join ( '\n' ) ; raiseErrorOrTriggerHook ( this . options . onUnknownArgName , { unknownArgNames : wrongArgNames , typeName : prismaOutputTypeName , fieldName , error : new Error ( message ) , } , message , stage ) ; return { wrongArgNames } ; } protected assertFilteringOrOrderingArgNameExists ( parentTypeName , prismaOutputTypeName , fieldName , config , stage ) { this . assertArgNameExists ( parentTypeName , prismaOutputTypeName , fieldName , config , stage , 'filtering' ) ; this . assertArgNameExists ( parentTypeName , prismaOutputTypeName , fieldName , config , stage , 'ordering' ) ; } }