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, 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).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.constructor as typeof 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);
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.
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.
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.