- Quick Start
- Author Note
- Installation
- What is
sequelizegql
? - Why use
sequelizegql
? - Advantages
- Examples
- API
If you just want to see the minimum example to get up and running quickly - go here!
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!
- better unit test coverage, Integrations tests against database
npm install sequelizegql
sequelizegql
generates a full CRUUD (Create-Read-Update-Upsert-Delete) GraphQL API (types, inputs, enums, queries, mutations, resolvers, as well as near parity with queryable Sequelize Operators by simply passing your sequelize instance.
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 graphQL/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.
- 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 viaomitInputAttributes
- 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
Example here uses sequelize-typescript
but this library works fine with sequelize
too. You can see examples of both here
const schema = SequelizeGraphql().generateSchema({ sequelize });
console.log(schema); // { resolvers, typedefs, typedefsstring }
// ... load returned schema into your graphql client
@Table({ underscored: true, tableName: 'author', paranoid: true })
export class Author extends Model<Author> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@AllowNull(false)
@Column
name: string;
@Column
username: 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;
@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
For the above example, the following Author
Queries and Mutations are available (and similar for every other model);
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] |
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 |
...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
andOR
are available at the root level of theWhereInput
to combine the root where input fields conditionallyFILTERS
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 |
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!, $booksOptions: OptionsInput) {
authors(where: $authorsWhereInput) {
id
name
books(where: $booksWhereInput, options: $booksOptions) {
id
libraries {
id
city {
name
}
}
}
}
}
and payload like:
{
"authorsWhereInput": {
"FILTERS": {
"name": { "LIKE": "daniel" }
}
},
"booksWhereInput": {
"createdAt": "01-01-2020",
"OR": [{ "name": "Foo" }, { "name": "Bar" }]
},
"booksOptions": {
"required": true
}
}
Will generate and execute a sequelize query like this:
Author.findAll({
where: { ...authorsWhereInput, name: { [Op.like]: '%daniel%' } },
attributes: ['id', 'name'],
include: [
{
association: 'books',
attributes: ['id'],
where: { ...booksWhereInput, [Op.or]: [{ name: 'Foo' }, { name: 'Bar' }] },
separate: true,
required: true, // INNER JOIN!
include: [
{
association: 'libraries', // LEFT JOIN!
attributes: ['id'],
separate: true,
include: [
{
association: 'city',
attributes: ['name'],
},
],
},
],
},
],
});
See full application schema example here
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 |