UNPKG

@nymphjs/nymph

Version:

Nymph.js - Nymph ORM

814 lines 27 kB
import { ConfigDefaults as defaults } from './conf/index.js'; import Entity from './Entity.js'; import { ClassNotAvailableError, InvalidParametersError, } from './errors/index.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. */ config; /** * The Nymph instance that this one was cloned from, or null if it's not a * clone. */ parent; /** * The Nymph database driver. */ driver; /** * An optional Tilmeld user/group manager instance. */ tilmeld = undefined; /** * A simple map of names to Entity classes. */ entityClasses = {}; /** * The entity class for this instance of Nymph. */ Entity; connectCallbacks = []; disconnectCallbacks = []; queryCallbacks = []; beforeGetEntityCallbacks = []; beforeGetEntitiesCallbacks = []; beforeSaveEntityCallbacks = []; afterSaveEntityCallbacks = []; failedSaveEntityCallbacks = []; beforeDeleteEntityCallbacks = []; afterDeleteEntityCallbacks = []; failedDeleteEntityCallbacks = []; beforeDeleteEntityByIDCallbacks = []; afterDeleteEntityByIDCallbacks = []; failedDeleteEntityByIDCallbacks = []; beforeNewUIDCallbacks = []; afterNewUIDCallbacks = []; failedNewUIDCallbacks = []; beforeSetUIDCallbacks = []; afterSetUIDCallbacks = []; failedSetUIDCallbacks = []; beforeRenameUIDCallbacks = []; afterRenameUIDCallbacks = []; failedRenameUIDCallbacks = []; beforeDeleteUIDCallbacks = []; afterDeleteUIDCallbacks = []; failedDeleteUIDCallbacks = []; beforeStartTransactionCallbacks = []; afterStartTransactionCallbacks = []; beforeCommitTransactionCallbacks = []; afterCommitTransactionCallbacks = []; beforeRollbackTransactionCallbacks = []; afterRollbackTransactionCallbacks = []; /** * 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. */ constructor(config, driver, tilmeld, parent) { 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].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, 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`. */ addEntityClass(EntityClass) { 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; constructor(...args) { super(...args); } }; return this.entityClasses[EntityClass.class]; } getEntityClass(className) { let key = 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. */ getEntityClassByEtype(etype) { 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. */ 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. */ async connect() { 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) { this.config.debugError('nymph', `Failed to connect: ${e}`); throw e; } } /** * Disconnect from the database. * * @returns Whether the instance is connected to the database. */ async disconnect() { 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) { this.config.debugError('nymph', `Failed to disconnect: ${e}`); throw e; } } /** * Run all the query callbacks on a query. */ runQueryCallbacks(options, selectors) { 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. */ async startTransaction(name) { 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) { 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. */ async commit(name) { 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) { 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. */ async rollback(name) { 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) { 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. */ async inTransaction() { 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. */ async newUID(name) { try { for (let callback of this.beforeNewUIDCallbacks) { if (callback) { await callback(this, name); } } let result; try { result = this.driver.newUID(name); } catch (e) { 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) { 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. */ async getUID(name) { try { return await this.driver.getUID(name); } catch (e) { 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. */ async setUID(name, value) { try { for (let callback of this.beforeSetUIDCallbacks) { if (callback) { await callback(this, name, value); } } let result; try { result = this.driver.setUID(name, value); } catch (e) { 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) { 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. */ async deleteUID(name) { try { for (let callback of this.beforeDeleteUIDCallbacks) { if (callback) { await callback(this, name); } } let result; try { result = this.driver.deleteUID(name); } catch (e) { 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) { 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. */ async renameUID(oldName, newName) { try { for (let callback of this.beforeRenameUIDCallbacks) { if (callback) { await callback(this, oldName, newName); } } let result; try { result = this.driver.renameUID(oldName, newName); } catch (e) { 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) { 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. */ async saveEntity(entity) { try { for (let callback of this.beforeSaveEntityCallbacks) { if (callback) { await callback(this, entity); } } let result; try { result = this.driver.saveEntity(entity); } catch (e) { 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) { this.config.debugError('nymph', `Failed to save entity: ${e}`); throw e; } } async getEntities(options = {}, ...selectors) { 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) { this.config.debugError('nymph', `Failed to get entities: ${e}`); throw e; } } async getEntity(options = {}, ...selectors) { 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); } } const entities = await this.driver.getEntities(options, ...selectors); if (options.return === 'count') { return entities; } if (!entities || !entities.length) { return null; } return entities[0]; } catch (e) { 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. */ async deleteEntity(entity) { try { for (let callback of this.beforeDeleteEntityCallbacks) { if (callback) { await callback(this, entity); } } let result; try { result = this.driver.deleteEntity(entity); } catch (e) { 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) { 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. */ async deleteEntityByID(guid, className) { try { for (let callback of this.beforeDeleteEntityByIDCallbacks) { if (callback) { await callback(this, guid, className); } } let result; try { result = this.driver.deleteEntityByID(guid, className); } catch (e) { 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) { 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. */ async export(filename) { try { return await this.driver.export(filename); } catch (e) { this.config.debugError('nymph', `Failed to export: ${e}`); throw e; } } /** * Export entities to the console. * * @returns True on success, false on failure. */ async exportPrint() { try { return await this.driver.exportPrint(); } catch (e) { 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. */ async import(filename) { try { return await this.driver.import(filename); } catch (e) { 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. */ async needsMigration() { return await this.driver.needsMigration(); } on(event, callback) { const prop = (event + 'Callbacks'); 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); } off(event, callback) { const prop = (event + 'Callbacks'); if (!(prop in this)) { return false; } // @ts-ignore: The callback should always be the right type here. const i = this[prop].indexOf(callback); if (i > -1) { // @ts-ignore: The callback should always be the right type here. this[prop].splice(i, 1); } return true; } } //# sourceMappingURL=Nymph.js.map