Views support for Sequelize
Add Views support for Sequelize.
You are currently looking at the Tutorials and Guides. You might also be interested in the API Reference.
Supported dialects
Quick example
const { Sequelize, Model, DataTypes } = require('sequelize');
const { View, decorateSequelize } = require('@exoshtw/sequelize-views-support');
const ViewsSupportedSequelize = decorateSequelize(Sequelize);
const sequelize = new ViewsSupportedSequelize(('sqlite::memory:'));
class User extends Model {};
User.init({
username: DataTypes.STRING,
birthday: DataTypes.DATE,
enabled: {type: DataTypes.BOOLEAN, defaultValue: true},
}, { sequelize, modelName: 'user' });
class EnabledUser extends View {};
EnabledUser.init({
username: DataTypes.STRING,
birthday: DataTypes.DATE,
}, {
sequelize,
modelName: 'enabled_user',
timestamps: false,
viewQueryOptions: {
model: User,
where: {
enabled: true,
},
},
});
(async () => {
await sequelize.sync();
const jane = await User.create({
username: 'janedoe',
birthday: new Date(1980, 6, 20),
enabled: true,
});
const robert = await User.create({
username: 'robert',
birthday: new Date(1987, 1, 25),
enabled: false,
});
const enableds = await EnabledUser.findAll();
console.log(enableds.map((user) => user.toJSON()));
})();
Supporting the project
This project is limmited to a few dialects, if you implement another dialect please make us a PR.
Install
Via package manager
$ npm install --save @exoshtw/sequelize-views-support
Or Yarn
$ yarn add @exoshtw/sequelize-views-support
Via Git
$ git clone https://github.com/exoshtw/sequelize-views-support.git
$ cd sequelize-views-support
$ npm install
Use in your project
This package provide a decorator for Sequelize
class, you need to use it
decorated version of the class instead the original:
import { Sequelize } from 'sequelize';
import { decorateSequelize } from '@exoshtw/sequelize-views-support';
const ViewSupporterSequelize = decorateSequelize(Sequelize);
const sequelize = new ViewSupporterSequelize(...);
Basics
The views support try to be the more transparent and squelize-styled as possible.
Define new View
To create a new View, you need to use a familiar syntax, as a normal Model, with the difference that you need to define the select query the view must to create.
There are two ways for it; Using an option called viewQueryOptions
or
override a static method called getQueryOptions
.
Using config:
The easy way is pass the query options by init options, ex:
class EnabledUser extends View {}
EnabledUser.init({
username: DataTypes.STRING,
enabled: DataTypes.BOOLEAN,
}, {
sequelize,
timestamps: false,
viewQueryOptions: {
model: User,
attributes: ['id', 'username'],
where: {
enabled: true,
},
},
});
Using it we are creating a view using info from model User, using a similar
syntax to do a findAll
.
Overriding getQueryOptions
The other way is overriding the static method getQueryOptions
, this gets as
first param the options of the View (as a model):
class EnabledUser extends View {
static getQueryOptions(options) {
return {
model: User,
attributes: ['id', 'username'],
where: {
enabled: true,
},
};
}
}
EnabledUser.init({
username: DataTypes.STRING,
enabled: DataTypes.BOOLEAN,
}, {
sequelize,
timestamps: false,
});
Using both
You can to override the method using the previous viewQueryOptions
option,
ex:
class EnabledUser extends View {
static getQueryOptions(options) {
return {
model: User,
attributes: ['id', 'username'],
...options.viewQueryOptions,
};
}
}
EnabledUser.init({
username: DataTypes.STRING,
enabled: DataTypes.BOOLEAN,
}, {
sequelize,
timestamps: false,
viewQueryOptions: {
where: { enabled: true },
},
});
Usage
The usage of a defined view is totally similar to use a standar model, using the View defined below, we can do:
const result = await EnabledUser.findAll();
console.log(result.map((user) => user.toJSON()));
The query options are applicable over the View, for example:
const result = await EnabledUser.findAll({
attributes: ['username'],
order: [
['username', 'DESC'],
],
});
All the supported methods by Sequelize.Model
are supported, like findOne
,
count
, etc.
Using with associations
The viewQueryOptions
options support the most features of a sequelize query
options, including include
, the only extra-step we need to do, is indicate to
the view, where are the information of the included fields.
For this proppose, there are an option param called fieldsMap
, this should to
be a dictionary like object using the view field as key, and the query field as
value.
Example:
// Create User Model
class User extends Model {};
User.init({
username: DataTypes.STRING,
birthday: DataTypes.DATE,
}, { sequelize, modelName: 'user' });
// Create Post Model
class Post extends Model {};
Post.init({
title: DataTypes.STRING,
content: DataTypes.TEXT,
}, { sequelize, modelName: 'post' });
// Create Post -> User sssociation
Post.belongsTo(User, {allowNull: false});
// Create View
class PublicPost extends View {};
PublicPost.init({
title: DataTypes.STRING,
author: DataTypes.STRING,
createdAt: DataTypes.DATE,
}, {
sequelize,
modelName: 'public_posts',
timestamps: false,
viewQueryOptions: {
model: Post,
attributes: ['id', 'title', 'createdAt'],
include: [{
model: User,
required: true,
attributes: ['username'],
}],
// Map user.username to author
fieldsMap: {
author: 'user.username',
},
},
});
(async () => {
await sequelize.sync();
const jane = await User.create({
username: 'janedoe',
birthday: new Date(1980, 6, 20),
});
const robert = await User.create({
username: 'robert',
birthday: new Date(1987, 1, 25),
});
const post1 = await Post.create({
title: 'Post 1',
content: 'This is the first post',
userId: jane.id,
});
const post2 = await Post.create({
title: 'Post 2',
content: 'This is the second post',
userId: robert.id,
});
const result = await PublicPost.findAll();
console.log(result.map((post) => post.toJSON()));
})();
this example should to return:
[
{
id: 1,
title: 'Post 1',
author: 'janedoe',
createdAt: 2022-03-01T01:05:53.411Z
},
{
id: 2,
title: 'Post 2',
author: 'robert',
createdAt: 2022-03-01T01:05:53.414Z
}
]
Using with aggregations fields
Views support make easy to work with aggregations fields, beig abble to use it as standar fields because this are going to be proccesed by the db engine.
Using the option param fieldsMap
, you can to map a View field to an
aggregation function.
Example using count
// Create User Model
class User extends Model {};
User.init({
username: DataTypes.STRING,
birthday: DataTypes.DATE,
}, { sequelize, modelName: 'user' });
// Create Post Model
class Post extends Model {};
Post.init({
title: DataTypes.STRING,
content: DataTypes.TEXT,
}, { sequelize, modelName: 'post' });
// Create Post -> User sssociation
Post.belongsTo(User, {allowNull: false});
User.hasMany(Post);
// Create View
class UserPosts extends View {};
UserPosts.init({
username: DataTypes.STRING,
posts: DataTypes.INTEGER,
}, {
sequelize,
modelName: 'user_posts',
timestamps: false,
viewQueryOptions: {
model: User,
attributes: [
'id',
'username',
[fn('count', 'post.id'), 'posts']
],
group: ['user.id'],
include: [{
model: Post,
attributes: [],
}],
},
});
(async () => {
await sequelize.sync();
const jane = await User.create({
username: 'janedoe',
birthday: new Date(1980, 6, 20),
});
const robert = await User.create({
username: 'robert',
birthday: new Date(1987, 1, 25),
});
const post1 = await Post.create({
title: 'Post 1',
content: 'This is the first post',
userId: jane.id,
});
const post2 = await Post.create({
title: 'Post 2',
content: 'This is the second post',
userId: robert.id,
});
const post3 = await Post.create({
title: 'Post 3',
content: 'This is the third post',
userId: robert.id,
});
const result = await UserPosts.findAll();
console.log(result.map((user) => user.toJSON()));
})();
This should to write out:
[
{ id: 1, username: 'janedoe', posts: 1 },
{ id: 2, username: 'robert', posts: 2 }
]
Using where/group/order with aggregated fields
The aesy way to make complex operations with aggregators is becouse you treat this like standar fields, being abble to do for ex:
const result = UserPost.findAll({
attributes: ['username', 'posts'],
where: {
posts: {
[Op.gte]: 10,
},
},
order: [
['posts', 'DESC'],
],
});
In this case, we are filtering and ordering using the final count of posts, in a squelize simple query, this is very complex becaouse we need to group and define aggregators in the query. Using views, there are simplest.
Using Views instead of virtual fields
Another usage for the Views impplementation must to be the use of pre-calculate or aggregated values instead of virtual fields, for querying implementation and performance.
In the sequelize style, we need to create a virtual field and define the get
property to for example do a concat.
Using views we are abble to use these fields for querying, filter, order, etc.
Example
class User extends Model {};
User.init({
name: DataTypes.STRING,
lastname: DataTypes.STRING,
}, { sequelize, modelName: 'user' });
class FullNameUser extends View {};
FullNameUser.init({
fullname: DataTypes.STRING,
}, {
sequelize,
modelName: 'fullname_user',
timestamps: false,
viewQueryOptions: {
model: User,
attributes: [
'id',
// Use fn('concat', ...) in postgres
[literal(`name || ' ' || lastname `), 'fullname']
],
},
});
(async () => {
await sequelize.sync();
const jane = await User.create({
name: 'Jane',
lastname: 'Doe',
});
const robert = await User.create({
name: 'Robert',
lastname: 'Jhonson',
});
const enableds = await FullNameUser.findAll();
console.log(enableds.map((user) => user.toJSON()));
})();
This should to output:
[
{ id: 1, fullname: 'Jane Doe' },
{ id: 2, fullname: 'Robert Jhonson' }
]
Obviuoly, we are limmited to use logic from the dialect and not in Javscript, but with the adventage to use it in queries, for ex:
const result = await FullNameUser.findAll({
where: {
fullname: {
[Op.like]: 'J% D%',
},
},
});
Combine concepts
If you read the previous examples, you are aware the power of Views, in this example, we are going to combine some concepts like associations and aggregations to make something more complex, in this case we are going to create a View thats resolve a User balance.
In this example with have two models:
- User: The users model getting info from simple users
- Pay: Pays to user, each register add an amount to a user
Example
// Create User Model
class User extends Model {};
User.init({
username: DataTypes.STRING,
birthday: DataTypes.DATE,
}, { sequelize, modelName: 'user' });
// Create Pays Model
class Pay extends Model {};
Pay.init({
amount: DataTypes.DECIMAL(),
observation: DataTypes.STRING,
}, { sequelize, modelName: 'pay' });
// Create Pay -> User sssociation
Pay.belongsTo(User, {allowNull: false});
User.hasMany(Pay);
// Create View
class UserBalance extends View {};
UserBalance.init({
username: DataTypes.STRING,
amount: DataTypes.DECIMAL,
}, {
sequelize,
modelName: 'user_balance',
timestamps: false,
viewQueryOptions: {
model: User,
attributes: [
'id',
'username',
[fn('coalesce', fn('sum', col('pays.amount')), 0), 'amount']
],
group: ['user.id'],
include: [{
model: Pay,
required: false,
}],
},
});
(async () => {
await sequelize.sync();
const jane = await User.create({
username: 'janedoe',
birthday: new Date(1980, 6, 20),
});
const robert = await User.create({
username: 'robert',
birthday: new Date(1987, 1, 25),
});
await Pay.create({
amount: 100,
observation: 'Salary',
userId: jane.id,
});
await Pay.create({
amount: 50,
observation: 'Gift',
userId: jane.id,
});
await Pay.create({
amount: 0.57,
observation: 'Interest',
userId: jane.id,
});
const result = await UserBalance.findAll({
// Use aggreation functions in order like a sr.
order: [['amount', 'ASC']],
});
console.log(result.map((user) => user.toJSON()));
})();
The output should to be:
[
{ id: 2, username: 'robert', amount: 0 },
{ id: 1, username: 'janedoe', amount: 150.57 }
]
Explanation
In this case we use an association with Pay
model, and an aggregation
function to sum
the amounts, getting a reduced balance from user, in this
case don't map fields becouse the amount
field is declared in the main Model,
this View can be used for obtain single registers without use aggregations:
const janedoeBalance = await UserBalance.findOne({
attributes: ['amount'],
where: {id: 1},
raw: true,
})
.then((result) => result.amount)
;
Polymorphism using Views
One of pilars of the object oriented programming is the polymorphism, but this paradigm is very hard to apply in relational databases, and this complexity is increased in the ORM layer.
Sequelie have an
example using
the beforeFind
hook and scopes.
There we explore an alternative using Views, with the adventage of the transparent usage in queries.
Stage
In this stage with have and entity called Vehicle
, with a serie of common
vehicles attributes, as the price or the name, and a attribute called type
that define who type of vehicle is, the rest of properties are in two
separate models: Car
and Bike
, this referers to the Vehicle
model with an
one to one relation.
Propouse
We propose to create two abstract class, VehicleChild
extending of Model
,
and VehicleChildView
extending of View
.
Implement
Create parent model
First, we are going the Parent entity model:
// Create Vehicle Model
class Vehicle extends Model {};
Vehicle.init({
type: DataTypes.ENUM('car', 'bike'),
name: DataTypes.STRING,
price: DataTypes.DECIMAL,
}, { sequelize, modelName: 'vehicle' });
This has the type of vehicle and the common information.
Create child abstract
Next, we are going to code a abstract class for use by the childs of Vehicle
:
// Create Child Model Abstract
class VehicleChildren extends Model {
// Override init for add id field
static init(fields, options = {}) {
super.init({
id: {
type: DataTypes.INTEGER,
allowNull: false,
primaryKey: true,
autoIncrement: false,
},
...fields,
}, {
timestamps: false,
...options,
});
}
static associate(models) {
Vehicle.hasOne(this, {
foreignKey: 'id',
sourceKey: 'id',
constraints: true,
});
}
};
In this case, we are overriding the method init
, to add an id field, in this
case, it's neccesary the child id was the same of the parent model, for this
reason set autoIncrement
in false.
By other hand, set timestamps
in false becouse the creation time is in the
parent model. By practical propouses we will not to worry for the updatedAt
field.
In the associate method, we are associating own child model with the parent.
Create child models
// Create Car Model
class Car extends VehicleChildren {};
Car.init({
transmission: DataTypes.ENUM('automatic', 'manual'),
doors: DataTypes.INTEGER,
style: DataTypes.ENUM('sedan', 'coupe', 'convertible'),
hp: DataTypes.INTEGER,
}, { sequelize, modelName: 'car'});
// Create Bike Model
class Bike extends VehicleChildren {};
Bike.init({
style: DataTypes.ENUM('road', 'mountain', 'hybrid'),
cicles: DataTypes.INTEGER,
cc: DataTypes.INTEGER,
}, { sequelize, modelName: 'bike'});
// Create associations
Car.associate();
Bike.associate();
We extends the VehicleChildren
and define a serie of own properties for each
vehicle type.
Create the abstract View
For DRY, we have to create an abstract View overriding some methods:
// Create View Base
class VehicleChildView extends View {
// Override getQueryOptions using props
static getQueryOptions(options) {
return {
model: Vehicle,
include: [{
model: this.model,
required: true,
}],
where: {
type: this.type,
},
...(options.viewQueryOptions || {}),
};
}
static init(fields, options = {}) {
const extraFields = Object.keys(fields).reduce((acc, key) => {
acc[key] = `${this.type}.${key}`;
return acc;
}, {});
const {viewQueryOptions} = options;
super.init({
name: DataTypes.STRING,
price: DataTypes.DECIMAL,
...fields,
}, {
...options,
viewQueryOptions: {
fieldsMap: {
...extraFields,
...(viewQueryOptions || {}),
},
...(viewQueryOptions && options.viewQueryOptions || {}),
}, }); }
// Override create
static async create(data, options = {}) {
return sequelize.transaction(async (transaction) => {
const {
name,
price,
...extra
} = data;
const vehicle = await Vehicle.create({
type: this.type,
name,
price,
}, { transaction });
const child = await this.model.create({
id: vehicle.id,
...extra,
}, { transaction });
return this.build({
...vehicle.get({plane: true}),
...child.get({plane: true}),
});
});
}
};
In this case we are using the method getQueryOptions
to define the View
query, equal for all childrens. We expect two static properties in the
definition of a VehicleChildView
; type
and model
, this properties are
used for abstract View to create the query options, the relation and to be used
in the overrided logic of create
method, in this case we are overriting
create
to desmotrate a use of a View like some kind of serializer.
We are auto-mapping the own-properties too.
Create the child views
With the code abstracted bellow, the definition of new Views that represent
a child of Vehicle
, must to be simple:
// Create Car View
class VehicleCar extends VehicleChildView {
static type = 'car';
static model = Car;
};
VehicleCar.init({
transmission: DataTypes.STRING,
doors: DataTypes.INTEGER,
style: DataTypes.STRING,
hp: DataTypes.INTEGER,
}, {
sequelize,
});
// Create Bike View
class VehicleBike extends VehicleChildView {
static type = 'bike';
static model = Bike;
};
VehicleBike.init({
style: DataTypes.STRING,
cicles: DataTypes.INTEGER,
cc: DataTypes.INTEGER,
}, {
sequelize,
});
Only extending the class VehicleChildView
and creating the properties
type
and model
, the rest of the logic runs over the abstract View.
Usage
The usage is very simple, we treat the final views like models with the full information of entitiy (parent and child attributes):
(async () => {
await sequelize.sync();
const car = await VehicleCar.create({
name: 'Audi',
price: 100000,
transmission: 'automatic',
doors: 4,
style: 'sedan',
hp: 200,
});
const bike = await VehicleBike.create({
name: 'BMX',
price: 10000,
style: 'road',
cicles: 100,
cc: 100,
});
const cars = await VehicleCar.findAll();
console.log(cars.map((car) => car.toJSON()));
const bikes = await VehicleBike.findAll();
console.log(bikes.map((bike) => bike.toJSON()));
})();
The result should to output:
[
{
id: 1,
name: 'Audi',
price: 100000,
transmission: 'automatic',
doors: 4,
style: 'sedan',
hp: 200,
createdAt: 2022-03-01T03:58:18.941Z,
updatedAt: 2022-03-01T03:58:18.941Z
}
]
[
{
id: 2,
name: 'BMX',
price: 10000,
style: 'road',
cicles: 100,
cc: 100,
createdAt: 2022-03-01T03:58:18.962Z,
updatedAt: 2022-03-01T03:58:18.962Z
}
]