sequelizegql
TypeScript icon, indicating that this package has built-in type declarations

0.0.28 • Public • Published

sequelizegql

Quick Start

If you just want to see the minimum example to get up and running quickly - go here!

Author Note

This is in alpha - it is not yet stable!

Just a quick personal note - you can find the technical reasons for building this project below. Personally, I enjoy building these sorts of abstractions and find them useful. I do plan on continued development to gain production-level stability. That said, I have family, hobbies, and plenty of things I enjoy doing away from screens, etc., so if you'd like to contribute, I'd love help! Please hit me up here or via email. Thanks!

TODO

  • better unit test coverage, Integrations tests against database

Installation

npm install sequelizegql

What?

sequelizegql generates a full CRUD GraphQL API (types, inputs, enums, queries, mutations, resolvers, as well as near parity - there are some slight differences - with Sequelize Operators) based on the provided sequelize models.

Why?

The inspiration for this library was simple: fantastic tools exist in the data-layer/GraphQL generation space for database-first here, postgres-first here, and prisma-first here, but missing for sequelize users or those who lean towards code-first data-layer design.

Sequelize ORM is battle-tested and mature. Greenfield graphqlQL/sequelize projects are common and legacy REST/sequelize projects may want to bring GraphQL into their ecosystem.

Popular generation tools hit a ceiling very quickly when systems mature and business logic becomes more complex. The allowable configuration options (on root and model level) are an attempt to remove that barrier and scale well long-term.

Advantages

  • Generated schema is similar API to sequelize itself, including APIs for query filters (sequelize operators)
  • Database agnostic by leveraging sequelize
  • Performant (no benchmarks yet): generated resolvers do not over-fetch - the resolver layer introspects the query fields and dynamically generates one sequelize query w/ only the requested includes and attributes (note that the 1:many get separated under the hood to boost performance see sequelize docs here and search for 'separate: true')
  • Configure precise generated endpoints via omitResolvers, generate options
  • Supply pre-built directives to individual endpoints via directive option
  • Limit which fields can be supplied in input in create/update mutations via omitInputAttributes
  • Execute business logic with middleware via onBeforeResolve, onAfterResolve - if complex business logic is needed, graphql-middleware is a cleaner option, imo
  • Works well with your own endpoints: take the output and merge into your own custom schema
  • Works well with federated schemas

Examples

Example here uses sequelize-typescript but this library works fine with sequelize too. You can see examples of both here

Basic

const schema = SequelizeGraphql().generateSchema({ sequelize });

console.log(schema); // { resolvers, typedefs, typedefsString }

// ... load returned schema into your graphql client

In-Depth

@Table({ underscored: true, tableName: 'author', paranoid: true })
export class Author extends Model<Author> {
  @PrimaryKey
  @AutoIncrement
  @Column
  id: number;

  @AllowNull(false)
  @Column
  name: String;

  @Column
  surname: String;

  // ...timestamps: createdAt, updatedAt, deletedAt, etc

  // Associations
  @BelongsToMany(() => Book, () => BookAuthor)
  books?: Book[];
}

@Table({ underscored: true, tableName: 'book', paranoid: true })
export class Book extends Model<Book> {
  @PrimaryKey
  @AutoIncrement
  @Column
  id: number;

  @Column
  isbn: String;

  @AllowNull(false)
  @Column
  title: String;

  // ...timestamps: createdAt, updatedAt, deletedAt, etc

  // Associations
  @BelongsTo(() => Category)
  category?: Category;

  @BelongsToMany(() => Author, () => BookAuthor)
  authors?: Author[];

  @BelongsToMany(() => Library, () => BookLibrary)
  libraries?: Library[];
}

@Table({ underscored: true, tableName: 'book_author', paranoid: true })
export class BookAuthor extends Model<BookAuthor> {
  @PrimaryKey
  @AutoIncrement
  @Column
  id: number;

  @Column
  @ForeignKey(() => Author)
  authorId: number;

  @Column
  @ForeignKey(() => Book)
  bookId: number;

