sql2gql

2.1.2 • Public • Published

sql2gql

Opininated Sequelize to GraphQL bridge, extending out graphql-sequelize with dynamically exposing relationships and functions via queries and mutations. We rely heavily on the Sequelize API for definition of the model classes.

Build Status

Requirements

Features

  • Dynamic mapping of sequelize models and relationships to graphql for query and mutation.
  • Exposing classMethods as mutation/query options.
  • Permissions to restrict access to parts of the graphql schema on generation.
  • Generation of subscriptions from sequelize hooks

API

Model

The model object is the bread and butter of the setup, it basically serves two purposes, creating the data model in sequelize and the graphql model.

Key type Description
name String(sequelize.define.modelName)) Model name
define Object(sequelize.define.attributes) Model fields
options Object(sequelize.define.options) Model sequelize.define options
relationships Array[Model.Relationship] Model relationships
expose Object(Model.Expose) this is used to make classMethods/instanceMethods available via queries or mutations in graphql
classMethods Object static typed functions that are set on the model under sequelize (for v3 this will be copied into options instead)
instanceMethods Object functions that are set on the model's prototype under sequelize (for v3 this will be copied into options instead)
ignoreFields Array[String] TODO
before ({params, args, context, info, type}) => return params This function is executed before graphql-sequelize resolver is tasked, you must return the params for it to be able to continue
after ({result, args, context, info, type}) => return result This function is executed after graphql-sequelize resolver has completed but before the result is passed up to graphql for queries, , , type determines ns
override HashObject[fieldName -> Model.override] overrides the field resolver functions to allow for complex types on on simple fields e.g. JSON,JSONB
resolver () => Object the replaces graphql-sequelize resolver completely
subscriptions HashObject[hookName -> (instance, args, req, gql) => {return instance}] overrides default action of subscription hook event, this occurs after all sequelize hooks and must return a model.

Model.Override

Key type Description
type {name: String, fields: GraphQLFieldConfigMap} GraphQLObject definition
output (result, args, context, info) => fieldData This function processes outgoing data for the the field, the result is the parent model.
output (field, args, context, info) => fieldData This function processes incoming data for the the field, the field param is the input argument set for the field.

Model.Expose

Key type Description
classMethods Object({query: Model.Expose.Definition, mutation: Model.Expose.Definition}) Object containing the graphql definition of classMethods you wish to expose on either query or mutation
instanceMethods Object({query: Model.Expose.Definition}) Object containing the graphql definition of instanceMethods you wish to expose as a field query

Model.Expose.Definition

This object is a HashObject which the key must match the function name targeted.

Key type Description
type String or GraphQLObjectType If this is a string it will use the models generated graphql type (use "List<ModelName>" for lists) as the return value, other wise if it is GraphQLObjectType it will use this instead
args Key value hash object that require GraphQLInputObjectType

Model.Relationship

Key type Description
name String Relationship field name for model
type String(sequelize.association) Executes the association function on the model.
model String(Model.name) target model of the association
options Object(sequelize.association.options) The available options depends on the type of association you pick

connect

This creates sequelize models and injects appropriate metadata for the createSchema function

import {connect} from "sql2gql";
Parameter Description
Array(Model) Array of models
Sequelize.instance Sequelize connection instance

createSchema

Generates a graphql schema from the metadata stored in the sequelize instance.

import {createSchema} from "sql2gql";
Parameter Description
Sequelize.connection Sequelize connection instance
Object(createSchema.options) createSchema options

createSchema.options

Generates a graphql schema from the metadata stored in the sequelize instance.

Returns Object(GraphQLSchema)

Key type Description
name String Relationship field name for model
permissions Object(createSchema.options.permissions) optional hooks to constrain visibility of fields and functions
query Object(GraphQLSchema) merges into base RootQuery field via Object.assign
mutation Object(GraphQLSchema) merges into base Mutation field via Object.assign
before (model: Model, findOptions, args, context, info) => return findOptions
after (model: Model, result, args, context, info) => return result
subscriptions Object[createSchema.options.subscriptions] config options for the subscriptions

createSchema.options.subscriptions

Key type Description
hookNames [String] list of hooks that will be registered per model, please see Sequelize.Hooks for the full list of available. ["afterCreate", "afterDestroy", "afterUpdate"] are the default settings. Currently only (instance, options) typed hooks are automatically support as each subscription is configured to return the type of the model it is for.
pubsub PubSub pubsub is required for handling events between sequelize and the graphql instance. Please see GraphQL Subscriptions for more information

createSchema.options.permissions

hooks to constrain visibility of fields and functions will only hide elements by default if hook is defined,

