0.0.28 • Published 2 years ago

sequelizegql v0.0.28

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

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

NameArgsReturn Type
authorwhere: AuthorWhereInput, options: OptionsInputAuthor
authorswhere: AuthorWhereInput, options: OptionsInput[Author]
authorsPagedwhere: AuthorWhereInput, options: OptionsInputPagedAuthorPagedResponse i.e.{ totalCount: Int, entities: [Author] }
allAuthorsnone[Author]

Mutations

NameArgsReturn Type
createAuthorinput: AuthorInput! note that create only allows one nested level of associations at the momentAuthor!
createManyAuthorsinput: [AuthorInput!]![Author!]!
updateAuthorwhere: AuthorWhereInput, input: UpdateAuthorInput!Author
upsertAuthorwhere: AuthorWhereInput, input: AuthorInput!Author
deleteAuthorwhere: AuthorWhereInput, options: DeleteOptionsDeleteResponse 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

 

NameFields
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]
FILTERSsee below

FILTERS

NameFields
NOT_LIKEString!
STARTS_WITHString!
ENDS_WITHString!
SUBSTRINGString!
EQ_STRINGString!
NE_STRINGString!
EQ_INTInt!
NE_INTInt!
NE_INTInt!
IS_NULLString!
NOT_STRINGString!
NOT_INTInt!
GTInt!
GTEInt!
LTInt!
LTEInt!
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

NameTypeDescription
sequelizeSequelizeYour Sequelize instance. The only required option
modelMapSchemaMap hereComplex object that allows configuration and overrides for every model
rootMapSchemaMapOptions hereSame as above, but will be applied to all models
deleteResponseGqlstringYour own slimmed-down delete response; by default - DeleteResponse
includeDeleteOptionsbooleanAllows for extra arg options: DeleteOptions on delete<*> endpoints

 

0.0.28

2 years ago

0.0.20

2 years ago

0.0.21

2 years ago

0.0.22

2 years ago

0.0.23

2 years ago

0.0.24

2 years ago

0.0.25

2 years ago

0.0.15

2 years ago

0.0.16

2 years ago

0.0.17

2 years ago

0.0.18

2 years ago

0.0.19

2 years ago

0.0.11

2 years ago

0.0.12

2 years ago

0.0.13

2 years ago

0.0.14

2 years ago

0.0.26

2 years ago

0.0.27

2 years ago

0.0.10

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.5

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago