Live Scripting - Pt3 - Data Structures#

Managing Data Structures#

Welcome to the third part of our series on scripting with Xsolla Backend. In part two, we discussed how to create new custom REST API endpoints. In this part, we will build upon that to create a REST API that serves as a RESTful interface to a custom data structure.

Postman Collection#

For convenience, the Postman collection used in this article is available here: Live Scripting Postman Collection. To configure for your custom cluster, duplicate the Demo environment and update the base_url, auth_username, and auth_password variables to match your Xsolla Backend cluster, and then make sure you set your environment active.

Scripts and Samples#

Getting Started#

Open your test workspace in Visual Studio Code, and let’s get started.

In Xsolla Backend, data storage is managed using a strictly typed system of classes stored in a database via an Object-Relational Mapping (ORM) layer, specifically TypeORM. This ORM system allows for easy switching between database types based on the structure and service needs. Most services in the Xsolla Backend ecosystem use MongoDB, which we will also use for today’s tutorial.

Let’s imagine we’re building an RPG where players can create unique characters. Each character will need attributes like health, mana, equipment, and inventory. We’ll start by creating the Character class.

Step 1: Creating the Character Class#

  1. Create a new file called Character.ts and place it in the models folder.

  2. Add the following content to define the Character class:

import { Column, Entity, Index } from "typeorm";
import { BaseMongoEntity, ModelDecorators } from "@composer-js/service-core";
import { AXRModelDecorators } from "@acceleratxr/service-core";
const { Cache, Identifier, Model } = ModelDecorators;
const { Protect } = AXRModelDecorators;

/**
 * An `Character` is a unique character of a user within the system. 
 * Users can have multiple characters per account and the character
 * can have associated data such as: inventory, progress, achievements.
 */
@Entity()
@Cache()
@Model("user")
@Protect({
    uid: "Character",
    records: [
        {
            userOrRoleId: "anonymous",
            create: false,
            read: false,
            update: false,
            delete: false,
            special: false,
            full: false,
        },
        {
            userOrRoleId: ".*",
            create: true,
            read: true,
            update: false,
            delete: false,
            special: false,
            full: false,
        },
    ],
})
export default class Character extends BaseMongoEntity {
    /**
     * The universally unique identifier of the user that the character
     * belongs to.
     */
    @Column()
    @Index()
    public userUid: string = "";

    /**
     * The unique name of the character.
     */
    @Identifier
    @Index()
    @Column()
    public name: string = "";

    /**
     * A biographical description of the character.
     */
    @Column()
    public biography: string = "";

    /**
     * The amount of health that the character currently has.
     */
    @Column()
    public health: number = 100;

    /**
     * The amount of mana that the character currently has.
     */
    @Column()
    public mana: number = 100;

    /**
     * A map of the current items the character has equipped. The key is 
     * the slot name, the value is the uid to the item that is equipped.
     */
    @Column()
    public equipment: Map<string, string> = new Map();

    /**
     * A list all items currently in the character’s possession.
     */
    @Column()
    public inventory: Array<string> = [];

    /**
     * An arbitrary map of key-value pairs containing the characteristics
     * of the character.
     */
    @Column()
    public attributes: any = undefined;

    constructor(other?: any) {
        super(other);

        if (other) {
            this.userUid = other.userUid ? other.userUid : this.userUid;
            this.name = other.name ? other.name : this.name;
            this.biography = other.biography ? other.biography : this.biography;
            this.health = other.health ? other.health : this.health;
            this.mana = other.mana ? other.mana : this.mana;
            this.equipment = other.equipment ? other.equipment : this.biography;
            this.inventory = other.inventory ? other.inventory : this.inventory;
            this.attributes = other.attributes ? other.attributes : this.attributes;
        }
    }
}

