UNPKG

@onehat/data

Version:

JS data modeling package with adapters for many storage mediums.

726 lines (640 loc) 19.6 kB
/** @module OneHatData */ import EventEmitter from '@onehat/events'; import CoreRepositoryTypes from './Repository/index.js'; import Entity from './Entity/Entity.js'; import { MODE_LOCAL_MIRROR, MODE_COMMAND_QUEUE, MODE_REMOTE_WITH_OFFLINE, } from './Repository/LocalFromRemote/index.js'; import { default as Schema, CoreSchemas, } from './Schema/index.js'; import { v4 as uuid, } from 'uuid'; import _ from 'lodash'; /** * OneHatData represents a collection of Repositories. * It is the top-level object for this module. * Normally used as a global singleton within an app, * using the exported 'oneHatData' constant * @extends EventEmitter */ export class OneHatData extends EventEmitter { constructor() { super(...arguments); /** * @member {object} schemas - Object of all Schemas, keyed by name (for quick access) * @private */ this.schemas = _.chain(_.map(CoreSchemas, (schema) => schema.clone())) // clone CoreSchemas so it remains untouched in case this instance of OneHatData gets destroyed .keyBy('name') .value(); /** * @member {Object} repositories - Object of all Repositories, keyed by id (for quick access) * @private */ this._repositoryTypes = _.clone(CoreRepositoryTypes); /** * @member {Object} _repositoryGlobals - Object of all global settings for all repositories. * @private */ this._repositoryGlobals = {}; /** * @member {Object} repositories - Object of all Repositories, keyed by id (for quick access) * @private */ this.repositories = {}; /** * @member {Object} uniqueRepositoryIdsMap - Object map of all unique Repositories, with signature of { mapName: id } * @private */ this.uniqueRepositoryIdsMap = {}; /** * @member {boolean} isDestroyed - Whether this object has been destroyed * @private */ this.isDestroyed = false; /** * @member {boolean} isOnline - Whether the remote Internet connection is active. * This must be managed by outside software, calling setIsOnline at appropriate times. * @private */ this.isOnline = true; this.registerEvents([ 'createRepository', 'deleteRepository', 'createSchema', 'deleteSchema', 'destroy', 'error', ]); } /** * Sets global config settings that will be passed into all Repositories. * Chainable. * @param {object} globals - Schema config object * @param {boolean} merge - Whether to merge or replace the globals * @return this */ setRepositoryGlobals = (globals, merge = true) => { if (this.isDestroyed) { throw new Error('this.setRepositoryGlobals is no longer valid. OneHatData has been destroyed.'); } if (merge) { _.merge(this._repositoryGlobals, globals); } else { _.assign(this._repositoryGlobals, globals); } return this; } /** * Sets options on all Repositories. * Chainable. * @param {object} options - Keys and properties will be set as options on the Repository * @return this */ setOptionsOnAllRepositories = (options) => { if (this.isDestroyed) { throw new Error('this.applyGlobalHeaders is no longer valid. OneHatData has been destroyed.'); } const repositories = this.getAllRepositories(); _.each(repositories, (repository) => { repository.setOptions(options); }); return this; } /** * Sets global error handler on all Repositories. * Chainable. * @param {function} handler - The global error handler * @return this */ setGlobalErrorHandler = (handler) => { if (this.isDestroyed) { throw new Error('this.setGlobalErrorHandler is no longer valid. OneHatData has been destroyed.'); } const repositories = this.getAllRepositories(); _.each(repositories, (repository) => { repository.setErrorHandler(handler); }); return this; } // ______ __ // / ____/_______ ____ _/ /____ // / / / ___/ _ \/ __ `/ __/ _ \ // / /___/ / / __/ /_/ / /_/ __/ // \____/_/ \___/\__,_/\__/\___/ /** * Creates one Schema and immediately returns it. * @return {object} schema - The newly created schema * @memberOf OneHatData */ createSchema = (config) => { if (this.isDestroyed) { throw new Error('this.createSchema is no longer valid. OneHatData has been destroyed.'); } return this._createSchema(config); } /** * Creates one or more Schemas at once. * Chainable. * @param {array|object} configs - Single or array of Schema config objects * @return this */ createSchemas = (configs) => { if (this.isDestroyed) { throw new Error('this.createSchemas is no longer valid. OneHatData has been destroyed.'); } if (!_.isArray(configs)) { configs = [configs]; } _.each(configs, (config) => { this._createSchema(config) }); return this; } /** * Creates a new Schema. * @param {object} config - Schema config object * @return {object} schema - The newly created schema * @private */ _createSchema = (config) => { if (this.isDestroyed) { throw new Error('this._createSchema is no longer valid. OneHatData has been destroyed.'); } if (config.name && this.hasSchemaWithName(config.name)) { throw new Error('Schema with name ' + config.name + ' already exists. Schema names must be unique.'); } const schema = new Schema(config); this.schemas[schema.name] = schema; this.emit('createSchema', schema); return schema; } /** * Creates a new Repository. * @param {object} config - Repository config object * @param {boolean} bound - Should this Repository be bound to its schema? Defaults to false. * @return {object} repository - The newly created repository */ createRepository = async (config, bound = false) => { if (this.isDestroyed) { throw new Error('this.createRepository is no longer valid. OneHatData has been destroyed.'); } if (_.isNil(config)) { config = {}; } else if (_.isString(config)) { config = { schema: config, }; } const { id } = config; let schema = config.schema; // Check required args if (id && this.hasRepositoryWithId(id)) { throw new Error('Repository with id ' + repository.id + ' already exists. Repository IDs must be unique.'); } if (!schema) { throw new Error('Schema cannot be empty. Perhaps you meant to use "KeyValues"?'); } // Get actual schema, if only name provided if (_.isString(schema)) { const name = schema; schema = this.getSchema(name); if (!schema) { throw new Error('No schema found with name ' + name); } config.schema = schema; } // Construct overall config const schemaRepositoryDef = _.isString(schema.repository) ? { type: schema.repository } : schema.repository; config = _.merge({}, schemaRepositoryDef, this._repositoryGlobals, config); if (!config.type) { throw new Error('Repository type not set'); } if (!this._repositoryTypes[config.type]) { throw new Error('Repository type does not exist'); } // Create the Repository const repository = await this._createRepository(config); this.repositories[repository.id] = repository; if (bound) { schema.setBoundRepository(repository); } if (repository.isRegisteredEvent('logout')) { // OneBuild repository emits this this.relayEventsFrom(repository, ['logout']); } this.emit('createRepository', repository); return repository; } /** * Helper for createRepository. */ _createRepository = async (config) => { // Special case: LocalFromRemoteRepository. if (config.type === 'lfr') { // We need to initiate *both the local AND the remote* sides first, // before we initialize the containing repository // Get general config settings shared by both (e.g. schema, isPaginated) const generalConfig = _.omit(config, ['type', 'local', 'remote']); // Convert string configs to objects if (_.isString(config.local)) { config.local = { type: config.local, }; } if (_.isString(config.remote)) { config.remote = { type: config.remote, }; } if (config.mode === MODE_COMMAND_QUEUE) { generalConfig.isPaginated = false; config.remote.type = 'command'; } // Apply the general config settings to each specific one const localConfig = _.merge({}, generalConfig, config.local), remoteConfig = _.merge({}, generalConfig, config.remote); // Actually create the local and remote repositories config.local = await this.createRepository(localConfig); config.remote = await this.createRepository(remoteConfig); } const Repository = this._repositoryTypes[config.type], repository = new Repository(config, this); await repository.initialize(); return repository; } /** * Creates multiple Repositories at once. * If no argument supplied, will create Repositories for all * schemas previously created. * Chainable. * @param {array} configs - Array of Repository config objects * @return this * @memberOf OneHatData */ createRepositories = async (schemas, bound = false) => { if (this.isDestroyed) { throw new Error('this.createRepositories is no longer valid. OneHatData has been destroyed.'); } const schemasArray = _.map(schemas, (schema) => schema); let i, schema, repository; for (i = 0; i < schemasArray.length; i++) { schema = schemasArray[i]; repository = await this.createRepository({ schema, }, bound); if (!repository) { throw new Error('Repository could not be created'); } } return this; } /** * Creates the bound Repositories for all Schemas. * Chainable. * @return this * @memberOf OneHatData */ createBoundRepositories = async () => { if (this.isDestroyed) { throw new Error('this.createBoundRepositories is no longer valid. OneHatData has been destroyed.'); } return await this.createRepositories(this.schemas, true); } /** * Destroys and clears the bound Repositories for all Schemas. * Chainable. * @return this * @memberOf OneHatData */ destroyBoundRepositories = async () => { _.each(this.schemas, (schema) => { const repository = schema.getBoundRepository(); if (repository) { schema.clearBoundRepository(); delete this.repositories[repository.id]; repository.destroy(); } }); return this; } /** * Alias for registerRepositoryTypes() * Chainable. * @return this * @memberOf OneHatData */ registerRepositoryType = (repositoryType) => { if (this.isDestroyed) { throw new Error('this.registerRepositoryType is no longer valid. OneHatData has been destroyed.'); } return this.registerRepositoryTypes(repositoryType); } /** * Registers one or more RepositoryTypes (plugin architecture). * Chainable. * @return this * @memberOf OneHatData */ registerRepositoryTypes = (repositoryTypes) => { if (this.isDestroyed) { throw new Error('this.registerRepositoryTypes is no longer valid. OneHatData has been destroyed.'); } if (!_.isArray(repositoryTypes)) { repositoryTypes = [repositoryTypes]; } _.each(repositoryTypes, (repositoryType) => { this._repositoryTypes[repositoryType.type] = repositoryType; }); return this; } /** * Registers a global error handler * Chainable. * @return this * @memberOf OneHatData */ createGlobalErrorHandler = (handler) => { if (this.isDestroyed) { throw new Error('this.createGlobalErrorHandler is no longer valid. OneHatData has been destroyed.'); } const repositories = this.getAllRepositories(); _.each(repositories, (repository) => { repository.on('error', handler); }); return this; } emitError = () => { this.emit('error', 'Test here'); } // ____ __ _ // / __ \___ / /______(_)__ _ _____ // / /_/ / _ \/ __/ ___/ / _ \ | / / _ \ // / _, _/ __/ /_/ / / / __/ |/ / __/ // /_/ |_|\___/\__/_/ /_/\___/|___/\___/ /** * Get a Schema by its name * @param {string} name - Name of Schema to get * @return {Schema} schema */ getSchema = (name) => { if (this.isDestroyed) { throw new Error('this.getSchema is no longer valid. OneHatData has been destroyed.'); } return this.schemas[name]; } /** * Get Schemas by a filter function * @param {function} filter - Filter function * @param {boolean} firstOnly - Whether to retrieve only the first item that passes the filter * @return {Repository[]} repositories */ getSchemasBy = (filter, firstOnly = false) => { if (this.isDestroyed) { throw new Error('this.getSchemasBy is no longer valid. OneHatData has been destroyed.'); } if (firstOnly) { return _.find(this.schemas, filter); } return _.filter(this.schemas, filter); } /** * Get all Repositories * @return {object} repositories */ getAllRepositories = () => { if (this.isDestroyed) { throw new Error('this.getAllRepositories is no longer valid. OneHatData has been destroyed.'); } return this.repositories; } /** * Get the Repository bound to the Schema with the supplied name. * @param {string} name - Name of Schema * @return {Repository} repository */ getRepository = (name, unique = false) => { if (this.isDestroyed) { throw new Error('this.getRepository is no longer valid. OneHatData has been destroyed.'); } const schema = this.getSchema(name); if (!schema) { return null; } if (unique) { const repoToClone = schema.getBoundRepository(), clone = _.cloneDeep(repoToClone); const id = uuid(); clone.name = clone.name + '-' + id; clone.id = id; clone.isUnique = true; return clone; } return schema.getBoundRepository(); } /** * Gets or creates a unique repository with the supplied schemaName and name * @param {string} mapName - Name of unique repository (will be internally mapped to an id) * @param {string} schemaName - Name of Schema * @return {Repository} repository */ getOrCreateUniqueRepository = async (mapName, schemaName) => { if (this.isDestroyed) { throw new Error('this.getUniqueRepository is no longer valid. OneHatData has been destroyed.'); } // Try to get it let id = this.uniqueRepositoryIdsMap[mapName]; if (id) { return this.getRepositoryById(id); } // Try to create it const schema = this.getSchema(schemaName); if (!schema) { return null; } const repository = await this.createRepository(schemaName); id = repository.id; repository.name += '-' + id; repository.isUnique = true; this.uniqueRepositoryIdsMap[mapName] = repository.id; return repository; } /** * Checks whether the requested bound Repository exists. * @param {string} name - Name of Schema * @return {boolean} hasRepository */ hasRepository = (name) => { if (this.isDestroyed) { throw new Error('this.getRepository is no longer valid. OneHatData has been destroyed.'); } const repository = this.getRepository(name); return !!repository; } /** * Get Repositories by a filter function * @param {function} filter - Filter function * @param {boolean} firstOnly - Whether to retrieve only the first item that passes the filter * @return {Repository[]} repositories */ getRepositoriesBy = (filter, firstOnly = false) => { if (this.isDestroyed) { throw new Error('this.getRepositoriesBy is no longer valid. OneHatData has been destroyed.'); } if (firstOnly) { return _.find(this.repositories, filter); } return _.filter(this.repositories, filter); } /** * Get a Repository by its id * @param {string} id - ID of Repository to get * @return {Repository} repository */ getRepositoryById = (id) => { if (this.isDestroyed) { throw new Error('this.getRepositoryById is no longer valid. OneHatData has been destroyed.'); } return this.repositories[id]; } /** * @member * Get Repositories that share the given Schema * @return {Repository[]} repositories */ getRepositoriesBySchema = (schema) => { if (this.isDestroyed) { throw new Error('this.getRepositoriesBySchema is no longer valid. OneHatData has been destroyed.'); } return this.getRepositoriesBy((repository) => { return repository.schema === schema; }); } /** * @member * Get Repositories that share the given type * @return {Repository[]} repositories */ getRepositoriesByType = (type) => { if (this.isDestroyed) { throw new Error('this.getRepositoriesByType is no longer valid. OneHatData has been destroyed.'); } return this.getRepositoriesBy((repository) => { return repository.getType() === type; }); } /** * Checks whether a Schema with the supplied name exists * @param {string} name - Name to check * @return {boolean} hasSchema */ hasSchemaWithName = (name) => { if (this.isDestroyed) { throw new Error('this.hasSchemaWithName is no longer valid. OneHatData has been destroyed.'); } return this.schemas && this.schemas.hasOwnProperty(name); } /** * Checks whether a Repository with the supplied ID exists * @param {string} id - ID to check * @return {boolean} hasRepository */ hasRepositoryWithId = (id) => { if (this.isDestroyed) { throw new Error('this.hasRepositoryWithId is no longer valid. OneHatData has been destroyed.'); } return this.repositories && this.repositories.hasOwnProperty(id); } /** * Sets isOnline for all remote repositories. * Remote repositories won't submit queries if !isOnline */ setIsOnline = (isOnline) => { this.isOnline = !!isOnline; const remoteRepositories = oneHatData.getRepositoriesBy((repository) => !!repository.setIsOnline); _.each(remoteRepositories, (repository) => { repository.setIsOnline(isOnline); }); } // ____ __ __ // / __ \___ / /__ / /____ // / / / / _ \/ / _ \/ __/ _ \ // / /_/ / __/ / __/ /_/ __/ // /_____/\___/_/\___/\__/\___/ /** * Deletes an existing Schema. * Chainable. * @param {string} name - Name of Schema to delete * @return this * @memberOf OneHatData */ deleteSchema = (name) => { if (this.isDestroyed) { throw new Error('this.deleteSchema is no longer valid. OneHatData has been destroyed.'); } const schema = this.getSchema(name); delete this.schemas[name]; this.emit('deleteSchema', schema); return this; } /** * Deletes an existing Repository. * Chainable. * @param {object} config - Repository config object * @return this * @memberOf OneHatData */ deleteRepository = (id) => { if (this.isDestroyed) { throw new Error('this.deleteRepository is no longer valid. OneHatData has been destroyed.'); } const repository = this.getRepositoryById(id); if (!repository) { return false; } repository.destroy(); delete this.repositories[id]; this.emit('deleteRepository', repository); return this; } // _ __ ___ __ __ // | | / /___ _/ (_)___/ /___ _/ /____ // | | / / __ `/ / / __ / __ `/ __/ _ \ // | |/ / /_/ / / / /_/ / /_/ / /_/ __/ // |___/\__,_/_/_/\__,_/\__,_/\__/\___/ /** * Determines if submitted object is an entity * @return boolean */ isEntity = (obj) => { return obj?.__proto__?.constructor === Entity; } /** * Destroy this object. * - Removes child objects * - Removes event listeners * @member * @fires destroy */ destroy = () => { // child objects _.each(this.schemas, (schema) => { schema.destroy(); }); this.schemas = null; _.each(this.repositories, (repository) => { repository.destroy(); }); this.repositories = null; this.emit('destroy'); this.isDestroyed = true; // listeners this.removeAllListeners(); } }; // Create and export a singleton const oneHatData = new OneHatData(); export default oneHatData;