@nymphjs/nymph
Version:
Nymph.js - Nymph ORM
1,343 lines (1,296 loc) • 55.1 kB
text/typescript
import { Config, ConfigDefaults as defaults } from './conf/index.js';
import type { NymphDriver } from './driver/index.js';
import Entity, {
type EntityInstanceType,
type EntityObjectType,
} from './Entity.js';
import type { EntityConstructor, EntityInterface } from './Entity.types.js';
import {
ClassNotAvailableError,
InvalidParametersError,
} from './errors/index.js';
import type {
Selector,
Options,
NymphConnectCallback,
NymphDisconnectCallback,
NymphBeforeGetEntityCallback,
NymphBeforeGetEntitiesCallback,
NymphBeforeSaveEntityCallback,
NymphAfterSaveEntityCallback,
NymphFailedSaveEntityCallback,
NymphBeforeDeleteEntityCallback,
NymphAfterDeleteEntityCallback,
NymphFailedDeleteEntityCallback,
NymphBeforeDeleteEntityByIDCallback,
NymphAfterDeleteEntityByIDCallback,
NymphFailedDeleteEntityByIDCallback,
NymphBeforeNewUIDCallback,
NymphAfterNewUIDCallback,
NymphFailedNewUIDCallback,
NymphBeforeSetUIDCallback,
NymphAfterSetUIDCallback,
NymphFailedSetUIDCallback,
NymphBeforeRenameUIDCallback,
NymphAfterRenameUIDCallback,
NymphFailedRenameUIDCallback,
NymphBeforeDeleteUIDCallback,
NymphAfterDeleteUIDCallback,
NymphFailedDeleteUIDCallback,
NymphBeforeStartTransactionCallback,
NymphAfterStartTransactionCallback,
NymphBeforeCommitTransactionCallback,
NymphAfterCommitTransactionCallback,
NymphBeforeRollbackTransactionCallback,
NymphAfterRollbackTransactionCallback,
NymphEventType,
NymphQueryCallback,
FormattedSelector,
TilmeldInterface,
} from './Nymph.types.js';
/**
* An object relational mapper for Node.js.
*
* Written by Hunter Perrin for SciActive.
*
* @author Hunter Perrin <hperrin@gmail.com>
* @copyright SciActive Inc
* @see http://nymph.io/
*/
export default class Nymph {
/**
* The Nymph config.
*/
public config: Config;
/**
* The Nymph instance that this one was cloned from, or null if it's not a
* clone.
*/
public parent: Nymph | null;
/**
* The Nymph database driver.
*/
public driver: NymphDriver;
/**
* An optional Tilmeld user/group manager instance.
*/
public tilmeld?: TilmeldInterface = undefined;
/**
* A simple map of names to Entity classes.
*/
private entityClasses: { [k: string]: EntityConstructor } = {};
/**
* The entity class for this instance of Nymph.
*/
public Entity: typeof Entity;
private connectCallbacks: NymphConnectCallback[] = [];
private disconnectCallbacks: NymphDisconnectCallback[] = [];
private queryCallbacks: NymphQueryCallback[] = [];
private beforeGetEntityCallbacks: NymphBeforeGetEntityCallback[] = [];
private beforeGetEntitiesCallbacks: NymphBeforeGetEntitiesCallback[] = [];
private beforeSaveEntityCallbacks: NymphBeforeSaveEntityCallback[] = [];
private afterSaveEntityCallbacks: NymphAfterSaveEntityCallback[] = [];
private failedSaveEntityCallbacks: NymphFailedSaveEntityCallback[] = [];
private beforeDeleteEntityCallbacks: NymphBeforeDeleteEntityCallback[] = [];
private afterDeleteEntityCallbacks: NymphAfterDeleteEntityCallback[] = [];
private failedDeleteEntityCallbacks: NymphFailedDeleteEntityCallback[] = [];
private beforeDeleteEntityByIDCallbacks: NymphBeforeDeleteEntityByIDCallback[] =
[];
private afterDeleteEntityByIDCallbacks: NymphAfterDeleteEntityByIDCallback[] =
[];
private failedDeleteEntityByIDCallbacks: NymphFailedDeleteEntityByIDCallback[] =
[];
private beforeNewUIDCallbacks: NymphBeforeNewUIDCallback[] = [];
private afterNewUIDCallbacks: NymphAfterNewUIDCallback[] = [];
private failedNewUIDCallbacks: NymphFailedNewUIDCallback[] = [];
private beforeSetUIDCallbacks: NymphBeforeSetUIDCallback[] = [];
private afterSetUIDCallbacks: NymphAfterSetUIDCallback[] = [];
private failedSetUIDCallbacks: NymphFailedSetUIDCallback[] = [];
private beforeRenameUIDCallbacks: NymphBeforeRenameUIDCallback[] = [];
private afterRenameUIDCallbacks: NymphAfterRenameUIDCallback[] = [];
private failedRenameUIDCallbacks: NymphFailedRenameUIDCallback[] = [];
private beforeDeleteUIDCallbacks: NymphBeforeDeleteUIDCallback[] = [];
private afterDeleteUIDCallbacks: NymphAfterDeleteUIDCallback[] = [];
private failedDeleteUIDCallbacks: NymphFailedDeleteUIDCallback[] = [];
private beforeStartTransactionCallbacks: NymphBeforeStartTransactionCallback[] =
[];
private afterStartTransactionCallbacks: NymphAfterStartTransactionCallback[] =
[];
private beforeCommitTransactionCallbacks: NymphBeforeCommitTransactionCallback[] =
[];
private afterCommitTransactionCallbacks: NymphAfterCommitTransactionCallback[] =
[];
private beforeRollbackTransactionCallbacks: NymphBeforeRollbackTransactionCallback[] =
[];
private afterRollbackTransactionCallbacks: NymphAfterRollbackTransactionCallback[] =
[];
/**
* Initialize Nymph.
*
* @param config The Nymph configuration.
* @param driver The Nymph database driver.
* @param tilmeld The Tilmeld user/group manager instance, if you want to use it.
* @param parent Used internally by Nymph. Don't set this.
*/
public constructor(
config: Partial<Config>,
driver: NymphDriver,
tilmeld?: TilmeldInterface,
parent?: Nymph,
) {
this.config = { ...defaults, ...config };
this.driver = driver;
this.parent = parent ?? null;
this.Entity = this.addEntityClass(Entity);
if (parent) {
for (const name in parent.entityClasses) {
if (
name === 'Entity' ||
(parent.entityClasses[name] as any).skipOnClone
) {
continue;
}
this.addEntityClass(parent.entityClasses[name]);
}
const events = [
'connect',
'disconnect',
'query',
'beforeGetEntity',
'beforeGetEntities',
'beforeSaveEntity',
'afterSaveEntity',
'failedSaveEntity',
'beforeDeleteEntity',
'afterDeleteEntity',
'failedDeleteEntity',
'beforeDeleteEntityByID',
'afterDeleteEntityByID',
'failedDeleteEntityByID',
'beforeNewUID',
'afterNewUID',
'failedNewUID',
'beforeSetUID',
'afterSetUID',
'failedSetUID',
'beforeRenameUID',
'afterRenameUID',
'failedRenameUID',
'beforeDeleteUID',
'afterDeleteUID',
'failedDeleteUID',
'beforeStartTransaction',
'afterStartTransaction',
'beforeCommitTransaction',
'afterCommitTransaction',
'beforeRollbackTransaction',
'afterRollbackTransaction',
];
for (let event of events) {
const prop = event + 'Callbacks';
// @ts-ignore: The callback should be the right type here.
const callbacks = parent[prop];
for (let callback of callbacks) {
if (callback.skipOnClone) {
continue;
}
this.on(event as NymphEventType, callback);
}
}
}
this.config.debugInfo('nymph', 'Nymph loaded.');
if (tilmeld != null) {
this.tilmeld = tilmeld;
}
this.driver.init(this);
this.config.debugInfo('nymph', 'Nymph driver loaded.');
if (this.tilmeld) {
this.tilmeld.init(this);
this.config.debugInfo('nymph', 'Tilmeld loaded.');
}
}
/**
* Add your class to this instance.
*
* This will create a class that extends your class within this instance of
* Nymph and return it. You can then use this class's constructor and methods,
* which will use this instance of Nymph.
*
* Because this creates a subclass, don't use the class
* returned from `getEntityClass` to check with `instanceof`.
*/
public addEntityClass<T extends EntityConstructor>(EntityClass: T): T {
if (EntityClass.class in this.entityClasses) {
this.config.debugLog(
'nymph',
`Adding duplicate class "${EntityClass.class}". This may be a mistake.`,
);
}
// Check that it doesn't have the same etype as any other class.
for (const ExistingClass of Object.values(this.entityClasses)) {
if (
EntityClass.ETYPE === ExistingClass.ETYPE &&
EntityClass.class !== EntityClass.class
) {
this.config.debugLog(
'nymph',
`Adding class "${EntityClass.class}" with same etype as existing class "${ExistingClass.class}". This may be a mistake.`,
);
}
}
const nymph = this;
this.entityClasses[EntityClass.class] = class extends EntityClass {
static nymph: Nymph = nymph;
constructor(...args: any[]) {
super(...args);
}
};
return this.entityClasses[EntityClass.class] as T;
}
/**
* Get the class that uses the specified class name.
*/
public getEntityClass(className: string): EntityConstructor;
public getEntityClass<T extends EntityConstructor>(className: T): T;
public getEntityClass<T extends EntityConstructor = EntityConstructor>(
className: T | string,
): T | EntityConstructor {
let key: string | null = null;
if (typeof className === 'string') {
key = className;
} else {
key = className.class;
}
if (key in this.entityClasses) {
return this.entityClasses[key];
}
this.config.debugError('nymph', `Tried to use missing class "${key}".`);
throw new ClassNotAvailableError(`Tried to use missing class "${key}".`);
}
/**
* Get the class that uses the specified etype.
*
* Note that it is fine, though unusual, for two classes to use the same
* etype. However, this can lead to very hard to diagnose bugs, so is
* generally discouraged.
*/
public getEntityClassByEtype(etype: string): EntityConstructor {
for (const EntityClass of Object.values(this.entityClasses)) {
if (EntityClass.ETYPE === etype) {
return EntityClass;
}
}
this.config.debugError(
'nymph',
`Tried to use missing class by etype "${etype}".`,
);
throw new ClassNotAvailableError(
`Tried to use missing class by etype "${etype}".`,
);
}
/**
* Get a clone of this instance with cloned classes and event listeners.
*
* @returns A clone of this instance.
*/
public clone() {
return new Nymph(
this.config,
this.driver.clone(),
this.tilmeld?.clone(),
this,
);
}
/**
* Connect to the database.
*
* @returns Whether the instance is connected to the database.
*/
public async connect(): Promise<boolean> {
try {
const result = this.driver.connect();
for (let callback of this.connectCallbacks) {
if (callback) {
await callback(this, result);
}
}
this.config.debugInfo('nymph', 'Driver connected.');
return await result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to connect: ${e}`);
throw e;
}
}
/**
* Disconnect from the database.
*
* @returns Whether the instance is connected to the database.
*/
public async disconnect(): Promise<boolean> {
try {
const result = this.driver.disconnect();
for (let callback of this.disconnectCallbacks) {
if (callback) {
await callback(this, result);
}
}
this.config.debugInfo('nymph', 'Driver disconnected.');
return await result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to disconnect: ${e}`);
throw e;
}
}
/**
* Run all the query callbacks on a query.
*/
public runQueryCallbacks(options: Options, selectors: FormattedSelector[]) {
for (let callback of this.queryCallbacks) {
if (callback) {
callback(this, options, selectors);
}
}
}
/**
* Start an atomic transaction and returns a new instance of Nymph.
*
* All proceeding changes using this new instance will wait to be written to
* the database's permanent storage until commit() is called. You can also
* undo all the changes since this function ran with rollback().
*
* Transactions will nest as long as every name is unique. Internally, Nymph
* uses names prefixed with "nymph-".
*
* @returns A new instance of Nymph that should be used for the transaction.
*/
public async startTransaction(name: string): Promise<Nymph> {
try {
for (let callback of this.beforeStartTransactionCallbacks) {
if (callback) {
await callback(this, name);
}
}
this.config.debugLog('nymph', `Starting transaction "${name}".`);
const result = await this.driver.startTransaction(name);
for (let callback of this.afterStartTransactionCallbacks) {
if (callback) {
await callback(this, name, result);
}
}
return result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to start transaction: ${e}`);
throw e;
}
}
/**
* Commit the named transaction.
*
* After this is called, the transaction instance should be discarded.
*
* @returns True on success, false on failure.
*/
public async commit(name: string): Promise<boolean> {
try {
for (let callback of this.beforeCommitTransactionCallbacks) {
if (callback) {
await callback(this, name);
}
}
this.config.debugLog('nymph', `Committing transaction "${name}".`);
const result = await this.driver.commit(name);
for (let callback of this.afterCommitTransactionCallbacks) {
if (callback) {
await callback(this, name, result);
}
}
return result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to commit transaction: ${e}`);
throw e;
}
}
/**
* Rollback the named transaction.
*
* After this is called, the transaction instance should be discarded.
*
* @returns True on success, false on failure.
*/
public async rollback(name: string): Promise<boolean> {
try {
for (let callback of this.beforeRollbackTransactionCallbacks) {
if (callback) {
await callback(this, name);
}
}
this.config.debugLog('nymph', `Rolling back transaction "${name}".`);
const result = await this.driver.rollback(name);
for (let callback of this.afterRollbackTransactionCallbacks) {
if (callback) {
await callback(this, name, result);
}
}
return result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to roll back transaction: ${e}`);
throw e;
}
}
/**
* Check if there is any open transaction.
*
* @returns True if there is a transaction.
*/
public async inTransaction(): Promise<boolean> {
return await this.driver.inTransaction();
}
/**
* Increment or create a unique ID and return the new value.
*
* Unique IDs, or UIDs are similar to GUIDs, but numeric and sequential.
*
* A UID can be used to identify an object when the GUID doesn't suffice. On
* a system where a new entity is created many times per second, referring
* to something by its GUID may be unintuitive. However, the component
* designer is responsible for assigning UIDs to the component's entities.
* Beware that if a UID is incremented for an entity, and the entity cannot
* be saved, there is no safe, and therefore, no recommended way to
* decrement the UID back to its previous value.
*
* If newUID() is passed the name of a UID which does not exist yet, one
* will be created with that name, and assigned the value 1. If the UID
* already exists, its value will be incremented. The new value will be
* returned.
*
* @param name The UID's name.
* @returns The UID's new value, or null on failure.
*/
public async newUID(name: string): Promise<number | null> {
try {
for (let callback of this.beforeNewUIDCallbacks) {
if (callback) {
await callback(this, name);
}
}
let result: Promise<number | null>;
try {
result = this.driver.newUID(name);
} catch (e: any) {
for (let callback of this.failedNewUIDCallbacks) {
if (callback) {
await callback(this, e);
}
}
throw e;
}
for (let callback of this.afterNewUIDCallbacks) {
if (callback) {
await callback(this, result);
}
}
return await result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to create UID "${name}": ${e}`);
throw e;
}
}
/**
* Get the current value of a unique ID.
* @param name The UID's name.
* @returns The UID's value, or null on failure and if it doesn't exist.
*/
public async getUID(name: string): Promise<number | null> {
try {
return await this.driver.getUID(name);
} catch (e: any) {
this.config.debugError('nymph', `Failed to get UID "${name}": ${e}`);
throw e;
}
}
/**
* Set the value of a UID.
*
* @param name The UID's name.
* @param value The value.
* @returns True on success, false on failure.
*/
public async setUID(name: string, value: number): Promise<boolean> {
try {
for (let callback of this.beforeSetUIDCallbacks) {
if (callback) {
await callback(this, name, value);
}
}
let result: Promise<boolean>;
try {
result = this.driver.setUID(name, value);
} catch (e: any) {
for (let callback of this.failedSetUIDCallbacks) {
if (callback) {
await callback(this, e);
}
}
throw e;
}
for (let callback of this.afterSetUIDCallbacks) {
if (callback) {
await callback(this, result);
}
}
return await result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to set UID "${name}": ${e}`);
throw e;
}
}
/**
* Delete a unique ID.
*
* @param name The UID's name.
* @returns True on success, false on failure.
*/
public async deleteUID(name: string): Promise<boolean> {
try {
for (let callback of this.beforeDeleteUIDCallbacks) {
if (callback) {
await callback(this, name);
}
}
let result: Promise<boolean>;
try {
result = this.driver.deleteUID(name);
} catch (e: any) {
for (let callback of this.failedDeleteUIDCallbacks) {
if (callback) {
await callback(this, e);
}
}
throw e;
}
for (let callback of this.afterDeleteUIDCallbacks) {
if (callback) {
await callback(this, result);
}
}
return await result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to delete UID "${name}": ${e}`);
throw e;
}
}
/**
* Rename a unique ID.
*
* @param oldName The old name.
* @param newName The new name.
* @returns True on success, false on failure.
*/
public async renameUID(oldName: string, newName: string): Promise<boolean> {
try {
for (let callback of this.beforeRenameUIDCallbacks) {
if (callback) {
await callback(this, oldName, newName);
}
}
let result: Promise<boolean>;
try {
result = this.driver.renameUID(oldName, newName);
} catch (e: any) {
for (let callback of this.failedRenameUIDCallbacks) {
if (callback) {
await callback(this, e);
}
}
throw e;
}
for (let callback of this.afterRenameUIDCallbacks) {
if (callback) {
await callback(this, result);
}
}
return await result;
} catch (e: any) {
this.config.debugError(
'nymph',
`Failed to rename UID "${oldName}" to "${newName}": ${e}`,
);
throw e;
}
}
/**
* Save an entity to the database.
*
* If the entity has never been saved (has no GUID), a variable "cdate"
* is set on it with the current Unix timestamp.
*
* The variable "mdate" is set to the current Unix timestamp.
*
* @param entity The entity.
* @returns True on success, false on failure.
*/
public async saveEntity(entity: EntityInterface): Promise<boolean> {
try {
for (let callback of this.beforeSaveEntityCallbacks) {
if (callback) {
await callback(this, entity);
}
}
let result: Promise<boolean>;
try {
result = this.driver.saveEntity(entity);
} catch (e: any) {
for (let callback of this.failedSaveEntityCallbacks) {
if (callback) {
await callback(this, e);
}
}
throw e;
}
for (let callback of this.afterSaveEntityCallbacks) {
if (callback) {
await callback(this, result);
}
}
return await result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to save entity: ${e}`);
throw e;
}
}
/**
* Get an array of entities.
*
* `options` is an object, which contains any of the following settings:
*
* - class - The class to create each entity with.
* - limit - The limit of entities to be returned.
* - offset - The offset from the oldest matching entity to start retrieving.
* - reverse - If true, entities will be retrieved from newest to oldest.
* Therefore, offset will be from the newest entity.
* - sort - How to sort the entities. Accepts "cdate", "mdate", or the name of
* a top-level property. The method of sorting properties other than cdate
* and mdate is driver dependent. The only hard rule is that numbers should
* be sorted numerically (2 before 10). Defaults to "cdate".
* - return - What to return. "entity", "object", "guid", or "count". Defaults
* to "entity".
* - source - Will be 'client' if the query came from a REST call.
* - skipCache - If true, Nymph will skip the cache and retrieve the entity
* from the DB.
* - skipAc - If true, Tilmeld will not filter returned entities according to
* access controls. (If Tilmeld is installed.) (This is *always* set to
* false by the REST server.)
*
* If a class is specified, it must have a factory() static method that
* returns a new instance.
*
* Selectors are objects. Any amount of selectors can be provided. Empty
* selectors will be ignored. The `type` property of a selector is required
* and can be one of the following strings:
*
* - & - (and) All values in the selector must be true.
* - | - (or) At least one value in the selector must be true.
* - !& - (not and) All values in the selector must be false.
* - !| - (not or) At least one value in the selector must be false.
*
* The rest of the properties in the selectors are called selector clauses,
* which can be any of the following (in the form selector.name = value, or
* selector.name = [value1, value2,...]):
*
* - guid - A GUID. True if the entity's GUID is equal.
* - tag - A tag. True if the entity has the tag.
* - defined - A name. True if the named property exists.
* - truthy - A name. True if the named property is defined and truthy.
* - equal - An array with a name, then value. True if the named property is
* defined and equal (their JSON strings are identical).
* - contain - An array with a name, then value. True if the named property
* contains the value (its JSON string is found within the property's JSON
* string).
* - match - An array with a name, then regular expression. True if the
* named property matches. Uses POSIX RegExp. Case sensitive. Must *not* be
* surrounded by any delimiters.
* - imatch - An array with a name, then regular expression. True if the
* named property matches. Uses POSIX RegExp. Case insensitive. Must *not*
* be surrounded by any delimiters.
* - like - An array with a name, then pattern. True if the named property
* matches. Uses % for variable length wildcard and _ for single character
* wildcard. Case sensitive.
* - ilike - An array with a name, then pattern. True if the named property
* matches. Uses % for variable length wildcard and _ for single character
* wildcard. Case insensitive.
* - gt - An array with a name, then value. True if the named property is
* greater than the value.
* - gte - An array with a name, then value. True if the named property is
* greater than or equal to the value.
* - lt - An array with a name, then value. True if the named property is
* less than the value.
* - lte - An array with a name, then value. True if the named property is
* less than or equal to the value.
* - ref - An array with a name, then either an entity, or a GUID. True if
* the named property is the entity or contains the entity.
* - qref - An array with a name, then a full query (including options) in an
* array. True if the named property is an entity that matches the query or
* contains an entity that matches the query.
* - selector - A selector. (Keep in mind, you can also use an array of these,
* just like any other clause.)
*
* These clauses can all be negated, by prefixing them with an exclamation
* point, such as "!truthy" to mean falsy (or undefined).
*
* Any clause that accepts an array of name and value can also accept a third
* element. If value is null and the third element is a string, the third
* element will be used with Locutus' strtotime function to set value to a
* relative timestamp. For example, the following selector will look for all
* entities that were created in the last day:
*
* ```
* {
* type: '&',
* gte: ['cdate', null, '-1 day']
* }
* ```
*
* Locutus' implementation: https://locutus.io/php/datetime/strtotime/
* PHP's documentation: https://www.php.net/manual/en/function.strtotime.php
*
* This example will retrieve the last two entities where:
*
* - It has 'person' tag.
* - spouse is defined.
* - gender is male and lname is Smith.
* - warnings is not an integer 0.
* - It has 'level1' and 'level2' tags, or it has 'access1' and 'access2'
* tags.
* - It has either 'employee' or 'manager' tag.
* - name is either Clark, James, Chris, Christopher, Jake, or Jacob.
* - If age is 22 or more, then pay is not greater than 8.
*
* ```
* const entities = Nymph.getEntities(
* { class: Entity, reverse: true, limit: 2 },
* {
* type: '&', // all must be true
* tag: 'person',
* defined: 'spouse',
* equal: [
* ['gender', 'male'],
* ['lname', 'Smith']
* ],
* '!equal': ['warnings', 0]
* },
* {
* type: '|', // at least one of the selectors in this must match
* selector: [
* {
* type: '&',
* tag: ['level1', 'level2']
* },
* {
* type: '&',
* tag: ['access1', 'access2']
* }
* ]
* },
* {
* type: '|', // at least one must be true
* tag: ['employee', 'manager']
* },
* {
* type: '|',
* equal: [
* ['name', 'Clark'],
* ['name', 'James']
* ],
* match: [
* ['name', 'Chris(topher)?'],
* ['name', 'Ja(ke|cob)']
* ]
* },
* {
* type: '!|', // at least one must be false
* gte: ['age', 22],
* gt: ['pay', 8]
* }
* );
* ```
*
* @param options The options.
* @param selectors Unlimited optional selectors to search for. If none are given, all entities are retrieved for the given options.
* @returns An array of entities or guids, or a count.
* @todo Use an asterisk to specify any variable.
*/
public async getEntities<T extends EntityConstructor = EntityConstructor>(
options: Options<T> & { return: 'count' },
...selectors: Selector[]
): Promise<number>;
public async getEntities<T extends EntityConstructor = EntityConstructor>(
options: Options<T> & { return: 'guid' },
...selectors: Selector[]
): Promise<string[]>;
public async getEntities<T extends EntityConstructor = EntityConstructor>(
options: Options<T> & { return: 'object' },
...selectors: Selector[]
): Promise<EntityObjectType<T>[]>;
public async getEntities<T extends EntityConstructor = EntityConstructor>(
options?: Options<T>,
...selectors: Selector[]
): Promise<EntityInstanceType<T>[]>;
public async getEntities<T extends EntityConstructor = EntityConstructor>(
options: Options<T> = {},
...selectors: Selector[]
): Promise<
EntityInstanceType<T>[] | EntityObjectType<T>[] | string[] | number
> {
if (options.source === 'client' && options.return === 'object') {
throw new InvalidParametersError(
'Object return type not allowed from client.',
);
}
try {
for (let callback of this.beforeGetEntitiesCallbacks) {
if (callback) {
await callback(this, options, selectors);
}
}
return await this.driver.getEntities(options, ...selectors);
} catch (e: any) {
this.config.debugError('nymph', `Failed to get entities: ${e}`);
throw e;
}
}
/**
* Get the first entity to match all options/selectors.
*
* options and selectors are the same as in getEntities().
*
* This function is equivalent to setting options.limit to 1 for
* getEntities(), except that it will return null if no entity is found.
* getEntities() would return an empty array.
*
* @param options The options.
* @param selectors Unlimited optional selectors to search for, or a single GUID. If none are given, all entities are searched for the given options.
* @returns An entity or guid, or null on failure or nothing found, or a number.
*/
public async getEntity<T extends EntityConstructor = EntityConstructor>(
options: Options<T> & { return: 'count' },
...selectors: Selector[]
): Promise<number>;
public async getEntity<T extends EntityConstructor = EntityConstructor>(
options: Options<T> & { return: 'guid' },
...selectors: Selector[]
): Promise<string | null>;
public async getEntity<T extends EntityConstructor = EntityConstructor>(
options: Options<T> & { return: 'object' },
...selectors: Selector[]
): Promise<EntityObjectType<T> | null>;
public async getEntity<T extends EntityConstructor = EntityConstructor>(
options: Options<T>,
...selectors: Selector[]
): Promise<EntityInstanceType<T> | null>;
public async getEntity<T extends EntityConstructor = EntityConstructor>(
options: Options<T> & { return: 'guid' },
guid: string,
): Promise<string | null>;
public async getEntity<T extends EntityConstructor = EntityConstructor>(
options: Options<T> & { return: 'object' },
guid: string,
): Promise<EntityObjectType<T> | null>;
public async getEntity<T extends EntityConstructor = EntityConstructor>(
options: Options<T>,
guid: string,
): Promise<EntityInstanceType<T> | null>;
public async getEntity<T extends EntityConstructor = EntityConstructor>(
options: Options<T> = {},
...selectors: Selector[] | string[]
): Promise<
EntityInstanceType<T> | EntityObjectType<T> | string | number | null
> {
if (options.source === 'client' && options.return === 'object') {
throw new InvalidParametersError(
'Object return type not allowed from client.',
);
}
try {
// Set up options and selectors.
if (typeof selectors[0] === 'string') {
selectors = [{ type: '&', guid: selectors[0] }];
}
options.limit = 1;
for (let callback of this.beforeGetEntityCallbacks) {
if (callback) {
await callback(this, options, selectors as Selector[]);
}
}
const entities = await this.driver.getEntities(
options,
...(selectors as Selector[]),
);
if (options.return === 'count') {
return entities as unknown as number;
}
if (!entities || !entities.length) {
return null;
}
return entities[0];
} catch (e: any) {
this.config.debugError('nymph', `Failed to get entity: ${e}`);
throw e;
}
}
/**
* Delete an entity from the database.
*
* @param entity The entity.
* @returns True on success, false on failure.
*/
public async deleteEntity(entity: EntityInterface): Promise<boolean> {
try {
for (let callback of this.beforeDeleteEntityCallbacks) {
if (callback) {
await callback(this, entity);
}
}
let result: Promise<boolean>;
try {
result = this.driver.deleteEntity(entity);
} catch (e: any) {
for (let callback of this.failedDeleteEntityCallbacks) {
if (callback) {
await callback(this, e);
}
}
throw e;
}
for (let callback of this.afterDeleteEntityCallbacks) {
if (callback) {
await callback(this, result);
}
}
return await result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to delete entity: ${e}`);
throw e;
}
}
/**
* Delete an entity from the database by its GUID.
*
* @param guid The entity's GUID.
* @param className The entity's class name.
* @returns True on success, false on failure.
*/
public async deleteEntityByID(
guid: string,
className?: string,
): Promise<boolean> {
try {
for (let callback of this.beforeDeleteEntityByIDCallbacks) {
if (callback) {
await callback(this, guid, className);
}
}
let result: Promise<boolean>;
try {
result = this.driver.deleteEntityByID(guid, className);
} catch (e: any) {
for (let callback of this.failedDeleteEntityByIDCallbacks) {
if (callback) {
await callback(this, e);
}
}
throw e;
}
for (let callback of this.afterDeleteEntityByIDCallbacks) {
if (callback) {
await callback(this, result);
}
}
return await result;
} catch (e: any) {
this.config.debugError('nymph', `Failed to delete entity by ID: ${e}`);
throw e;
}
}
/**
* Export entities to a local file.
*
* This is the file format:
*
* ```
* #nex2
* # The above line must be the first thing in the file.
* # Comments begin with #
* # And can have white space before them.
* # This defines a UID.
* <name/of/uid>[5]
* <another uid>[8000]
* # For UIDs, the name is in angle brackets (<>) and the value follows
* # in square brackets ([]).
* # This starts a new entity.
* {1234abcd}<etype>[tag,list,with,commas]
* # For entities, the GUID is in curly brackets ({}), then the etype in
* # angle brackets, then the comma separated tag list follows in square
* # brackets ([]).
* # Properties are stored like this:
* # propname=JSON.stringify(value)
* abilities=["system/admin"]
* groups=[]
* inheritAbilities=false
* name="admin"
* # White space before/after "=" and at beginning/end of line is ignored.
* username = "admin"
* {2}<etype>[tag,list]
* another="This is another entity."
* newline="\n"
* ```
*
* @param filename The file to export to.
* @returns True on success, false on failure.
*/
public async export(filename: string): Promise<boolean> {
try {
return await this.driver.export(filename);
} catch (e: any) {
this.config.debugError('nymph', `Failed to export: ${e}`);
throw e;
}
}
/**
* Export entities to the console.
*
* @returns True on success, false on failure.
*/
public async exportPrint(): Promise<boolean> {
try {
return await this.driver.exportPrint();
} catch (e: any) {
this.config.debugError('nymph', `Failed to export: ${e}`);
throw e;
}
}
/**
* Import entities from a file.
*
* @param filename The file to import from.
* @returns True on success, false on failure.
*/
public async import(filename: string): Promise<boolean> {
try {
return await this.driver.import(filename);
} catch (e: any) {
this.config.debugError('nymph', `Failed to import: ${e}`);
throw e;
}
}
/**
* Detect whether the database needs to be migrated.
*
* If true, the database should be exported with an old version of Nymph, then
* imported into a fresh database with this version.
*/
public async needsMigration(): Promise<boolean> {
return await this.driver.needsMigration();
}
public on<T extends NymphEventType>(
event: T,
callback: T extends 'connect'
? NymphConnectCallback
: T extends 'disconnect'
? NymphDisconnectCallback
: T extends 'query'
? NymphQueryCallback
: T extends 'beforeGetEntity'
? NymphBeforeGetEntityCallback
: T extends 'beforeGetEntities'
? NymphBeforeGetEntitiesCallback
: T extends 'beforeSaveEntity'
? NymphBeforeSaveEntityCallback
: T extends 'afterSaveEntity'
? NymphAfterSaveEntityCallback
: T extends 'failedSaveEntity'
? NymphFailedSaveEntityCallback
: T extends 'beforeDeleteEntity'
? NymphBeforeDeleteEntityCallback
: T extends 'afterDeleteEntity'
? NymphAfterDeleteEntityCallback
: T extends 'failedDeleteEntity'
? NymphFailedDeleteEntityCallback
: T extends 'beforeDeleteEntityByID'
? NymphBeforeDeleteEntityByIDCallback
: T extends 'afterDeleteEntityByID'
? NymphAfterDeleteEntityByIDCallback
: T extends 'failedDeleteEntityByID'
? NymphFailedDeleteEntityByIDCallback
: T extends 'beforeNewUID'
? NymphBeforeNewUIDCallback
: T extends 'afterNewUID'
? NymphAfterNewUIDCallback
: T extends 'failedNewUID'
? NymphFailedNewUIDCallback
: T extends 'beforeSetUID'
? NymphBeforeSetUIDCallback
: T extends 'afterSetUID'
? NymphAfterSetUIDCallback
: T extends 'failedSetUID'
? NymphFailedSetUIDCallback
: T extends 'beforeRenameUID'
? NymphBeforeRenameUIDCallback
: T extends 'afterRenameUID'
? NymphAfterRenameUIDCallback
: T extends 'failedRenameUID'
? NymphFailedRenameUIDCallback
: T extends 'beforeDeleteUID'
? NymphBeforeDeleteUIDCallback
: T extends 'afterDeleteUID'
? NymphAfterDeleteUIDCallback
: T extends 'failedDeleteUID'
? NymphFailedDeleteUIDCallback
: T extends 'beforeStartTransaction'
? NymphBeforeStartTransactionCallback
: T extends 'afterStartTransaction'
? NymphAfterStartTransactionCallback
: T extends 'beforeCommitTransaction'
? NymphBeforeCommitTransactionCallback
: T extends 'afterCommitTransaction'
? NymphAfterCommitTransactionCallback
: T extends 'beforeRollbackTransaction'
? NymphBeforeRollbackTransactionCallback
: T extends 'afterRollbackTransaction'
? NymphAfterRollbackTransactionCallback
: never,
) {
const prop = (event + 'Callbacks') as T extends 'connect'
? 'connectCallbacks'
: T extends 'disconnect'
? 'disconnectCallbacks'
: T extends 'query'
? 'queryCallbacks'
: T extends 'beforeGetEntity'
? 'beforeGetEntityCallbacks'
: T extends 'beforeGetEntities'
? 'beforeGetEntitiesCallbacks'
: T extends 'beforeSaveEntity'
? 'beforeSaveEntityCallbacks'
: T extends 'afterSaveEntity'
? 'afterSaveEntityCallbacks'
: T extends 'failedSaveEntity'
? 'failedSaveEntityCallbacks'
: T extends 'beforeDeleteEntity'
? 'beforeDeleteEntityCallbacks'
: T extends 'afterDeleteEntity'
? 'afterDeleteEntityCallbacks'
: T extends 'failedDeleteEntity'
? 'failedDeleteEntityCallbacks'
: T extends 'beforeDeleteEntityByID'
? 'beforeDeleteEntityByIDCallbacks'
: T extends 'afterDeleteEntityByID'
? 'afterDeleteEntityByIDCallbacks'
: T extends 'failedDeleteEntityByID'
? 'failedDeleteEntityByIDCallbacks'
: T extends 'beforeNewUID'
? 'beforeNewUIDCallbacks'
: T extends 'afterNewUID'
? 'afterNewUIDCallbacks'
: T extends 'failedNewUID'
? 'failedNewUIDCallbacks'
: T extends 'beforeSetUID'
? 'beforeSetUIDCallbacks'
: T extends 'afterSetUID'
? 'afterSetUIDCallbacks'
: T extends 'failedSetUID'
? 'failedSetUIDCallbacks'
: T extends 'beforeRenameUID'
? 'beforeRenameUIDCallbacks'
: T extends 'afterRenameUID'
? 'afterRenameUIDCallbacks'
: T extends 'failedRenameUID'
? 'failedRenameUIDCallbacks'
: T extends 'beforeDeleteUID'
? 'beforeDeleteUIDCallbacks'
: T extends 'afterDeleteUID'
? 'afterDeleteUIDCallbacks'
: T extends 'failedDeleteUID'
? 'failedDeleteUIDCallbacks'
: T extends 'beforeStartTransaction'
? 'beforeStartTransactionCallbacks'
: T extends 'afterStartTransaction'
? 'afterStartTransactionCallbacks'
: T extends 'beforeCommitTransaction'
? 'beforeCommitTransactionCallbacks'
: T extends 'afterCommitTransaction'
? 'afterCommitTransactionCallbacks'
: T extends 'beforeRollbackTransaction'
? 'beforeRollbackTransactionCallbacks'
: T extends 'afterRollbackTransaction'
? 'afterRollbackTransactionCallbacks'
: never;
if (!(prop in this)) {
throw new Error('Invalid event type.');
}
// @ts-ignore: The callback should be the right type here.
this[prop].push(callback);
return () => this.off(event, callback);
}
public off<T extends NymphEventType>(
event: T,
callback: T extends 'connect'
? NymphConnectCallback
: T extends 'disconnect'
? NymphDisconnectCallback
: T extends 'query'
? NymphQueryCallback
: T extends 'beforeGetEntity'
? NymphBeforeGetEntityCallback
: T extends 'beforeGetEntities'
? NymphBeforeGetEntitiesCallback
: T extends 'beforeSaveEntity'
? NymphBeforeSaveEntityCallback
: T extends 'afterSaveEntity'
? NymphAfterSaveEntityCallback
: T extends 'failedSaveEntity'
? NymphFailedSaveEntityCallback
: T extends 'beforeDeleteEntity'
? NymphBeforeDeleteEntityCallback
: T extends 'afterDeleteEntity'
? NymphAfterDeleteEntityCallback
: T extends 'failedDeleteEntity'
? NymphFailedDeleteEntityCallback
: T extends 'beforeDeleteEntityByID'
? NymphBeforeDeleteEntityByIDCallback
: T extends 'afterDeleteEntityByID'
? NymphAfterDeleteEntityByIDCallback
: T extends 'failedDeleteEntityByID'
? NymphFailedDeleteEntityByIDCallback
: T extends 'beforeNewUID'
? NymphBeforeNewUIDCallback
: T extends 'afterNewUID'
? NymphAfterNewUIDCallback
: T extends 'failedNewUID'
? NymphFailedNewUIDCallback
: T extends 'beforeSetUID'
? NymphBeforeSetUIDCallback
: T extends 'afterSetUID'
? NymphAfterSetUIDCallback
: T extends 'failedSetUID'
? NymphFailedSetUIDCallback
: T extends 'beforeRenameUID'
? NymphBeforeRenameUIDCallback
: T extends 'afterRenameUID'
? NymphAfterRenameUIDCallback
: T extends 'failedRenameUID'
? NymphFailedRenameUIDCallback
: T extends 'beforeDeleteUID'
? NymphBeforeDeleteUIDCallback
: T extends 'afterDeleteUID'
? NymphAfterDeleteUIDCallback
: T extends 'failedDeleteUID'
? NymphFailedDeleteUIDCallback
: T extends 'beforeStartTransaction'
? NymphBeforeStartTransactionCallback
: T extends 'afterStartTransaction'
? NymphAfterStartTransactionCallback
: T extends 'beforeCommitTransaction'
? NymphBeforeCommitTransactionCallback
: T extends 'afterCommitTransaction'
? NymphAfterCommitTransactionCallback
: T exten