Overview
The schema builder is a library for the programmatic creation of GraphQL typeDef and resolvers. This allows for typeDefs and resolvers to be organized within multiple files, functions, classes, etc and compiled together at runtime.
In addition it exposes an authorization construct referred to as guards
.
Guards are basically simple conditional structures, where the GraphQL context is passed in and a user define check is performed.
Using guards, a schema can be generated specific to a context.
This allows limiting the introspection to only what a context is authorized to consume. Ie, if an application has a set of admin specific entrypoints, these can be hidden from non admin users.
TODO
- Add multi-file example that better demonstrate the use case the library was designed for
API
SchemaBuilderGuard
Guards are simply the implementation of the abstract Guard class. SchemaBuilderGuard
is an abstract class that is extended to implmeent custom guards.
It expects the id
and validate
functions to be overridden
id()
Return a string containing the guard id
validate(context)
Return a boolean depending on whether the context meets the guards condition. Context is specific to the application, the base guard is agnostic of the context
Examples
Assuming a context with a structure as described below
{
authToken: String | null
user: {
id: String | Null
email: String | Null
isAdmin: Boolean
}
}
One could define a guard checking if a user is authenticated with
class Authenticated extends Guard {
id() {
return "authenticated"
}
validate(context) {
return (context.authToken !== null);
}
}
And define a guard to check if a user is an admin with
class IsAdmin extends Guard {
id() {
return "is_admin"
}
validate(context) {
return (context.user && context.user.isAdmin);
}
}
SchemaBuilder
registerGuards(guards)
guards
can be either a single guard class, or an array of guard classes.
Each guard will instantiated and mapped using its implemented #id()
function
getGuardInstance(guardId)
guardId
is a string that should match the return value of a registered guards #id()
function
getGuardInstances()
returns an object containing a id: class Instance
mapping of all registered guards
getGuardIds
returns an array of registered guard ids
getGuards
returns an array of guard classes
validateGuards(guardIds, context)
returns a boolean if all guard validations return true for a given context throws an error on the first occurrence of a guard validation failure
guardIds
is an array of guard ids to validate.
context
a user defined GraphQL context object.
addTypeDef(typeDef, [guards = []], [group = null]
Adds a single typeDef to the schema builder. Any guards passed are associated with the typDef when generating the full list
typeDef
a string containing the typeDef
guards
an array of guard ids to associate with the typeDef
group
Denotes a typeDef as an entrypoint type of data type. Valid group options are query
, mutation
, subscription
, or null
.
A null tells the builder the typeDef is a top level datatype. If it is one of the other options it will nest the typeDef in the corresponding entrypoint schemas
addTypeResolver(resolverName, resolverDef, [guards = []])
Adds a single resolver to the schema builder for a top level typeDef
resolverName
is a string that should match to a typeDef type name. ie., if you have a typeDef of type Foo { id: String }
, resolverName
should be Foo
resolverDef
is a resolver object for a typeDef. ie., if you have a typeDef of type Foo { id: String }
, resolverDef
could be { id: function(value) { return value.id } }
guards
is an optional array of guard ids. The guards must have already been registered
addResolver(resolverName, resolverDef, [guards = []])
Adds a single resolver to the schema builder for a specific group
resolverName
is a string that should make to a typeDef type name. ie., if you have a typeDef of type Foo { id: String }
, resolverName
should be Foo
resolverDef
is a resolver object for a typeDef. ie., if you have a typeDef of type Foo { id: String }
, resolverDef
could be { id: function(value) { return value.id } }
group
Denotes a resolver as an entrypoint resolver of data type resolver. Valid group options are query
, mutation
, subscription
, or null
.
A null tells the builder the resolver is for a top level data type. If it is one of the other options it will nest the resolver in the corresponding entrypoint object
guards
an optional array of guard ids. The guards must have already been registered
addType(name, typeDef, resolverDef, [guards = []])
Adds a data type with the typeDef and resolver.
name
is a string that should make to a typeDef type name. ie., if you have a typeDef of type Foo { id: String }
, name
should be Foo
typeDef
a string containing the GraphQL typeDef
resolverDef
is a resolver object for a typeDef. ie., if you have a typeDef of type Foo { id: String }
, resolverDef
could be { id: function(value) { return value.id } }
guards
an optional array of guard ids. The guards must have already been registered
addEntrypoint(name, group, typeDef, resolver, [guards = []])
Adds the typeDef and resolver for an entrypoint all at once. This is the perferred method for adding entrypoints as ensure consistency.
name
is a string that should match to a typeDef type name. ie., if you have a typeDef of type Foo { id: String }
, name
should be Foo
group
the entrypoint group, one of query
, mutation
, or subscription
typeDef
the GraphQL typeDef for the entrypoint
resolver
the resolver object containing functions that map to the typeDef
guards
an optional array of guard ids. The guards must have already been registered
genearteTypeDefs(context, guardWhitelist = null)
Generates an array of typeDefs from all the registered typeDefs in the schema builder. A guardWhitelist can be specified to only check specific guards.
Any associated guard of a typeDef that is checked and returns false
will result in the typeDef being omitted from the generated schema
context
A user defined GraphQL context to validate against the guards
guardWhitelist
An array of guard ids. If omitted will result in all registered guards being whitelisted.
When generating a schema, the intersection of guards associated with a typeDef and whitelistGuards are validated, all validations must return true
for the typeDef to be included in the final generated array.
generateResolvers(context, guardWhitelist = null)
Generates an object of resolvers from all the registered resolvers in the schema builder,
A guardWhitelist can be specified to only check specific guards.
Any associated guard of a resolver that is checked and returns false
will result in the resolver being omitted from the generated schema
context
A user defined GraphQL context to validate against the guards
guardWhitelist
An array of guard ids. If omitted will result in all registered guards being whitelisted.
When generating a schema, the intersection of guards associated with a resolver and whitelistGuards are validated, all validations must return true
for the resolver to be included in the final generated object.
Example
// Common way of manually creating a schema
const typeDefs = `
type User {
id: ID!
name: String
}
type Post {
id: ID!
title: String
author: User
}
# query entrypoints
type Query {
posts: [Post]
}
# mutation entrypoints
type Mutation {
addPost (
postId: ID!
title: String!
): Post
}
schema {
query: Query
mutation: Mutation
}
`;
const resolvers = {
Query: {
posts() {
return posts;
},
},
Mutation: {
addPost(rootValue, args, context) {
// ... code to insert post into database
// Post = insert_into_db(args, context);
return {
id: Post.id,
title: Post.title,
author: Post.author
}
},
},
User: {
id(value) {
return value.id
},
name(value) {
return value.name
}
},
Post: {
id(value) {
return value.id
},
title(value) {
return value.title
}
author(value) {
return value.author;
}
},
};
// Creating schemas using the query builder
const { SchemaBuilder, Guard } = require("sealab-schema-builder");
class LoggedIn extends Guards {
id() {
return "logged_in";
}
validate(context) {
return context.token !== null;
}
}
const builder = new SchemaBuilder();
# register guards
builder.registerGuards(LoggedIn);
# add typeDefs
builder.addTypeDef(`type User {
id: ID!
name: String
}`);
builder.addTypeDef(`type Post {
id: ID!
title: String
author: User
}`);
builder.addTypeDef(`type Query {
posts: [Post]
}`);
builder.addTypeDef(`type Mutation {
addPost (
postId: ID!
title: String!
): Post
}`);
builder.addTypeDef(`schema {
query: Query
mutation: Mutation
}`);
# addResolvers
builder.addTypeResolver("User", {
id(value) {
return value.id
},
name(value) {
return value.name
}
});
builder.addTypeResolver("Post", {
id(value) {
return value.id
},
title(value) {
return value.title
}
author(value) {
return value.author;
}
});
builder.addResolver("posts", function() {
// database call to get a list of posts
// posts = get_posts_from_db()
return posts
}, "query");
builder.addResolver("posts", function(rootValue, args, context) {
// ... code to insert post into database
// Post = insert_into_db(args, context);
return {
id: Post.id,
title: Post.title,
author: Post.author
}
}, "mutation");
const context = {
// assume we have some function defined to get token from header and validate it
token: get_token_from_header()
}
const typeDefs = builder.generateTypeDefs(context);
const resolvers = builder.generateResolvers(context);
Testing
Run npm run test
License
MIT