@bluejay/sequelize-service
TypeScript icon, indicating that this package has built-in type declarations

6.3.2 • Public • Published

SequelizeService

npm npm npm

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.

Readme

Keywords

none

Package Sidebar

Install

npm i @bluejay/sequelize-service

Weekly Downloads

16

Version

6.3.2

License

MIT

Unpacked Size

135 kB

Total Files

179

Last publish

Collaborators

  • asifarran
  • bluebirds