  // ...timestamps: createdAt, updatedAt, deletedAt, etc

  // ...associations
}

@Table({ underscored: true, tableName: 'library', paranoid: true })
export class Library extends Model<Library> {
  @PrimaryKey
  @AutoIncrement
  @Column
  id: number;

  @Column
  @ForeignKey(() => City)
  cityId: number;

  @AllowNull(false)
  @Column
  name: String;

  @Column
  address: String;

  @Column
  description: String;

  @AllowNull(false)
  @Default(LibraryStatus.ACTIVE)
  @Column(DataType.ENUM(...Object.values(LibraryStatus)))
  status: LibraryStatus;

  // ...timestamps: createdAt, updatedAt, deletedAt, etc

  // Associations
  @BelongsTo(() => City)
  city?: City;

  @BelongsToMany(() => Book, () => BookLibrary)
  books?: Book[];
}


@Table({ underscored: true, tableName: 'city', paranoid: true })
export class City extends Model<City> {
  @PrimaryKey
  @AutoIncrement
  @Column
  id: number;

  @AllowNull(false)
  @Column
  name: String;

  // ...timestamps: createdAt, updatedAt, deletedAt, etc
}

// sequelize.addModels[Author, Book, AuthorBook, City];

/***
 * IMPORTANT
 *
 * `modelMap` config takes precedence over the `rootMap`;
 *  under the hood, psuedocode looks like `merge(rootMap, modelMap)`
 */

// `rootMap` applies to *every* model's generated endpoints
const rootMap = {
  directive: `@acl(role: ['ADMIN', 'USER'])`;      // added to every endpoint
  whereInputAttributes?: ['id'];                   // queries will only be able to filter on 'id'
  omitInputAttributes?: ['id', 'createdAt', 'updatedAt', 'deletedAt']; // applies to 'create', 'update' and 'upsert'
  omitResolvers: [GeneratedResolverField.DELETE_MUTATION]; // don't generate any delete endpoints
  onBeforeResolve?: (args) => { /* ...do some business logic */};
  onAfterResolve?: (args) => { /* ...notify some integration */};
  fieldNameMappers?: {
    FILTERS: 'MY_CUSTOM_FILTERS_NAME';             // defaults to 'FILTERS'
  };
}

// `modelMap` applies to *specified* model's generated endpoints
const modelMap = {
  // note: case-sensitivity is not strict
  author: {
    whereInputAttributes: ['id', 'name', 'surname'], // i.e. now 'name' and 'surname' are searchable for 'Author' model
    resolvers: {
      [GeneratedResolverField.UPSERT]: { generate: false }, // i.e. `upsertAuthor` endpoint will not be generated
    },
  },
  Book: {
    resolvers: {
      [GeneratedResolverField.DELETE]: { generate: true }, // i.e. override the `rootMap`
    },
    omitResolvers: [GeneratedResolverField.FIND_ALL], // i.e. `allBooks` endpoint not generated
  },
  BookAuthor: {
    generate: false, // i.e. all `bookAuthor` queries and mutations will not be generated
  },
  City: {
    omitResolvers: [GeneratedResolverField.FIND_ONE], // i.e. `city` query will not be generated
  },
};

const graphqlSequelize = SequelizeGraphql().generateSchema({
  rootMap,
  modelMap,
  sequelize, // your sequelize instance
});

console.log(schema); // { resolvers, typedefs, typedefsString }

// ... load returned schema into your graphql client

Generated Example

For the above example, the following Author Queries and Mutations are available (and similar for every other model);

Queries

Name Args Return Type
author where: AuthorWhereInput, options: OptionsInput Author
authors where: AuthorWhereInput, options: OptionsInput [Author]
authorsPaged where: AuthorWhereInput, options: OptionsInputPaged AuthorPagedResponse i.e.{ totalCount: Int, entities: [Author] }
allAuthors none [Author]

Mutations

