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,
  EntityUniqueConstraintError,
  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? = [];

  constructor() {
    super();

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

  async $getUniques() {
    // Make sure this isn't a duplicate Todo for this user.
    return [`${this.$data.user.guid}:${this.$data.name}`];
  }

  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: '
    );

    try {
      return await super.$save();
    } catch (e: any) {
      if (e instanceof EntityUniqueConstraintError) {
        throw new Error('There is already a todo for that.');
      }
      throw e;
    }
  }
}

// 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';

  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.

Nymph provides a mechanism to ensure uniqueness among entities. Any strings returned by the $getUniques method will have a uniqueness constraint enforced by the database across this entity's etype. The Todo class returns a string containing both the user's GUID and the todo name. This ensures that the user can't have two todos with the same name. The $save method checks for a thrown EntityUniqueConstraintError when calling the super class' $save.

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