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 anexpress
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
- Getting Started
- Classes
- Users
- Objects
- Queries
- Collections
- Triggers
- Functions
- Restful API
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 mainindex.ts
file.
npm install --save reflect-metadata
;
Getting Started
To start using Warp
, we need to create an instance of it.
; const databaseURI = 'mysql://youruser:password@yourserver.com:3306/yourdatabase?charset=utf8'; const service = databaseURI ; // Inititialize the serviceservice;
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 useawait
.
; const databaseURI = 'mysql://youruser:password@yourserver.com:3306/yourdatabase?charset=utf8'; const service = databaseURI ; // Wrap the service in an asynchronous functionasync { // Inititialize the service await service; /** Start using the service here */ };
Aside from databaseURI
, there are other options that we can configure for our Warp
instance.
Configuration Options
Name | Format | Description |
---|---|---|
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 tablecreatedAt
: 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
, andupdated_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
.
; @define
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.
@
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
.
; @define @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 insnake_case
format. If this were shown as a table, it would look similar to the following.
Table: Dog
id | name | age | height | weight | created_at | updated_at | deleted_at |
---|---|---|---|---|---|---|---|
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.
; @define @ 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.
; interface FoodPreferences taste: string; cuisine: string; @define @ preferences: FoodPreferences; @ 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
.
; @define /** shortened for brevity */ @define @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.
; @define /** shortened for brevity */ @define @key name: string; @ 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 = /** some configuration **/ ; // Create a queryconst employeeQuery = Employee; // Get employeeconst employee = await serviceclasses; // employee.department is an instance of the `Department` class// so we can even retrieve the department's nameconst departmentName = employeedepartmentname;
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 = ;employeedepartment = 1; // OKemployeedepartment = 3; // This will cause an error await serviceclasses;
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.
; @define @key name: string; @hidden @key secretName: string; // Will be omitted from query results
NOTE:
secretName
can still be retrieved in theClass
object. Only the results in therestful API
, and the result ofdog.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.
; @define @key name: string; @guarded @key eyeColor: string; const daschund = ;dogeyeColor = 'green'; // OK const corgi = eye_color: 'brown' ; // Will throw an error
@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
; @define @key name: string; @key weight: number; @key height: number; @computed @key get : number return thisweight / thisheight * thisheight; const daschund = ;dogheight = 15;dogweight = 35; const bmi = dogbmi; // Will return weight/(height * height) dogbmi = 32; // Will throw an error
In the restful API
, the value will also be included.
@length
If you want to limit the length of a string key, you can use the @length
decorator.
; @define @ @key name: string; // Minimum of 3 characters @ @key alias: string; // Maximum of 5 characters @ @key code: string; // Between 3 to 5 characters
@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.
; @define @ @key age: number; // Minimum value of 3 @ @key height: number; // Maximum value of 5 @ @key weight: number; // Between 3 to 5.5
@rounded
If you want to add rounding to the value of a number key, you can use the @rounded
decorator.
; @define @ @key weight: number; // Round to 2 decimals places
Additionally, you can specify the rounding rule
to be used. This can either be off
, up
, or down
. The default rule is off
.
@define @ @key weight: number; @ @key height: number; @ @key rating: number; const corgi = ;corgiweight = 22365; // 22.37corgiheight = 1152; // 1.16corgirating = 4318; // 4.31
@enumerated
If you want to check if a key
's value is one of pre-defined values, you can use the @enumerated
decorator.
; @define @ // Can be an array of values @key status: string; @ // Can be a Map @key trainingLevel: number | string; const corgi = ;corgistatus = 'active'; // OKcorgistatus = 'deactivated'; // throws an Error corgitrainingLevel = 'basic'; // throws an Error corgitrainingLevel = 1; // OKconst trainingLevel = corgitrainingLevel; // returns 'basic';
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 = /** some configuration **/ ; @define /** shortened for brevity **/ serviceclasses;
The classes.register()
accepts a mapping of classes, so you can add several classes at once.
@define /** shortened for brevity **/ @define /** shortened for brevity **/ serviceclasses;
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.
; @define 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 authemail
: a valid email address that can be used for verificationpassword
: 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.
// Define Warp serviceconst service = /** some configuration **/ ; // Define UserUser /** shortened for brevity **/ // A middleware that maps req.user before reaching service.routerconst mapUser = { // We assume req.user is an object containing user details requser = requser; ;}; // Use the middlewarereq;
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 the class@define @key name: string; @key age: number; @key height: number; @key weight: number; @key awardsWon: number; @key owner: Person; get : number return thisweight / thisheight * thisheight; // Instantiate the classconst corgi = ;
We can set the values of its keys
using the properties we defined.
corginame = 'Bingo';corgiage = 5;corgiweight = 325;corgiowner = 5; // person with id `5`
It also validates if we provide wrong data types.
corgiweight = 'heavy'; // This will cause a validation error
Alternatively, we can define the values of keys
inside the object constructor.
const corgi = name: 'Bingo' age: 5 weight: 325 ;
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.
// The save method is a promise, so we can await itawait serviceclasses;
NOTE: Don't forget to
initialize()
before runningclasses.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.
// Prepare the objectconst corgi = ;corginame = 'Bingo';corgiage = 5;corgiweight = 325; // Save the objectawait serviceclasses; // Change a Keycorgiweight = 35; // Update the objectawait corgi;
Alternatively, if we know the id
of the row we want to update, use the withId()
method.
const daschund = DogwithId<Dog>25; // id is 25daschundname = 'Brownie'; // Update the objectawait serviceclasses;
Or simply pass the id inside the Class
constructor.
const shitzu = 16; // id is 16shitzuname = 'Fluffy'; // Update the objectawait serviceclasses;
Or pass the id
along with other keys
inside the constructor.
const beagle = id: 34 name: 'River' ; // Update the objectawait serviceclasses;
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.
// Increase awardsWon by 1corgi;
Conversely, if we want to decrease a number
key, we would use a negative value.
// Decrease the weight by 5.2corgi;
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@define /** other keys **/ @key preferences: object; // Create a new objectconst labrador = ;labradorpreferences = food: 'meat' ; // Save the objectawait serviceclasses; // Change preferenceslabrador; // Update the objectawait serviceclasses;
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.
labradorpreferences = toys: 'plushie' 'ball' ; // Save the objectawait serviceclasses; // Append a new toylabrador; // Update the objectawait serviceclasses;
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 = ;dog; await serviceclasses;
You can also use .setNewId()
for existing objects.
const dog = '123-456-789';dog; await serviceclasses; const id = dogid; // 789-012-345
IMPORTANT: Make sure to use
.setNewId()
for manually setting theid
. Setting theid
with the following methods will not work.
dogid = 3; // Will throw an error because id is readonly const corgi = 3; // Will assume `3` already exsists in the table so `.save()` will only update the row with id `3` instead of creating a new row const shitzu = id: 'abc-123' ; // Will assume `abc-123` already exists in the table so `.save()` will only update the row with id `abc-123` instead of creating a new row
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.
; @beforeSave { ifthisisNew this; }
Destroying an Object
If we want to delete an Object
, we can use the classes.destroy()
method.
await serviceclasses;
NOTE: In
Warp
, there is no hard delete, only soft deletes. Whenever an object is destroyed, it is preserved, but itsdeleted_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
.
; @define /** shortened for brevity */ // Define the queryconst dogQuery = Dog;
Once created, we can fetch the results via classes.find()
.
const dogs = await serviceclasses;
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.
// You can pass a single valuedogQuery; // You can also pass an arraydogQuery; // Or you can define multiple argumetnsdogQuery; // Or you can chain multiple select methodsdogQuery;
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;
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;
NOTE: If you are already using
select
, there is no need to useinclude
. You must put allkeys
insideselect
.
Defining Constraints
Constraints help filter the results of a query. In order to pass constraints, use any of the following methods.
// Prepare queryconst dogQuery = Dog; // Find an exact match for the specified keydogQuery;dogQuery; // If the key is ordinal (i.e. a string, a number or a date), you can use the following constraintsdogQuery;dogQuery;dogQuery;dogQuery; // If you need to check if a field is null or not nulldogQuery;dogQuery; // If you need to check if a given key is found in a list, you can use the following constraintsdogQuery;dogQuery;dogQuery; // If you need to check if a key contains a stringdogQuery;dogQuery;dogQuery; // If you need to check if a key contains several substringsdogQuery; // If you need to check if a key contains all substringsdogQuery;
TIP: Each constraint returns the query, so you can chain them, such as the following.
const dogQuery = Dog ;
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.
// Prepare subqueryconst urbanCityQuery = Location; // Prepare main queryconst dogQuery = Dog ; // Get dogsconst dogs = await serviceclasses;
If we want to see if a value exists in either of multiple queries, we can use .foundInEither()
.
// Prepare subqueriesconst urbanCityQuery = Location;const ruralCityQuery = Location; // Prepare main queryconst dogQuery = Dog ; // Get dogsconst dogs = await serviceclasses;
If we want to see if a value exists in all of the given queries, we can use .foundInAll()
.
// Prepare subqueriesvar urbanCityQuery = Location;var smallCityQuery = Location; // Prepare main queryvar dogQuery = Dog ; // Get dogsconst dogs = await serviceclasses;
Conversely, you can use .notFoundIn()
, .notFoundInEither()
, and .notFoundInAll()
to retrieve objects whose key is not found in the given subqueries.
Pagination
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; // Top 1000 results
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; // Top 10 results; Skip the first 20 results dogQuery; // Top 1000 resultsdogQuery; // Skip 1000 results
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; // Sorts the query by age, in ascending orderdogQuery; // You can also use an array to sort by multiple keys // You can also enter the keys as separate parametersdogQuery;
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.
// Prepare queryconst dogQuery = Dog; // Get dogsconst dogs = await serviceclasses; // Gets the total countconst total = dogslength;
Filtering Collections
To filter the results and return a new collection based on these filters, use the following methods.
// Returns the first Objectconst firstDog = dogs; // Returns the last Objectconst lastDog = dogs; // Returns a new collection of objects that return true for the given functionconst oldDogsOnly = dogs;
Manipulating Collections
To manipulate the results, use the following methods.
// Loops through each Object and applies the given functiondogs; // Returns an array of whatever the given function returnsconst names = dogs; // Loops through each Object and asynchronously executes every function one after the otherdogs; // Loops through each Object and asynchronously executes all functions in paralleldogsall serviceclasses; // Iterate through every Objectforconst dog of dogs console;
Converting Collections
Oftentimes, you may opt to use native data types to handle Objects. To accomodate this, Collections contain the following methods.
// Returns an array of Objectsconst dogArray = dogs; // Returns an array of object literalsconst dogJSON = dogs; // Returns a Map of Objects mapped by `id`const dogMappedById = dogs; // Returns a Map of Objects mapped by `name`const dogMappedByName = dogs; // Returns a Map of Objects mapped by `owner.id`const dogMappedByOwner = dogs;
TIP: Since some methods return new Collections, you can chain several methods together, as needed.
// Prepare queryconst dogQuery = Dog; // Get dogsconst dogs = await serviceclasses; // Find corgis, and return their namesconst firstCorgiNames = dogs ;
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 /** some keys **/ // You can validate input @beforeSave { ifthisage > 30 throw 'This dog is too old!'; } // You can change key values @beforeSave { thisweight = thisweight * 22; } // You can set default values @beforeSave { ifthisisNew // If you are creating a new Dog thisdescription = 'I am cute dog.'; } // You can update other Objects @beforeSave async { ifthisisNew const owner = thisowner; classes; await classes; } // You can check access @beforeSave async { if!thisisNew && thisownerid !== userid || !master throw '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 /** some keys **/ // Throwing an error will not stop the program @afterSave { ifthisage > 30 throw 'This will not stop the program'; } // You can save other Objects @afterSave async { ifthisisNew const pet = ; petdog = this; await classes; } // You can send a notification @afterSave async { SomeService; }
Before Destroy
To make sure a method is run before the class is destroyed, describe it with @beforeDestroy()
.
@define /** some keys **/ // You can validate input @beforeDestroy { ifthisage < 18 throw 'This dog is too young to destroy!'; } // You can change key values @beforeDestroy { thisstatus = 'removed'; } // You can update other Objects @beforeDestroy async { ifthisisNew const owner = thisowner; classes; await classes; } // You can check access @beforeDestroy async { if!thisisNew && thisownerid !== userid || !master throw '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 /** some keys **/ // You can update other Objects @afterDestroy async { ifthisisNew const petQuery = Pet ; const pet = classes; await pet; } // You can send a notification @afterDestroy async { SomeService; }
Before Find, First, Get
To make sure a method is run before the class is fetched, describe it with @beforeFind()
, @beforeFirst
, and @beforeGet
.
@define /** some keys **/ // You can limit the result @beforeFind { query; } // You can put additional constraints @beforeFirst { query; } // You can check access @beforeGet async { if!thisisNew && thisownerid !== userid || !master throw '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;; // Optional method static { return false; } async { // collection_id was passed to the request const collectionID = keyscollection_id; // Do some work here... const favoriteDogs = await ; // Throw an error instead of a result throw 'Cannot get your favorite dogs'; // Return the result 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()
.
// Add the GetFavoriteDogs functionservicefunctions; // Apply the router afterapp;
functions.register()
accepts a mapping of Functions
, so you can do the following.
// Add multiple functionsservicefunctions;