strapi/packages/core/database/lib/entity-manager.js

346 lines
9.0 KiB
JavaScript
Raw Normal View History

2021-06-17 16:17:15 +02:00
'use strict';
2021-06-22 17:13:11 +02:00
const _ = require('lodash/fp');
const types = require('./types');
2021-06-17 16:17:15 +02:00
const { createQueryBuilder } = require('./query');
const { createRepository } = require('./entity-repository');
2021-06-23 15:37:20 +02:00
const pickRowAttributes = (metadata, data = {}) => {
2021-06-22 17:13:11 +02:00
const { attributes } = metadata;
2021-06-23 15:37:20 +02:00
const obj = {};
2021-06-22 17:13:11 +02:00
2021-06-23 15:37:20 +02:00
// pick attribute
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
if (types.isScalar(attribute.type) && _.has(attributeName, data)) {
// NOTE: we convert to column name
obj[_.snakeCase(attributeName)] = data[attributeName];
}
if (types.isRelation(attribute.type)) {
// oneToOne & manyToOne
if (attribute.joinColumn && attribute.owner) {
// TODO: ensure joinColumn name respect convention ?
const joinColumnName = attribute.joinColumn.name;
const attrValue = data[attributeName] || data[joinColumnName];
if (!_.isUndefined(attrValue)) {
obj[joinColumnName] = attrValue;
}
}
}
}
return obj;
};
const attachRelations = async (em, metadata, id, data) => {
const { attributes } = metadata;
// TODO: optimize later for createMany
for (const attributeName in attributes) {
const attribute = attributes[attributeName];
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;
// TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL)
if (data[attributeName]) {
await em
.createQueryBuilder(target)
.update({ [attribute.joinColumn.referencedColumn]: id })
// NOTE: works if it is an array or a single id
.where({ id: data[attributeName] })
.execute();
}
}
if (attribute.joinTable) {
// need to set the column on the target
const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable;
// TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL)
if (data[attributeName]) {
const insert = Array.isArray(data[attributeName])
? data[attributeName].map(datum => {
return {
[joinColumn.name]: id,
[inverseJoinColumn.name]: datum,
};
})
: {
[joinColumn.name]: id,
[inverseJoinColumn.name]: data[attributeName],
};
await em
.createQueryBuilder(joinTable.name)
.insert(insert)
.execute();
}
}
/*
oneToOne
if owner
if joinColumn
TODO: We might actually want to make the column unique and throw -> doing this makes the code generic and doesn't require specific logic
removing existing relation
-> Id should have been added in the column of the model table beforehand to avoid extra updates
if joinTable
-> clear join Table assoc
-> add relation
if not owner
if joinColumn
remove existing relation
-> add relation
if joinTable
-> clear join Table assoc
-> add relation in join table
oneToMany
owner -> cannot be owner
not owner
joinColumn
-> add relations in target
joinTable
-> add relations in join table
manyToOne
not owner -> must be owner
owner
join Column
-> Id should have been added in the column of the model table beforehand to avoid extra updates
joinTable
-> add relation in join table
manyToMany
-> add relation in join table
*/
}
2021-06-22 17:13:11 +02:00
};
2021-06-17 16:17:15 +02:00
const createEntityManager = db => {
const repoMap = {};
return {
async findOne(uid, params) {
const qb = this.createQueryBuilder(uid)
.init(params)
.first();
return await qb.execute();
},
// should we name it findOne because people are used to it ?
async findMany(uid, params) {
const qb = this.createQueryBuilder(uid).init(params);
return await qb.execute();
},
// support search directly in find & count -> a search param ? a different feature with a search tables rather
async findWithCount(uid, params) {
return await Promise.all([this.findMany(uid, params), this.count(uid, params)]);
},
// TODO: define api
2021-06-22 17:13:11 +02:00
async count(uid, params = {}) {
2021-06-17 16:17:15 +02:00
const qb = this.createQueryBuilder(uid).where(params.where);
const res = await qb
.count()
.first()
.execute();
return Number(res.count);
},
// TODO: make it create one somehow
async create(uid, params) {
// create entry in DB
2021-06-23 15:37:20 +02:00
const metadata = db.metadata.get(uid);
2021-06-17 16:17:15 +02:00
// select fields that go into db
// format input values for the db
// change name to column names
// select relations
const { data } = params;
// remove unknow fields or throw
// rename to columns
// transform value to storage value
// apply programatic defaults if any -> I think this should be handled outside of this layer as we might have some applicative rules in the entity service
2021-06-23 15:37:20 +02:00
// remove relation entries except for joinColumns
const dataToInsert = pickRowAttributes(db.metadata.get(uid), data);
2021-06-17 16:17:15 +02:00
2021-06-23 15:37:20 +02:00
console.log(dataToInsert);
2021-06-22 17:13:11 +02:00
2021-06-17 16:17:15 +02:00
const [id] = await this.createQueryBuilder(uid)
2021-06-22 17:13:11 +02:00
.insert(dataToInsert)
2021-06-17 16:17:15 +02:00
.execute();
// create relation associations or move this to the entity service & call attach on the repo instead
2021-06-23 15:37:20 +02:00
await attachRelations(this, metadata, id, data);
2021-06-17 16:17:15 +02:00
return this.findOne(uid, { where: { id }, select: params.select, populate: params.populate });
},
async createMany(uid, params) {
const { data } = params;
2021-06-23 15:37:20 +02:00
const metadata = db.metadata.get(uid);
// TODO: pick scalar fields
// TODO: pick joinColumns to create directly on the table
const dataToInsert = data.map(datum => pickRowAttributes(metadata, datum));
2021-06-22 17:13:11 +02:00
2021-06-17 16:17:15 +02:00
const ids = await this.createQueryBuilder(uid)
2021-06-22 17:13:11 +02:00
.insert(dataToInsert)
2021-06-17 16:17:15 +02:00
.execute();
2021-06-22 17:13:11 +02:00
// TODO: create relation links
2021-06-17 16:17:15 +02:00
return ids.map(id => ({ id }));
},
// TODO: make it update one somehow
// findOne + update with a return
async update(uid, params) {
const { where, data } = params;
2021-06-23 15:37:20 +02:00
const metadata = db.metadata.get(uid);
const dataToUpdate = pickRowAttributes(metadata, data);
2021-06-22 17:13:11 +02:00
2021-06-17 16:17:15 +02:00
/*const r =*/ await this.createQueryBuilder(uid)
.where(where)
2021-06-22 17:13:11 +02:00
.update(dataToUpdate)
2021-06-17 16:17:15 +02:00
.execute();
return {};
},
async updateMany(uid, params) {
const { where, data } = params;
2021-06-23 15:37:20 +02:00
const metadata = db.metadata.get(uid);
const dataToUpdate = data.map(datum => pickRowAttributes(metadata, datum));
2021-06-22 17:13:11 +02:00
2021-06-17 16:17:15 +02:00
return this.createQueryBuilder(uid)
.where(where)
2021-06-22 17:13:11 +02:00
.update(dataToUpdate)
2021-06-17 16:17:15 +02:00
.execute();
},
// TODO: make it deleteOne somehow
// findOne + delete with a return -> should go in the entity service
async delete(uid, params) {
return await this.createQueryBuilder(uid)
.init(params)
.delete()
.execute();
},
async deleteMany(uid, params) {
const { where } = params;
return await this.createQueryBuilder(uid)
.where(where)
.delete()
.execute();
},
// populate already loaded entry
async populate(uid, entry, name, params) {
return {
...entry,
relation: await this.load(entry, name, params),
};
},
// loads a relation
load(uid, entry, name, params) {
const { attributes } = db.metadata.get(uid);
return this.getRepository(attributes[name].target.uid).findMany({
...params,
where: {
...params.where,
// [parent]: entry.id,
},
});
},
// method to work with components & dynamic zones
// addComponent() {},
// removeComponent() {},
// setComponent() {},
// method to work with relations
attachRelation() {},
detachRelation() {},
setRelation() {},
// cascading
// aggregations
// -> avg
// -> min
// -> max
// -> grouping
// formulas
// custom queries
// utilities
// -> format
// -> parse
// -> map result
// -> map input
// -> validation
// 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,
};