Understanding the Character Class#

  • The class extends BaseMongoEntity, which provides core fields such as uid, dateCreated, and version.

  • Decorators like @Entity, @Model, and @Cache are used to define this class as a database entity, bind it to the MongoDB database, and enable caching.

  • Each property is marked with @Column, which tells TypeORM how to store the data in the database.

Now that we’ve created the data structure, let’s write a route handler to manage it.

Step 2: Creating the Character Route#

  1. Create a new file called CharacterRoute.ts in the routes folder.

  2. Add the following content to define the route handler:

import { MongoRepository as Repo } from "typeorm";
import { Request as XRequest, Response as XResponse } from "express";
import { AXRModelRoute } from "@acceleratxr/service-core";
import { JWTUser } from "@composer-js/core";
import { DatabaseDecorators, ObjectDecorators, RouteDecorators } from "@composer-js/service-core";
const { MongoRepository } = DatabaseDecorators;
const { Config, Logger } = ObjectDecorators;
const {
    Auth,
    Head,
    Model,
    Param,
    Query,
    Route,
    Get,
    Post,
    Put,
    Delete,
    Request,
    Response,
    User: AuthUser,
} = RouteDecorators;

import Character from "../models/Character";

/**
 * Handles all REST API requests for the endpoint `/characters`.
 */
@Model(Character)
@Route("/characters")
export default class CharacterRoute extends AXRModelRoute<Character> {
    @Config()
    protected config: any;

    @Logger
    protected logger: any;

    @MongoRepository(Character)
    protected repo?: Repo<Character>;

    /**
     * Initializes a new instance with the specified defaults.
     */
    constructor() {
        super();
    }

    /**
     * Returns the count of characters based on the given criteria.
     */
    @Head()
    private count(
        @Param() params: any,
        @Query() query: any,
        @Response res: XResponse,
        @AuthUser user?: JWTUser
    ): Promise<any> {
        return super.doCount({ params, query, recordEvent: true, res, user });
    }

    /**
     * Returns all characters from the system that the user has access
     * to based upon the given criteria.
     */
    @Get()
    private findAll(@Param() params: any, @Query() query: any, @AuthUser user?: JWTUser): Promise<Array<Character>> {
        return super.doFindAll({ params, query, user });
    }

    /**
     * Creates a new character.
     */
    @Auth(["jwt"])
    @Post()
    private async create(
        obj: Character,
        @Request req: XRequest,
        @AuthUser user?: JWTUser
    ): Promise<Character | Character[]> {
        return super.doCreate(obj, { req, user });
    }

    /**
     * Deletes all characters from the service.
     */
    @Auth(["jwt"])
    @Delete()
    private truncate(
        @Param() params: any,
        @Query() query: any,
        @Request req: XRequest,
        @AuthUser user?: JWTUser
    ): Promise<void> {
        return super.doTruncate({ params, query, recordEvent: true, req, user });
    }

    /**
     * Returns a single character from the system that the user has access to.
     */
    @Get("/:id")
    private findById(
        @Param("id") id: string,
        @Query() query: any,
        @AuthUser user?: JWTUser
    ): Promise<Character | undefined> {
        return super.doFindById(id, { query, user });
    }

    /**
     * Updates a single character.
     */
    @Auth(["jwt"])
    @Put("/:id")
    private update(
        @Param("id") id: string,
        obj: Character,
        @Request req: XRequest,
        @AuthUser user?: JWTUser
    ): Promise<Character> {
        return super.doUpdate(id, obj, { recordEvent: true, req, user });
    }

    /**
     * Deletes the skill from the service.
     */
    @Auth(["jwt"])
    @Delete("/:id")
    private delete(@Param("id") id: string, @Request req: XRequest, @AuthUser user?: JWTUser): Promise<void> {
        return super.doDelete(id, { recordEvent: true, req, user });
    }
}

Understanding the Character Route#

  • @Model(Character): This decorator binds the route handler to the Character model.

  • The class extends AXRModelRoute, a base class that provides built-in CRUD operations.

  • Functions like findAll, create, update, and delete use built-in functions from the base class to handle database interactions.

