node_modules ignore

This commit is contained in:
2025-05-08 23:43:47 +02:00
parent e19d52f172
commit 4574544c9f
65041 changed files with 10593536 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
import type { QueryBuilder } from '../../query-builder';
import type { Database } from '../../..';
type Context = {
db: Database;
qb: QueryBuilder;
uid: string;
};
type Row = Record<string, unknown>;
declare const applyPopulate: (results: Row[], populate: Record<string, any>, ctx: Context) => Promise<Row[] | undefined>;
export default applyPopulate;
//# sourceMappingURL=apply.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../../../src/query/helpers/populate/apply.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AASzC,KAAK,OAAO,GAAG;IACb,EAAE,EAAE,QAAQ,CAAC;IACb,EAAE,EAAE,YAAY,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAoBF,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA4nBnC,QAAA,MAAM,aAAa,YAAmB,GAAG,EAAE,YAAY,OAAO,MAAM,EAAE,GAAG,CAAC,OAAO,OAAO,+BAgEvF,CAAC;AAEF,eAAe,aAAa,CAAC"}

View File

@@ -0,0 +1,592 @@
'use strict';
var _ = require('lodash/fp');
var transform = require('../transform.js');
// We must select the join column id, however whatever it is named will overwrite an attribute of the same name
// Therefore, we will prefix with something unlikely to conflict with a user attribute
// TODO: ...and completely restrict the strapi_ prefix for an attribute name in the future
const joinColPrefix = '__strapi';
/**
* Populate oneToOne and manyToOne relation
* @param {*} input
* @param {*} ctx
* @returns
*/ const XtoOne = async (input, ctx)=>{
const { attribute, attributeName, results, populateValue, targetMeta, isCount } = input;
const { db, qb } = ctx;
const fromTargetRow = (rowOrRows)=>transform.fromRow(targetMeta, rowOrRows);
if ('joinColumn' in attribute && 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 ('joinTable' in attribute && 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 joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
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] = 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(joinColSelect).where({
[joinColAlias]: referencedValues
}).execute({
mapResults: false
});
const map = _.groupBy(joinColRenameAs)(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)=>transform.fromRow(targetMeta, rowOrRows);
if ('joinColumn' in attribute && attribute.joinColumn) {
const { name: joinColumnName, referencedColumn: referencedColumnName, on } = 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,
...on && typeof on === 'function' ? on({
populateValue,
results
}) : {}
}).execute({
mapResults: false
});
const map = _.groupBy(referencedColumnName)(rows);
results.forEach((result)=>{
result[attributeName] = fromTargetRow(map[result[joinColumnName]] || []);
});
return;
}
if ('joinTable' in attribute && 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 joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
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([
joinColSelect,
qb.raw('count(*) AS count')
]).where({
[joinColAlias]: referencedValues
}).groupBy(joinColAlias).execute({
mapResults: false
});
const map = rows.reduce((map, row)=>{
map[row[joinColRenameAs]] = {
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: _.mapValues((v)=>populateValue.ordering || v, joinTable.orderBy)
}).addSelect(joinColSelect).where({
[joinColAlias]: referencedValues
}).execute({
mapResults: false
});
const map = _.groupBy(joinColRenameAs)(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)=>transform.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 joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
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: _.mapValues((v)=>populateValue.ordering || v, joinTable.orderBy)
}).addSelect(joinColSelect).where({
[joinColAlias]: referencedValues
}).execute({
mapResults: false
});
const map = _.groupBy(joinColRenameAs)(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)=>transform.fromRow(targetMeta, rowOrRows);
const { target, morphBy } = attribute;
const targetAttribute = db.metadata.get(target).attributes[morphBy];
if (targetAttribute.type === 'relation' && 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.type === 'relation' && 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: _.mapValues((v)=>populateValue.ordering || v, 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 || {},
// If the populateValue contains an "on" property,
// only populate the types defined in it
...'on' in populateValue ? {
[morphColumn.typeColumn.name]: Object.keys(populateValue.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 = {};
const { on, ...typePopulate } = populateValue;
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(on?.[type] ?? typePopulate).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 targetMeta = db.metadata.get(type);
const fromTargetRow = (rowOrRows)=>transform.fromRow(targetMeta, 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 (!(typeValue in acc)) {
acc[typeValue] = [];
}
acc[typeValue].push(idValue);
return acc;
}, {});
const map = {};
const { on, ...typePopulate } = populateValue;
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(on?.[type] ?? typePopulate).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)=>transform.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 = (populate)=>{
const fieldsToPick = [
'select',
'count',
'where',
'populate',
'orderBy',
'filters',
'ordering',
'on'
];
if (populate.count !== true) {
fieldsToPick.push('limit', 'offset');
}
return _.pick(fieldsToPick, populate);
};
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];
if (attribute.type !== 'relation') {
throw new Error(`Invalid populate attribute ${attributeName}`);
}
const populateValue = {
filters: qb.state.filters,
...pickPopulateParams(populate[attributeName])
};
const isCount = 'count' in populateValue && populateValue.count === true;
switch(attribute.relation){
case 'oneToOne':
case 'manyToOne':
{
const targetMeta = db.metadata.get(attribute.target);
const input = {
attribute,
attributeName,
results,
populateValue,
targetMeta,
isCount
};
await XtoOne(input, ctx);
break;
}
case 'oneToMany':
{
const targetMeta = db.metadata.get(attribute.target);
const input = {
attribute,
attributeName,
results,
populateValue,
targetMeta,
isCount
};
await oneToMany(input, ctx);
break;
}
case 'manyToMany':
{
const targetMeta = db.metadata.get(attribute.target);
const input = {
attribute,
attributeName,
results,
populateValue,
targetMeta,
isCount
};
await manyToMany(input, ctx);
break;
}
case 'morphOne':
case 'morphMany':
{
const targetMeta = db.metadata.get(attribute.target);
const input = {
attribute,
attributeName,
results,
populateValue,
targetMeta,
isCount
};
await morphX(input, ctx);
break;
}
case 'morphToMany':
{
const input = {
attribute,
attributeName,
results,
populateValue,
isCount
};
await morphToMany(input, ctx);
break;
}
case 'morphToOne':
{
const input = {
attribute,
attributeName,
results,
populateValue,
isCount
};
await morphToOne(input, ctx);
break;
}
}
}
};
module.exports = applyPopulate;
//# sourceMappingURL=apply.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,590 @@
import _ from 'lodash/fp';
import { fromRow } from '../transform.mjs';
// We must select the join column id, however whatever it is named will overwrite an attribute of the same name
// Therefore, we will prefix with something unlikely to conflict with a user attribute
// TODO: ...and completely restrict the strapi_ prefix for an attribute name in the future
const joinColPrefix = '__strapi';
/**
* Populate oneToOne and manyToOne relation
* @param {*} input
* @param {*} ctx
* @returns
*/ const XtoOne = async (input, ctx)=>{
const { attribute, attributeName, results, populateValue, targetMeta, isCount } = input;
const { db, qb } = ctx;
const fromTargetRow = (rowOrRows)=>fromRow(targetMeta, rowOrRows);
if ('joinColumn' in attribute && 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 ('joinTable' in attribute && 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 joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
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] = 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(joinColSelect).where({
[joinColAlias]: referencedValues
}).execute({
mapResults: false
});
const map = _.groupBy(joinColRenameAs)(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 ('joinColumn' in attribute && attribute.joinColumn) {
const { name: joinColumnName, referencedColumn: referencedColumnName, on } = 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,
...on && typeof on === 'function' ? on({
populateValue,
results
}) : {}
}).execute({
mapResults: false
});
const map = _.groupBy(referencedColumnName)(rows);
results.forEach((result)=>{
result[attributeName] = fromTargetRow(map[result[joinColumnName]] || []);
});
return;
}
if ('joinTable' in attribute && 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 joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
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([
joinColSelect,
qb.raw('count(*) AS count')
]).where({
[joinColAlias]: referencedValues
}).groupBy(joinColAlias).execute({
mapResults: false
});
const map = rows.reduce((map, row)=>{
map[row[joinColRenameAs]] = {
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: _.mapValues((v)=>populateValue.ordering || v, joinTable.orderBy)
}).addSelect(joinColSelect).where({
[joinColAlias]: referencedValues
}).execute({
mapResults: false
});
const map = _.groupBy(joinColRenameAs)(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 joinColRenameAs = `${joinColPrefix}${joinColumnName}`;
const joinColSelect = `${joinColAlias} as ${joinColRenameAs}`;
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: _.mapValues((v)=>populateValue.ordering || v, joinTable.orderBy)
}).addSelect(joinColSelect).where({
[joinColAlias]: referencedValues
}).execute({
mapResults: false
});
const map = _.groupBy(joinColRenameAs)(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.type === 'relation' && 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.type === 'relation' && 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: _.mapValues((v)=>populateValue.ordering || v, 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 || {},
// If the populateValue contains an "on" property,
// only populate the types defined in it
...'on' in populateValue ? {
[morphColumn.typeColumn.name]: Object.keys(populateValue.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 = {};
const { on, ...typePopulate } = populateValue;
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(on?.[type] ?? typePopulate).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 targetMeta = db.metadata.get(type);
const fromTargetRow = (rowOrRows)=>fromRow(targetMeta, 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 (!(typeValue in acc)) {
acc[typeValue] = [];
}
acc[typeValue].push(idValue);
return acc;
}, {});
const map = {};
const { on, ...typePopulate } = populateValue;
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(on?.[type] ?? typePopulate).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 = (populate)=>{
const fieldsToPick = [
'select',
'count',
'where',
'populate',
'orderBy',
'filters',
'ordering',
'on'
];
if (populate.count !== true) {
fieldsToPick.push('limit', 'offset');
}
return _.pick(fieldsToPick, populate);
};
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];
if (attribute.type !== 'relation') {
throw new Error(`Invalid populate attribute ${attributeName}`);
}
const populateValue = {
filters: qb.state.filters,
...pickPopulateParams(populate[attributeName])
};
const isCount = 'count' in populateValue && populateValue.count === true;
switch(attribute.relation){
case 'oneToOne':
case 'manyToOne':
{
const targetMeta = db.metadata.get(attribute.target);
const input = {
attribute,
attributeName,
results,
populateValue,
targetMeta,
isCount
};
await XtoOne(input, ctx);
break;
}
case 'oneToMany':
{
const targetMeta = db.metadata.get(attribute.target);
const input = {
attribute,
attributeName,
results,
populateValue,
targetMeta,
isCount
};
await oneToMany(input, ctx);
break;
}
case 'manyToMany':
{
const targetMeta = db.metadata.get(attribute.target);
const input = {
attribute,
attributeName,
results,
populateValue,
targetMeta,
isCount
};
await manyToMany(input, ctx);
break;
}
case 'morphOne':
case 'morphMany':
{
const targetMeta = db.metadata.get(attribute.target);
const input = {
attribute,
attributeName,
results,
populateValue,
targetMeta,
isCount
};
await morphX(input, ctx);
break;
}
case 'morphToMany':
{
const input = {
attribute,
attributeName,
results,
populateValue,
isCount
};
await morphToMany(input, ctx);
break;
}
case 'morphToOne':
{
const input = {
attribute,
attributeName,
results,
populateValue,
isCount
};
await morphToOne(input, ctx);
break;
}
}
}
};
export { applyPopulate as default };
//# sourceMappingURL=apply.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import applyPopulate from './apply';
import processPopulate from './process';
export { applyPopulate, processPopulate };
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/query/helpers/populate/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,SAAS,CAAC;AACpC,OAAO,eAAe,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,CAAC"}

View File

@@ -0,0 +1,24 @@
import type { QueryBuilder } from '../../query-builder';
import type { Database } from '../../..';
type Context = {
qb: QueryBuilder;
db: Database;
uid: string;
};
type PopulateMap = {
[key: string]: true | {
populate?: PopulateMap | true | string[];
};
};
/**
* 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
*/
declare const processPopulate: (populate: unknown, ctx: Context) => PopulateMap | null;
export default processPopulate;
//# sourceMappingURL=process.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../../../src/query/helpers/populate/process.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAezC,KAAK,OAAO,GAAG;IACb,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,QAAQ,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,CAAC,GAAG,EAAE,MAAM,GACR,IAAI,GACJ;QACE,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,MAAM,EAAE,CAAC;KAC1C,CAAC;CACP,CAAC;AAEF;;;;;;;;GAQG;AACH,QAAA,MAAM,eAAe,aAAc,OAAO,OAAO,OAAO,uBAwEvD,CAAC;AAEF,eAAe,eAAe,CAAC"}

View File

@@ -0,0 +1,92 @@
'use strict';
var _ = require('lodash/fp');
var types = require('../../../utils/types.js');
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]) {
const populateValue = populateMap[root];
if (populateValue === true) {
populateMap[root] = {
populate: [
subPopulate
]
};
} else {
populateValue.populate = [
subPopulate
].concat(populateValue.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 to query the join column value if needed,
// so that we can apply the populate later on
if ('joinColumn' in attribute && attribute.joinColumn) {
qb.addSelect(attribute.joinColumn.name);
}
// 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;
//# sourceMappingURL=process.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,90 @@
import _ from 'lodash/fp';
import { isRelation } from '../../../utils/types.mjs';
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]) {
const populateValue = populateMap[root];
if (populateValue === true) {
populateMap[root] = {
populate: [
subPopulate
]
};
} else {
populateValue.populate = [
subPopulate
].concat(populateValue.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 (!isRelation(attribute.type)) {
continue;
}
// Make sure to query the join column value if needed,
// so that we can apply the populate later on
if ('joinColumn' in attribute && attribute.joinColumn) {
qb.addSelect(attribute.joinColumn.name);
}
// Make sure id is present for future populate queries
if (_.has('id', meta.attributes)) {
qb.addSelect('id');
}
finalPopulate[key] = populateMap[key];
}
return finalPopulate;
};
export { processPopulate as default };
//# sourceMappingURL=process.mjs.map

File diff suppressed because one or more lines are too long