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(guid?: string): Todo & TodoData {
    return super.factorySync(guid) as Todo & TodoData;
  }

  constructor(guid?: string) {
    super(guid);

    if (this.guid == null) {
      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).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.constructor as typeof Todo,
        },
        selector
      )
    ) {
      throw new Error('There is already a todo for that.');
    }

    return await super.$save();
  }
}

// Elsewhere, after initializing Nymph.
nymph.addEntityClass(Todo);
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(guid?: string) {
    super(guid);

    if (guid == null) {
      this.$data.name = '';
      this.$data.done = false;
    }
  }

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

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

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

// Elsewhere, after initializing Nymph.
nymph.addEntityClass(Todo);

In both cases, defaults are set in the constructor. In this case, 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). Using this instance is especially important in Node.js for Nymph transactions and Tilmeld authentication. 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, it is less important to use these instances, unless you run multiple instances of the Nymph client in your app.

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 each 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.