Commit and Publish Changes#

  • Commit and publish the changes to make the new scripts live, then wait for the server to restart:

chracter-publish

Step 3: Testing and Exploring#

With the Character model and CharacterRoute route handler set up, you can now perform CRUD operations via the REST API:

  • GET /characters Retrieve all characters.

  • HEAD /characters Retrieve the number of character records.

  • POST /characters Create a new character.

  • GET /characters/:id Retrieve a character by ID.

  • PUT /characters/:id Update a character by ID.

  • DELETE /characters/:id Delete a character by ID.

To test thse operations, you can use the Postman collection provided above, starting with the Post Characters Route, which has sample dataset above supplied in the body, which creates four new characters:

[
  {
    "name": "Thorin Oakshield",
    "biography": "A brave dwarf warrior seeking to reclaim his homeland.",
    "inventory": ["Healing Potion", "Rope", "Torch"],
    // ...
  },
  {
    "name": "Elara Moonshadow",
    "biography": "An elven mage with a deep connection to nature.",
    "inventory": ["Spellbook", "Herb Pouch", "Crystal Ball"],
    // ...
  },
  {
    "name": "Garrett Swiftblade",
    "biography": "A cunning thief known for his agility and wit.",
    "inventory": ["Lockpicks", "Smoke Bomb", "Throwing Knives"],
    // ...
  },
  {
    "name": "Lyra Windrunner",
    "biography": "A noble ranger who protects the forest and its creatures.",
    "inventory": ["Arrows", "Rations", "Hunting Trap"],
    // ...
  }
]

Create Characters, Get Count, and List Characters#

First let’s try creating a set of characters, then retrieve the count and list:

  1. Run the Post Characters Route request to create four new characters. Verify the response body to ensure the characters were created successfully.

  2. Run the Count Characters Count Route request to retrieve the number of characters. Verify the response header Content-Length to ensure the count is accurate.

  3. Run the Get Characters Route request to retrieve all characters. Verify the response body to ensure all characters are returned.

character-create-count-get

Get and Update Character by Identifier#

Next, let’s try retrieving a character by an identifier and updating it by changing its inventory. Because the character name field is one of its unique identifier properties, you can use the character’s name as the id path parameter to perform the document lookup:

  1. Run the Find Character By ID request to fetch the character record for characters/Garrett Swiftblade. We store the uid from this first response to a variable called found_character_uid to use later.

The original character inventory should look like this:

"inventory": [
  "Lockpicks",
  "Smoke Bomb",
  "Throwing Knives"
],

character-by-id

  1. Now let’s update the character’s inventory by running the Update Character By ID request to and supplying the character’s version, uid, and updated inventory in the body of the update request:

{
  "uid": "{{found_character_uid}}",
  "version": 0,
  "inventory": [
    "Lock Parts",
    "Stink Bomb",
    "Hucking Sporks"
  ]
}

character-update

Delete Character by Identifier, and Delete All#

Finally, let’s delete a character by its identifier using the name field, check the count to ensure the character was removed, and then delete all characters:

  1. Run the Delete Character By ID request to delete the character Elara Moonshadow. Verify the response status is 204.

  2. Run the Count Characters Count Route request to retrieve the number of characters. Verify the response header Content-Length to ensure the count is accurate, it should now be 3.

  3. Run the Delete All Characters Route request to delete all characters. Verify the response status is 204.

  4. Run the Count Characters Count Route request once again to check that the count is now 0.

character-delete

Conclusion#

In this part, we explored how to create and manage custom data structures in Xsolla Backend using TypeORM and the MongoDB database. You now know how to:

  • Define a data structure using the Character class.

  • Create a REST API with CRUD operations for managing that data structure.

In the next part of the series, we’ll discuss how to create background jobs to handle scheduled tasks and other automated processes.