Miss any of our Open RFC calls?Watch the recordings here! »

@travetto/model

1.1.0 • Public • Published

Data Modeling

Datastore abstraction for CRUD operations with advanced query support.

Install: @travetto/model

npm install @travetto/model

This module provides a clean interface to data model persistence, modification and retrieval. This module builds heavily upon the Schema, which is used for data model validation.

The module can be segmented into three main areas: declaration, access/storage, and querying

Declaration

Models are declared via the @Model decorator, which allows the system to know that this is a class that is compatible with the module.

Code: Extending BaseModel

import { Model } from '@travetto/model/src/registry/decorator';
import { BaseModel } from '@travetto/model/src/model/base';
 
@Model()
export class User extends BaseModel {
  name: string;
  age: number;
  contact?: boolean;
}

The User model is now ready to be used with the model services.

Access/Storage

The ModelService is the foundation for all access to the storage layer, and provides a comprehensive set of functionality. The service includes support for modifying individual records, bulk update/insert/delete, partial updates, finding records, and more. This should be the expected set of functionality for storage and retrieval.

Code: Using ModelService with the User model

import { Injectable, Inject } from '@travetto/di';
import { ModelService } from '@travetto/model/src/service/model';
import { User } from './user';
 
@Injectable()
export class UserManager {
 
  @Inject()
  private service: ModelService;
 
  async register(user: User) {
    const created = await this.service.save(User, user);
    // send welcome email
    return created;
  }
 
  async bulkCreate(users: User[]) {
    const res = await this.service.bulkProcess(User, users.map(user => ({ insert: user })));
    // notify administrator of completion
    return res;
  }
}

The ModelService itself relies upon a ModelSource which is the driver for the storage layer.

During development, ModelSource supports the ability to respond to model changes in real-time, and to modify the underlying storage mechanism. An example of this would be elasticsearch schemas being updated as fields are added or removed from the @Model class.

Querying

One of the complexities of abstracting multiple storage mechanisms, is providing a consistent query language. The query language the module uses is a derivation of mongodb's query language, with some restrictions, additions, and caveats. Additionally, given the nature of typescript, all queries are statically typed, and will catch type errors at compile time.

General Fields

  • field: { $eq: T } to determine if a field is equal to a value
  • field: { $ne: T } to determine if a field is not equal to a value
  • field: { $exists: boolean } to determine if a field exists or not
  • field: T to see if the field is equal to whatever value is passed in

General Single Valued Fields

  • field: { $in: T[] } to see if a record's value appears in the array provided to $in
  • field: { $nin: T[] } to see if a record's value does not appear in the array provided to $in

Ordered Fields

  • field: { $lt: T } checks if value is less than
  • field: { $lte: T } checks if value is less than or equal to
  • field: { $gt: T } checks if value is greater than
  • field: { $gte: T } checks if value is greater than or equal to

Array Fields

  • field: { $all: T[]] } checks to see if the records value contains everything within $all

String Fields

  • field: { $regex: RegExp | string; } checks the field against the regular expression

Geo Point Fields

  • field: { $geoWithin: Point[] } determines if the value is within the bounding region of the points
  • field: { $near: Point, $maxDistance: number, $unit: 'km' | 'm' | 'mi' | 'ft' } searches at a point, and looks out radially

Groupings

  • { $and: [] } provides a grouping in which all sub clauses are required
  • { $or: [] } provides a grouping in which at least one of the sub clauses is required
  • { $not: { } } negates a clause

A sample query for User's might be:

Code: Using the query structure for specific queries

import { ModelService } from '@travetto/model/src/service/model';
import { User } from './user';
 
export class UserSearch {
  service: ModelService;
 
  find() {
    return this.service.getAllByQuery(User, {
      where: {
        $and: [
          {
            $not: {
              age: {
                $lt: 35
              }
            }
          },
          {
            contact: {
              $exists: true
            }
          }
        ]
      }
    });
  }
}

This would find all users who are over 35 and that have the contact field specified.

Query Language

In addition to the standard query interface, the module also supports querying by query language to facilitate end-user queries. This is meant to act as an interface that is simpler to write than the default object structure.

The language itself is fairly simple, boolean logic, with parenthetical support. The operators supported are:

  • <, <= - Less than, and less than or equal to
  • >, >= - Greater than, and greater than or equal to
  • !=, == - Not equal to, and equal to
  • ~ - Matches regular expression, supports the i flag to trigger case insensitive searches
  • !, not - Negates a clause
  • in, not-in - Supports checking if a field is in a list of literal values
  • and, && - Intersection of clauses
  • or, || - Union of clauses

All sub fields are dot separated for access, e.g. user.address.city. A query language version of the previous query could look like:

Code: Query language with boolean checks and exists check

not (age < 35and contact != null

A more complex query would look like:

Code: Query language with more complex needs

user.role in ['admin''root'] && (user.address.state == 'VA' || user.address.city == 'Springfield')

Regular Expression

When querying with regular expressions,patterns can be specified as 'strings' or as /patterns/. The latter allows for the case insensitive modifier: /pattern/i. Supporting the insensitive flag is up to the underlying model implementation.

Extension - Rest

To facilitate common RESTful patterns, the module exposes RESTful API support in the form of @ModelController.

Code: ModelController example

import { Inject } from '@travetto/di';
 
import { ModelController } from '@travetto/model/src/extension/rest';
import { ModelService } from '@travetto/model/src/service/model';
import { User } from './user';
 
@ModelController('/user', User)
class UserController {
  @Inject()
  source: ModelService;
}

is a shorthand that is equal to:

Code: Comparable UserController, built manually

import { Path, Controller, Get, Request, Delete, Post, Put, Query } from '@travetto/rest';
import { Inject } from '@travetto/di';
import { User } from './user';
import { ModelService } from '@travetto/model/src/service/model';
import { SchemaBody } from '@travetto/schema';
import { ValidStringFields } from '@travetto/model/src/service/source';
 
@Controller('/user')
class UserController {
 
  @Inject()
  source: ModelService;
 
  @Get('')
  async getAllUser(req: Request) {
    return await this.source.getAllByQuery(User, JSON.parse(req.params.q));
  }
 
  @Get('/suggest/:field')
  async suggest(@Query('q') q: string, @Path('field') field: ValidStringFields<User>) {
    return this.source.suggest(User, field, q);
  }
 
  @Get(':id')
  async getUser(req: Request) {
    return await this.source.getById(User, req.params.id);
  }
 
  @Delete(':id')
  async deleteUser(req: Request) {
    return await this.source.deleteById(User, req.params.id);
  }
 
  @Post('')
  async saveUser(@SchemaBody() user: User) {
    return await this.source.save(User, user);
  }
 
  @Put('')
  async updateUser(@SchemaBody() user: User) {
    return await this.source.update(User, user);
  }
}

Install

npm i @travetto/model

DownloadsWeekly Downloads

211

Version

1.1.0

License

MIT

Unpacked Size

95.4 kB

Total Files

27

Homepage

travetto.io

Last publish

Collaborators

  • avatar