Name Args Return Type
createAuthor input: AuthorInput! note that create only allows one nested level of associations at the moment Author!
createManyAuthors input: [AuthorInput!]! [Author!]!
updateAuthor where: AuthorWhereInput, input: UpdateAuthorInput! Author
upsertAuthor where: AuthorWhereInput, input: AuthorInput! Author
deleteAuthor where: AuthorWhereInput, options: DeleteOptions DeleteResponse by default

Types and Inputs

  • ...modelFields represents the root fields on each model, i.e. id, name, surname, etc.
  • ...allAssociations model's associations, i.e. Author->Books->Libraries->City
  • ...oneLevelOfAssociations represents one layer of associations (TODO: make recursive)
  • The following are customizable via the modelMap where you can define fields to omit for both queries (WhereInput) and mutations (Input)
  • AND and OR are available at the root level of the WhereInput to combine the root where input fields conditionally
  • FILTERS is a map of where input fields where sequelize operators (mostly similar w/ exception to polymorphic operators) can be applied

 

Name Fields
AuthorWhereInput ...modelFields, ...allAssociations, OR, AND, FILTERS
AuthorInput ...modelFields, ...oneLevelOfAssociations
UpdateAuthorInput ...modelFields
DeleteOptions { force: boolean } (setting force: true will hard delete)
DeleteResponse { id: JSON, deletedCount: Int }
AND [AuthorWhereInput]
OR [AuthorWhereInput]
FILTERS see below

FILTERS

Name Fields
NOT_LIKE String!
STARTS_WITH String!
ENDS_WITH String!
SUBSTRING String!
EQ_STRING String!
NE_STRING String!
EQ_INT Int!
NE_INT Int!
NE_INT Int!
IS_NULL String!
NOT_STRING String!
NOT_INT Int!
GT Int!
GTE Int!
LT Int!
LTE Int!
BETWEEN_INT [Int!]!
BETWEEN_DATE [Int!]!
NOT_BETWEEN_INT [Int!]!
NOT_BETWEEN_DATE [DateTime!]1
IN_INT [Int!]!
IN_STRING [String!]!
NOT_IN_INT [Int!]!
NOT_IN_STRING [String!]!

  A query (pseudocode) like this:

Query GetAuthors($authorsWhereInput: AuthorWhereInput!, $booksWhereInput: BookWhereInput!) {
  authors(where: $authorsWhereInput) {
    id
    name
    books(where: $booksWhereInput) {
      id
      libraries {
        id
        city {
          name
        }
      }
    }
  }
}

and payload like:

{
  "authorsWhereInput": {
    "FILTERS": {
      "name": { "LIKE": "daniel" }
    }
  },
  "booksWhereInput": {
    "createdAt": "01-01-2020",
    "OR": [{ "name": "Foo" }, { "name": "Bar" }]
  }
}

  Will generate and execute a sequelize query like this:

Author.findAll({
  where: { ...authorsWhereInput, name: { [Op.like]: '%daniel%' } },
  attributes: ['id', 'name', 'books'],
  include: [
    {
      association: 'books',
      attributes: ['id'],
      where: { ...booksWhereInput, [Op.or]: [{ name: 'Foo' }, { name: 'Bar' }] },
      separate: true,
      include: [
        {
          association: 'libraries',
          attributes: ['id'],
          include: [
            {
              association: 'city',
              attributes: ['name'],
            },
          ],
        },
      ],
    },
  ],
});

  See full application schema example here

 

API

Options

Name Type Description
sequelize Sequelize Your Sequelize instance. The only required option
modelMap SchemaMap here Complex object that allows configuration and overrides for every model
rootMap SchemaMapOptions here Same as above, but will be applied to all models
deleteResponseGql string Your own slimmed-down delete response; by default - DeleteResponse
includeDeleteOptions boolean Allows for extra arg options: DeleteOptions on delete<*> endpoints

 

Readme

Keywords

Package Sidebar

Install

npm i sequelizegql

Weekly Downloads

0

Version

0.0.28

License

MIT

Unpacked Size

197 kB

Total Files

210

Last publish

Collaborators

  • dlemburg1