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.
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);
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.