Nukem's Possible Manifestation

    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

    Keywords

    none

    Install

    npm i sql2gql

    DownloadsWeekly Downloads

    14

    Version

    2.1.2

    License

    GPL-3.0

    Last publish

    Collaborators

    • vostronet