@kitmi/data
Version:
Jacaranda Framework Data Access Model
620 lines (619 loc) • 26.6 kB
JavaScript
"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