SequelizeService
Sequelize model based service, exposes basic CRUD methods, pub/sub mechanisms, computed properties through decorations.
Requirements
node >= 7.10
typescript >= 2.4
inversify >= 4.13
Installation
npm i inversify reflect-metadata @bluejay/sequelize-service
Usage
Creating a SequelizeService
The only required dependency is a Sequelize model.
-
TUserWriteProperties
is an interface describing a user's properties used during write and update operations. It is recommended to require properties that are required to create the object and leaving the others as optional. -
TUserReadProperties
is an interface describing a user's properties used during read operations. It is recommend to define as readonly. -
TUserComputedProperties
is an interface describing the possible computed properties for a user object. -
UserModel
is the User Sequelize model.
Manually
const userService = new SequelizeService<TUserWriteProperties, TUserReadProperties, TUserComputedProperties>(UserModel);
Using inversify
// TUserReadProperties is the most inclusive interface, describing all the fields from the User schema
@injectable()
export class UserService extends SequelizeService<TUserWriteProperties, TUserReadProperties, TUserComputedProperties> {
@inject(ID.UserModel) protected model: Sequelize.Model<TUserReadProperties, TUserReadProperties>;
}
// ---
// Make sure to declare the service as a singleton to ensure events are caught by all subscribers
container.bind<ISequelizeService<TUserWriteProperties, TUserReadProperties, TUserComputedProperties>>(ID.UserService).to(UserService).inSingletonScope();
Sessions
All hooks and many methods receive a Session
object which provides various handful methods to deal with a set of data, in a particular context.
A Session
inherits from Collection, so you can manipulate/iterate data asynchronous without the need of external libraries.
Sessions also expose various methods related to the type of query you're currently dealing with. For example during an update:
class UserService extends SequelizeService<TUserWriteProperties, TUserReadProperties, TUserComputedProperties> {
// ...
protected async beforeUpdate(session: UpdateSession<TUserWriteProperties, TUserReadProperties, TUserComputedProperties>) {
session.getOption('transaction'); // Get the transaction under which this update is being performed
session.hasFilter('email'); // Whether os not this update is filtered by email
session.getValue('email'); // Get the update value of `email`, if present
await session.fetch(); // Performs a select
await session.ensureIdentified(); // Make sure all objects corresponding to the filters have their primary key set
await session.ensureProperties({ select: ['email', 'age'] }); // Only performs a SELECT if not all objects have their age and email set
// Run a callback for each user, in parallel
await session.forEachParallel(async user => {
});
// Run a callback for each user, in series
await session.forEachSeries(async user => {
});
// Get all the users IDs
const ids = session.mapByProperty('id');
}
}
You might notice that this documentation never refers to particular instances, this is because this entire module is based on sessions, which in turn encourage you to think all queries as bulk operations, and therefore optimize for multiple objects.
Querying
All methods returning multiple objects return a Collection. Methods returning a single return an unwrapped object.
Creating objects
Creating a single object
const user = await userService.create({ email: 'foo@example.com', password: 'abcdefg' });
console.log(user.email); // foo@example.com
Creating multiple objects
const users = await userService.createMany([{ email: 'foo@example.com', password: 'abcdefg' } /*, ...*/ ]);
users.forEach(user => console.log(user.email));
Finding objects
// Find multiple users
const users = await userService.find({ email: { in: ['foo@example.com', 'foo@bar.com'] } });
// Find a single user by its primary key
const user = await userService.findByPrimaryKey(1);
// Find multiple users by their primary key
const users = await userService.findByPrimaryKeys([1, 2]);
Updating objects
// Change the user email to another value
await userService.update({ email: 'foo@example.comn' }, { email: 'foo@bar.com' });
// Target a user by primary key
await userService.updateByPrimaryKey(1, { email: 'foo@bar.com' });
Upserting an obejct
Caution: This methods will lock the "candidate" row, and perform either a create
or an update
depending on if a candidate was found.
Note: For consistency, the created/updated row is always returned, meaning that we perform a SELECT after the update, if necessary.
This method uses a more MongoDB like syntax, allowing you to pass complex filters as part of the matching criteria.
// Make sure a user older than 21 exists
await userService.replaceOne({ email: 'foo@example.com', age: { gte: 21 } }, { email: 'foo@example.com', age: 21 });
Deleting objects
// Remove all users younger than 21
await userService.delete({ age: { lt: 21 } });
Counting objects
const count = await userService.count({ age: { lt: 30 } });
Working with transactions
Note: All writing methods automatically create a transaction, which encapsulates both the hooks and the model call itself, meaning that if an afterCreate
hooks fails, for example, the entire creation will be rolled back,.
Creating a transaction
Within your service, the protected
transaction()
method allows you to create a new transaction.
return await this.transaction({}, async transaction => {
// Your code here
});
Ensuring a transaction
There are times when you want to make sure you're running under a transaction, but not necessarily create a new one, if, for example, your current method's caller already created one.
The transaction()
method takes an options
parameter, which may contain a transaction
property, in which the transaction will be reused.
return await this.transaction({}, async transaction => {
return await this.transaction({ transaction }, newTransaction => {
console.log(newTransaction === transaction); // true
});
});
Passing transactions to methods
All query methods accept an optional transaction
.
await userService.find({}, { transaction });
await userService.create({ email: 'foo@example.com', password: 'abcdefg' }, { transaction });
Working with hooks
Hooks are a convenient way to validate and transform you data.
Hooks are ran for all write queries, before and after the query.
class UserService extends SequelizeService<TUserWriteProperties, TUserReadProperties, TUserComputedProperties> {
/// ...
protected async beforeCreate(session: CreateSession<TUserWriteProperties, TUserReadProperties, TUserComputedProperties>) {
await session.forEachParallel(async user => {
// Your code here
});
}
}
Working with events
Computed properties
Computed properties allow you to decorate objects with properties that are not stored in your DB.
Computed properties are instances of the abstract ComputedProperty
class and must implement their transform
method, which takes a Session
as an argument.
class UserAge extends ComputedProperty<TUserWriteProperties, TUserReadProperties, TUserComputedProperties, number> { // "age" is a number
public async transform(session: Session<TUserWriteProperties, TUserReadProperties, TUserComputedProperties>) { // "transform" is abstract and must be implemented
session.forEach(user => {
user.age = moment.duration(moment().diff(user.date_of_birth)).get('years'); // We set "age" on each object of the session, based on the date of birth
});
}
}
class UserIsAdult extends ComputedProperty<TUserWriteProperties, TUserReadProperties, TUserComputedProperties, boolean> {
public async transform(session: Session<TUserWriteProperties, TUserReadProperties, TUserComputedProperties>) {
await session.ensureProperties({ compute: ['age'] }); // We make sure the age is set
session.forEach(user => {
user.isAdult = user.age >= 21
});
}
}
To make your service aware of the computed properties, you need to create a manager:
class UserComputedPropertiesManager extends UserComputedPropertiesManager<TUserWriteProperties, TUserReadProperties, TUserComputedProperties> {
protected map() {
return {
age: new UserAge(),
isAdult: { property: new UserIsAdult(), dependencies: ['age'] } // We make sure that the age is fetched before
};
}
}
And finally you can set the manager on your service:
class UserService extends SequelizeService<TUserWriteProperties, TUserReadProperties, TUserComputedProperties> {
protected computedPropertiesManager = new UserComputedPropertiesManager();
// ...
}
You can now request age
and isAdult
to be computed using the compute
option:
const myUser = await userService.findByPrimaryKey(1, { compute: ['age', 'isAdult'] });
Documentation
See Github Pages.