UNPKG

@kitmi/data

Version:

Jacaranda Framework Data Access Model

620 lines (619 loc) 26.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); function _export(target, all) { for(var name in all)Object.defineProperty(target, name, { enumerable: true, get: all[name] }); } _export(exports, { default: function() { return _default; }, putInTopoSort: function() { return putInTopoSort; } }); const _utils = require("@kitmi/utils"); const _EntityModel = /*#__PURE__*/ _interop_require_wildcard(require("../../EntityModel")); const _types = require("@kitmi/types"); const _algo = require("@kitmi/algo"); const _helpers = require("../../helpers"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interop_require_wildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = { __proto__: null }; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for(var key in obj){ if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function putInTopoSort(topoSort, assoc, anchor) { if (assoc.on) { _utils._.each(assoc.on, (value, key)=>{ const pos = key.indexOf('.'); if (pos > 0) { const base = key.substring(0, pos); if (base !== anchor) { topoSort.add(base, anchor); } } if (typeof value === 'object' && value.$xr === 'Column') { const pos2 = value.name.indexOf('.'); if (pos2 > 0) { const base = value.name.substring(0, pos2); if (base !== anchor) { topoSort.add(base, anchor); } } } }); } } /** * Relational entity model class. */ class RelationalEntityModel extends _EntityModel.default { /** * [specific] Check if this entity has auto increment feature. */ get hasAutoIncrement() { const autoId = this.meta.features.autoId; return autoId && this.meta.fields[autoId.field].autoIncrementId; } _ensureNoAssociations(data) { for(const k in data){ if (k[0] === ':' || k[0] === '@') { throw new _types.ValidationError(`Association data "${k}" is not allowed in entity "${this.meta.name}".`); } } } _uniqueRelations(relations) { const [normalAssocs, customAssocs] = _utils._.partition(relations, (assoc)=>typeof assoc === 'string'); return customAssocs.concat(_utils._.uniq(normalAssocs).sort()); } /** * Preprocess relationships for non-ORM operations. * @param {*} findOptions */ _prepareAssociations(findOptions, isOne) { const associations = this._uniqueRelations(findOptions.$relation); const assocTable = {}; const cache = {}; const topoSort = new _algo.TopoSort(); associations.forEach((assoc)=>{ if (typeof assoc === 'object') { let { anchor, alias, ...override } = assoc; if (anchor) { this._loadAssocIntoTable(findOptions, assocTable, cache, anchor, override); return; } if (!alias) { alias = assoc.entity; } if (this.meta.associations && alias in this.meta.associations) { throw new _types.InvalidArgument(`Alias "${alias}" conflicts with a predefined association.`, { entity: this.meta.name, alias }); } if (!assoc.entity) { throw new _types.InvalidArgument('Missing "entity" for custom association.', { entity: this.meta.name, alias }); } putInTopoSort(topoSort, assoc, alias); cache[alias] = assocTable[alias] = { ...assoc }; } else { this._loadAssocIntoTable(findOptions, assocTable, cache, assoc); } }); const dependencies = topoSort.sort(); dependencies.forEach((dep)=>{ const item = assocTable[dep]; if (item == null) { throw new _types.InvalidArgument(`Association "${dep}" not found.`, { entity: this.meta.name, assoc: dep }); } delete assocTable[dep]; assocTable[dep] = item; }); if (!isOne && associations.length > 0 && !findOptions.$skipOrm) { // to ensure correct merging, root id is required for fetching a list findOptions.$select?.add(this.meta.keyField); } return assocTable; } /** * Load association into table * @param {*} assocTable - Hierarchy with subAssocs * @param {*} cache - Dotted path as key * @param {*} assoc - Dotted path */ _loadAssocIntoTable(findOptions, assocTable, cache, assoc, override) { if (cache[assoc]) return cache[assoc]; const lastPos = assoc.lastIndexOf('.'); let result; if (lastPos === -1) { // direct association const assocInfo = { ...this.meta.associations[assoc], ...override }; if ((0, _utils.isEmpty)(assocInfo)) { throw new _types.InvalidArgument(`Entity "${this.meta.name}" does not have the association "${assoc}".`); } if (assocInfo.list && !findOptions.$skipOrm && !(0, _helpers.isSelectAll)(findOptions.$select)) { findOptions.$select.add(assoc + '.' + assocInfo.key); } result = assocTable[assoc] = assocInfo; cache[assoc] = result; } else { const base = assoc.substring(0, lastPos); const last = assoc.substring(lastPos + 1); let baseNode = cache[base]; if (!baseNode) { baseNode = this._loadAssocIntoTable(findOptions, assocTable, cache, base); } const entity = this.db.entity(baseNode.entity); const assocInfo = { ...entity.meta.associations[last], ...override }; if ((0, _utils.isEmpty)(assocInfo)) { throw new _types.InvalidArgument(`Entity "${entity.meta.name}" does not have the association "${assoc}".`); } result = assocInfo; if (assocInfo.list && !findOptions.$skipOrm && !(0, _helpers.isSelectAll)(findOptions.$select)) { findOptions.$select.add(assoc + '.' + assocInfo.key); } if (!baseNode.subAssocs) { baseNode.subAssocs = {}; } baseNode.subAssocs[last] = result; cache[assoc] = result; } return result; } /** * Pre-process assoicated db operation * @param {*} data * @param {*} isNew - New record flag, true for creating, false for updating * @returns {Array} [raw, assocs, refs]; */ _extractAssociations(data, isNew) { const raw = {}; const assocs = {}; const refs = {}; const meta = this.meta.associations; _utils._.each(data, (v, k)=>{ if (k[0] === ':') { // cascade update const anchor = k.substring(1); const assocMeta = meta?.[anchor]; if (!assocMeta) { throw new _types.ValidationError(`Unknown association "${anchor}" of entity "${this.meta.name}".`); } if (isNew && assocMeta.type && anchor in data) { throw new _types.ValidationError(`Association data ":${anchor}" of entity "${this.meta.name}" conflicts with input value of field "${anchor}".`); } assocs[anchor] = v; return; } if (k[0] === '@') { // update by reference const anchor = k.substring(1); const assocMeta = meta?.[anchor]; if (!assocMeta) { throw new _types.ValidationError(`Unknown association "${anchor}" of entity "${this.meta.name}".`); } if (!assocMeta.type) { throw new _types.ValidationError(`Association "${anchor}" cannot be used for updating by reference. Only "refersTo" or "belongsTo" is suppported.`, { entity: this.meta.name, anchor }); } if (isNew && anchor in data) { throw new _types.ValidationError(`Association reference "@${anchor}" of entity "${this.meta.name}" conflicts with input value of field "${anchor}".`); } const assocAnchor = ':' + anchor; if (assocAnchor in data) { throw new _types.ValidationError(`Association reference "@${anchor}" of entity "${this.meta.name}" conflicts with association data "${assocAnchor}".`); } if (v == null) { raw[anchor] = null; } else { if (typeof v !== 'object') { throw new _types.ValidationError(`Association reference "@${anchor}" of entity "${this.meta.name}" expects an object as query condition.`, { entity: this.meta.name, anchor }); } refs[anchor] = v; } return; } raw[k] = v; }); return [ raw, assocs, refs ]; } async _populateReferences_(context, references) { const meta = this.meta.associations; await (0, _utils.eachAsync_)(references, async (refQuery, anchor)=>{ const assocMeta = meta[anchor]; const ReferencedEntity = this.db.entity(assocMeta.entity); const created = await ReferencedEntity.findOne_({ ...refQuery, $select: [ assocMeta.field ] }); if (created == null) { throw new _types.ReferencedNotExist(`Referenced entity "${ReferencedEntity.meta.name}" with ${JSON.stringify(refQuery)} not exist.`); } context.raw[anchor] = created[assocMeta.field]; }); } async _createAssocs_(context, assocs, beforeEntityCreate) { const meta = this.meta.associations; const opOptions = context.options; let keyValue; if (!beforeEntityCreate) { keyValue = context.result.data[this.meta.keyField]; if (keyValue == null) { if (context.result.affectedRows === 0) { // only happens when insert ignored const query = this.getUniqueKeyValuePairsFrom(context.result.data); const data = await this.findOne_({ $select: [ this.meta.keyField ], $where: query }); if (data == null) { throw new _types.ApplicationError('The parent entity cannot be found using the unique key(s) from data.', { entity: this.meta.name, query }); } Object.assign(context.result.data, data); keyValue = data[this.meta.keyField]; } if (keyValue == null) { throw new _types.ApplicationError('Missing required primary key field value.', { entity: this.meta.name, data: context.result.data }); } } } const pendingAssocs = {}; const finished = {}; // todo: double check to ensure including all required options const passOnOptions = _utils._.pick(opOptions, [ '$skipModifiers', '$migration', '$ctx', '$dryRun' ]); await (0, _utils.eachAsync_)(assocs, async (data, anchor)=>{ const assocMeta = meta?.[anchor]; if (assocMeta == null) { throw new _types.InvalidArgument(`Association "${anchor}" of entity "${this.meta.name}" not found.`, { entity: this.meta.name, anchor }); } if (beforeEntityCreate && !assocMeta.type) { // reverse reference should be created after the entity is created pendingAssocs[anchor] = data; return; } const assocModel = this.db.entity(assocMeta.entity); if (assocMeta.list) { // hasMany if (!Array.isArray(data)) { throw new _types.InvalidArgument(`Association "${anchor}" is a list, array is expected.`, { entity: this.meta.name, anchor, remote: assocMeta.entity }); } return (0, _utils.batchAsync_)(data, (item)=>assocModel.create_({ ...item, [assocMeta.field]: keyValue }, { ...passOnOptions, $upsert: true })); } if (!(0, _utils.isPlainObject)(data)) { throw new _types.InvalidArgument(`Association "${anchor}" expects an object as input.`, { entity: this.meta.name, anchor, remote: assocMeta.entity }); } if (!beforeEntityCreate) { // hasOne data = { ...data, [assocMeta.field]: keyValue }; } let created = await assocModel.create_(data, { ...passOnOptions, $upsert: true }); if (!beforeEntityCreate) { return; } if (created.affectedRows === 0 || assocModel.hasAutoIncrement && created.insertId === 0) { // insert ignored or upserted const assocQuery = assocModel.getUniqueKeyValuePairsFrom(data); const _data = await assocModel.findOne_({ $select: [ assocModel.meta.keyField ], $where: assocQuery }); if (_data == null) { throw new _types.ApplicationError('The child entity cannot be found using the unique key(s) from data.', { entity: assocModel.meta.name, query: assocQuery }); } } finished[anchor] = opOptions.$dryRun ? _EntityModel.FLAG_DRY_RUN_IGNORE : created.data[assocMeta.field]; }); if (beforeEntityCreate) { Object.assign(context.raw, finished); } return pendingAssocs; } async _updateAssocs_(context, assocs, beforeEntityUpdate) { const meta = this.meta.associations; const opOptions = context.options; let currentKeyValue; if (!beforeEntityUpdate) { currentKeyValue = (0, _helpers.getValueFromAny)([ opOptions.$where, context.result.data ], this.meta.keyField); if (currentKeyValue == null) { // should have in updating throw new _types.ApplicationError('Missing required primary key field value.', { entity: this.meta.name, query: opOptions.$where, data: context.result.data }); } } const pendingAssocs = {}; // todo: double check to ensure including all required options const passOnOptions = _utils._.pick(context.options, [ '$skipModifiers', '$migration', '$ctx', '$upsert' ]); await (0, _utils.eachAsync_)(assocs, async (data, anchor)=>{ const assocMeta = meta?.[anchor]; if (assocMeta == null) { throw new _types.InvalidArgument(`Association "${anchor}" of entity "${this.meta.name}" not found.`, { entity: this.meta.name, anchor }); } if (beforeEntityUpdate && !assocMeta.type) { // reverse reference should be updated after the entity is updated pendingAssocs[anchor] = data; return; } const assocModel = this.db.entity(assocMeta.entity); if (assocMeta.list) { // has many if (!Array.isArray(data)) { if (typeof data === 'object') { const { $delete, $update, $create, ...others } = data; if (!(0, _utils.isEmpty)(others)) { throw new _types.InvalidArgument(`Association "${anchor}" is a list and array or object with { $delete, $update, $create } is expected.`, { entity: this.meta.name, anchor, remote: assocMeta.entity }); } if ($delete) { if (!Array.isArray($delete)) { throw new _types.InvalidArgument(`"$delete" operation of "hasMany" association "${anchor}" requires an array.`, { entity: this.meta.name, anchor, remote: assocMeta.entity }); } const keysToDelete = $delete.map((item, index)=>{ const keyValue = item[assocModel.meta.keyField]; if (keyValue == null) { throw new _types.InvalidArgument(`The key field "${assocModel.meta.keyField}" is required for deletion.`, { mainEntity: this.meta.name, childEntity: assocModel.meta.name, index, item }); } return keyValue; }); await assocModel.deleteMany_({ [assocModel.meta.keyField]: { $in: keysToDelete } }, { ...passOnOptions }); } if ($update) { if (!Array.isArray($update)) { throw new _types.InvalidArgument(`"$update" operation of "hasMany" association "${anchor}" requires an array.`, { entity: this.meta.name, anchor, remote: assocMeta.entity }); } await (0, _utils.batchAsync_)($update, async (item, index)=>{ const keyValue = item[assocModel.meta.keyField]; if (keyValue == null) { throw new _types.InvalidArgument(`The key field "${assocModel.meta.keyField}" is required for updating.`, { mainEntity: this.meta.name, childEntity: assocModel.meta.name, index, item }); } await assocModel.updateOne_({ ...item, [assocModel.meta.keyField]: undefined }, { ...passOnOptions, $where: { [assocModel.meta.keyField]: keyValue } }); }); } if ($create) { if (!Array.isArray($create)) { throw new _types.InvalidArgument(`"$create" operation of "hasMany" association "${anchor}" requires an array.`, { entity: this.meta.name, anchor, remote: assocMeta.entity }); } await (0, _utils.batchAsync_)($create, (item)=>assocModel.create_({ ...item, [assocMeta.field]: currentKeyValue }, { ...passOnOptions, $getCreated: null })); } return; } throw new _types.InvalidArgument(`Association "${anchor}" is a list and array is expected.`, { entity: this.meta.name, anchor, remote: assocMeta.entity }); } return (0, _utils.batchAsync_)(data, (item)=>assocModel.create_({ ...item, [assocMeta.field]: currentKeyValue }, { ...passOnOptions, $upsert: true, $getCreated: null })); } if (!(0, _utils.isPlainObject)(data)) { throw new _types.InvalidArgument(`Association "${anchor}" expects an object as input.`, { entity: this.meta.name, anchor, remote: assocMeta.entity }); } if (beforeEntityUpdate) { if ((0, _utils.isEmpty)(data)) return; // refersTo or belongsTo let destEntityId = (0, _helpers.getValueFromAny)([ context.existing, context.options.$where, context.raw ], anchor); if (destEntityId == null) { if ((0, _utils.isEmpty)(context.existing)) { context.existing = await this.findOne_({ $where: context.options.$where }); if (!context.existing) { throw new _types.ValidationError(`The entity "${this.meta.name}" to be update is not found.`, { query: context.options.$where }); } destEntityId = context.existing[anchor]; } if (destEntityId == null) { // to create the associated, existing is null let created = await assocModel.create_(data, { ...passOnOptions, $upsert: true, $getCreated: [ assocMeta.field ] }); if (created.affectedRows === 0) { throw new _types.DatabaseError('Failed to create or update the referenced entity.', { entity: assocModel.meta.name, data }); } context.raw[anchor] = created.data[assocMeta.field]; return; } } return assocModel.updateOne_(data, { ...passOnOptions, [assocMeta.field]: destEntityId }); } // hasOne return assocModel.create_({ ...data, [assocMeta.field]: currentKeyValue }, { ...passOnOptions, $upsert: true, $getCreated: null }); }); return pendingAssocs; } } const _default = RelationalEntityModel; //# sourceMappingURL=EntityModel.js.map