@moilandtoil/sealab-schema-builder

A library for programatically managing and generating GraphQL typeDefs and resolvers, while handling authorization

Stats

StarsIssuesVersionUpdatedCreatedSize
@moilandtoil/sealab-schema-builder
0.0.63 years ago4 years agoMinified + gzip package size for @moilandtoil/sealab-schema-builder in KB

Readme

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

If you find any bugs or have a feature request, please open an issue on github!

The npm package download data comes from npm's download counts api and package details come from npms.io.