Files
pole-book/server/node_modules/@strapi/database/dist/entity-manager/index.mjs

1185 lines
58 KiB
JavaScript

import { pick, isPlainObject, isArray, isEmpty, has, isNil, uniqBy, isNull, compact, differenceWith, isEqual, difference, map, castArray, isObject, isString, isInteger, isNumber, isUndefined, uniqWith } from 'lodash/fp';
import { isScalarAttribute, isRelationalAttribute } from '../utils/types.mjs';
import { createField } from '../fields/index.mjs';
import createQueryBuilder from '../query/query-builder.mjs';
import { createRepository } from './entity-repository.mjs';
import { encodePolymorphicRelation, deleteRelatedMorphOneRelationsAfterMorphToManyUpdate, encodePolymorphicId } from './morph-relations.mjs';
import { isBidirectional, isOneToAny, hasOrderColumn, hasInverseOrderColumn, isAnyToOne } from '../metadata/relations.mjs';
import '../utils/identifiers/index.mjs';
import { deletePreviousOneToAnyRelations, deleteRelations, cleanOrderColumns, deletePreviousAnyToOneRelations } from './regular-relations.mjs';
import { relationsOrderer } from './relations-orderer.mjs';
const isRecord = (value)=>isObject(value) && !isNil(value);
const toId = (value)=>{
if (isRecord(value) && 'id' in value && isValidId(value.id)) {
return value.id;
}
if (isValidId(value)) {
return value;
}
throw new Error(`Invalid id, expected a string or integer, got ${JSON.stringify(value)}`);
};
const toIds = (value)=>castArray(value || []).map(toId);
const isValidId = (value)=>isString(value) || isInteger(value);
const isValidObjectId = (value)=>isRecord(value) && 'id' in value && isValidId(value.id);
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 (!isValidObjectId(datum)) {
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) || isRecord(data) && 'id' in data) {
return {
set: isNull(data) ? data : toIdArray(data)
};
}
if (data?.set) {
return {
set: isNull(data.set) ? data.set : toIdArray(data.set)
};
}
return {
options: {
strict: data?.options?.strict
},
connect: toIdArray(data?.connect).map((elm)=>({
id: elm.id,
position: elm.position ? elm.position : {
end: true
},
__pivot: elm.__pivot ?? {},
__type: elm.__type
})),
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 (isScalarAttribute(attribute)) {
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 ('validate' in field && 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 (isRelationalAttribute(attribute)) {
// oneToOne & manyToOne
if ('joinColumn' in attribute && attribute.joinColumn && attribute.owner) {
const joinColumnName = attribute.joinColumn.name;
// allow setting to null
const attrValue = !isUndefined(data[attributeName]) ? data[attributeName] : data[joinColumnName];
if (isNull(attrValue)) {
obj[joinColumnName] = attrValue;
} else if (!isUndefined(attrValue)) {
obj[joinColumnName] = toId(attrValue);
}
continue;
}
if ('morphColumn' in attribute && 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 = isRecord(res[0]) ? res[0].id : res[0];
const trx = await strapi.db.transaction();
try {
await this.attachRelations(uid, id, data, {
transaction: trx.get()
});
await trx.commit();
} catch (e) {
await trx.rollback();
await this.createQueryBuilder(uid).where({
id
}).delete().execute();
throw e;
}
// 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,
filters: params.filters
});
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');
}
const createdEntries = await this.createQueryBuilder(uid).insert(dataToInsert).execute();
const result = {
count: data.length,
ids: createdEntries.map((entry)=>typeof entry === 'object' ? entry?.id : entry)
};
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('*').where(where).first().execute({
mapResults: false
});
if (!entity) {
return null;
}
const { id } = entity;
const dataToUpdate = processData(metadata, data);
if (!isEmpty(dataToUpdate)) {
await this.createQueryBuilder(uid).where({
id
}).update(dataToUpdate).execute();
}
const trx = await strapi.db.transaction();
try {
await this.updateRelations(uid, id, data, {
transaction: trx.get()
});
await trx.commit();
} catch (e) {
await trx.rollback();
await this.createQueryBuilder(uid).where({
id
}).update(entity).execute();
throw e;
}
// TODO: do not trigger the findOne lifecycles ?
const result = await this.findOne(uid, {
where: {
id
},
select: params.select,
populate: params.populate,
filters: params.filters
});
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();
const trx = await strapi.db.transaction();
try {
await this.deleteRelations(uid, id, {
transaction: trx.get()
});
await trx.commit();
} catch (e) {
await trx.rollback();
throw e;
}
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({
mapResults: false
});
const result = {
count: deletedRows
};
await db.lifecycles.run('afterDeleteMany', uid, {
params,
result
}, states);
return result;
},
/**
* Attach relations to a new entity
*/ async attachRelations (uid, id, data, options) {
const { attributes } = db.metadata.get(uid);
const { transaction: trx } = options ?? {};
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') {
/**
* morphOne and morphMany relations
*/ const { target, morphBy } = attribute;
const targetAttribute = db.metadata.get(target).attributes[morphBy];
if (targetAttribute.type !== 'relation') {
throw new Error(`Expected target attribute ${target}.${morphBy} to be a relation attribute`);
}
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
}).transacting(trx).execute();
} else if (targetAttribute.relation === 'morphToMany') {
const { joinTable } = targetAttribute;
const { joinColumn, morphColumn } = joinTable;
const { idColumn, typeColumn } = morphColumn;
if (isEmpty(cleanRelationData.set)) {
continue;
}
const rows = cleanRelationData.set?.map((data, idx)=>{
return {
[joinColumn.name]: data.id,
[idColumn.name]: id,
[typeColumn.name]: uid,
...'on' in joinTable && joinTable.on || {},
...data.__pivot || {},
order: idx + 1,
field: attributeName
};
}) ?? [];
await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
}
continue;
} else if (attribute.relation === 'morphToOne') {
continue;
} else if (attribute.relation === 'morphToMany') {
/**
* morphToMany
*/ const { joinTable } = attribute;
const { joinColumn, morphColumn } = joinTable;
const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
if (isEmpty(cleanRelationData.set) && isEmpty(cleanRelationData.connect)) {
continue;
}
// set happens before connect/disconnect
const dataset = cleanRelationData.set || cleanRelationData.connect || [];
const rows = dataset.map((data, idx)=>({
[joinColumn.name]: id,
[idColumn.name]: data.id,
[typeColumn.name]: data[typeField],
...'on' in joinTable && joinTable.on || {},
...data.__pivot || {},
order: idx + 1
}));
const orderMap = relationsOrderer([], morphColumn.idColumn.name, 'order', true // Always make a strict connect when inserting
).connect(// Merge id & __type to get a single id key
dataset.map(encodePolymorphicRelation({
idColumn: 'id',
typeColumn: typeField
}))).get()// set the order based on the order of the ids
.reduce((acc, rel, idx)=>({
...acc,
[rel.id]: idx + 1
}), {});
rows.forEach((row)=>{
const rowId = row[morphColumn.idColumn.name];
const rowType = row[morphColumn.typeColumn.name];
const encodedId = encodePolymorphicId(rowId, rowType);
row.order = orderMap[encodedId];
});
// delete previous relations
await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
uid,
attributeName,
joinTable,
db,
transaction: trx
});
await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
continue;
}
if ('joinColumn' in attribute && 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
}).transacting(trx).execute();
}
continue;
}
// oneToOne oneToMany on the non owning side
if ('joinColumn' in attribute && 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
}).transacting(trx).execute();
await this.createQueryBuilder(target).update({
[attribute.joinColumn.referencedColumn]: id
})// NOTE: works if it is an array or a single id
.where({
id: relIdsToAdd
}).transacting(trx).execute();
}
if ('joinTable' in attribute && attribute.joinTable) {
// need to set the column on the target
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
const relsToAdd = (cleanRelationData.set || cleanRelationData.connect) ?? [];
const relIdsToadd = toIds(relsToAdd);
if (isBidirectional(attribute) && isOneToAny(attribute)) {
await deletePreviousOneToAnyRelations({
id,
attribute,
relIdsToadd,
db,
transaction: trx
});
}
// prepare new relations to insert
const insert = uniqBy('id', relsToAdd).map((data)=>{
return {
[joinColumn.name]: id,
[inverseJoinColumn.name]: data.id,
...'on' in joinTable && joinTable.on || {},
...data.__pivot || {}
};
});
// add order value
if (cleanRelationData.set && hasOrderColumn(attribute)) {
insert.forEach((data, idx)=>{
data[orderColumnName] = idx + 1;
});
} else if (cleanRelationData.connect && hasOrderColumn(attribute)) {
// use position attributes to calculate order
const orderMap = relationsOrderer([], inverseJoinColumn.name, joinTable.orderColumnName, true // Always make an strict connect when inserting
).connect(relsToAdd).get()// set the order based on the order of the ids
.reduce((acc, rel, idx)=>({
...acc,
[rel.id]: idx
}), {});
insert.forEach((row)=>{
row[orderColumnName] = orderMap[row[inverseJoinColumn.name]];
});
}
// add inv_order value
if (hasInverseOrderColumn(attribute)) {
const maxResults = await db.getConnection().select(inverseJoinColumn.name).max(inverseOrderColumnName, {
as: 'max'
}).whereIn(inverseJoinColumn.name, relIdsToadd).where(joinTable.on || {}).groupBy(inverseJoinColumn.name).from(joinTable.name).transacting(trx);
const maxMap = maxResults.reduce((acc, res)=>Object.assign(acc, {
[res[inverseJoinColumn.name]]: res.max
}), {});
insert.forEach((rel)=>{
rel[inverseOrderColumnName] = (maxMap[rel[inverseJoinColumn.name]] || 0) + 1;
});
}
if (insert.length === 0) {
continue;
}
// insert new relations
await this.createQueryBuilder(joinTable.name).insert(insert).transacting(trx).execute();
}
}
},
/**
* Updates relations of an existing entity
*/ // TODO: check relation exists (handled by FKs except for polymorphics)
async updateRelations (uid, id, data, options) {
const { attributes } = db.metadata.get(uid);
const { transaction: trx } = options ?? {};
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.type === 'relation' && 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
}).transacting(trx).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
}).transacting(trx).execute();
}
} else if (targetAttribute.type === 'relation' && targetAttribute.relation === 'morphToMany') {
const { joinTable } = targetAttribute;
const { joinColumn, morphColumn } = joinTable;
const { idColumn, typeColumn } = morphColumn;
const hasSet = !isEmpty(cleanRelationData.set);
const hasConnect = !isEmpty(cleanRelationData.connect);
const hasDisconnect = !isEmpty(cleanRelationData.disconnect);
// for connect/disconnect without a set, only modify those relations
if (!hasSet && (hasConnect || hasDisconnect)) {
// delete disconnects and connects (to prevent duplicates when we add them later)
const idsToDelete = [
...cleanRelationData.disconnect || [],
...cleanRelationData.connect || []
];
if (!isEmpty(idsToDelete)) {
const where = {
$or: idsToDelete.map((item)=>{
return {
[idColumn.name]: id,
[typeColumn.name]: uid,
[joinColumn.name]: item.id,
...joinTable.on || {},
field: attributeName
};
})
};
await this.createQueryBuilder(joinTable.name).delete().where(where).transacting(trx).execute();
}
// connect relations
if (hasConnect) {
// Query database to find the order of the last relation
const start = await this.createQueryBuilder(joinTable.name).where({
[idColumn.name]: id,
[typeColumn.name]: uid,
...joinTable.on || {},
...data.__pivot || {}
}).max('order').first().transacting(trx).execute();
const startOrder = start?.max || 0;
const rows = (cleanRelationData.connect ?? []).map((data, idx)=>({
[joinColumn.name]: data.id,
[idColumn.name]: id,
[typeColumn.name]: uid,
...joinTable.on || {},
...data.__pivot || {},
order: startOrder + idx + 1,
field: attributeName
}));
await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
}
continue;
}
// delete all relations
await this.createQueryBuilder(joinTable.name).delete().where({
[idColumn.name]: id,
[typeColumn.name]: uid,
...joinTable.on || {},
field: attributeName
}).transacting(trx).execute();
if (hasSet) {
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
}));
await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
}
}
continue;
}
if (attribute.relation === 'morphToOne') {
continue;
}
if (attribute.relation === 'morphToMany') {
const { joinTable } = attribute;
const { joinColumn, morphColumn } = joinTable;
const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
const hasSet = !isEmpty(cleanRelationData.set);
const hasConnect = !isEmpty(cleanRelationData.connect);
const hasDisconnect = !isEmpty(cleanRelationData.disconnect);
// for connect/disconnect without a set, only modify those relations
if (!hasSet && (hasConnect || hasDisconnect)) {
// delete disconnects and connects (to prevent duplicates when we add them later)
const idsToDelete = [
...cleanRelationData.disconnect || [],
...cleanRelationData.connect || []
];
const rowsToDelete = [
...(cleanRelationData.disconnect ?? []).map((data, idx)=>({
[joinColumn.name]: id,
[idColumn.name]: data.id,
[typeColumn.name]: data[typeField],
...'on' in joinTable && joinTable.on || {},
...data.__pivot || {},
order: idx + 1
})),
...(cleanRelationData.connect ?? []).map((data, idx)=>({
[joinColumn.name]: id,
[idColumn.name]: data.id,
// @ts-expect-error TODO
[typeColumn.name]: data[typeField],
...'on' in joinTable && joinTable.on || {},
...data.__pivot || {},
order: idx + 1
}))
];
const adjacentRelations = await this.createQueryBuilder(joinTable.name).where({
$or: [
{
[joinColumn.name]: id,
[idColumn.name]: {
$in: compact(cleanRelationData.connect?.map((r)=>r.position?.after || r.position?.before))
}
},
{
[joinColumn.name]: id,
order: this.createQueryBuilder(joinTable.name).max('order').where({
[joinColumn.name]: id
}).where(joinTable.on || {}).transacting(trx).getKnexQuery()
}
]
}).where(joinTable.on || {}).transacting(trx).execute();
if (!isEmpty(idsToDelete)) {
const where = {
$or: idsToDelete.map((item)=>{
return {
[idColumn.name]: item.id,
[typeColumn.name]: item[typeField],
[joinColumn.name]: id,
...joinTable.on || {}
};
})
};
// delete previous relations
await this.createQueryBuilder(joinTable.name).delete().where(where).transacting(trx).execute();
await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rowsToDelete, {
uid,
attributeName,
joinTable,
db,
transaction: trx
});
}
// connect relations
if (hasConnect) {
const dataset = cleanRelationData.connect || [];
const rows = dataset.map((data)=>({
[joinColumn.name]: id,
[idColumn.name]: data.id,
[typeColumn.name]: data[typeField],
...joinTable.on || {},
...data.__pivot || {},
field: attributeName
}));
const orderMap = relationsOrderer(// Merge id & __type to get a single id key
adjacentRelations.map(encodePolymorphicRelation({
idColumn: idColumn.name,
typeColumn: typeColumn.name
})), idColumn.name, 'order', cleanRelationData.options?.strict).connect(// Merge id & __type to get a single id key
dataset.map(encodePolymorphicRelation({
idColumn: 'id',
typeColumn: '__type'
}))).getOrderMap();
rows.forEach((row)=>{
const rowId = row[idColumn.name];
const rowType = row[typeColumn.name];
const encodedId = encodePolymorphicId(rowId, rowType);
row.order = orderMap[encodedId];
});
await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
}
continue;
}
if (hasSet) {
// delete all relations for this entity
await this.createQueryBuilder(joinTable.name).delete().where({
[joinColumn.name]: id,
...joinTable.on || {}
}).transacting(trx).execute();
const rows = (cleanRelationData.set ?? []).map((data, idx)=>({
[joinColumn.name]: id,
[idColumn.name]: data.id,
[typeColumn.name]: data[typeField],
field: attributeName,
...joinTable.on || {},
...data.__pivot || {},
order: idx + 1
}));
await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
uid,
attributeName,
joinTable,
db,
transaction: trx
});
await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
}
continue;
}
if ('joinColumn' in attribute && attribute.joinColumn && attribute.owner) {
continue;
}
// oneToOne oneToMany on the non owning side.
// Since it is a join column no need to remove previous relations
if ('joinColumn' in attribute && 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
}).transacting(trx).execute();
if (!isNull(cleanRelationData.set)) {
const relIdsToAdd = toIds(cleanRelationData.set);
await this.createQueryBuilder(target).where({
id: relIdsToAdd
}).update({
[attribute.joinColumn.referencedColumn]: id
}).transacting(trx).execute();
}
}
if (attribute.joinTable) {
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
const select = [
joinColumn.name,
inverseJoinColumn.name
];
if (hasOrderColumn(attribute)) {
select.push(orderColumnName);
}
if (hasInverseOrderColumn(attribute)) {
select.push(inverseOrderColumnName);
}
// only delete relations
if (isNull(cleanRelationData.set)) {
await deleteRelations({
id,
attribute,
db,
relIdsToDelete: 'all',
transaction: trx
});
} else {
const isPartialUpdate = !has('set', cleanRelationData);
let relIdsToaddOrMove;
if (isPartialUpdate) {
if (isAnyToOne(attribute)) ;
relIdsToaddOrMove = toIds(cleanRelationData.connect);
const relIdsToDelete = toIds(differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect ?? []));
if (!isEmpty(relIdsToDelete)) {
await deleteRelations({
id,
attribute,
db,
relIdsToDelete,
transaction: trx
});
}
if (isEmpty(cleanRelationData.connect)) {
continue;
}
// Fetch current relations to handle ordering
let currentMovingRels = [];
if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) {
currentMovingRels = await this.createQueryBuilder(joinTable.name).select(select).where({
[joinColumn.name]: id,
[inverseJoinColumn.name]: {
$in: relIdsToaddOrMove
}
}).where(joinTable.on || {}).transacting(trx).execute();
}
// prepare relations to insert
const insert = uniqBy('id', cleanRelationData.connect).map((relToAdd)=>({
[joinColumn.name]: id,
[inverseJoinColumn.name]: relToAdd.id,
...joinTable.on || {},
...relToAdd.__pivot || {}
}));
if (hasOrderColumn(attribute)) {
// Get all adjacent relations and the one with the highest order
const adjacentRelations = await this.createQueryBuilder(joinTable.name).where({
$or: [
{
[joinColumn.name]: id,
[inverseJoinColumn.name]: {
$in: compact(cleanRelationData.connect?.map((r)=>r.position?.after || r.position?.before))
}
},
{
[joinColumn.name]: id,
[orderColumnName]: this.createQueryBuilder(joinTable.name).max(orderColumnName).where({
[joinColumn.name]: id
}).where(joinTable.on || {}).transacting(trx).getKnexQuery()
}
]
}).where(joinTable.on || {}).transacting(trx).execute();
const orderMap = relationsOrderer(adjacentRelations, inverseJoinColumn.name, joinTable.orderColumnName, cleanRelationData.options?.strict).connect(cleanRelationData.connect ?? []).getOrderMap();
insert.forEach((row)=>{
row[orderColumnName] = orderMap[row[inverseJoinColumn.name]];
});
}
// add inv order value
if (hasInverseOrderColumn(attribute)) {
const nonExistingRelsIds = difference(relIdsToaddOrMove, map(inverseJoinColumn.name, currentMovingRels));
const maxResults = await db.getConnection().select(inverseJoinColumn.name).max(inverseOrderColumnName, {
as: 'max'
}).whereIn(inverseJoinColumn.name, nonExistingRelsIds).where(joinTable.on || {}).groupBy(inverseJoinColumn.name).from(joinTable.name).transacting(trx);
const maxMap = maxResults.reduce((acc, res)=>Object.assign(acc, {
[res[inverseJoinColumn.name]]: res.max
}), {});
insert.forEach((row)=>{
row[inverseOrderColumnName] = (maxMap[row[inverseJoinColumn.name]] || 0) + 1;
});
}
// insert rows
const query = this.createQueryBuilder(joinTable.name).insert(insert).onConflict(joinTable.pivotColumns).transacting(trx);
if (hasOrderColumn(attribute)) {
query.merge([
orderColumnName
]);
} else {
query.ignore();
}
await query.execute();
// remove gap between orders
await cleanOrderColumns({
attribute,
db,
id,
transaction: trx
});
} else {
if (isAnyToOne(attribute)) {
cleanRelationData.set = cleanRelationData.set?.slice(-1);
}
// overwrite all relations
relIdsToaddOrMove = toIds(cleanRelationData.set);
await deleteRelations({
id,
attribute,
db,
relIdsToDelete: 'all',
relIdsToNotDelete: relIdsToaddOrMove,
transaction: trx
});
if (isEmpty(cleanRelationData.set)) {
continue;
}
const insert = uniqBy('id', cleanRelationData.set).map((relToAdd)=>({
[joinColumn.name]: id,
[inverseJoinColumn.name]: relToAdd.id,
...joinTable.on || {},
...relToAdd.__pivot || {}
}));
// add order value
if (hasOrderColumn(attribute)) {
insert.forEach((row, idx)=>{
row[orderColumnName] = idx + 1;
});
}
// add inv order value
if (hasInverseOrderColumn(attribute)) {
const existingRels = await this.createQueryBuilder(joinTable.name).select(inverseJoinColumn.name).where({
[joinColumn.name]: id,
[inverseJoinColumn.name]: {
$in: relIdsToaddOrMove
}
}).where(joinTable.on || {}).transacting(trx).execute();
const inverseRelsIds = map(inverseJoinColumn.name, existingRels);
const nonExistingRelsIds = difference(relIdsToaddOrMove, inverseRelsIds);
const maxResults = await db.getConnection().select(inverseJoinColumn.name).max(inverseOrderColumnName, {
as: 'max'
}).whereIn(inverseJoinColumn.name, nonExistingRelsIds).where(joinTable.on || {}).groupBy(inverseJoinColumn.name).from(joinTable.name).transacting(trx);
const maxMap = maxResults.reduce((acc, res)=>Object.assign(acc, {
[res[inverseJoinColumn.name]]: res.max
}), {});
insert.forEach((row)=>{
row[inverseOrderColumnName] = (maxMap[row[inverseJoinColumn.name]] || 0) + 1;
});
}
// insert rows
const query = this.createQueryBuilder(joinTable.name).insert(insert).onConflict(joinTable.pivotColumns).transacting(trx);
if (hasOrderColumn(attribute)) {
query.merge([
orderColumnName
]);
} else {
query.ignore();
}
await query.execute();
}
// Delete the previous relations for oneToAny relations
if (isBidirectional(attribute) && isOneToAny(attribute)) {
await deletePreviousOneToAnyRelations({
id,
attribute,
relIdsToadd: relIdsToaddOrMove,
db,
transaction: trx
});
}
// Delete the previous relations for anyToOne relations
if (isAnyToOne(attribute)) {
await deletePreviousAnyToOneRelations({
id,
attribute,
relIdToadd: relIdsToaddOrMove[0],
db,
transaction: trx
});
}
}
}
}
},
/**
* 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
*/ async deleteRelations (uid, id, options) {
const { attributes } = db.metadata.get(uid);
const { transaction: trx } = options ?? {};
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.type === 'relation' && 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
}).transacting(trx).execute();
} else if (targetAttribute.type === 'relation' && 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
}).transacting(trx).execute();
}
continue;
}
/*
if morphToOne
nothing to do
*/ if (attribute.relation === 'morphToOne') ;
/*
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 || {}
}).transacting(trx).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 ('joinColumn' in attribute && attribute.joinColumn && attribute.owner) {
continue;
}
// oneToOne oneToMany on the non owning side.
if ('joinColumn' in attribute && 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
}).transacting(trx).execute();
}
if ('joinTable' in attribute && attribute.joinTable) {
await deleteRelations({
id,
attribute,
db,
relIdsToDelete: 'all',
transaction: trx
});
}
}
},
// 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, populate) {
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] = populate || 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];
}
};
};
export { createEntityManager };
//# sourceMappingURL=index.mjs.map