
Warp is an ORM for the scalable web.
With Warp, you can:
- Define your
tables
- Describe how your
columns are parsed and validated
- Determine
triggers that run before or after database operations are made
- Build
queries to find the data that you need
- Run
functions that handle complex logic
- Restrict access based on
user details
- Implement a
restful API using an express middleware
NOTE: Currently, only mysql is supported. But database adapters for other databases are coming.
NOTE: This documentation is only for versions 6+. For versions 5.* or legacy versions (i.e. versions < 5.0.0), see readme-v5.md or readme-legacy.md
Table of Contents
Installation
To install Warp, use the npm install command.
npm install --save warp-server
As Warp uses advanced javascript features, you must transpile your project using a tool like typescript.
For typescript, Make sure to add the following in your tsconfig.json "compilerOptions".
"experimentalDecorators": true,
"emitDecoratorMetadata": true
Also, you need to install and import the reflect-metadata library in order for Warp to properly define classes.
NOTE: Remember to import reflect-metadata only once in your project, ideally in your main index.ts file.
npm install --save reflect-metadata
import 'reflect-metadata';
Getting Started
To start using Warp, we need to create an instance of it.
import Warp from 'warp-server';
const databaseURI = 'mysql://youruser:password@yourserver.com:3306/yourdatabase?charset=utf8';
const service = new Warp({ databaseURI });
service.initialize().then(() => { });
In the example above, we created a new Warp service. We also defined how we would connect to our database by setting the databaseURI configuration.
TIP: The initialize() method is asynchronous. Aside from using .then(), we can also use await.
import Warp from 'warp-server';
const databaseURI = 'mysql://youruser:password@yourserver.com:3306/yourdatabase?charset=utf8';
const service = new Warp({ databaseURI });
(async () => {
await service.initialize();
})();
Aside from databaseURI, there are other options that we can configure for our Warp instance.
Configuration Options
| databaseURI | URI | URI of your database (required) |
| persistent | boolean | State whether db pool connections are recycled or auto-disposed (default: false) |
| restful | boolean | State whether you want to use the REST API feature |
| apiKey | string | API key for your REST API (required if restful is true) |
| masterKey | string | Master key for your REST API (required if restful is true), NOTE: Only admin users should know this key |
| customResponse | boolean | State whether the response is going to be handled by Warp or passed to the next middleware (default: false) |
URI
The URI format follows the syntax below.
databaseURI: 'protocol://user:password@server:port/database?optionalParamter1=value&optionalParamter2=value'
By using a URI, we are able to define the connection definition, all in one string. Additionally, if you would like to use multiple connections for your application, such as defining master and slave databases, you can set the value as an array.
databaseURI: [
{
uri: 'protocol://user:password@server:port/database?optionalParamter1=value',
action: 'write'
},
{
uri: 'protocol://user:password@server:port/database?optionalParamter1=value&optionalParamter2=value',
action: 'read'
}
]
Note the other property called action that determines which sort of database operations are assigned to this connection. This can either be read or write. It is adivsable to only have one write connection. However, you can have multiple read connections.
databaseURI: [
{
uri: 'protocol://user:password@server:port/database?optionalParamter1=value',
action: 'write'
},
{
uri: 'protocol://user:password@server:port/database?optionalParamter1=value&optionalParamter2=value',
action: 'read'
},
{
uri: 'protocol://user:password@server:port/database?optionalParamter1=value&optionalParamter2=value',
action: 'read'
}
]
Now that we've initialized Warp, we can now start using it!
Classes
A Class is a representation of a table inside the database. Inside the Class are keys, which represent how columns in the database are parsed and formatted.
For example, a dog table will have a corresponding class called Dog that has different keys such as name, age, height, and weight.
Among these keys are three special ones that are automatically set by the server and cannot be manually edited.
id: a unique identifier that distinguishes an object inside a table
createdAt: a timestamp that records the date and time when an object was created (UTC)
updatedAt: a timestamp that records the date and time when an object was last modified (UTC)
NOTE: Be sure to have id, created_at, and updated_at fields in your table to avoid conflicts.
NOTE: Aside from the three keys above, you also need to make sure that the table has a deleted_at field for deletion operations.
Defining a Class
To create a Class, simply extend from Warp.Class.
import { Class, define } from 'warp-server';
@define class Dog extends Class { }
In the example above, you can see that we defined a new class called Dog. Using the @define decorator, we tell Warp that the table name for this is dog (i.e., The snake_case version of the class name).
If we wanted the class name to be different from the table name, we can define it manually.
@define({ className: 'dog', source: 'canine' })
class Dog extends Class { }
We now have a class name of dog that points to a table called canine. The class name is the one used to define the routes in our restful API. For more information, see the restful API section.
Defining Keys
Now, let's add in some keys to our Class.
import { Class, define, key } from 'warp-server';
@define class Dog extends Class {
@key name: string;
@key age: number;
@key height: number;
@key weight: number;
bmi: number;
}
In order to add our keys, we defined them as properties inside the Class. It's worth mentioning that we used the @key decorator to tell Warp that the properties are columns. Otherwise, they would simply be ignored during database operations.
In this example, since the bmi proeprty is not defined as a key, then it is ignored when we're saving, destroying or querying Dog.
Aside from informing Warp that the keys are columns, the @key decorator also infers the data type of the field based on its typescript type. By doing so, it validates and parses the fields automatically.
NOTE: You do not need to include id, createdAt, and updatedAt keys because they are already defined in Class.
NOTE: We used camelCase format for the properties. Inside the database, we expect the columns to be in snake_case format. If this were shown as a table, it would look similar to the following.
Table: Dog
| 1 | Bingo | 4 | 1.5 | 43.2 | 2018-03-09 12:38:56 | 2018-03-09 12:38:56 | null |
| 2 | Ringo | 5 | 1.25 | 36 | 2018-03-09 12:38:56 | 2018-03-09 12:38:56 | null |
If the database column name is different from the property name of the Class, you can define it using the from option.
import { Class, define, key } from 'warp-server';
@define class Dog extends Class {
@key({ from: 'nickname' })
nick: string;
}
Also, if you want to manually define the type of the key instead of relying on type inference, you can use the type option.
import { Class, define, key } from 'warp-server';
interface FoodPreferences {
taste: string;
cuisine: string;
}
@define class Dog extends Class {
@key({ type: 'json' })
preferences: FoodPreferences;
@key({ type: 'array' })
pseudonyms: string[];
}
Defining Relations
One of the biggest features of relational databases is their ability to define relations between tables. This makes it more convenient to link and retrieve entities.
If two tables have a one-to-many relation, we can define the type of the key with an instance of a Class. This type allows us to define from which class our key belongs to.
Later on, when we're querying, the key will automatically return an instance of the Class that we defined. Additionally, it validates whether the value we set to our key matches the correct Class.
import { Class, key } from 'warp-server';
@define class Department extends Class { }
@define class Employee extends Class {
@key name: string;
@key department: Department;
}
In the example above, we tell Warp that our department key belongs to the Department class.
Inside our database, every time we save or query Employee, it automatically maps the column employee.department_id to department.id.
If you want to define a different column for the mapping, you can set it using the from and to options.
import { Class, key } from 'warp-server';
@define class Department extends Class { }
@define class Employee extends Class {
@key name: string;
@key({ from: 'employee.deparment_code', to: 'department.code' })
department: Department;
}
Now that we've defined our relation, we can start using it in our code.
Below is an example of a query with a relation. For more information on queries, see the Queries section.
const service = new Warp({ });
const employeeQuery = new Query(Employee).include('department.name');
const employee = await service.classes.first(employeeQuery);
const departmentName = employee.department.name;
Another example can be found below, this time it's about saving objects. For more information on saving and destroying objects, see the Objects section.
const employee = new Employee;
employee.department = new Department(1);
employee.department = new Country(3);
await service.classes.save(employee);
Adding Key Modifiers
To enhance how keys are validated, parsed, and formatted, we can add Key Modifiers.
@hidden
If you use the restful API and want to hide a key from the results, you can use the @hidden decorator.
import { Class, define, key, hidden } from 'warp-server';
@define class Dog extends Class {
@key name: string;
@hidden @key secretName: string;
}
NOTE: secretName can still be retrieved in the Class object. Only the results in the restful API, and the result of dog.toJSON() will have it hidden.
@guarded
If you want to guard the key from being updated over the restful API or via the Class constructor, you can use the @guarded decorator.
import { Class, define, key, guarded } from 'warp-server';
@define class Dog extends Class {
@key name: string;
@guarded @key eyeColor: string;
}
const daschund = new Dog;
dog.eyeColor = 'green';
const corgi = new Dog({ eye_color: 'brown' });
@computed
Sometimes, you have keys that are computed which are, hence, not stored in the database. If you want to display a key in the restful API, you can use the @computed decorator.
NOTE: Once a key is defined as @computed, you cannot manually set its value
import { Class, define, key, computed } from 'warp-server';
@define class Dog extends Class {
@key name: string;
@key weight: number;
@key height: number;
@computed
@key get bmi(): number {
return this.weight / (this.height * this.height);
}
}
const daschund = new Dog;
dog.height = 1.5;
dog.weight = 35;
const bmi = dog.bmi;
dog.bmi = 32;
In the restful API, the value will also be included.
{
"result": [
{
"id": 42,
"height": 1.5,
"weight": 35,
"bmi": 15.555555555555555
}
]
}
@length
If you want to limit the length of a string key, you can use the @length decorator.
import { Class, define, key, length } from 'warp-server';
@define class Dog extends Class {
@length(3) @key name: string;
@length(0, 5) @key alias: string;
@length(3, 5) @key code: string;
}
@min, @max, @between
If you want to limit the range of values for a number key, you can use the @min, @max, and @between decorators.
import { Class, define, key, min, max, between } from 'warp-server';
@define class Dog extends Class {
@min(3) @key age: number;
@max(5) @key height: number;
@between(3, 5.5) @key weight: number;
}
@rounded
If you want to add rounding to the value of a number key, you can use the @rounded decorator.
import { Class, define, key, range } from 'warp-server';
@define class Dog extends Class {
@rounded(2) @key weight: number;
}
Additionally, you can specify the rounding rule to be used. This can either be off, up, or down. The default rule is off.
@define class Dog extends Class {
@rounded(2) @key weight: number;
@rounded(2, 'up') @key height: number;
@rounded(2, 'down') @key rating: number;
}
const corgi = new Dog;
corgi.weight = 22.365;
corgi.height = 1.152;
corgi.rating = 4.318;
@enumerated
If you want to check if a key's value is one of pre-defined values, you can use the @enumerated decorator.
import { Class, define, key, enumerated } from 'warp-server';
@define class Dog extends Class {
@enumerated(['active', 'inactive'])
@key status: string;
@enumerated(new Map([[1, 'basic'], [2, 'intermediate'], [3, 'advanced']]))
@key trainingLevel: number | string;
}
const corgi = new Dog;
corgi.status = 'active';
corgi.status = 'deactivated';
corgi.trainingLevel = 'basic';
corgi.trainingLevel = 1;
const trainingLevel = corgi.trainingLevel;
Registering Classes
Now that we've created our Class, we can start using it in our application. For more information, see the Objects section.
However, if you are using the restful API feature and want the class to be recognized, you need to register it.
To do so, simply use the classes.register() method.
const service = new Warp({ });
@define class Dog extends Class { }
service.classes.register({ Dog });
The classes.register() accepts a mapping of classes, so you can add several classes at once.
@define class Dog extends Class { }
@define class Cat extends Class { }
service.classes.register({ Dog, Cat });
Users
In most applications, a user table is usually defined and is often used to authenticate whether certain parts of the app are accessible. For Warp there is a built-in User class that we can extend from to define our user table.
Defining a User class
To define a user, simply extend the Warp.User class.
import Warp, { define, key } from 'warp-server';
@define class User extends Warp.User {
@key firstName: string;
@key lastName: string;
}
Because User is a special class, it has pre-defined keys that are helpful for authentication and authorization.
username: a unique string used for auth
email: a valid email address that can be used for verification
password: a secret string used for auth
In addition, the User class has built-in Triggers that check whether the supplied username and email keys are valid and unique. It also prevents users from retrieving the raw password field, as well as ensuring that database operations to the user are only made by the user itself or by administrators using master mode.
Authentication
Starting from version 6.0.0, Warp no longer implements its own auth mechanism. However, this now opens up an opportunity for developers to make use of other popular and stable implementations such as passport, OAuth2, and OpenID Connect.
Ideally, you would use an auth library or middleware to authenticate and retrieve the user from the database. Afterwards, you can map the user identity to your defined User class and use this class for database operations.
const service = new Warp({ });
class User extends Warp.User { }
const mapUser = (req, res, next) => {
req.user = new User(req.user);
next();
};
req.use('/api/', someAuthMiddleware, mapUser, service.router);
By default, the restful API tries to get the req.user parameter
Objects
An Object is the representation of a single row inside a table.
For example, a Dog class can have an instance of an Object called corgi, that has different properties such as name, age, and weight.
Creating an Object
To create an Object, simply instantiate the Class.
@define class Dog extends Class {
@key name: string;
@key age: number;
@key height: number;
@key weight: number;
@key awardsWon: number;
@key owner: Person;
get bmi(): number {
return this.weight / (this.height * this.height);
}
}
const corgi = new Dog;
We can set the values of its keys using the properties we defined.
corgi.name = 'Bingo';
corgi.age = 5;
corgi.weight = 32.5;
corgi.owner = new Person(5);
It also validates if we provide wrong data types.
corgi.weight = 'heavy';
Alternatively, we can define the values of keys inside the object constructor.
const corgi = new Dog({ name: 'Bingo', age: 5, weight: 32.5 });
TIP: The validation of data types still works when using the object constructor approach. Also, @guarded keys will throw an error if you try to assign them using this approach.
Once we've finished setting our keys, we can now save the Object using the classes.save() method.
await service.classes.save(corgi);
NOTE: Don't forget to initialize() before running classes.save() methods.
Updating Objects
The classes.save() method inserts a new row if the object was just newly created. If, however, the object already exists (i.e. has an id), then it will update instead the values inside the row.
const corgi = new Dog;
corgi.name = 'Bingo';
corgi.age = 5;
corgi.weight = 32.5;
await service.classes.save(corgi);
corgi.weight = 35;
await corgi.save();
Alternatively, if we know the id of the row we want to update, use the withId() method.
const daschund = Dog.withId<Dog>(25);
daschund.name = 'Brownie';
await service.classes.save(daschund);
Or simply pass the id inside the Class constructor.
const shitzu = new Dog(16);
shitzu.name = 'Fluffy';
await service.classes.save(shitzu);
Or pass the id along with other keys inside the constructor.
const beagle = new Dog({ id: 34, name: 'River' });
await service.classes.save(daschund);
Incrementing Numeric Keys
If the key we are trying to update is defined as a number and we want to atomically increase or decrease its value without knowing the original value, we can opt to use the .increment() method.
For example, if we want to increase the age by 1, we would use the following code.
corgi.increment('awards_won', 1);
Conversely, if we want to decrease a number key, we would use a negative value.
corgi.increment('weight', -5.2);
Updating JSON Keys
Recently, relational databases have slowly introducted JSON data structures into their systems. This allows for more complex use cases which might have originally needed a NoSQL database to implement.
NOTE: JSON data types are still in its early stages and performance has not yet been thoroughly investigated yet. Hence, only place mid to shallow JSON structures inside your databases for the time being.
JSON Objects
If the node of the JSON data we want to modify is an object, we can use the json().set() method to update its value.
@define class Dog extends Class {
@key preferences: object;
}
const labrador = new Dog;
labrador.preferences = { food: 'meat' };
await service.classes.save(labrador);
labrador.json('preferences').set('$.food', 'vegetables');
await service.classes.save(labrador);
Notice the first argument of the set() method. This represents the path of the JSON column that we want to modify. For more information, see the documentation of path syntax on the MySQL website.
JSON Arrays
If the node of the JSON data we want to edit is an array, we can use the json().append() method to add to its value.
labrador.preferences = { toys: ['plushie', 'ball'] };
await service.classes.save(labrador);
labrador.json('preferences').append('$.toys', 'bone');
await service.classes.save(labrador);
Setting the ID
By default, Warp assumes that tables have an auto-increment id. However, if you want to manually define the value of the id, you need to use the .setNewId() method.
const dog = new Dog;
dog.setNewId('D39t2h28-ug4822G-K24u5H4-24mU24');
await service.classes.save(dog);
You can also use .setNewId() for existing objects.
const dog = new Dog('123-456-789');
dog.setNewId('789-012-345');
await service.classes.save(dog);
const id = dog.id;
IMPORTANT: Make sure to use .setNewId() for manually setting the id. Setting the id with the following methods will not work.
dog.id = 3;
const corgi = new Dog(3);
const shitzu = new Dog({ id: 'abc-123' });
As an example of how .setNewId() might be useful, below, you can use the uniqid library and also set a beforeSave trigger in order to programmatically create new id's.
import uniqid from 'uniqid';
class Dog extends Class {
@beforeSave
createNewId() {
if(this.isNew) this.setNewId(uniqid());
}
}
Destroying an Object
If we want to delete an Object, we can use the classes.destroy() method.
await service.classes.destroy(labrador);
NOTE: In Warp, there is no hard delete, only soft deletes. Whenever an object is destroyed, it is preserved, but its deleted_at column is set to the current timestamp. During queries, the "deleted" objects are omitted from the results automatically. You do not need to filter them out.
Queries
Now that we have a collection of Objects inside our database, we would need a way to retrieve them. For Warp, we do this via Queries.
Creating a Query
To create a query, wrap the Class inside a Query.
import { Class, define, Query } from 'warp-server';
@define class Dog extends Class { }
const dogQuery = new Query(Dog);
Once created, we can fetch the results via classes.find().
const dogs = await service.classes.find(dogQuery);
We now have a Collection of Dog objects. This collection helps us iterate through the different rows of the dog table. To learn more about collections, see the Collections section.
Selecting Keys
By default, Warp fetches all of the visible keys in a Class (i.e. keys not marked as @hidden).
However, if we consider performance and security, it is recommended that we pre-define the keys we would like to fetch. This helps reduce the size of the data retrieved from the database, and reduce the scope of the data accessed.
To define the keys you want to fetch, use the select() method.
dogQuery.select('name');
dogQuery.select(['name', 'age']);
dogQuery.select('name', 'age', 'weight');
dogQuery.select('name').select('age').select('weight');
If you want to include keys from relation keys. You must include them in the select() method. Otherwise, they won't be fetched from the database.
dogQuery.select('location.id', 'location.name');
If, on the other hand, you plan on fetching all visible keys, and include relation keys, you can use the include() method instead of having to call the select() method on all the keys.
dogQuery.include('location.name', 'location.address');
NOTE: If you are already using select, there is no need to use include. You must put all keys inside select.
Defining Constraints
Constraints help filter the results of a query. In order to pass constraints, use any of the following methods.
const dogQuery = new Query(Dog);
dogQuery.equalTo('name', 'Bingo');
dogQuery.notEqualTo('name', 'Ringo');
dogQuery.lessThan('age', 21);
dogQuery.lessThanOrEqualTo('name', 'Zack');
dogQuery.greaterThanOrEqualTo('weight', 30);
dogQuery.greaterThan('created_at', '2018-03-12 17:30:00');
dogQuery.exists('breed');
dogQuery.doesNotExist('breed');
dogQuery.containedIn('breed', ['Malamute', 'Japanese Spitz']);
dogQuery.containedInOrDoesNotExist('breed', ['Beagle', 'Daschund']);
dogQuery.notContainedIn('age', [18, 20]);
dogQuery.startsWith('name', 'Bing');
dogQuery.endsWith('name', 'go');
dogQuery.contains('name', 'in');
dogQuery.containsEither('description', ['small','cute','cuddly']);
dogQuery.containsAll('name', ['big','brave','trustworthy']);
TIP: Each constraint returns the query, so you can chain them, such as the following.
const dogQuery = new Query(Dog)
.greaterThanOrEqualTo('age', 18)
.contains('name', 'go')
.containedIn('breed', ['Malamute', 'Japanse Spitz']);
Using Subqueries
The constraints above are usually enough for filtering queries; however, if the scenario calls for a more complex approach, you may nest queries within other queries.
For example, if we want to retrieve all the dogs who are residents of urban cities, we may use the .foundIn() method.
const urbanCityQuery = new Query(Location).equalTo('type', 'urban');
const dogQuery = new Query(Dog)
.foundIn('location.id', 'id', urbanCityQuery);
const dogs = await service.classes.find(dogQuery);
If we want to see if a value exists in either of multiple queries, we can use .foundInEither().
const urbanCityQuery = new Query(Location).equalTo('type', 'urban');
const ruralCityQuery = new Query(Location).equalTo('type', 'rural');
const dogQuery = new Query(Dog)
.foundInEither('location.id', [
{ 'id': urbanCityQuery },
{ 'id': ruralCityQuery }
]);
const dogs = await service.classes.find(dogQuery);
If we want to see if a value exists in all of the given queries, we can use .foundInAll().
var urbanCityQuery = new Query(Location).equalTo('type', 'urban');
var smallCityQuery = new Query(Location).equalTo('size', 'small');
var dogQuery = new Query(Dog)
.foundInAll('location.id', [
{ 'id': urbanCityQuery },
{ 'id': smallCityQuery }
]);
const dogs = await service.classes.find(dogQuery);
Conversely, you can use .notFoundIn(), .notFoundInEither(), and .notFoundInAll() to retrieve objects whose key is not found in the given subqueries.
By default, Warp limits results to the top 100 objects that satisfy the query criteria. In order to increase the limit, we can specify the desired value via the .limit() method.
dogQuery.limit(1000);
Also, in order to implement pagination for the results, we can combine .limit() with .skip(). The .skip() method indicates how many items are to be skipped when executing the query. In terms of performance, we suggest limiting results to a maximum of 1000 and use skip to determine pagination.
dogQuery.limit(10).skip(20);
dogQuery.limit(1000);
dogQuery.skip(1000);
TIP: We recommend using the sorting methods in order to retrieve predictable results. For more info, see the section below.
Sorting
Sorting determines the order by which the results are returned. They are also crucial when using the limit and skip parameters. To sort the query, use the following methods.
dogQuery.sortBy('age');
dogQuery.sortByDescending(['created_at', 'weight']);
dogQuery.sortByDescending('crated_at', 'weight');
Collections
When using queries, the result returned is a Collection of Objects. Collections are a special iterable for Warp that allows you to filter, sort and manipulate list items using a set of useful methods.
Counting Collections
To count the results, use the length property.
const dogQuery = new Query(Dog);
const dogs = await service.classes.find(dogQuery);
const total = dogs.length;
Filtering Collections
To filter the results and return a new collection based on these filters, use the following methods.
const firstDog = dogs.first();
const lastDog = dogs.last();
const oldDogsOnly = dogs.where(dog => dog.age > 12);
Manipulating Collections
To manipulate the results, use the following methods.
dogs.forEach(dog => console.log(`I am ${dog.name}`));
const names = dogs.map(dog => dog.name);
dogs.each(dog => service.classes.destroy(dog));
dogs.all(dog => service.classes.destroy(dog));
for(const dog of dogs) {
console.log(`I am ${dog.name} and my owner is ${dog.owner.name}`);
}
Converting Collections
Oftentimes, you may opt to use native data types to handle Objects. To accomodate this, Collections contain the following methods.
const dogArray = dogs.toArray();
const dogJSON = dogs.toJSON();
const dogMappedById = dogs.toMap();
const dogMappedByName = dogs.toMap('name');
const dogMappedByOwner = dogs.toMap(dog => dog.owner.id);
TIP: Since some methods return new Collections, you can chain several methods together, as needed.
const dogQuery = new Query(Dog);
const dogs = await service.classes.find(dogQuery);
const firstCorgiNames = dogs.where(dog => dog.breed === 'corgi')
.map(dog => dog.name);
Triggers
If a Class needs to be manipulated before or after it is queried, saved, or destroyed, you can use Triggers.
Triggers allow you to specify which methods must be executed when certain events occur. You can consider these as hooks to your classes where you can perform additional logic outside of the basic parsing and formatting of Warp.
Before Save
To make sure a method is run before the class is saved (whether created or updated), describe it with @beforeSave().
@define class Dog extends Class {
@beforeSave
validateAge() {
if(this.age > 30) throw new Error('This dog is too old!');
}
@beforeSave
convertWeight() {
this.weight = this.weight * 2.2;
}
@beforeSave
setDefaultDescription() {
if(this.isNew) {
this.description = 'I am cute dog.';
}
}
@beforeSave
async updateOwner(classes) {
if(this.isNew) {
const owner = this.owner;
classes.increment(owner, 'dog_count', 1);
await classes.save(owner);
}
}
@beforeSave
async checkAccess(classes, { user, master }) {
if(!this.isNew && this.owner.id !== user.id || !master) {
throw new Error('Only owners of dogs, or administrators can edit their info');
}
}
}
After Save
To make sure a method is run after the class is saved (whether created or updated), describe it with @afterSave().
NOTE: Since these functions are run in the background, errors thrown here will not affect the program. Hence, it is better to catch them and log them.
@define class Dog extends Class {
@afterSave
uselessError() {
if(this.age > 30) throw new Error('This will not stop the program');
}
@afterSave
async addNewPet(classes) {
if(this.isNew) {
const pet = new Pet;
pet.dog = this;
await classes.save(pet);
}
}
@afterSave
async sendNotification(classes, { user }) {
SomeService.Notify('You have successsfully saved a dog!', user.email);
}
}
Before Destroy
To make sure a method is run before the class is destroyed, describe it with @beforeDestroy().
@define class Dog extends Class {
@beforeDestroy
validateAge() {
if(this.age < 18) throw new Error('This dog is too young to destroy!');
}
@beforeDestroy
changeStatus() {
this.status = 'removed';
}
@beforeDestroy
async updateOwner(classes) {
if(this.isNew) {
const owner = this.owner;
classes.increment(owner, 'dog_count', -1);
await classes.save(owner);
}
}
@beforeDestroy
async checkAccess(classes, { user, master }) {
if(!this.isNew && this.owner.id !== user.id || !master) {
throw new Error('Only owners of dogs, or administrators can destroy their info');
}
}
}
After Destroy
To make sure a method is run after the class is destroyed, describe it with @afterDestroy().
NOTE: Since these functions are run in the background, errors thrown here will not affect the program. Hence, it is better to catch them and log them.
@define class Dog extends Class {
@afterDestroy
async updateOwner(classes) {
if(this.isNew) {
const petQuery = new Query(Pet)
.equalTo('dog.id', this.id);
const pet = classes.first(petQuery);
await pet.destroy();
}
}
@afterDestroy
async sendNotification(classes, { user }) {
SomeService.Notify('You have successsfully removed a dog!', user.email);
}
}
Before Find, First, Get
To make sure a method is run before the class is fetched, describe it with @beforeFind(), @beforeFirst, and @beforeGet.
@define class Dog extends Class {
@beforeFind
limitResult(query) {
query.limit(5);
}
@beforeFirst
removeOldDogs() {
query.greaterThan('age', 10);
}
@beforeGet
async checkAccess(query, { user, master }) {
if(!this.isNew && this.owner.id !== user.id || !master) {
throw new Error('Only owners of dogs, or administrators can get their info');
}
}
}
Functions
Ideally, you can perform a multitude of tasks using classes. However, for special operations that you need to perform inside the server, you can use Functions.
A Function is a piece of code that can be executed via a named endpoint. It receives input keys that it processes in order to produce a result.
Defining a Function
To define a Function, use the Function class.
import { Function } from 'warp-server';
import getDogsPromise from './get-dogs';
class GetFavoriteDogs extends Function {
static get masterOnly() {
return false;
}
async run(keys) {
const collectionID = keys.collection_id;
const favoriteDogs = await getDogsPromise(collectionId);
throw new Error('Cannot get your favorite dogs');
return favoriteDogs;
}
}
For the above example, you can see that we declared a run() method to execute our logic. This is the only method you need in order to define a function.
However, you might notice the masterOnly getter declared atop. What this does is basically limit access to the function to masters (i.e. requests made using the X-Warp-Master-Key). You can omit this code as this defaults to false.
Registering a Function
Right now, the Function you created is still not recognized by Warp. To register its definition, use functions.register().
service.functions.register({ GetFavoriteDogs });
app.use('/api/1', service.router);
functions.register() accepts a mapping of Functions, so you can do the following.
service.functions.register({ GetFavoriteDogs, GetGoodDogs });