Key type Description
model (modelName: String) => Boolean False ensures the model itself is no where available across the entire schema
field (modelName: String, fieldName: String) => Boolean False ensures model field "query {model {modelName {fieldName}}}" is unavailable
relationship (modelName: String,relationshipName: String, targetModelName: String) => Boolean False ensures model field option "modelName {relationshipName}" is unavailable
query (modelName: String) => Boolean False ensures query option "query {model {modelName}}" is unavailable
queryClassMethods (modelName: String, methodName: String) => Boolean False ensures query option "query {classMethods {modelName {methodName}}}" is unavailable
queryInstanceMethods (modelName: String, methodName: String) => Boolean False ensures query option "query {classMethods {modelName {methodName}}}" is unavailable
mutation (modelName) => Boolean False ensures mutation option "mutation {models {modelName}}" is unavailable
mutationUpdate (modelName) => Boolean False ensures mutation option "mutation {models {modelName{update}}}" is unavailable
mutationCreate (modelName) => Boolean False ensures mutation option "mutation {models {modelName{create}}}" is unavailable
mutationDelete (modelName) => Boolean False ensures mutation option "mutation {models {modelName{delete}}}" is unavailable
mutationUpdateAll (modelName) => Boolean False ensures mutation option "mutation {models {modelName{updateAll}}}" is unavailable
mutationDeleteAll (modelName) => Boolean False ensures mutation option "mutation {models {modelName{deleteAll}}}" is unavailable
mutationClassMethods (modelName: String, methodName: String) => Boolean False ensures mutation option "mutation {classMethods {modelName {methodName}}}" is unavailable
subscription (modelName, hookName) => Boolean False ensures mutation option "subscription { `{$hookName}${modelName}`{ id } }" is unavailable

GraphqlSchema

graphql-sequlize helpers

Query
query {
  models {
    modelName(defaultListArgs): GraphQLList<Model> {
      field
      relationship(args) {
        field
      }
    }
  }
  classMethods {
    modelName {
      functionName(params) {
        {definedModel}
      }
    }
  }
}
Mutation
mutation {
  models {
    modelName {
      create(input: modelDefinition) {
        field
      }
      update(where: SequelizeJSONType, input: modelDefinition) {
        field
      }
      classMethod(params) {
        field
      }
    }
  }
}
Subscription

Reference

subscription X {
  afterCreateTask {
    id,
    name
  }
  afterUpdateTask {
    id,
    name
  }
}

Example

Github Repo

import Sequelize from "sequelize";
import {connect, createSchema} from "sql2gql";
import expect from "expect";
import {
  graphql,
  GraphQLString,
  GraphQLNonNull,
  GraphQLInputObjectType,
  GraphQLObjectType,
  GraphQLInt,
} from "graphql";
 
const TaskModel = {
  name: "Task",
  define: {
    name: {
      type: Sequelize.STRING,
      allowNull: false,
      validate: {
        isAlphanumeric: {
          msg: "Your task name can only use letters and numbers",
        },
        len: {
          args: [1, 50],
          msg: "Your task name must be between 1 and 50 characters",
        },
      },
    },
    options: {
      type: Sequelize.STRING,
      allowNull: true,
    },
  },
  before(findOptions, args, context, info) {
    return findOptions;
  },
  after(result, args, context, info) {
    return result;
  },
  override: {
    options: {
      type: {
        name: "TaskOptions",
        fields: {
          hidden: {type: GraphQLString},
        },
      },
      output(result, args, context, info) {
        return JSON.parse(result.get("options"));
      },
      input(field, args, context, info) {
        return JSON.stringify(field);
      },
    },
  },
  relationships: [{
    type: "hasMany",
    model: "TaskItem",
    name: "items",
  }],
  expose: {
    classMethods: {
      mutations: {
        reverseName: {
          type: "Task",
          args: {
            input: {
              type: new GraphQLNonNull(new GraphQLInputObjectType({
                name: "TaskReverseNameInput",
                fields: {
                  amount: {type: new GraphQLNonNull(GraphQLInt)},
                },
              })),
            },
          },
        },
      },
      query: {
        getHiddenData: {
          type: new GraphQLObjectType({
            name: "TaskHiddenData",
            fields: () => ({
              hidden: {type: GraphQLString},
            }),
          }),
          args: {},
        },
        getHiddenData2: {
          type: new GraphQLObjectType({
            name: "TaskHiddenData2",
            fields: () => ({
              hidden: {type: GraphQLString},
            }),
          }),
          args: {},
        },
      },
    },
  },
  options: {
    tableName: "tasks",
    classMethods: {
      reverseName({input: {amount}}, context) {
        return {
          id: 1,
          name: `reverseName${amount}`,
        };
      },
      getHiddenData(args, context) {
        return {
          hidden: "Hi",
        };
      },
      getHiddenData2(args, context) {
        return {
          hidden: "Hi2",
        };
      },
    },
    hooks: {
      beforeFind(options) {
        return undefined;
      },
      beforeCreate(instance, options) {
        return undefined;
      },
      beforeUpdate(instance, options) {
        return undefined;
      },
      beforeDestroy(instance, options) {
        return undefined;
      },
    },
    indexes: [
      // {unique: true, fields: ["name"]},
    ],
    //instanceMethods: {}, //TODO: figure out a way to expose this on graphql
  },
};
 
 
const schemas = [TaskModel];
 
