npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

sql2gql

v2.1.2

Published

Two way Sequelize to GraphQL bridge, extending out graphql-sequelize

Downloads

29

Readme

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