Reference - Server-Side Events
Server events are triggered in the generated REST API server. For each of these, you can write an event handler in JavaScript - this custom function can:
- perform validation on incoming “create” or “update” domain objects
- veto the event, e.g. prevent a login occurring
- modify the event data, e.g. change or remove a field in the response object, or add new “derived” fields
- check that a user is allowed to access (or modify) the data they’re requesting
- log the event on a message queue
- do pretty much anything else you need it to
To define an event, add it to the relevant domain class as an Operation. The operation name may follow the naming convention described near the end of this page.
Where the event handlers are placed in the generated code
Currently these event handlers are treated as “API layer” events, and so are placed in the respective domain-class API module (e.g. “AccountApi.js”). However, please note that they’ll soon be moved to the application layer, as they’re really just another place to define and enforce application logic/business rules (validation, etc). This shouldn’t affect how they’re written, but it does mean that they’ll also be available in the separate “Application” codebase - see the application
folder in the generated zipfile.
“Request-triggered” event types
The majority of server events are triggered by an incoming request.
For each such request, the following types of event are triggered, and can be “caught” by an event handler:
Event type | When called… | Example uses |
---|---|---|
on ... | Just before the requested operation happens. This allows the event itself to be vetoed before it happens. | Rules-based data validation that goes beyond the built-in attribute type checking; e.g. validate an incoming ‘create’ object, and veto the request if the object validation fails. |
on ... success | Just after an operation has successfully completed, before the response is sent back to the client. | Modify the response object in some way, e.g. rename, add or remove fields; additional logging; post the result to a message queue. |
on ... failure | Just after an operation failed in some way, before the response is sent back to the client. | Modify the error message; additional error logging; trigger a “rainy day” execution flow in the case of a particular error. |
“Request-triggered” events
The following server-side events are triggered by incoming API requests:
API endpoint, Events | Notes |
---|---|
POST /count:on count on count success on count failure | Returns the number of items in this domain that match the query provided in the request. |
POST :on create on create success on create failure | Creates a new domain object/record. |
DELETE :on delete file on delete file success on delete file failure | Deletes a file that belongs to a domain object and attribute. |
DELETE :on delete many on delete many success on delete many failure | Deletes any items in this domain that match the query provided in the request. |
DELETE :on delete one on delete one success on delete one failure | Deletes a single item in this domain if it matches the ID provided in the request. |
GET :on download file on download file success on download file failure | Downloads a file that belongs to a domain object and attribute. |
POST /find:on find many on find many success on find many failure | Returns all items in this domain that match the query provided in the request body. |
GET :on find one on find one success on find one failure | Returns up to 1 item in this domain that matches the query provided in the request body. |
GET :on get many on get many success on get many failure | Returns all items in this domain that match the query provided in the request query parameters. When multiple records are returned, the event handler is called once for each record. A ‘veto’ will prevent the whole result-set from being sent. |
POST :on login on login success on login failure | Authenticates the user via username/password in the request body. |
GET :on read one on read one success on read one failure | Returns up to 1 item in this domain that matches the ID provided in the request path. |
POST :on register on register success on register failure | Creates a new user record, or ‘identity’. The password is hashed before being written to the database. |
PUT :on replace on replace success on replace failure | Replaces the whole record that matches the ID provided in the request path, given a JSON object in the request body. The JSON object doesn’t need to include the ID itself. |
PUT :on replace file on replace file success on replace file failure | Uploads a replacement file that belongs to a domain object and attribute. |
PATCH :on update on update success on update failure | Updates (patches) individual fields in the record that matches the ID provided in the request path. The fields to patch are provided as a JSON object in the request body. |
POST :on upload file on upload file success on upload file failure | Uploads a new file (binary or otherwise) that belongs to a domain object and attribute. |
/task/…. on (task) on (task) success on (task) failure | Given a custom task-based API endpoint, these server events are triggered and can be handled just like the built-in CRUD API events. |
Other server events
The above events are triggered by incoming API requests. Additionally, a small number of “non-request” server events can be listened for:
Event | Notes |
---|---|
on start | Called once when the domain API starts up. This may be used, for example, to call the associated DAO to create “bootstrap” data. |
_init | Allows variables or resources to be initialised at the “module scope” of the domain API. _init code shouldn’t be wrapped in an arrow function; instead the block of code is placed and run directly at the top of the module. This allows variables to be declared that can then be accessed by other functions in the same module. Use with care! |
If the operation involves multiple records, the event will be called once for each record (except for on read many
, which is called once with the complete record-set). A ‘veto’ will prevent the operation completing for the whole record-set.
Naming
In the above tables, we’ve used “lower sentence case” for each event/Operation name. We recommend this convention is used in your own domain model, as it retains a “business specification” feel to the domain model - plus it’s just nicer to read.
In the JavaScript-based Express REST API, each Operation name is morphed into “lowerCamelCase”.
Function arguments
Although some input arguments aren’t relevant for all events, we’ve tried to keep the argument ordering consistent:
async (entityName, loggedInUser, directive) => {}
where entityName
is the domain class name in lowerCamelCase, e.g. account
, hotelReview
or badDriverReport
.
(entityName)
is the data object, either incoming (for write operations such ason create
andon replace
), or outgoing (on read
etc).loggedInUser
is, as you’d expect, the data object loaded from the DB for the logged-in user identified by their JWT token.directive
allows you to have some additional control over the response. Thedirective
object is placed directly in the response JSON, so you can add custom fields which your own client code can then read.
directive
is useful particularly for UX, to direct the user to a different page.
There are some exceptions to the above function arguments, as follows.
onReadMany
function arguments
The onReadMany
function arguments are as follows:
async (data, loggedInUser, directive) => {}
data
is an object containing a single field calleditems
- this being an array of all the entity items that have been read.loggedInUser
- as above.directive
- as above.
This structure enables the array to be more easily filtered or mapped-over. Just ensure the data.items
reference is updated with the new array.
For example:
data.items = data.items.filter(review => review.rating == 5);
onDeleteMany
function arguments
The onDeleteMany
function arguments are as follows:
async (query, loggedInUser, directive) => {}
Because this event handler is run prior to the delete operation taking place, and no “read” is performed first, no “data” object or array is passed in.
Instead, the first arg is instead called query
, and contains a JavaScript object containing the attribute names and values that will be used in the delete operation.
If you did want to load the same records to check them before deletion, you could do this in the
onDeleteMany
event handler, by calling the relevant DAO function directly.
The ‘veto’ object
The same ‘vetoable’ mechanism is used consistently for all event types. So, to veto any event - i.e. prevent it from occurring - return one of the following from the event handler function:
Return a String containing a ‘reason’ message
(invoice, loggedInUser) => {
return "All invoices are being rejected today.";
}
The message will be returned from the API, as a JSON object:
{
"message": "All invoices are being rejected today."
}
The message field can either be a string (as above) or an array of strings, if multiple records were vetoed (see “Combining veto responses”, below).
The response will have a 400
status code.
Return a JSON object
To have more control over the veto response, return a JSON object from the event handler, with the following structure:
(invoice, loggedInUser) => {
return {
msg: "All invoices are being rejected today.",
status: 500
};
}
The message will be returned as a JSON object (as in the previous example), with a 500
response.
Combining veto responses
In the case where an operation involves multiple records (therefore potentially multiple vetoes), the vetoes will be filtered to remove duplicates, then returned in a string array:
{
"message": [
"First rejection reason",
"Second rejection reason"
]
}
Additionally, the first occurring status code will be used, or the default 400
if none was specified.