(async() => {
  let instance = new Sequelize("database", "username", "password", {
    dialect: "sqlite",
    logging: false
  });
  connect(schemas, instance, {}); // this populates the sequelize instance with the appropriate models and referential information for schema generation
  await instance.sync();
 
  const schema = await createSchema(instance); //creates graphql schema
  const mutation = `mutation {
    models {
      Task {
        create(input: {name: "item1", options: {hidden: "nowhere"}}) {
          id, 
          name
          options {
            hidden
          }
        }
      }
    }
  }`; // create item in database
  const mutationResult = await graphql(schema, mutation);
  expect(mutationResult.data.models.Task.create.options.hidden).toEqual("nowhere");
  const queryResult = await graphql(schema, "query { models { Task { id, name, options {hidden} } } }"); // retrieves information from database
  return expect(queryResult.data.models.Task[0].options.hidden).toEqual("nowhere");
})();

Permission Helper

The is a simple role base helper for hooking into the permission events for deny and allowing sections during schema generation based on roles provided.

/* 
  options = {
    defaultDeny: true
    defaults: {
 
    }
  }
  defaultPerm = {
    "fields": {
      "Task": {
        "options": "deny",
      },
    },
    "classMethods": {
      "User": {
        "login": "allow",
        "logout": "allow",
      },
    },
  };
 
  rules = {
    "admin": {
      "field": {
        "User": "allow",
      } 
      "model": "allow",
      "classMethods": {
        "User": {
          "login": "deny",
        },
      },
    },
    "user": {
      "mutation": "deny",
    },
  };
 
*/
 
const ruleSet = {
  "someone": "deny",
  "anyone": {
    "query": "allow",
    "model": {
      "Task": "allow",
    },
    "field": {
      "Task": {
        "name": "allow",
      },
    },
  },
};
 
const anyoneSchema = await createSchema(instance, {
  permission: permissionHelper("anyone", ruleSet)
});
 
const someoneSchema = await createSchema(instance, {
  permission: permissionHelper("someone", ruleSet)
});
 

ChangeLog

1.1.0

  • delete mutations now return object that it has deleted instead of boolean - [Breaking change from 1.0.0]
  • subscriptions are now supported, defaults will hook into afterCreate, afterUpdate, afterDestroy on the sequelize models
  • added extend to options in createScheme for supporting unknown/future root variables
  • set all functions to export to allow for anyone wanting to use the api directly

1.2.0

  • changed before and after hooks on the model definition to include mutations, the arguments have been reduced to a single object - [Breaking change from 1.1.0]

1.2.1

  • fixed before, after hooks arguments for mutations

1.2.2

  • adding rootValue and context to the findOptions statement provided to sequelize. accessible from the hook beforeFind.
  • updated base model before after hooks.

1.2.4

  • updated override to allow scalar and enum types to be set as the field type directly

1.2.5

  • added Instance Methods to the query field definition if exposed.

1.2.6

  • exposed generated types via $sql2gql.types on the schema returned from connect

1.2.7

  • Added field permission option

2.0.0

  • Added subscription permission option
  • added a simple role based permission helper
  • fixed field permissions return value to be correct
  • removed some let over debugging mechanics from 1.2.7
  • updated package dependencies
  • switch to the official graphql subscriptions mechanic in the test cases
  • updated all the tests to match the jest version of expect
  • implemented a basic role based permission helper.
  • added checks for over aggressive permission handling
  • added the default fields to the permission check
  • dropping node support for anything lesser then current LTS aka compile target is currently v6.11.3

2.0.1

  • total overcomplicated implementation for field filtering in permissions - now fixed.
  • added a new permissions test

2.0.2

  • added list types for all available models for instanceMethods, classMethods and schema.$sql2gql.types. format is "List<${modelName}>"

2.1.0

  • BUGFIX #21 - Unable to use list types in for instanceMethods
  • [Breaking Change] switch formatting from List to Object[] to be more consistent with javascript syntax

2.1.1

  • Added model to parameters of override.input on updates

2.1.2

  • Something went wrong with npm publish

Versions

Current Tags

Version History

Package Sidebar

Install

npm i sql2gql

Weekly Downloads

1

Version

2.1.2

License

GPL-3.0

Last publish

Collaborators

  • vostronet