Defining Entities

To create a new type of data object in Nymph, you extend the Entity class. This is equivalent to creating a new table in a relational database. If you are going to use the class on the client side, you also need to create a corresponding client class. Below are two examples, one for Node.js, and one for the client. A more in depth explanation follows the examples.

Extending Entity in Node.js
import { Entity, Selector, nymphJoiProps } from '@nymphjs/nymph';
import { tilmeldJoiProps } from '@nymphjs/tilmeld';
import Joi from 'joi';

export type TodoData = {
  name?: string;
  done?: boolean;
};

export class Todo extends Entity<TodoData> {
  static ETYPE = 'todo';
  static class = 'Todo';

  protected $clientEnabledMethods = ['$archive'];
  protected $allowlistData? = ['name', 'done'];
  protected $protectedTags = ['archived'];
  protected $allowlistTags? = [];

  static async factory(guid?: string): Promise<Todo & TodoData> {
    return (await super.factory(guid)) as Todo & TodoData;
  }

  static factorySync(): Todo & TodoData {
    return super.factorySync() as Todo & TodoData;
  }

  constructor() {
    super();

    this.$data.name = '';
    this.$data.done = false;
  }

  async $archive() {
    if (this.$hasTag('archived')) {
      return true;
    }
    this.$addTag('archived');
    return await this.$save();
  }

  async $save() {
    if (!this.$nymph.tilmeld?.gatekeeper()) {
      // Only allow logged in users to save.
      throw new Error('You are not logged in.');
    }

    // Validate the entity's data.
    Joi.attempt(
      this.$getValidatable(),
      Joi.object().keys({
        ...nymphJoiProps,
        ...tilmeldJoiProps,

        name: Joi.string().trim(false).max(500, 'utf8').required(),
        done: Joi.boolean().required(),
      }),
      'Invalid Todo: '
    );

    // Check that this is not a duplicate Todo.
    const selector: Selector = {
      type: '&',
      equal: ['name', this.$data.name],
    };
    if (this.guid) {
      selector['!guid'] = this.guid;
    }
    if (
      await this.$nymph.getEntity(
        {
          class: this.$nymph.getEntityClass(Todo),
        },
        selector
      )
    ) {
      throw new Error('There is already a todo for that.');
    }

    return await super.$save();
  }
}

// Elsewhere, after initializing Nymph.
import { Todo as TodoClass } from './Todo.js';
const Todo = nymph.addEntityClass(TodoClass);
Extending Entity in the Client
import { Entity } from '@nymphjs/client';

export type TodoData = {
  name?: string;
  done?: boolean;
};

export class Todo extends Entity<TodoData> {
  // The name of the server class
  public static class = 'Todo';

  static async factory(guid?: string): Promise<Todo & TodoData> {
    return (await super.factory(guid)) as Todo & TodoData;
  }

  static factorySync(): Todo & TodoData {
    return super.factorySync() as Todo & TodoData;
  }

  constructor() {
    super();

    this.$data.name = '';
    this.$data.done = false;
  }

  async $archive(): Promise<boolean> {
    return await this.$serverCall('$archive', []);
  }
}

// Elsewhere, after initializing Nymph.
import { Todo as TodoClass } from './Todo.js';
const Todo = nymph.addEntityClass(TodoClass);

In both cases, defaults are set in the constructor (the done property is set to false and the name property is set to an empty string). You can see that from within the methods of an entity, the entity's data (other than guid, cdate, mdate, and tags) are accessed from this.$data. The $data part is not necessary outside of the entity's own methods.

You'll also notice that when using Nymph from within an entity's methods, there is an instance of Nymph available in this.$nymph (or this.nymph in static methods). In Node.js, these instances will know which user is logged in and add appropriate permission checks, and will maintain a persistent DB connection during a transaction. On the client, these instances will know how to communicate with the configured REST server. Basically, you have to use these instances. You can also use this.$nymph.getEntityClass and this.nymph.getEntityClass to get the right class for Nymph queries.

In Node.js, the etype is set to "todo". The etype of an entity determines which table(s) the entity will be placed in. When you search for an entity, you give Nymph a class. Nymph will use that class' etype to determine where to search for entities. If you don't provide a class, the Entity class and the "entity" etype will be used.

The $clientEnabledMethods property and the clientEnabledStaticMethods static property in Node.js determine which methods and static methods can be called from the client using $serverCall and serverCallStatic. In the client class, the return await this.$serverCall('$archive', []); statement takes advantage of this feature.

On both the Node.js class and the client class, the class name is set in the class static property. This class name should match on each side. It is how Nymph maps the client class to the Node.js class and vice versa.

Finally, in Node.js, the Todo class validates all of its data in the $save method using Joi. Without this validation, a malicious user could send invalid data types or even megabytes worth of data in an entity. Any validation library should support validation in Nymph using the $getValidatable method. The $allowlistData property will ensure no extra properties are set.

Previous: Entity Class Next: UIDs