'use strict'; const { isUndefined, castArray, isNil, has, isString, isInteger, pick, isPlainObject, isEmpty, isArray, isNull, uniqWith, isEqual, differenceWith, isNumber, } = require('lodash/fp'); const types = require('../types'); const { createField } = require('../fields'); const { createQueryBuilder } = require('../query'); const { createRepository } = require('./entity-repository'); const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morph-relations'); const { isBidirectional, isManyToAny, isAnyToOne, isAnyToMany } = require('../metadata/relations'); const { deletePreviousOneToAnyRelations, deletePreviousAnyToOneRelations, deleteAllRelations, } = require('./utils'); const toId = (value) => value.id || value; const toIds = (value) => castArray(value || []).map(toId); const isValidId = (value) => isString(value) || isInteger(value); const toIdArray = (data) => { const array = castArray(data) .filter((datum) => !isNil(datum)) .map((datum) => { // if it is a string or an integer return an obj with id = to datum if (isValidId(datum)) { return { id: datum, __pivot: {} }; } // if it is an object check it has at least a valid id if (!has('id', datum) || !isValidId(datum.id)) { throw new Error(`Invalid id, expected a string or integer, got ${datum}`); } return datum; }); return uniqWith(isEqual, array); }; const toAssocs = (data) => { if (isArray(data) || isString(data) || isNumber(data) || isNull(data) || data?.id) { return { set: isNull(data) ? data : toIdArray(data), }; } if (data?.set) { return { set: isNull(data.set) ? data.set : toIdArray(data.set), }; } return { connect: toIdArray(data?.connect), disconnect: toIdArray(data?.disconnect), }; }; const processData = (metadata, data = {}, { withDefaults = false } = {}) => { const { attributes } = metadata; const obj = {}; for (const attributeName of Object.keys(attributes)) { const attribute = attributes[attributeName]; if (types.isScalar(attribute.type)) { const field = createField(attribute); if (isUndefined(data[attributeName])) { if (!isUndefined(attribute.default) && withDefaults) { if (typeof attribute.default === 'function') { obj[attributeName] = attribute.default(); } else { obj[attributeName] = attribute.default; } } continue; } if (typeof field.validate === 'function' && data[attributeName] !== null) { field.validate(data[attributeName]); } const val = data[attributeName] === null ? null : field.toDB(data[attributeName]); obj[attributeName] = val; } if (types.isRelation(attribute.type)) { // oneToOne & manyToOne if (attribute.joinColumn && attribute.owner) { const joinColumnName = attribute.joinColumn.name; // allow setting to null const attrValue = !isUndefined(data[attributeName]) ? data[attributeName] : data[joinColumnName]; if (!isUndefined(attrValue)) { obj[joinColumnName] = attrValue; } continue; } if (attribute.morphColumn && attribute.owner) { const { idColumn, typeColumn, typeField = '__type' } = attribute.morphColumn; const value = data[attributeName]; if (value === null) { Object.assign(obj, { [idColumn.name]: null, [typeColumn.name]: null, }); continue; } if (!isUndefined(value)) { if (!has('id', value) || !has(typeField, value)) { throw new Error(`Expects properties ${typeField} an id to make a morph association`); } Object.assign(obj, { [idColumn.name]: value.id, [typeColumn.name]: value[typeField], }); } } } } return obj; }; const createEntityManager = (db) => { const repoMap = {}; return { async findOne(uid, params) { const states = await db.lifecycles.run('beforeFindOne', uid, { params }); const result = await this.createQueryBuilder(uid).init(params).first().execute(); await db.lifecycles.run('afterFindOne', uid, { params, result }, states); return result; }, // should we name it findOne because people are used to it ? async findMany(uid, params) { const states = await db.lifecycles.run('beforeFindMany', uid, { params }); const result = await this.createQueryBuilder(uid).init(params).execute(); await db.lifecycles.run('afterFindMany', uid, { params, result }, states); return result; }, async count(uid, params) { const states = await db.lifecycles.run('beforeCount', uid, { params }); const res = await this.createQueryBuilder(uid) .init(pick(['_q', 'where', 'filters'], params)) .count() .first() .execute(); const result = Number(res.count); await db.lifecycles.run('afterCount', uid, { params, result }, states); return result; }, async create(uid, params = {}) { const states = await db.lifecycles.run('beforeCreate', uid, { params }); const metadata = db.metadata.get(uid); const { data } = params; if (!isPlainObject(data)) { throw new Error('Create expects a data object'); } const dataToInsert = processData(metadata, data, { withDefaults: true }); const res = await this.createQueryBuilder(uid).insert(dataToInsert).execute(); const id = res[0].id || res[0]; await this.attachRelations(uid, id, data); // TODO: in case there is no select or populate specified return the inserted data ? // TODO: do not trigger the findOne lifecycles ? const result = await this.findOne(uid, { where: { id }, select: params.select, populate: params.populate, }); await db.lifecycles.run('afterCreate', uid, { params, result }, states); return result; }, // TODO: where do we handle relation processing for many queries ? async createMany(uid, params = {}) { const states = await db.lifecycles.run('beforeCreateMany', uid, { params }); const metadata = db.metadata.get(uid); const { data } = params; if (!isArray(data)) { throw new Error('CreateMany expects data to be an array'); } const dataToInsert = data.map((datum) => processData(metadata, datum, { withDefaults: true }) ); if (isEmpty(dataToInsert)) { throw new Error('Nothing to insert'); } await this.createQueryBuilder(uid).insert(dataToInsert).execute(); const result = { count: data.length }; await db.lifecycles.run('afterCreateMany', uid, { params, result }, states); return result; }, async update(uid, params = {}) { const states = await db.lifecycles.run('beforeUpdate', uid, { params }); const metadata = db.metadata.get(uid); const { where, data } = params; if (!isPlainObject(data)) { throw new Error('Update requires a data object'); } if (isEmpty(where)) { throw new Error('Update requires a where parameter'); } const entity = await this.createQueryBuilder(uid).select('id').where(where).first().execute(); if (!entity) { return null; } const { id } = entity; const dataToUpdate = processData(metadata, data); if (!isEmpty(dataToUpdate)) { await this.createQueryBuilder(uid).where({ id }).update(dataToUpdate).execute(); } await this.updateRelations(uid, id, data); // TODO: do not trigger the findOne lifecycles ? const result = await this.findOne(uid, { where: { id }, select: params.select, populate: params.populate, }); await db.lifecycles.run('afterUpdate', uid, { params, result }, states); return result; }, // TODO: where do we handle relation processing for many queries ? async updateMany(uid, params = {}) { const states = await db.lifecycles.run('beforeUpdateMany', uid, { params }); const metadata = db.metadata.get(uid); const { where, data } = params; const dataToUpdate = processData(metadata, data); if (isEmpty(dataToUpdate)) { throw new Error('Update requires data'); } const updatedRows = await this.createQueryBuilder(uid) .where(where) .update(dataToUpdate) .execute(); const result = { count: updatedRows }; await db.lifecycles.run('afterUpdateMany', uid, { params, result }, states); return result; }, async delete(uid, params = {}) { const states = await db.lifecycles.run('beforeDelete', uid, { params }); const { where, select, populate } = params; if (isEmpty(where)) { throw new Error('Delete requires a where parameter'); } // TODO: do not trigger the findOne lifecycles ? const entity = await this.findOne(uid, { select: select && ['id'].concat(select), where, populate, }); if (!entity) { return null; } const { id } = entity; await this.createQueryBuilder(uid).where({ id }).delete().execute(); await this.deleteRelations(uid, id); await db.lifecycles.run('afterDelete', uid, { params, result: entity }, states); return entity; }, // TODO: where do we handle relation processing for many queries ? async deleteMany(uid, params = {}) { const states = await db.lifecycles.run('beforeDeleteMany', uid, { params }); const { where } = params; const deletedRows = await this.createQueryBuilder(uid).where(where).delete().execute(); const result = { count: deletedRows }; await db.lifecycles.run('afterDeleteMany', uid, { params, result }, states); return result; }, /** * Attach relations to a new entity * * @param {EntityManager} em - entity manager instance * @param {Metadata} metadata - model metadta * @param {ID} id - entity ID * @param {object} data - data received for creation */ // TODO: wrap Transaction async attachRelations(uid, id, data) { const { attributes } = db.metadata.get(uid); for (const attributeName of Object.keys(attributes)) { const attribute = attributes[attributeName]; const isValidLink = has(attributeName, data) && !isNil(data[attributeName]); if (attribute.type !== 'relation' || !isValidLink) { continue; } const cleanRelationData = toAssocs(data[attributeName]); if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') { const { target, morphBy } = attribute; const targetAttribute = db.metadata.get(target).attributes[morphBy]; if (targetAttribute.relation === 'morphToOne') { // set columns const { idColumn, typeColumn } = targetAttribute.morphColumn; const relId = toId(cleanRelationData.set[0]); await this.createQueryBuilder(target) .update({ [idColumn.name]: id, [typeColumn.name]: uid }) .where({ id: relId }) .execute(); } else if (targetAttribute.relation === 'morphToMany') { const { joinTable } = targetAttribute; const { joinColumn, morphColumn } = joinTable; const { idColumn, typeColumn } = morphColumn; const rows = cleanRelationData.set.map((data, idx) => { return { [joinColumn.name]: data.id, [idColumn.name]: id, [typeColumn.name]: uid, ...(joinTable.on || {}), ...(data.__pivot || {}), order: idx + 1, field: attributeName, }; }); if (isEmpty(rows)) { continue; } await this.createQueryBuilder(joinTable.name).insert(rows).execute(); } continue; } else if (attribute.relation === 'morphToOne') { // handled on the entry itself continue; } else if (attribute.relation === 'morphToMany') { const { joinTable } = attribute; const { joinColumn, morphColumn } = joinTable; const { idColumn, typeColumn, typeField = '__type' } = morphColumn; const rows = cleanRelationData.set.map((data) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], ...(joinTable.on || {}), ...(data.__pivot || {}), })); if (isEmpty(rows)) { continue; } // delete previous relations await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, { uid, attributeName, joinTable, db, }); await this.createQueryBuilder(joinTable.name).insert(rows).execute(); continue; } if (attribute.joinColumn && attribute.owner) { const relIdsToAdd = toIds(cleanRelationData.set); if ( attribute.relation === 'oneToOne' && isBidirectional(attribute) && relIdsToAdd.length ) { await this.createQueryBuilder(uid) .where({ [attribute.joinColumn.name]: relIdsToAdd, id: { $ne: id } }) .update({ [attribute.joinColumn.name]: null }) .execute(); } continue; } // oneToOne oneToMany on the non owning side if (attribute.joinColumn && !attribute.owner) { // need to set the column on the target const { target } = attribute; // TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL) const relIdsToAdd = toIds(cleanRelationData.set); await this.createQueryBuilder(target) .where({ [attribute.joinColumn.referencedColumn]: id }) .update({ [attribute.joinColumn.referencedColumn]: null }) .execute(); await this.createQueryBuilder(target) .update({ [attribute.joinColumn.referencedColumn]: id }) // NOTE: works if it is an array or a single id .where({ id: relIdsToAdd }) .execute(); } if (attribute.joinTable) { // need to set the column on the target const { joinTable } = attribute; const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; const select = [joinColumn.name]; if (isAnyToMany(attribute)) { select.push(orderColumnName); } const relsToAdd = cleanRelationData.set || cleanRelationData.connect; const relIdsToadd = toIds(relsToAdd); await deletePreviousOneToAnyRelations({ id, attribute, joinTable, relIdsToadd, db }); // prepare new relations to insert const insert = relsToAdd.map((data) => { return { [joinColumn.name]: id, [inverseJoinColumn.name]: data.id, ...(joinTable.on || {}), ...(data.__pivot || {}), }; }); // add order value when relevant if (isAnyToMany(attribute)) { insert.forEach((rel, idx) => { rel[orderColumnName] = idx + 1; }); } // add inv_order value when relevant if (isBidirectional(attribute) && isManyToAny(attribute)) { const maxMap = {}; await Promise.all( relIdsToadd.map(async (relId) => { const { max } = await this.createQueryBuilder(joinTable.name) .max(inverseOrderColumnName) .where({ [inverseJoinColumn.name]: relId }) .where(joinTable.on || {}) .first() .execute(); maxMap[relId] = max; }) ); insert.forEach((rel) => { rel[inverseOrderColumnName] = maxMap[rel[inverseJoinColumn.name]] + 1; }); } if (insert.length === 0) { continue; } // insert new relations await this.createQueryBuilder(joinTable.name).insert(insert).execute(); } } }, /** * Updates relations of an existing entity * * @param {EntityManager} em - entity manager instance * @param {Metadata} metadata - model metadta * @param {ID} id - entity ID * @param {object} data - data received for creation */ // TODO: check relation exists (handled by FKs except for polymorphics) // TODO: wrap Transaction async updateRelations(uid, id, data) { const { attributes } = db.metadata.get(uid); for (const attributeName of Object.keys(attributes)) { const attribute = attributes[attributeName]; if (attribute.type !== 'relation' || !has(attributeName, data)) { continue; } const cleanRelationData = toAssocs(data[attributeName]); if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') { const { target, morphBy } = attribute; const targetAttribute = db.metadata.get(target).attributes[morphBy]; if (targetAttribute.relation === 'morphToOne') { // set columns const { idColumn, typeColumn } = targetAttribute.morphColumn; // update instead of deleting because the relation is directly on the entity table // and not in a join table await this.createQueryBuilder(target) .update({ [idColumn.name]: null, [typeColumn.name]: null }) .where({ [idColumn.name]: id, [typeColumn.name]: uid }) .execute(); if (!isNull(cleanRelationData.set)) { const relId = toIds(cleanRelationData.set[0]); await this.createQueryBuilder(target) .update({ [idColumn.name]: id, [typeColumn.name]: uid }) .where({ id: relId }) .execute(); } } else if (targetAttribute.relation === 'morphToMany') { const { joinTable } = targetAttribute; const { joinColumn, morphColumn } = joinTable; const { idColumn, typeColumn } = morphColumn; await this.createQueryBuilder(joinTable.name) .delete() .where({ [idColumn.name]: id, [typeColumn.name]: uid, ...(joinTable.on || {}), field: attributeName, }) .execute(); const rows = cleanRelationData.set.map((data, idx) => ({ [joinColumn.name]: data.id, [idColumn.name]: id, [typeColumn.name]: uid, ...(joinTable.on || {}), ...(data.__pivot || {}), order: idx + 1, field: attributeName, })); if (isEmpty(rows)) { continue; } await this.createQueryBuilder(joinTable.name).insert(rows).execute(); } continue; } if (attribute.relation === 'morphToOne') { // handled on the entry itself continue; } if (attribute.relation === 'morphToMany') { const { joinTable } = attribute; const { joinColumn, morphColumn } = joinTable; const { idColumn, typeColumn, typeField = '__type' } = morphColumn; await this.createQueryBuilder(joinTable.name) .delete() .where({ [joinColumn.name]: id, ...(joinTable.on || {}), }) .execute(); const rows = cleanRelationData.set.map((data) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], ...(joinTable.on || {}), ...(data.__pivot || {}), })); if (isEmpty(rows)) { continue; } // delete previous relations await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, { uid, attributeName, joinTable, db, }); await this.createQueryBuilder(joinTable.name).insert(rows).execute(); continue; } if (attribute.joinColumn && attribute.owner) { // handled in the row itself continue; } // oneToOne oneToMany on the non owning side. // Since it is a join column no need to remove previous relations if (attribute.joinColumn && !attribute.owner) { // need to set the column on the target const { target } = attribute; await this.createQueryBuilder(target) .where({ [attribute.joinColumn.referencedColumn]: id }) .update({ [attribute.joinColumn.referencedColumn]: null }) .execute(); if (!isNull(cleanRelationData.set)) { const relIdsToAdd = toIds(cleanRelationData.set); await this.createQueryBuilder(target) .where({ id: relIdsToAdd }) .update({ [attribute.joinColumn.referencedColumn]: id }) .execute(); } } if (attribute.joinTable) { const { joinTable } = attribute; const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; const select = [joinColumn.name, inverseJoinColumn.name]; if (isAnyToMany(attribute)) { select.push(orderColumnName); } if (isBidirectional(attribute) && isManyToAny(attribute)) { select.push(inverseOrderColumnName); } // only delete relations if (isNull(cleanRelationData.set)) { await deleteAllRelations({ id, attribute, joinTable, db }); } else { const isPartialUpdate = !has('set', cleanRelationData); let relIdsToaddOrMove; if (isPartialUpdate) { // does not support pivot if (isAnyToOne(attribute)) { cleanRelationData.connect = cleanRelationData.connect.slice(-1); cleanRelationData.disconnect = []; } relIdsToaddOrMove = toIds(cleanRelationData.connect); // DELETE relations in disconnect const relIdsToDelete = toIds( differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect) ); await deleteAllRelations({ id, attribute, joinTable, onlyFor: relIdsToDelete, db }); // add/move let max; const currentMovingRels = await this.createQueryBuilder(joinTable.name) .select(select) .where({ [joinColumn.name]: id, [inverseJoinColumn.name]: { $in: relIdsToaddOrMove }, }) .where(joinTable.on || {}) .execute(); const currentMovingRelsMap = currentMovingRels.reduce( (acc, rel) => Object.assign(acc, { [rel[inverseJoinColumn.name]]: rel }), {} ); if (isAnyToMany(attribute)) { max = ( await this.createQueryBuilder(joinTable.name) .max(orderColumnName) .where({ [joinColumn.name]: id }) .where(joinTable.on || {}) .first() .execute() ).max; } for (const relToAddOrMove of cleanRelationData.connect) { const currentRel = currentMovingRelsMap[relToAddOrMove.id]; if (currentRel && isAnyToMany(attribute)) { const currentOrderIsNull = currentRel[orderColumnName] === null; if (!currentOrderIsNull) { await this.createQueryBuilder(joinTable.name) .decrement(orderColumnName, 1) .where({ [joinColumn.name]: id, [orderColumnName]: { $gt: currentRel[orderColumnName] }, }) .where(joinTable.on || {}) .execute(); currentMovingRels.forEach((rel) => { if (rel[orderColumnName] > currentRel[orderColumnName]) { rel[orderColumnName] -= 1; } }); } if (currentOrderIsNull) { max += 1; } await this.createQueryBuilder(joinTable.name) .update({ [orderColumnName]: max, }) .where({ [joinColumn.name]: id, [inverseJoinColumn.name]: relToAddOrMove.id, }) .where(joinTable.on || {}) .execute(); } else if (!currentRel) { const insert = { [joinColumn.name]: id, [inverseJoinColumn.name]: relToAddOrMove.id, ...(relToAddOrMove.__pivot || {}), ...(joinTable.on || {}), }; if (isAnyToMany(attribute)) { insert[orderColumnName] = max + 1; } if (isBidirectional(attribute) && isManyToAny(attribute)) { const { max: reverseMax } = await this.createQueryBuilder(joinTable.name) .max(inverseOrderColumnName) .where({ [inverseJoinColumn.name]: relToAddOrMove.id }) .where(joinTable.on || {}) .first() .execute(); insert[inverseOrderColumnName] = reverseMax + 1; } await this.createQueryBuilder(joinTable.name).insert(insert).execute(); max += 1; } } } else { // overwrite all relations relIdsToaddOrMove = toIds(cleanRelationData.set); await deleteAllRelations({ id, attribute, joinTable, except: relIdsToaddOrMove, db }); const currentMovingRels = await this.createQueryBuilder(joinTable.name) .select(select) .where({ [joinColumn.name]: id, [inverseJoinColumn.name]: { $in: relIdsToaddOrMove }, }) .where(joinTable.on || {}) .execute(); const currentMovingRelsMap = currentMovingRels.reduce( (acc, rel) => Object.assign(acc, { [rel[inverseJoinColumn.name]]: rel }), {} ); let index = 0; for (const relToAdd of cleanRelationData.set) { const currentRel = currentMovingRelsMap[relToAdd.id]; if (currentRel && isAnyToMany(attribute)) { const update = { [orderColumnName]: index + 1 }; await this.createQueryBuilder(joinTable.name) .update(update) .where({ [joinColumn.name]: id, [inverseJoinColumn.name]: relToAdd.id, }) .where(joinTable.on || {}) .execute(); } else if (!currentRel) { const insert = { [joinColumn.name]: id, [inverseJoinColumn.name]: relToAdd.id, ...(joinTable.on || {}), ...(relToAdd.__pivot || {}), }; if (isAnyToMany(attribute)) { insert[orderColumnName] = index + 1; } // can be optimized in one query if (isBidirectional(attribute) && isManyToAny(attribute)) { const { max: reverseMax } = await this.createQueryBuilder(joinTable.name) .max(inverseOrderColumnName) .where({ [inverseJoinColumn.name]: id }) .where(joinTable.on || {}) .first() .execute(); insert[inverseOrderColumnName] = reverseMax + 1; } await this.createQueryBuilder(joinTable.name).insert(insert).execute(); } index += 1; } } // Delete the previous relations for oneToAny relations await deletePreviousOneToAnyRelations({ id, attribute, joinTable, relIdsToadd: relIdsToaddOrMove, db, }); // Delete the previous relations for anyToOne relations await deletePreviousAnyToOneRelations({ id, attribute, joinTable, relIdsToadd: relIdsToaddOrMove, db, }); } } } }, /** * Delete relational associations of an existing entity * This removes associations but doesn't do cascade deletions for components for example. This will be handled on the entity service layer instead * NOTE: Most of the deletion should be handled by ON DELETE CASCADE for dialects that have FKs * * @param {EntityManager} em - entity manager instance * @param {Metadata} metadata - model metadta * @param {ID} id - entity ID */ // TODO: wrap Transaction async deleteRelations(uid, id) { const { attributes } = db.metadata.get(uid); for (const attributeName of Object.keys(attributes)) { const attribute = attributes[attributeName]; if (attribute.type !== 'relation') { continue; } /* if morphOne | morphMany if morphBy is morphToOne set null if morphBy is morphToOne delete links */ if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') { const { target, morphBy } = attribute; const targetAttribute = db.metadata.get(target).attributes[morphBy]; if (targetAttribute.relation === 'morphToOne') { // set columns const { idColumn, typeColumn } = targetAttribute.morphColumn; await this.createQueryBuilder(target) .update({ [idColumn.name]: null, [typeColumn.name]: null }) .where({ [idColumn.name]: id, [typeColumn.name]: uid }) .execute(); } else if (targetAttribute.relation === 'morphToMany') { const { joinTable } = targetAttribute; const { morphColumn } = joinTable; const { idColumn, typeColumn } = morphColumn; await this.createQueryBuilder(joinTable.name) .delete() .where({ [idColumn.name]: id, [typeColumn.name]: uid, ...(joinTable.on || {}), field: attributeName, }) .execute(); } continue; } /* if morphToOne nothing to do */ if (attribute.relation === 'morphToOne') { // do nothing } /* if morphToMany delete links */ if (attribute.relation === 'morphToMany') { const { joinTable } = attribute; const { joinColumn } = joinTable; await this.createQueryBuilder(joinTable.name) .delete() .where({ [joinColumn.name]: id, ...(joinTable.on || {}), }) .execute(); continue; } // do not need to delete links when using foreign keys if (db.dialect.usesForeignKeys()) { return; } // NOTE: we do not remove existing associations with the target as it should handled by unique FKs instead if (attribute.joinColumn && attribute.owner) { // nothing to do => relation already added on the table continue; } // oneToOne oneToMany on the non owning side. if (attribute.joinColumn && !attribute.owner) { // need to set the column on the target const { target } = attribute; await this.createQueryBuilder(target) .where({ [attribute.joinColumn.referencedColumn]: id }) .update({ [attribute.joinColumn.referencedColumn]: null }) .execute(); } if (attribute.joinTable) { const { joinTable } = attribute; await deleteAllRelations({ id, attribute, joinTable, db }); } } }, // TODO: add lifecycle events async populate(uid, entity, populate) { const entry = await this.findOne(uid, { select: ['id'], where: { id: entity.id }, populate, }); return { ...entity, ...entry }; }, // TODO: add lifecycle events async load(uid, entity, fields, params) { const { attributes } = db.metadata.get(uid); const fieldsArr = castArray(fields); fieldsArr.forEach((field) => { const attribute = attributes[field]; if (!attribute || attribute.type !== 'relation') { throw new Error(`Invalid load. Expected ${field} to be a relational attribute`); } }); const entry = await this.findOne(uid, { select: ['id'], where: { id: entity.id }, populate: fieldsArr.reduce((acc, field) => { acc[field] = params || true; return acc; }, {}), }); if (!entry) { return null; } if (Array.isArray(fields)) { return pick(fields, entry); } return entry[fields]; }, // cascading // aggregations // -> avg // -> min // -> max // -> grouping // formulas // custom queries // utilities // -> map result // -> map input // extra features // -> virtuals // -> private createQueryBuilder(uid) { return createQueryBuilder(uid, db); }, getRepository(uid) { if (!repoMap[uid]) { repoMap[uid] = createRepository(uid, db); } return repoMap[uid]; }, clearRepositories() { repoMap.clear(); }, }; }; module.exports = { createEntityManager, };