GRAphQL Model builder is a utility to generate all necessary GraphQL query, mutation and subscription Types. It enforces a specific structure for your schema.
npm i gram- Automatic Interface generation
- Strict schema layout
- Easy adding of resolvers for attribute fields
- Connect a service and go
- Context dependent builds
Basic usage would let you generate a model by just defining it and adding some attributes. It requires you to have some service that provides necessary CRUD-style getter and setter functions (findMany, findOne, update, create, remove).
If you have a service called Animals you could create a schema like this:
import{GraphQLString,GraphQLInt,GraphQLBoolean}from'graphql'import{createSchemaBuilder}from'gram'import{Animals}from'./my-services/animals'constbuilder=createSchemaBuilder()constanimal=builder.model('Animal',Animals)// animal type like 'dog', 'cat'// field is requiredanimal.attr('type',GraphQLString).isNonNull()// animal name like 'Fluffy', 'Rex'// field is not requiredanimal.attr('name',GraphQLString)// is it a tame animalanimal.attr('tame',GraphQLBoolean)// age of the animalanimal.attr('age',GraphQLInt)constschema=builder.build()This will generate a graphql schema like:
typeAnimalimplementsNode{id: IDcreatedAt: StringupdatedAt: StringdeletedAt: Stringtype: String!name: Stringtame: Booleanage: Int } inputAnimalFilter{… } inputAnimalPage{limit: Intoffset: Int } typeAnimalsimplementsList{page: Pagenodes: [Animal!]! } enumAnimalSortOrder{… } inputAnimalWhere{… } inputCreateAnimalData{… } interfaceList{page: Pagenodes: [Node!]! } typeMutation{createAnimal(data: CreateAnimalData!): AnimalupdateAnimal(data: UpdateAnimalData!, where: AnimalWhere!): [Animal!]!deleteAnimals(where: AnimalWhere!): [Animal!]! } interfaceNode{id: IDcreatedAt: StringupdatedAt: StringdeletedAt: String } typePage{page: Intlimit: Intoffset: Int } typeQuery{getAnimal(where: AnimalWhere!, order: AnimalSortOrder): AnimalgetAnimals(order: AnimalSortOrder, page: AnimalPage, where: AnimalWhere!): Animals } typeSubscription{onCreateAnimal: Animal!onUpdateAnimal: [Animal!]!onDeleteAnimals: [Animal!]! } inputUpdateAnimalData{… }As you can see, some types and interfaces were added. Gram is a little opinionated about Node, Page and List. It will generate the Node and List interfaces and apply them to you models by itself. Your models will have to implement the Node interface and the return value of the findMany method will have to return a Page type as the getAnimals query returns Animals which is of interface List.
Gram will automatically assume all implemented methods on the service will work. If you, for example, do not have a findMany method, Gram will remove the getAnimals method and just generate the rest.
If you want to generate more than one schema from your models, there will be a feature implemented in the near future to enable and disable parts depending on the context given in the build method.
To add a resolver to the model we can use the .resolve(() => Resolver) method on the model. In this example the database does not have an age column, and we need to calculate the age of the animal in the resolver.
constanimal=builder.model('Animal',Animals)// animal type like 'dog', 'cat'// field is requiredanimal.attr('type',GraphQLString).isNonNull()// animal name like 'Fluffy', 'Rex'// field is not requiredanimal.attr('name',GraphQLString)// is it a tame animalanimal.attr('tame',GraphQLBoolean)// age of the animalanimal.attr('age',GraphQLInt)animal.resolve(()=>({// calculate the age of the animalage: animal=>Math.floor((Date.now()-animal.birthdate)/1000/60/60/24/365)}))Of course our Animal model is just an interface to help us build Cat and Dog models. We will convert our Animal into an interface and generate a Cat model that implements the interface. There will be no use for that type attribute any more, we will just skip it.
constbuilder=createSchemaBuilder()constanimal=builder.interface('Animal')// animal name like 'Fluffy', 'Rex'// field is not requiredanimal.attr('name',GraphQLString)// is it a tame animalanimal.attr('tame',GraphQLBoolean)// age of the animalanimal.attr('age',GraphQLInt)constcat=builder.model('Cat',Animals)cat.interface('Animal')interfaceAnimal{name: Stringtame: Boolean } typeCatimplementsNode&Animal{id: IDcreatedAt: DateupdatedAt: DatedeletedAt: Datename: Stringtame: Boolean } typeQuery{getCat(where: CatWhere!, order: CatSortOrder): CatgetCats(order: CatSortOrder, page: CatPage, where: CatWhere!): Cats }As you can see it now added the attributes to the Cat model and we can now easily add more animal-type models to our system. We moved the service to the Cat model, which is not quite right. It would be best to have a Cat specific service, which will use the Animal service in turn, by adding some filters. But we will want to fetch any Animals as well.
interfaceAnimal{type: AnimalTypesname: stringtame: booleanage: number}interfaceCatextendsAnimal{type: AnimalTypes.Cat,}interfaceDogextendsAnimal{type: AnimalTypes.Dog,}constAnimals: Service<Animal>={findOne: async({ order, where })=>Magically.findData(where,order),}constCats: Service<Cat>={findOne: async({ order, where })=>Animals.findOne({ order,where: { ...where,type: AnimalTypes.Cat},})asPromise<Cat>,}constDogs: Service<Dog>={findOne: async({ order, where })=>Animals.findOne({ order,where: { ...where,type: AnimalTypes.Dog},})asPromise<Dog>,}// after defining all interfaces we setup the schemaconstbuilder=createSchemaBuilder()constanimal=builder.interface('Animal',Animals)animal.attr('name',GraphQLString)animal.attr('tame',GraphQLBoolean)animal.attr('age',GraphQLInt)constcat=builder.model('Cat',Cats)cat.interface('Animal')constdog=builder.model('Dog',Dogs)dog.interface('Animal')interfaceAnimal{name: Stringtame: Booleanage: Int } typeCatimplementsNode&Animal{id: IDcreatedAt: DateupdatedAt: DatedeletedAt: Datename: Stringtame: Booleanage: Int } typeDogimplementsNode&Animal{id: IDcreatedAt: DateupdatedAt: DatedeletedAt: Datename: Stringtame: Booleanage: Int } typeQuery{getAnimal(where: AnimalWhere!, order: AnimalSortOrder): AnimalgetCat(where: CatWhere!, order: CatSortOrder): CatgetDog(where: DogWhere!, order: DogSortOrder): Dog }Now we can find any animal with a getAnimal(where:{name: "Fluffy" }){... on Dog{… }} or find our Dog directly getDog(where:{name: "Fluffy" }){… }.
For use as DateTime type inside this library @saeris/graphql-scalars is used. It is applied in the pagination system and allows more types to be setup, see the documentation for more information. Sadly this library has no typing.
To install another type, simply attach it to the schemabuilder.
constEatingType=newGraphQLScalarType({name: 'EatingType',description: 'What is this animal eating',serialize: val=>val,})constbuilder=createSchemaBuilder()builder.setScalar('EatingType',EatingType)constanimal=builder.model('Animal',Animals)constDateTime=builder.getScalar('DateTime')// animal name like 'Fluffy', 'Rex'// field is not requiredanimal.attr('name',GraphQLString)// is it a tame animalanimal.attr('tame',GraphQLBoolean)// age of the animalanimal.attr('dateOfBirth',DateTime)// feeding type of the animalanimal.attr('feed',EatingType)"""What is this animal eating"""scalarEatingTypetypeAnimalimplementsNode{name: Stringtame: BooleandateOfBirth: DateTimefeed: EatingTypeid: IDcreatedAt: DateTimeupdatedAt: DateTimedeletedAt: DateTime }There is no filter strategy setup for the new scalar type yet. To add this we will to setup this.
constcheckFn=isSpecificScalarType('EatingType')constfilterFn=filters.joinFilters([filters.equals,// adds equal & not-equal filterfilters.record,// adds in && not-in filters])builder.addFilter(checkFn,filterFn)inputAnimalFilter{{…} feed: EatingTypefeed_not: EatingTypefeed_in: [EatingType!] feed_not_in: [EatingType!]{…} }With gram you could also just build up a graphQL schema by hand, it provides all necessary method to do so.
constbuilder=createSchemaBuilder()builder.addQuery('random',GraphQLFloat,()=>Math.random)typeQuery{random: Float }Since version 2.1.2 it is now possible to set attribute types as strings, and not as GraphQLType objects.
constbuilder=createSchemaBuilder()constanimal=builder.model('Animal',AnimalService)// animal name like 'Fluffy', 'Rex'// field is requiredanimal.attr('name','String!')// parentsanimal.attr('mother','Animal')animal.attr('father','Animal')// childrenanimal.attr('children','[Animal!]!')typeAnimalimplementsNode{name: String!mother: Animalfather: Animalchildren: [Animal!]!id: IDcreatedAt: DateTimeupdatedAt: DateTimedeletedAt: DateTime }The schema builder will allow you to create different schemas from one definition. When accessing a graphql endpoint we always need to keep in mind who and with what rights the access is done.
typeSchemaTypes='admin'|'user'constbuilder=createSchemaBuilder<SchemaTypes>()constuser=builder.model('User')user.attr('email',GraphQLString)builder.addQuery('context',GraphQLString,({ context })=>()=>context)builder.addQuery('me',user,({context: schemaContext})=>(root,args,context?: GQLContext)=>{if(!context)thrownewError('Need an authToken-context')if(schemaContext==='user'&&!context.authToken)thrownewError('Need an user:authToken')if(!context.authId)thrownewError('Need an authID')return{id: context.authId,email: schemaContext==='admin' ? 'admin' : context.authToken,}},)constadminSchema=builder.build('admin')constuserSchema=builder.build('user')constquery=`{context me{email }}`constadminResult=awaitgraphql({schema: adminSchema,source: query,contextValue: {authId: 'AdminID',},})constuserResult=awaitgraphql({schema: userSchema,source: query,contextValue: {authId: 'AuthenticationID',authToken: '[email protected]',},})This will generate 2 different graphql schema. The user-schema will be used for requests against the graphql system when the request is authenticated and identified to be a normal user. The admin-schema will then be used for requests from the system administrators. The build can also be generated as typeDefs and resolvers pair.
If you want to help with this project, just leave a bug report or pull request. I'll try to come back to you as soon as possible
MIT