This is part three of our series on scripting with AcceleratXR. In part two we discussed how to create new custom REST API endpoints. In part three we will build upon that to create a REST API that serves as a RESTful interface to a custom data structure. Go ahead and open up your test workspace in Visual Studio Code and let’s get started.
In AcceleratXR, data storage is managed using a strictly typed system of classes that are stored in a database using an Object-Relational Mapping layer (TypeORM). This makes it possible to easily switch between database types based on the different needs of the data structure and service. In fact, it’s possible to have two classes be stored in two entirely different databases within the same service. The ORM framework supports popular SQL databases as well as NoSQL such as MongoDB. Most services in the AcceleratXR platform have been written to use MongoDB. We will continue that tradition for today’s tutorial.
Let’s imagine that we’re building an RPG in which players can create unique characters (let’s also imagine that AcceleratXR doesn’t already have a system for this). Each character needs certain attributes stored that matter to our gameplay. Things like health, mana, current equipment and an inventory of items. We’ll start by creating the Character
class.
First create a new file called Character.ts
. Like routes
, it’s recommended practice to place your data structure classes in the models
sub-folder.
Now paste the following contents into your file.
constructor(other?: any) {
The first thing you may notice about this class is that inherits from BaseMongoEntity
. The BaseMongoEntity
class provides basic information for data that is to be stored in a MongoDB instance. This includes properties such as uid
, dataCreated
, dateModifed
and version
. The uid
property uniquely identifies the object across all others in the database. The date fields should be self explanatory and the version
field is used for optimistic locking. Note that if you were writing for a SQL database you would use the BaseEntity
class instead.
You may also notice the three decorators at the top of the class; @Cache
, @Entity
and @Model
. These are all very important as they tell the server what you intend to do with this class. The @Entity
decorator is used to indicate that instances of this class will be stored in a database managed by TypeORM. The @Model
decorator is used to identify which database connection that instances of the class should be bound to. In our example, instances of the Character
class are bound to the mongodb
database connection. Finally, the @Cache
decorator tells the server that queries to this class should be cached. Caching speeds up searches for data by storing the results of frequently requested queries in memory. This is also commonly referred to as a Second-Level Cache.
The contents of the class is pretty straight forward. You’ll notice that it is essentially a struct
with a simple copy constructor. Each property that will be stored in the database is decorated with @Column
. This again tells TypeORM what and how to store instances of the object in the database. The name
property also has the @Identifier
and @Index
decorators. These tell the server that the name
property should be indexed in the database (to further speed up queries) and that the property is also a unique identifier (more on this later).
The copy constructor is very simple, it takes an object of type any
and then selectively copies all existing values corresponding to properties defined in the class. This serves two purposes. First, it makes converting any arbitrary object to our data type easy. Second, it provides an automatic filtering mechanism, getting rid of any properties we don’t actually care about.
Now that we’ve created our data structure let’s look at how to write a route handler that will allow us to manage it. Create a new file routes/CharacterRoute.ts
and paste the following contents.
You may immediately notice a couple things that are different from the route handlers we wrote in part two. First, there is a @Model(Character)
decorator in addition to the @Route
decorator. This decorator is used to tell the server that this route handler is responsible for managing instances of the Character
class. The second thing you may notice is that the class inherits from ModelRoute
. The ModelRoute
class is a special base class containing built-in behaviors and utilities for working with data structures. Everything from the basic business logic to handle CRUD operations to automatic permission checking and second level caching is included in this base class. The last major thing you should notice about this route handler is the repo
property. The repo
property is a reference to the storage interface of the database. The @Repository(Character)
decorator tells the server to automatically inject this reference at server start up.
There’s also a new function in this route handler class called getDefaultACL
. This function is used to define the base permissions that govern user access to all class level operations (create
, count
, findAll
and truncate
). The defaultACL
is also used to establish per-document permissions when objects are created using the create
operation. In this example, we are setting the default permissions to allow any logged in user with the ability to create or read records and deny unauthenticated users from performing any operations.
The remainder of the route handler functions follow the common CRUD pattern. The first function, findAll
is a search function that accepts query parameters and performs a search against the database for all objects matching the specified criteria. This is easily accomplished by simply calling the doFindAll
function in the base class. There are a variety of built-in functions available in the base class described here. The count
built-in, for instance, returns the number of results matching the given search criteria.
The create
function is a bit different. Here we first create a new Character object using the data that was passed in by the client. This provides that automatic filtering mentioned before. Then we automatically fill in the userUid
property with the authenticated user’s if not already provided (for convenience).
When we defined our default permissions above you’ll notice that we allow any authenticated user to create an object. This means that anyone can create a valid Character
record with any userUid
association. However, since we’re dealing with player characters we want to prevent a player from creating a Character for another player’s account. So we test to make sure that the userUid
in the provided character object matches that of the authenticated user. The only exception to this rule is platform super users (such as admins or moderators). We test this by checking to see if the authenticated user has a trusted role
.
Finally, we call super.doCreate
, passing in the new character object to create as well as the authenticated user. By default the base class will automatically create permissions for the object, inheriting from the default permissions defined above. The authenticated user will also be set with full access for the object as its owner. It is possible to override this behavior by setting the third argument to a custom acl object.
The remaining functions aren’t too complicated as they simply call their corresponding built-in function. The built-in function will automatically take care of all the dirty work when it comes to retrieving objects from the database, validating permissions, caching data and so on.
You may notice that the doUpdate
and doFindById
handler functions take an id
path parameter as the first argument. If you recall from our data structure above we marked certain fields with the @Identifier
decorator. When searching for an existing object the system uses these properties to build a special query. This makes it possible to retrieve a character object by either it’s uid
or it’s name
property all from the same REST API and without requiring multiple lookups.
Go ahead and try for yourself. See what new kinds of REST APIs you can create for your custom data structures. One last thing to note, the order of the function definition does matter. For example, we have count
with the path /characters/count
in addition to the delete
, findById
and update
functions that each have the path /characters/:id
. If the count
function were defined at the bottom of the class it wouldn’t work. This is because the :id
parameter in the path is a wildcard and so it’ll assume that a request to /characters/count
is really a search for a character named count
. This is due to the way the underlying web server processes incoming web requests (ExpressJS). So make sure that you define your functions carefully.
In next week’s part four we’ll discuss how to create a background job to create automatic time based services.
Stay tuned!
One comment