mirror of
https://github.com/strapi/strapi.git
synced 2025-11-16 10:07:55 +00:00
Refactor populate
This commit is contained in:
parent
76665ae056
commit
7c4b7e02c8
@ -4,6 +4,7 @@ const sqlite = {
|
|||||||
filename: '.tmp/data.db',
|
filename: '.tmp/data.db',
|
||||||
},
|
},
|
||||||
useNullAsDefault: true,
|
useNullAsDefault: true,
|
||||||
|
debug: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const postgres = {
|
const postgres = {
|
||||||
|
|||||||
@ -2,8 +2,23 @@ const { createCoreController } = require('@strapi/strapi').factories;
|
|||||||
|
|
||||||
module.exports = createCoreController('api::address.address', {
|
module.exports = createCoreController('api::address.address', {
|
||||||
async find(ctx) {
|
async find(ctx) {
|
||||||
const { results } = await strapi.service('api::address.address').find();
|
// const { results } = await strapi.service('api::address.address').find();
|
||||||
|
|
||||||
ctx.body = await this.sanitizeOutput(results);
|
const r = await strapi.db.query('api::address.address').load(
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
'categories',
|
||||||
|
{
|
||||||
|
limit: 2,
|
||||||
|
orderBy: 'asc|desc',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(r);
|
||||||
|
|
||||||
|
ctx.body = r;
|
||||||
|
|
||||||
|
// ctx.body = await this.sanitizeOutput(results);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1063,6 +1063,10 @@ const createEntityManager = (db) => {
|
|||||||
return { ...entity, ...entry };
|
return { ...entity, ...entry };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async LoadPage() {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
// TODO: add lifecycle events
|
// TODO: add lifecycle events
|
||||||
async load(uid, entity, fields, params) {
|
async load(uid, entity, fields, params) {
|
||||||
const { attributes } = db.metadata.get(uid);
|
const { attributes } = db.metadata.get(uid);
|
||||||
|
|||||||
@ -33,7 +33,12 @@ const processOrderBy = (orderBy, ctx) => {
|
|||||||
const attribute = attributes[key];
|
const attribute = attributes[key];
|
||||||
|
|
||||||
if (!attribute) {
|
if (!attribute) {
|
||||||
throw new Error(`Attribute ${key} not found on model ${uid}`);
|
// throw new Error(`Attribute ${key} not found on model ${uid}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
column: key,
|
||||||
|
order: direction,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (types.isScalar(attribute.type)) {
|
if (types.isScalar(attribute.type)) {
|
||||||
|
|||||||
@ -1,649 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const _ = require('lodash/fp');
|
|
||||||
|
|
||||||
const types = require('../../types');
|
|
||||||
const { fromRow } = require('./transform');
|
|
||||||
|
|
||||||
const getRootLevelPopulate = (meta) => {
|
|
||||||
const populate = {};
|
|
||||||
|
|
||||||
for (const attributeName of Object.keys(meta.attributes)) {
|
|
||||||
const attribute = meta.attributes[attributeName];
|
|
||||||
if (attribute.type === 'relation') {
|
|
||||||
populate[attributeName] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return populate;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts and prepares the query for populate
|
|
||||||
*
|
|
||||||
* @param {boolean|string[]|object} populate populate param
|
|
||||||
* @param {object} ctx query context
|
|
||||||
* @param {object} ctx.db database instance
|
|
||||||
* @param {object} ctx.qb query builder instance
|
|
||||||
* @param {string} ctx.uid model uid
|
|
||||||
*/
|
|
||||||
const processPopulate = (populate, ctx) => {
|
|
||||||
const { qb, db, uid } = ctx;
|
|
||||||
const meta = db.metadata.get(uid);
|
|
||||||
|
|
||||||
let populateMap = {};
|
|
||||||
|
|
||||||
if (populate === false || _.isNil(populate)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (populate === true) {
|
|
||||||
populateMap = getRootLevelPopulate(meta);
|
|
||||||
} else if (Array.isArray(populate)) {
|
|
||||||
for (const key of populate) {
|
|
||||||
const [root, ...rest] = key.split('.');
|
|
||||||
|
|
||||||
if (rest.length > 0) {
|
|
||||||
const subPopulate = rest.join('.');
|
|
||||||
|
|
||||||
if (populateMap[root]) {
|
|
||||||
if (populateMap[root] === true) {
|
|
||||||
populateMap[root] = {
|
|
||||||
populate: [subPopulate],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
populateMap[root].populate = [subPopulate].concat(populateMap[root].populate || []);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
populateMap[root] = {
|
|
||||||
populate: [subPopulate],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
populateMap[root] = populateMap[root] ? populateMap[root] : true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
populateMap = populate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isPlainObject(populateMap)) {
|
|
||||||
throw new Error('Populate must be an object');
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalPopulate = {};
|
|
||||||
for (const key of Object.keys(populateMap)) {
|
|
||||||
const attribute = meta.attributes[key];
|
|
||||||
|
|
||||||
if (!attribute) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!types.isRelation(attribute.type)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure id is present for future populate queries
|
|
||||||
if (_.has('id', meta.attributes)) {
|
|
||||||
qb.addSelect('id');
|
|
||||||
}
|
|
||||||
|
|
||||||
finalPopulate[key] = populateMap[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalPopulate;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Omit limit & offset to avoid needing a query per result to avoid making too many queries
|
|
||||||
const pickPopulateParams = _.pick([
|
|
||||||
'select',
|
|
||||||
'count',
|
|
||||||
'where',
|
|
||||||
'populate',
|
|
||||||
'orderBy',
|
|
||||||
'limit',
|
|
||||||
'offset',
|
|
||||||
'filters',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// TODO: cleanup code
|
|
||||||
// TODO: create aliases for pivot columns
|
|
||||||
// TODO: optimize depth to avoid overfetching
|
|
||||||
// TODO: handle count for join columns
|
|
||||||
// TODO: cleanup count
|
|
||||||
const applyPopulate = async (results, populate, ctx) => {
|
|
||||||
const { db, uid, qb } = ctx;
|
|
||||||
const meta = db.metadata.get(uid);
|
|
||||||
|
|
||||||
if (_.isEmpty(results)) {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of Object.keys(populate)) {
|
|
||||||
const attribute = meta.attributes[key];
|
|
||||||
const targetMeta = db.metadata.get(attribute.target);
|
|
||||||
|
|
||||||
const populateValue = {
|
|
||||||
filters: qb.state.filters,
|
|
||||||
...pickPopulateParams(populate[key]),
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCount = populateValue.count === true;
|
|
||||||
|
|
||||||
const fromTargetRow = (rowOrRows) => fromRow(targetMeta, rowOrRows);
|
|
||||||
|
|
||||||
if (attribute.relation === 'oneToOne' || attribute.relation === 'manyToOne') {
|
|
||||||
if (attribute.joinColumn) {
|
|
||||||
const { name: joinColumnName, referencedColumn: referencedColumnName } =
|
|
||||||
attribute.joinColumn;
|
|
||||||
|
|
||||||
const referencedValues = _.uniq(
|
|
||||||
results.map((r) => r[joinColumnName]).filter((value) => !_.isNil(value))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_.isEmpty(referencedValues)) {
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await db.entityManager
|
|
||||||
.createQueryBuilder(targetMeta.uid)
|
|
||||||
.init(populateValue)
|
|
||||||
.addSelect(`${qb.alias}.${referencedColumnName}`)
|
|
||||||
.where({ [referencedColumnName]: referencedValues })
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const map = _.groupBy(referencedColumnName, rows);
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = fromTargetRow(_.first(map[result[joinColumnName]]));
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attribute.joinTable) {
|
|
||||||
const { joinTable } = attribute;
|
|
||||||
|
|
||||||
const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
|
|
||||||
|
|
||||||
const { name: joinColumnName, referencedColumn: referencedColumnName } =
|
|
||||||
joinTable.joinColumn;
|
|
||||||
|
|
||||||
const alias = qb.getAlias();
|
|
||||||
const joinColAlias = `${alias}.${joinColumnName}`;
|
|
||||||
|
|
||||||
const referencedValues = _.uniq(
|
|
||||||
results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_.isEmpty(referencedValues)) {
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = null;
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await qb
|
|
||||||
.init(populateValue)
|
|
||||||
.join({
|
|
||||||
alias,
|
|
||||||
referencedTable: joinTable.name,
|
|
||||||
referencedColumn: joinTable.inverseJoinColumn.name,
|
|
||||||
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
|
||||||
rootTable: qb.alias,
|
|
||||||
on: joinTable.on,
|
|
||||||
orderBy: joinTable.orderBy,
|
|
||||||
})
|
|
||||||
.addSelect(joinColAlias)
|
|
||||||
.where({ [joinColAlias]: referencedValues })
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const map = _.groupBy(joinColumnName, rows);
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = fromTargetRow(_.first(map[result[referencedColumnName]]));
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
} else if (attribute.relation === 'oneToMany') {
|
|
||||||
if (attribute.joinColumn) {
|
|
||||||
const { name: joinColumnName, referencedColumn: referencedColumnName } =
|
|
||||||
attribute.joinColumn;
|
|
||||||
|
|
||||||
const referencedValues = _.uniq(
|
|
||||||
results.map((r) => r[joinColumnName]).filter((value) => !_.isNil(value))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_.isEmpty(referencedValues)) {
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = null;
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await db.entityManager
|
|
||||||
.createQueryBuilder(targetMeta.uid)
|
|
||||||
.init(populateValue)
|
|
||||||
.addSelect(`${qb.alias}.${referencedColumnName}`)
|
|
||||||
.where({ [referencedColumnName]: referencedValues })
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const map = _.groupBy(referencedColumnName, rows);
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = fromTargetRow(map[result[joinColumnName]] || []);
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attribute.joinTable) {
|
|
||||||
const { joinTable } = attribute;
|
|
||||||
|
|
||||||
const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
|
|
||||||
|
|
||||||
const { name: joinColumnName, referencedColumn: referencedColumnName } =
|
|
||||||
joinTable.joinColumn;
|
|
||||||
|
|
||||||
const alias = qb.getAlias();
|
|
||||||
const joinColAlias = `${alias}.${joinColumnName}`;
|
|
||||||
|
|
||||||
const referencedValues = _.uniq(
|
|
||||||
results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isCount) {
|
|
||||||
if (_.isEmpty(referencedValues)) {
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = { count: 0 };
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await qb
|
|
||||||
.init(populateValue)
|
|
||||||
.join({
|
|
||||||
alias,
|
|
||||||
referencedTable: joinTable.name,
|
|
||||||
referencedColumn: joinTable.inverseJoinColumn.name,
|
|
||||||
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
|
||||||
rootTable: qb.alias,
|
|
||||||
on: joinTable.on,
|
|
||||||
})
|
|
||||||
.select([joinColAlias, qb.raw('count(*) AS count')])
|
|
||||||
.where({ [joinColAlias]: referencedValues })
|
|
||||||
.groupBy(joinColAlias)
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const map = rows.reduce((map, row) => {
|
|
||||||
map[row[joinColumnName]] = { count: Number(row.count) };
|
|
||||||
return map;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = map[result[referencedColumnName]] || { count: 0 };
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.isEmpty(referencedValues)) {
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = [];
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await qb
|
|
||||||
.init(populateValue)
|
|
||||||
.join({
|
|
||||||
alias,
|
|
||||||
referencedTable: joinTable.name,
|
|
||||||
referencedColumn: joinTable.inverseJoinColumn.name,
|
|
||||||
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
|
||||||
rootTable: qb.alias,
|
|
||||||
on: joinTable.on,
|
|
||||||
orderBy: joinTable.orderBy,
|
|
||||||
})
|
|
||||||
.addSelect(joinColAlias)
|
|
||||||
.where({ [joinColAlias]: referencedValues })
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const map = _.groupBy(joinColumnName, rows);
|
|
||||||
|
|
||||||
results.forEach((r) => {
|
|
||||||
r[key] = fromTargetRow(map[r[referencedColumnName]] || []);
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
} else if (attribute.relation === 'manyToMany') {
|
|
||||||
const { joinTable } = attribute;
|
|
||||||
|
|
||||||
const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
|
|
||||||
|
|
||||||
const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
|
|
||||||
|
|
||||||
const alias = qb.getAlias();
|
|
||||||
const joinColAlias = `${alias}.${joinColumnName}`;
|
|
||||||
const referencedValues = _.uniq(
|
|
||||||
results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isCount) {
|
|
||||||
if (_.isEmpty(referencedValues)) {
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = { count: 0 };
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await qb
|
|
||||||
.init(populateValue)
|
|
||||||
.join({
|
|
||||||
alias,
|
|
||||||
referencedTable: joinTable.name,
|
|
||||||
referencedColumn: joinTable.inverseJoinColumn.name,
|
|
||||||
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
|
||||||
rootTable: qb.alias,
|
|
||||||
on: joinTable.on,
|
|
||||||
})
|
|
||||||
.select([joinColAlias, qb.raw('count(*) AS count')])
|
|
||||||
.where({ [joinColAlias]: referencedValues })
|
|
||||||
.groupBy(joinColAlias)
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const map = rows.reduce((map, row) => {
|
|
||||||
map[row[joinColumnName]] = { count: Number(row.count) };
|
|
||||||
return map;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = map[result[referencedColumnName]] || { count: 0 };
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.isEmpty(referencedValues)) {
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = [];
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await qb
|
|
||||||
.init(populateValue)
|
|
||||||
.join({
|
|
||||||
alias,
|
|
||||||
referencedTable: joinTable.name,
|
|
||||||
referencedColumn: joinTable.inverseJoinColumn.name,
|
|
||||||
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
|
||||||
rootTable: qb.alias,
|
|
||||||
on: joinTable.on,
|
|
||||||
orderBy: joinTable.orderBy,
|
|
||||||
})
|
|
||||||
.addSelect(joinColAlias)
|
|
||||||
.where({ [joinColAlias]: referencedValues })
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const map = _.groupBy(joinColumnName, rows);
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = fromTargetRow(map[result[referencedColumnName]] || []);
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
} else if (['morphOne', 'morphMany'].includes(attribute.relation)) {
|
|
||||||
const { target, morphBy } = attribute;
|
|
||||||
|
|
||||||
const targetAttribute = db.metadata.get(target).attributes[morphBy];
|
|
||||||
|
|
||||||
if (targetAttribute.relation === 'morphToOne') {
|
|
||||||
const { idColumn, typeColumn } = targetAttribute.morphColumn;
|
|
||||||
|
|
||||||
const referencedValues = _.uniq(
|
|
||||||
results.map((r) => r[idColumn.referencedColumn]).filter((value) => !_.isNil(value))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_.isEmpty(referencedValues)) {
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = await db.entityManager
|
|
||||||
.createQueryBuilder(target)
|
|
||||||
.init(populateValue)
|
|
||||||
// .addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
|
|
||||||
.where({ [idColumn.name]: referencedValues, [typeColumn.name]: uid })
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const map = _.groupBy(idColumn.name, rows);
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
const matchingRows = map[result[idColumn.referencedColumn]];
|
|
||||||
|
|
||||||
const matchingValue =
|
|
||||||
attribute.relation === 'morphOne' ? _.first(matchingRows) : matchingRows;
|
|
||||||
|
|
||||||
result[key] = fromTargetRow(matchingValue);
|
|
||||||
});
|
|
||||||
} else if (targetAttribute.relation === 'morphToMany') {
|
|
||||||
const { joinTable } = targetAttribute;
|
|
||||||
|
|
||||||
const { joinColumn, morphColumn } = joinTable;
|
|
||||||
|
|
||||||
const { idColumn, typeColumn } = morphColumn;
|
|
||||||
|
|
||||||
const referencedValues = _.uniq(
|
|
||||||
results.map((r) => r[idColumn.referencedColumn]).filter((value) => !_.isNil(value))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_.isEmpty(referencedValues)) {
|
|
||||||
results.forEach((result) => {
|
|
||||||
result[key] = attribute.relation === 'morphOne' ? null : [];
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// find with join table
|
|
||||||
const qb = db.entityManager.createQueryBuilder(target);
|
|
||||||
|
|
||||||
const alias = qb.getAlias();
|
|
||||||
|
|
||||||
const rows = await qb
|
|
||||||
.init(populateValue)
|
|
||||||
.join({
|
|
||||||
alias,
|
|
||||||
referencedTable: joinTable.name,
|
|
||||||
referencedColumn: joinColumn.name,
|
|
||||||
rootColumn: joinColumn.referencedColumn,
|
|
||||||
rootTable: qb.alias,
|
|
||||||
on: {
|
|
||||||
...(joinTable.on || {}),
|
|
||||||
field: key,
|
|
||||||
},
|
|
||||||
orderBy: joinTable.orderBy,
|
|
||||||
})
|
|
||||||
.addSelect([`${alias}.${idColumn.name}`, `${alias}.${typeColumn.name}`])
|
|
||||||
.where({
|
|
||||||
[`${alias}.${idColumn.name}`]: referencedValues,
|
|
||||||
[`${alias}.${typeColumn.name}`]: uid,
|
|
||||||
})
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const map = _.groupBy(idColumn.name, rows);
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
const matchingRows = map[result[idColumn.referencedColumn]];
|
|
||||||
|
|
||||||
const matchingValue =
|
|
||||||
attribute.relation === 'morphOne' ? _.first(matchingRows) : matchingRows;
|
|
||||||
|
|
||||||
result[key] = fromTargetRow(matchingValue);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
} else if (attribute.relation === 'morphToMany') {
|
|
||||||
// find with join table
|
|
||||||
const { joinTable } = attribute;
|
|
||||||
|
|
||||||
const { joinColumn, morphColumn } = joinTable;
|
|
||||||
const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
|
|
||||||
|
|
||||||
// fetch join table to create the ids map then do the same as morphToOne without the first
|
|
||||||
|
|
||||||
const referencedValues = _.uniq(
|
|
||||||
results.map((r) => r[joinColumn.referencedColumn]).filter((value) => !_.isNil(value))
|
|
||||||
);
|
|
||||||
|
|
||||||
const qb = db.entityManager.createQueryBuilder(joinTable.name);
|
|
||||||
|
|
||||||
const joinRows = await qb
|
|
||||||
.where({
|
|
||||||
[joinColumn.name]: referencedValues,
|
|
||||||
...(joinTable.on || {}),
|
|
||||||
})
|
|
||||||
.orderBy([joinColumn.name, 'order'])
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
const joinMap = _.groupBy(joinColumn.name, joinRows);
|
|
||||||
|
|
||||||
const idsByType = joinRows.reduce((acc, result) => {
|
|
||||||
const idValue = result[morphColumn.idColumn.name];
|
|
||||||
const typeValue = result[morphColumn.typeColumn.name];
|
|
||||||
|
|
||||||
if (!idValue || !typeValue) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.has(typeValue, acc)) {
|
|
||||||
acc[typeValue] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[typeValue].push(idValue);
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const map = {};
|
|
||||||
for (const type of Object.keys(idsByType)) {
|
|
||||||
const ids = idsByType[type];
|
|
||||||
|
|
||||||
// type was removed but still in morph relation
|
|
||||||
if (!db.metadata.get(type)) {
|
|
||||||
map[type] = {};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qb = db.entityManager.createQueryBuilder(type);
|
|
||||||
|
|
||||||
const rows = await qb
|
|
||||||
.init(populateValue)
|
|
||||||
.addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
|
|
||||||
.where({ [idColumn.referencedColumn]: ids })
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
map[type] = _.groupBy(idColumn.referencedColumn, rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
const joinResults = joinMap[result[joinColumn.referencedColumn]] || [];
|
|
||||||
|
|
||||||
const matchingRows = joinResults.flatMap((joinResult) => {
|
|
||||||
const id = joinResult[idColumn.name];
|
|
||||||
const type = joinResult[typeColumn.name];
|
|
||||||
|
|
||||||
const fromTargetRow = (rowOrRows) => fromRow(db.metadata.get(type), rowOrRows);
|
|
||||||
|
|
||||||
return (map[type][id] || []).map((row) => {
|
|
||||||
return {
|
|
||||||
[typeField]: type,
|
|
||||||
...fromTargetRow(row),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
result[key] = matchingRows;
|
|
||||||
});
|
|
||||||
} else if (attribute.relation === 'morphToOne') {
|
|
||||||
const { morphColumn } = attribute;
|
|
||||||
const { idColumn, typeColumn } = morphColumn;
|
|
||||||
|
|
||||||
// make a map for each type what ids to return
|
|
||||||
// make a nested map per id
|
|
||||||
|
|
||||||
const idsByType = results.reduce((acc, result) => {
|
|
||||||
const idValue = result[morphColumn.idColumn.name];
|
|
||||||
const typeValue = result[morphColumn.typeColumn.name];
|
|
||||||
|
|
||||||
if (!idValue || !typeValue) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.has(typeValue, acc)) {
|
|
||||||
acc[typeValue] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[typeValue].push(idValue);
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const map = {};
|
|
||||||
for (const type of Object.keys(idsByType)) {
|
|
||||||
const ids = idsByType[type];
|
|
||||||
|
|
||||||
// type was removed but still in morph relation
|
|
||||||
if (!db.metadata.get(type)) {
|
|
||||||
map[type] = {};
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qb = db.entityManager.createQueryBuilder(type);
|
|
||||||
|
|
||||||
const rows = await qb
|
|
||||||
.init(populateValue)
|
|
||||||
.addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
|
|
||||||
.where({ [idColumn.referencedColumn]: ids })
|
|
||||||
.execute({ mapResults: false });
|
|
||||||
|
|
||||||
map[type] = _.groupBy(idColumn.referencedColumn, rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
const id = result[idColumn.name];
|
|
||||||
const type = result[typeColumn.name];
|
|
||||||
|
|
||||||
if (!type || !id) {
|
|
||||||
result[key] = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingRows = map[type][id];
|
|
||||||
|
|
||||||
const fromTargetRow = (rowOrRows) => fromRow(db.metadata.get(type), rowOrRows);
|
|
||||||
|
|
||||||
result[key] = fromTargetRow(_.first(matchingRows));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
processPopulate,
|
|
||||||
applyPopulate,
|
|
||||||
};
|
|
||||||
612
packages/core/database/lib/query/helpers/populate/apply.js
Normal file
612
packages/core/database/lib/query/helpers/populate/apply.js
Normal file
@ -0,0 +1,612 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const _ = require('lodash/fp');
|
||||||
|
|
||||||
|
const { fromRow } = require('../transform');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate X to One relation
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} ctx
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const XtoOne = async (input, ctx) => {
|
||||||
|
const { attribute, attributeName, results, populateValue, targetMeta } = input;
|
||||||
|
const { db, qb } = ctx;
|
||||||
|
|
||||||
|
const fromTargetRow = (rowOrRows) => fromRow(targetMeta, rowOrRows);
|
||||||
|
|
||||||
|
if (attribute.joinColumn) {
|
||||||
|
const { name: joinColumnName, referencedColumn: referencedColumnName } = attribute.joinColumn;
|
||||||
|
|
||||||
|
const referencedValues = _.uniq(
|
||||||
|
results.map((r) => r[joinColumnName]).filter((value) => !_.isNil(value))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_.isEmpty(referencedValues)) {
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db.entityManager
|
||||||
|
.createQueryBuilder(targetMeta.uid)
|
||||||
|
.init(populateValue)
|
||||||
|
.addSelect(`${qb.alias}.${referencedColumnName}`)
|
||||||
|
.where({ [referencedColumnName]: referencedValues })
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const map = _.groupBy(referencedColumnName, rows);
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = fromTargetRow(_.first(map[result[joinColumnName]]));
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribute.joinTable) {
|
||||||
|
const { joinTable } = attribute;
|
||||||
|
|
||||||
|
const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
|
||||||
|
|
||||||
|
const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
|
||||||
|
|
||||||
|
const alias = qb.getAlias();
|
||||||
|
const joinColAlias = `${alias}.${joinColumnName}`;
|
||||||
|
|
||||||
|
const referencedValues = _.uniq(
|
||||||
|
results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_.isEmpty(referencedValues)) {
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await qb
|
||||||
|
.init(populateValue)
|
||||||
|
.join({
|
||||||
|
alias,
|
||||||
|
referencedTable: joinTable.name,
|
||||||
|
referencedColumn: joinTable.inverseJoinColumn.name,
|
||||||
|
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
||||||
|
rootTable: qb.alias,
|
||||||
|
on: joinTable.on,
|
||||||
|
orderBy: joinTable.orderBy,
|
||||||
|
})
|
||||||
|
.addSelect(joinColAlias)
|
||||||
|
.where({ [joinColAlias]: referencedValues })
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const map = _.groupBy(joinColumnName, rows);
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = fromTargetRow(_.first(map[result[referencedColumnName]]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const oneToMany = async (input, ctx) => {
|
||||||
|
const { attribute, attributeName, results, populateValue, targetMeta, isCount } = input;
|
||||||
|
const { db, qb } = ctx;
|
||||||
|
|
||||||
|
const fromTargetRow = (rowOrRows) => fromRow(targetMeta, rowOrRows);
|
||||||
|
|
||||||
|
if (attribute.joinColumn) {
|
||||||
|
const { name: joinColumnName, referencedColumn: referencedColumnName } = attribute.joinColumn;
|
||||||
|
|
||||||
|
const referencedValues = _.uniq(
|
||||||
|
results.map((r) => r[joinColumnName]).filter((value) => !_.isNil(value))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_.isEmpty(referencedValues)) {
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db.entityManager
|
||||||
|
.createQueryBuilder(targetMeta.uid)
|
||||||
|
.init(populateValue)
|
||||||
|
.addSelect(`${qb.alias}.${referencedColumnName}`)
|
||||||
|
.where({ [referencedColumnName]: referencedValues })
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const map = _.groupBy(referencedColumnName, rows);
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = fromTargetRow(map[result[joinColumnName]] || []);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribute.joinTable) {
|
||||||
|
const { joinTable } = attribute;
|
||||||
|
|
||||||
|
const qb = db.entityManager.createQueryBuilder(targetMeta.uid);
|
||||||
|
|
||||||
|
const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
|
||||||
|
|
||||||
|
const alias = qb.getAlias();
|
||||||
|
const joinColAlias = `${alias}.${joinColumnName}`;
|
||||||
|
|
||||||
|
const referencedValues = _.uniq(
|
||||||
|
results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCount) {
|
||||||
|
if (_.isEmpty(referencedValues)) {
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = { count: 0 };
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await qb
|
||||||
|
.init(populateValue)
|
||||||
|
.join({
|
||||||
|
alias,
|
||||||
|
referencedTable: joinTable.name,
|
||||||
|
referencedColumn: joinTable.inverseJoinColumn.name,
|
||||||
|
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
||||||
|
rootTable: qb.alias,
|
||||||
|
on: joinTable.on,
|
||||||
|
})
|
||||||
|
.select([joinColAlias, qb.raw('count(*) AS count')])
|
||||||
|
.where({ [joinColAlias]: referencedValues })
|
||||||
|
.groupBy(joinColAlias)
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const map = rows.reduce((map, row) => {
|
||||||
|
map[row[joinColumnName]] = { count: Number(row.count) };
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = map[result[referencedColumnName]] || { count: 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isEmpty(referencedValues)) {
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = [];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await qb
|
||||||
|
.init(populateValue)
|
||||||
|
.join({
|
||||||
|
alias,
|
||||||
|
referencedTable: joinTable.name,
|
||||||
|
referencedColumn: joinTable.inverseJoinColumn.name,
|
||||||
|
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
||||||
|
rootTable: qb.alias,
|
||||||
|
on: joinTable.on,
|
||||||
|
orderBy: joinTable.orderBy,
|
||||||
|
})
|
||||||
|
.addSelect(joinColAlias)
|
||||||
|
.where({ [joinColAlias]: referencedValues })
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const map = _.groupBy(joinColumnName, rows);
|
||||||
|
|
||||||
|
results.forEach((r) => {
|
||||||
|
r[attributeName] = fromTargetRow(map[r[referencedColumnName]] || []);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const manyToMany = async (input, ctx) => {
|
||||||
|
const { attribute, attributeName, results, populateValue, targetMeta, isCount } = input;
|
||||||
|
const { db } = ctx;
|
||||||
|
|
||||||
|
const fromTargetRow = (rowOrRows) => fromRow(targetMeta, rowOrRows);
|
||||||
|
|
||||||
|
const { joinTable } = attribute;
|
||||||
|
|
||||||
|
const populateQb = db.entityManager.createQueryBuilder(targetMeta.uid);
|
||||||
|
|
||||||
|
const { name: joinColumnName, referencedColumn: referencedColumnName } = joinTable.joinColumn;
|
||||||
|
|
||||||
|
const alias = populateQb.getAlias();
|
||||||
|
const joinColAlias = `${alias}.${joinColumnName}`;
|
||||||
|
const referencedValues = _.uniq(
|
||||||
|
results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isCount) {
|
||||||
|
if (_.isEmpty(referencedValues)) {
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = { count: 0 };
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await populateQb
|
||||||
|
.init(populateValue)
|
||||||
|
.join({
|
||||||
|
alias,
|
||||||
|
referencedTable: joinTable.name,
|
||||||
|
referencedColumn: joinTable.inverseJoinColumn.name,
|
||||||
|
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
||||||
|
rootTable: populateQb.alias,
|
||||||
|
on: joinTable.on,
|
||||||
|
})
|
||||||
|
.select([joinColAlias, populateQb.raw('count(*) AS count')])
|
||||||
|
.where({ [joinColAlias]: referencedValues })
|
||||||
|
.groupBy(joinColAlias)
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const map = rows.reduce((map, row) => {
|
||||||
|
map[row[joinColumnName]] = { count: Number(row.count) };
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = map[result[referencedColumnName]] || { count: 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isEmpty(referencedValues)) {
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = [];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await populateQb
|
||||||
|
.init(populateValue)
|
||||||
|
.join({
|
||||||
|
alias,
|
||||||
|
referencedTable: joinTable.name,
|
||||||
|
referencedColumn: joinTable.inverseJoinColumn.name,
|
||||||
|
rootColumn: joinTable.inverseJoinColumn.referencedColumn,
|
||||||
|
rootTable: populateQb.alias,
|
||||||
|
on: joinTable.on,
|
||||||
|
orderBy: joinTable.orderBy,
|
||||||
|
})
|
||||||
|
.addSelect(joinColAlias)
|
||||||
|
.where({ [joinColAlias]: referencedValues })
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const map = _.groupBy(joinColumnName, rows);
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = fromTargetRow(map[result[referencedColumnName]] || []);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const morphX = async (input, ctx) => {
|
||||||
|
const { attribute, attributeName, results, populateValue, targetMeta } = input;
|
||||||
|
const { db, uid } = ctx;
|
||||||
|
|
||||||
|
const fromTargetRow = (rowOrRows) => fromRow(targetMeta, rowOrRows);
|
||||||
|
|
||||||
|
const { target, morphBy } = attribute;
|
||||||
|
|
||||||
|
const targetAttribute = db.metadata.get(target).attributes[morphBy];
|
||||||
|
|
||||||
|
if (targetAttribute.relation === 'morphToOne') {
|
||||||
|
const { idColumn, typeColumn } = targetAttribute.morphColumn;
|
||||||
|
|
||||||
|
const referencedValues = _.uniq(
|
||||||
|
results.map((r) => r[idColumn.referencedColumn]).filter((value) => !_.isNil(value))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_.isEmpty(referencedValues)) {
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db.entityManager
|
||||||
|
.createQueryBuilder(target)
|
||||||
|
.init(populateValue)
|
||||||
|
// .addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
|
||||||
|
.where({ [idColumn.name]: referencedValues, [typeColumn.name]: uid })
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const map = _.groupBy(idColumn.name, rows);
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
const matchingRows = map[result[idColumn.referencedColumn]];
|
||||||
|
|
||||||
|
const matchingValue =
|
||||||
|
attribute.relation === 'morphOne' ? _.first(matchingRows) : matchingRows;
|
||||||
|
|
||||||
|
result[attributeName] = fromTargetRow(matchingValue);
|
||||||
|
});
|
||||||
|
} else if (targetAttribute.relation === 'morphToMany') {
|
||||||
|
const { joinTable } = targetAttribute;
|
||||||
|
|
||||||
|
const { joinColumn, morphColumn } = joinTable;
|
||||||
|
|
||||||
|
const { idColumn, typeColumn } = morphColumn;
|
||||||
|
|
||||||
|
const referencedValues = _.uniq(
|
||||||
|
results.map((r) => r[idColumn.referencedColumn]).filter((value) => !_.isNil(value))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_.isEmpty(referencedValues)) {
|
||||||
|
results.forEach((result) => {
|
||||||
|
result[attributeName] = attribute.relation === 'morphOne' ? null : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find with join table
|
||||||
|
const qb = db.entityManager.createQueryBuilder(target);
|
||||||
|
|
||||||
|
const alias = qb.getAlias();
|
||||||
|
|
||||||
|
const rows = await qb
|
||||||
|
.init(populateValue)
|
||||||
|
.join({
|
||||||
|
alias,
|
||||||
|
referencedTable: joinTable.name,
|
||||||
|
referencedColumn: joinColumn.name,
|
||||||
|
rootColumn: joinColumn.referencedColumn,
|
||||||
|
rootTable: qb.alias,
|
||||||
|
on: {
|
||||||
|
...(joinTable.on || {}),
|
||||||
|
field: attributeName,
|
||||||
|
},
|
||||||
|
orderBy: joinTable.orderBy,
|
||||||
|
})
|
||||||
|
.addSelect([`${alias}.${idColumn.name}`, `${alias}.${typeColumn.name}`])
|
||||||
|
.where({
|
||||||
|
[`${alias}.${idColumn.name}`]: referencedValues,
|
||||||
|
[`${alias}.${typeColumn.name}`]: uid,
|
||||||
|
})
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const map = _.groupBy(idColumn.name, rows);
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
const matchingRows = map[result[idColumn.referencedColumn]];
|
||||||
|
|
||||||
|
const matchingValue =
|
||||||
|
attribute.relation === 'morphOne' ? _.first(matchingRows) : matchingRows;
|
||||||
|
|
||||||
|
result[attributeName] = fromTargetRow(matchingValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const morphToMany = async (input, ctx) => {
|
||||||
|
const { attribute, attributeName, results, populateValue } = input;
|
||||||
|
const { db } = ctx;
|
||||||
|
|
||||||
|
// find with join table
|
||||||
|
const { joinTable } = attribute;
|
||||||
|
|
||||||
|
const { joinColumn, morphColumn } = joinTable;
|
||||||
|
const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
|
||||||
|
|
||||||
|
// fetch join table to create the ids map then do the same as morphToOne without the first
|
||||||
|
|
||||||
|
const referencedValues = _.uniq(
|
||||||
|
results.map((r) => r[joinColumn.referencedColumn]).filter((value) => !_.isNil(value))
|
||||||
|
);
|
||||||
|
|
||||||
|
const qb = db.entityManager.createQueryBuilder(joinTable.name);
|
||||||
|
|
||||||
|
const joinRows = await qb
|
||||||
|
.where({
|
||||||
|
[joinColumn.name]: referencedValues,
|
||||||
|
...(joinTable.on || {}),
|
||||||
|
})
|
||||||
|
.orderBy([joinColumn.name, 'order'])
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
const joinMap = _.groupBy(joinColumn.name, joinRows);
|
||||||
|
|
||||||
|
const idsByType = joinRows.reduce((acc, result) => {
|
||||||
|
const idValue = result[morphColumn.idColumn.name];
|
||||||
|
const typeValue = result[morphColumn.typeColumn.name];
|
||||||
|
|
||||||
|
if (!idValue || !typeValue) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.has(typeValue, acc)) {
|
||||||
|
acc[typeValue] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[typeValue].push(idValue);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const map = {};
|
||||||
|
for (const type of Object.keys(idsByType)) {
|
||||||
|
const ids = idsByType[type];
|
||||||
|
|
||||||
|
// type was removed but still in morph relation
|
||||||
|
if (!db.metadata.get(type)) {
|
||||||
|
map[type] = {};
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qb = db.entityManager.createQueryBuilder(type);
|
||||||
|
|
||||||
|
const rows = await qb
|
||||||
|
.init(populateValue)
|
||||||
|
.addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
|
||||||
|
.where({ [idColumn.referencedColumn]: ids })
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
map[type] = _.groupBy(idColumn.referencedColumn, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
const joinResults = joinMap[result[joinColumn.referencedColumn]] || [];
|
||||||
|
|
||||||
|
const matchingRows = joinResults.flatMap((joinResult) => {
|
||||||
|
const id = joinResult[idColumn.name];
|
||||||
|
const type = joinResult[typeColumn.name];
|
||||||
|
|
||||||
|
const fromTargetRow = (rowOrRows) => fromRow(db.metadata.get(type), rowOrRows);
|
||||||
|
|
||||||
|
return (map[type][id] || []).map((row) => {
|
||||||
|
return {
|
||||||
|
[typeField]: type,
|
||||||
|
...fromTargetRow(row),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
result[attributeName] = matchingRows;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const morphToOne = async (input, ctx) => {
|
||||||
|
const { attribute, attributeName, results, populateValue } = input;
|
||||||
|
const { db } = ctx;
|
||||||
|
|
||||||
|
const { morphColumn } = attribute;
|
||||||
|
const { idColumn, typeColumn } = morphColumn;
|
||||||
|
|
||||||
|
// make a map for each type what ids to return
|
||||||
|
// make a nested map per id
|
||||||
|
|
||||||
|
const idsByType = results.reduce((acc, result) => {
|
||||||
|
const idValue = result[morphColumn.idColumn.name];
|
||||||
|
const typeValue = result[morphColumn.typeColumn.name];
|
||||||
|
|
||||||
|
if (!idValue || !typeValue) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.has(typeValue, acc)) {
|
||||||
|
acc[typeValue] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[typeValue].push(idValue);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const map = {};
|
||||||
|
for (const type of Object.keys(idsByType)) {
|
||||||
|
const ids = idsByType[type];
|
||||||
|
|
||||||
|
// type was removed but still in morph relation
|
||||||
|
if (!db.metadata.get(type)) {
|
||||||
|
map[type] = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qb = db.entityManager.createQueryBuilder(type);
|
||||||
|
|
||||||
|
const rows = await qb
|
||||||
|
.init(populateValue)
|
||||||
|
.addSelect(`${qb.alias}.${idColumn.referencedColumn}`)
|
||||||
|
.where({ [idColumn.referencedColumn]: ids })
|
||||||
|
.execute({ mapResults: false });
|
||||||
|
|
||||||
|
map[type] = _.groupBy(idColumn.referencedColumn, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
const id = result[idColumn.name];
|
||||||
|
const type = result[typeColumn.name];
|
||||||
|
|
||||||
|
if (!type || !id) {
|
||||||
|
result[attributeName] = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingRows = map[type][id];
|
||||||
|
|
||||||
|
const fromTargetRow = (rowOrRows) => fromRow(db.metadata.get(type), rowOrRows);
|
||||||
|
|
||||||
|
result[attributeName] = fromTargetRow(_.first(matchingRows));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Omit limit & offset to avoid needing a query per result to avoid making too many queries
|
||||||
|
const pickPopulateParams = _.pick([
|
||||||
|
'select',
|
||||||
|
'count',
|
||||||
|
'where',
|
||||||
|
'populate',
|
||||||
|
'orderBy',
|
||||||
|
'limit',
|
||||||
|
'offset',
|
||||||
|
'filters',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const applyPopulate = async (results, populate, ctx) => {
|
||||||
|
const { db, uid, qb } = ctx;
|
||||||
|
const meta = db.metadata.get(uid);
|
||||||
|
|
||||||
|
if (_.isEmpty(results)) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attributeName of Object.keys(populate)) {
|
||||||
|
const attribute = meta.attributes[attributeName];
|
||||||
|
const targetMeta = db.metadata.get(attribute.target);
|
||||||
|
|
||||||
|
const populateValue = {
|
||||||
|
filters: qb.state.filters,
|
||||||
|
...pickPopulateParams(populate[attributeName]),
|
||||||
|
};
|
||||||
|
|
||||||
|
const isCount = populateValue.count === true;
|
||||||
|
|
||||||
|
const input = { attribute, attributeName, results, populateValue, targetMeta, isCount };
|
||||||
|
|
||||||
|
switch (attribute.relation) {
|
||||||
|
case 'oneToOne':
|
||||||
|
case 'manyToOne': {
|
||||||
|
await XtoOne(input, ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'oneToMany': {
|
||||||
|
await oneToMany(input, ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'manyToMany': {
|
||||||
|
await manyToMany(input, ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'morphOne':
|
||||||
|
case 'morphMany': {
|
||||||
|
await morphX(input, ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'morphToMany': {
|
||||||
|
await morphToMany(input, ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'morphToOne': {
|
||||||
|
await morphToOne(input, ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = applyPopulate;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const applyPopulate = require('./apply');
|
||||||
|
const processPopulate = require('./process');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
applyPopulate,
|
||||||
|
processPopulate,
|
||||||
|
};
|
||||||
96
packages/core/database/lib/query/helpers/populate/process.js
Normal file
96
packages/core/database/lib/query/helpers/populate/process.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const _ = require('lodash/fp');
|
||||||
|
|
||||||
|
const types = require('../../../types');
|
||||||
|
|
||||||
|
const getRootLevelPopulate = (meta) => {
|
||||||
|
const populate = {};
|
||||||
|
|
||||||
|
for (const attributeName of Object.keys(meta.attributes)) {
|
||||||
|
const attribute = meta.attributes[attributeName];
|
||||||
|
if (attribute.type === 'relation') {
|
||||||
|
populate[attributeName] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return populate;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts and prepares the query for populate
|
||||||
|
*
|
||||||
|
* @param {boolean|string[]|object} populate populate param
|
||||||
|
* @param {object} ctx query context
|
||||||
|
* @param {object} ctx.db database instance
|
||||||
|
* @param {object} ctx.qb query builder instance
|
||||||
|
* @param {string} ctx.uid model uid
|
||||||
|
*/
|
||||||
|
const processPopulate = (populate, ctx) => {
|
||||||
|
const { qb, db, uid } = ctx;
|
||||||
|
const meta = db.metadata.get(uid);
|
||||||
|
|
||||||
|
let populateMap = {};
|
||||||
|
|
||||||
|
if (populate === false || _.isNil(populate)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (populate === true) {
|
||||||
|
populateMap = getRootLevelPopulate(meta);
|
||||||
|
} else if (Array.isArray(populate)) {
|
||||||
|
for (const key of populate) {
|
||||||
|
const [root, ...rest] = key.split('.');
|
||||||
|
|
||||||
|
if (rest.length > 0) {
|
||||||
|
const subPopulate = rest.join('.');
|
||||||
|
|
||||||
|
if (populateMap[root]) {
|
||||||
|
if (populateMap[root] === true) {
|
||||||
|
populateMap[root] = {
|
||||||
|
populate: [subPopulate],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
populateMap[root].populate = [subPopulate].concat(populateMap[root].populate || []);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
populateMap[root] = {
|
||||||
|
populate: [subPopulate],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
populateMap[root] = populateMap[root] ? populateMap[root] : true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
populateMap = populate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isPlainObject(populateMap)) {
|
||||||
|
throw new Error('Populate must be an object');
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalPopulate = {};
|
||||||
|
for (const key of Object.keys(populateMap)) {
|
||||||
|
const attribute = meta.attributes[key];
|
||||||
|
|
||||||
|
if (!attribute) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!types.isRelation(attribute.type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure id is present for future populate queries
|
||||||
|
if (_.has('id', meta.attributes)) {
|
||||||
|
qb.addSelect('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPopulate[key] = populateMap[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalPopulate;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = processPopulate;
|
||||||
@ -326,14 +326,14 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
|
|||||||
processSelect() {
|
processSelect() {
|
||||||
state.select = state.select.map((field) => helpers.toColumnName(meta, field));
|
state.select = state.select.map((field) => helpers.toColumnName(meta, field));
|
||||||
|
|
||||||
if (this.shouldUseDistinct()) {
|
// if (this.shouldUseDistinct()) {
|
||||||
const joinsOrderByColumns = state.joins.flatMap((join) => {
|
// const joinsOrderByColumns = state.joins.flatMap((join) => {
|
||||||
return _.keys(join.orderBy).map((key) => this.aliasColumn(key, join.alias));
|
// return _.keys(join.orderBy).map((key) => this.aliasColumn(key, join.alias));
|
||||||
});
|
// });
|
||||||
const orderByColumns = state.orderBy.map(({ column }) => column);
|
// const orderByColumns = state.orderBy.map(({ column }) => column);
|
||||||
|
|
||||||
state.select = _.uniq([...joinsOrderByColumns, ...orderByColumns, ...state.select]);
|
// state.select = _.uniq([...joinsOrderByColumns, ...orderByColumns, ...state.select]);
|
||||||
}
|
// }
|
||||||
},
|
},
|
||||||
|
|
||||||
getKnexQuery() {
|
getKnexQuery() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user