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,8 @@
export * from './search';
export * from './order-by';
export * from './join';
export * from './populate';
export * from './where';
export * from './transform';
export * from './streams';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/query/helpers/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC"}

View File

@@ -0,0 +1,30 @@
import type { Knex } from 'knex';
import type { Ctx } from '../types';
export interface Join {
method?: 'leftJoin' | 'innerJoin';
alias: string;
referencedTable: string;
referencedColumn: string;
rootColumn: string;
rootTable?: string;
on?: Record<string, any>;
orderBy?: Record<string, 'asc' | 'desc'>;
}
interface JoinOptions {
alias: string;
refAlias?: string;
attributeName: string;
attribute: any;
}
interface PivotJoinOptions {
alias: string;
refAlias?: string;
joinTable: any;
targetMeta: any;
}
declare const createPivotJoin: (ctx: Ctx, { alias, refAlias, joinTable, targetMeta }: PivotJoinOptions) => string;
declare const createJoin: (ctx: Ctx, { alias, refAlias, attributeName, attribute }: JoinOptions) => string;
declare const applyJoin: (qb: Knex.QueryBuilder, join: Join) => void;
declare const applyJoins: (qb: Knex.QueryBuilder, joins: Join[]) => void;
export { createJoin, createPivotJoin, applyJoins, applyJoin };
//# sourceMappingURL=join.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"join.d.ts","sourceRoot":"","sources":["../../../src/query/helpers/join.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,MAAM,WAAW,IAAI;IACnB,MAAM,CAAC,EAAE,UAAU,GAAG,WAAW,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC;CAC1C;AAED,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,GAAG,CAAC;CAChB;AAED,UAAU,gBAAgB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,GAAG,CAAC;IACf,UAAU,EAAE,GAAG,CAAC;CACjB;AAED,QAAA,MAAM,eAAe,QACd,GAAG,8CACoC,gBAAgB,WAuB7D,CAAC;AAEF,QAAA,MAAM,UAAU,QAAS,GAAG,iDAAiD,WAAW,WAoFvF,CAAC;AAGF,QAAA,MAAM,SAAS,OAAQ,KAAK,YAAY,QAAQ,IAAI,SA6BnD,CAAC;AAEF,QAAA,MAAM,UAAU,OAAQ,KAAK,YAAY,SAAS,IAAI,EAAE,SAEvD,CAAC;AAEF,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC"}

View File

@@ -0,0 +1,127 @@
'use strict';
const createPivotJoin = (ctx, { alias, refAlias, joinTable, targetMeta })=>{
const { qb } = ctx;
const joinAlias = qb.getAlias();
qb.join({
alias: joinAlias,
referencedTable: joinTable.name,
referencedColumn: joinTable.joinColumn.name,
rootColumn: joinTable.joinColumn.referencedColumn,
rootTable: alias,
on: joinTable.on
});
const subAlias = refAlias || qb.getAlias();
qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: joinTable.inverseJoinColumn.referencedColumn,
rootColumn: joinTable.inverseJoinColumn.name,
rootTable: joinAlias
});
return subAlias;
};
const createJoin = (ctx, { alias, refAlias, attributeName, attribute })=>{
const { db, qb, uid } = ctx;
if (attribute.type !== 'relation') {
throw new Error(`Cannot join on non relational field ${attributeName}`);
}
const targetMeta = db.metadata.get(attribute.target);
if ([
'morphOne',
'morphMany'
].includes(attribute.relation)) {
const targetAttribute = targetMeta.attributes[attribute.morphBy];
// @ts-expect-error - morphBy is not defined on the attribute
const { joinTable, morphColumn } = targetAttribute;
if (morphColumn) {
const subAlias = refAlias || qb.getAlias();
qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: morphColumn.idColumn.name,
rootColumn: morphColumn.idColumn.referencedColumn,
rootTable: alias,
on: {
[morphColumn.typeColumn.name]: uid,
...morphColumn.on
}
});
return subAlias;
}
if (joinTable) {
const joinAlias = qb.getAlias();
qb.join({
alias: joinAlias,
referencedTable: joinTable.name,
referencedColumn: joinTable.morphColumn.idColumn.name,
rootColumn: joinTable.morphColumn.idColumn.referencedColumn,
rootTable: alias,
on: {
[joinTable.morphColumn.typeColumn.name]: uid,
field: attributeName
}
});
const subAlias = refAlias || qb.getAlias();
qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: joinTable.joinColumn.referencedColumn,
rootColumn: joinTable.joinColumn.name,
rootTable: joinAlias
});
return subAlias;
}
return alias;
}
const { joinColumn } = attribute;
if (joinColumn) {
const subAlias = refAlias || qb.getAlias();
qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: joinColumn.referencedColumn,
rootColumn: joinColumn.name,
rootTable: alias
});
return subAlias;
}
const { joinTable } = attribute;
if (joinTable) {
return createPivotJoin(ctx, {
alias,
refAlias,
joinTable,
targetMeta
});
}
return alias;
};
// TODO: toColumnName for orderBy & on
const applyJoin = (qb, join)=>{
const { method = 'leftJoin', alias, referencedTable, referencedColumn, rootColumn, // FIXME: qb.alias can't exist here
rootTable, on, orderBy } = join;
qb[method](`${referencedTable} as ${alias}`, (inner)=>{
inner.on(`${rootTable}.${rootColumn}`, `${alias}.${referencedColumn}`);
if (on) {
for (const key of Object.keys(on)){
inner.onVal(`${alias}.${key}`, on[key]);
}
}
});
if (orderBy) {
Object.keys(orderBy).forEach((column)=>{
const direction = orderBy[column];
qb.orderBy(`${alias}.${column}`, direction);
});
}
};
const applyJoins = (qb, joins)=>{
return joins.forEach((join)=>applyJoin(qb, join));
};
exports.applyJoin = applyJoin;
exports.applyJoins = applyJoins;
exports.createJoin = createJoin;
exports.createPivotJoin = createPivotJoin;
//# sourceMappingURL=join.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,122 @@
const createPivotJoin = (ctx, { alias, refAlias, joinTable, targetMeta })=>{
const { qb } = ctx;
const joinAlias = qb.getAlias();
qb.join({
alias: joinAlias,
referencedTable: joinTable.name,
referencedColumn: joinTable.joinColumn.name,
rootColumn: joinTable.joinColumn.referencedColumn,
rootTable: alias,
on: joinTable.on
});
const subAlias = refAlias || qb.getAlias();
qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: joinTable.inverseJoinColumn.referencedColumn,
rootColumn: joinTable.inverseJoinColumn.name,
rootTable: joinAlias
});
return subAlias;
};
const createJoin = (ctx, { alias, refAlias, attributeName, attribute })=>{
const { db, qb, uid } = ctx;
if (attribute.type !== 'relation') {
throw new Error(`Cannot join on non relational field ${attributeName}`);
}
const targetMeta = db.metadata.get(attribute.target);
if ([
'morphOne',
'morphMany'
].includes(attribute.relation)) {
const targetAttribute = targetMeta.attributes[attribute.morphBy];
// @ts-expect-error - morphBy is not defined on the attribute
const { joinTable, morphColumn } = targetAttribute;
if (morphColumn) {
const subAlias = refAlias || qb.getAlias();
qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: morphColumn.idColumn.name,
rootColumn: morphColumn.idColumn.referencedColumn,
rootTable: alias,
on: {
[morphColumn.typeColumn.name]: uid,
...morphColumn.on
}
});
return subAlias;
}
if (joinTable) {
const joinAlias = qb.getAlias();
qb.join({
alias: joinAlias,
referencedTable: joinTable.name,
referencedColumn: joinTable.morphColumn.idColumn.name,
rootColumn: joinTable.morphColumn.idColumn.referencedColumn,
rootTable: alias,
on: {
[joinTable.morphColumn.typeColumn.name]: uid,
field: attributeName
}
});
const subAlias = refAlias || qb.getAlias();
qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: joinTable.joinColumn.referencedColumn,
rootColumn: joinTable.joinColumn.name,
rootTable: joinAlias
});
return subAlias;
}
return alias;
}
const { joinColumn } = attribute;
if (joinColumn) {
const subAlias = refAlias || qb.getAlias();
qb.join({
alias: subAlias,
referencedTable: targetMeta.tableName,
referencedColumn: joinColumn.referencedColumn,
rootColumn: joinColumn.name,
rootTable: alias
});
return subAlias;
}
const { joinTable } = attribute;
if (joinTable) {
return createPivotJoin(ctx, {
alias,
refAlias,
joinTable,
targetMeta
});
}
return alias;
};
// TODO: toColumnName for orderBy & on
const applyJoin = (qb, join)=>{
const { method = 'leftJoin', alias, referencedTable, referencedColumn, rootColumn, // FIXME: qb.alias can't exist here
rootTable, on, orderBy } = join;
qb[method](`${referencedTable} as ${alias}`, (inner)=>{
inner.on(`${rootTable}.${rootColumn}`, `${alias}.${referencedColumn}`);
if (on) {
for (const key of Object.keys(on)){
inner.onVal(`${alias}.${key}`, on[key]);
}
}
});
if (orderBy) {
Object.keys(orderBy).forEach((column)=>{
const direction = orderBy[column];
qb.orderBy(`${alias}.${column}`, direction);
});
}
};
const applyJoins = (qb, joins)=>{
return joins.forEach((join)=>applyJoin(qb, join));
};
export { applyJoin, applyJoins, createJoin, createPivotJoin };
//# sourceMappingURL=join.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
import knex from 'knex';
import type { Ctx } from '../types';
type OrderByCtx = Ctx & {
alias?: string;
};
type OrderBy = string | {
[key: string]: 'asc' | 'desc';
} | OrderBy[];
type OrderByValue = {
column: string;
order?: 'asc' | 'desc';
};
export declare const processOrderBy: (orderBy: OrderBy, ctx: OrderByCtx) => OrderByValue[];
export declare const getStrapiOrderColumnAlias: (column: string) => string;
/**
* Wraps the original Knex query with deep sorting functionality.
*
* The function takes an original query and an OrderByCtx object as parameters and returns a new Knex query with deep sorting applied.
*/
export declare const wrapWithDeepSort: (originalQuery: knex.Knex.QueryBuilder, ctx: OrderByCtx) => knex.Knex.QueryBuilder<any, any>;
export {};
//# sourceMappingURL=order-by.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"order-by.d.ts","sourceRoot":"","sources":["../../../src/query/helpers/order-by.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,MAAM,CAAC;AAMxB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,KAAK,UAAU,GAAG,GAAG,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3C,KAAK,OAAO,GAAG,MAAM,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;CAAE,GAAG,OAAO,EAAE,CAAC;AACtE,KAAK,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,CAAC;AAK/D,eAAO,MAAM,cAAc,YAAa,OAAO,OAAO,UAAU,KAAG,YAAY,EAwD9E,CAAC;AAEF,eAAO,MAAM,yBAAyB,WAAY,MAAM,WAIvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,kBAAmB,KAAK,IAAI,CAAC,YAAY,OAAO,UAAU,qCAsItF,CAAC"}

View File

@@ -0,0 +1,167 @@
'use strict';
var _ = require('lodash/fp');
var types = require('../../utils/types.js');
var join = require('./join.js');
var transform = require('./transform.js');
const COL_STRAPI_ROW_NUMBER = '__strapi_row_number';
const COL_STRAPI_ORDER_BY_PREFIX = '__strapi_order_by';
const processOrderBy = (orderBy, ctx)=>{
const { db, uid, qb, alias } = ctx;
const meta = db.metadata.get(uid);
const { attributes } = meta;
if (typeof orderBy === 'string') {
const attribute = attributes[orderBy];
if (!attribute) {
throw new Error(`Attribute ${orderBy} not found on model ${uid}`);
}
const columnName = transform.toColumnName(meta, orderBy);
return [
{
column: qb.aliasColumn(columnName, alias)
}
];
}
if (Array.isArray(orderBy)) {
return orderBy.flatMap((value)=>processOrderBy(value, ctx));
}
if (_.isPlainObject(orderBy)) {
return Object.entries(orderBy).flatMap(([key, direction])=>{
const value = orderBy[key];
const attribute = attributes[key];
if (!attribute) {
throw new Error(`Attribute ${key} not found on model ${uid}`);
}
if (types.isScalar(attribute.type)) {
const columnName = transform.toColumnName(meta, key);
return {
column: qb.aliasColumn(columnName, alias),
order: direction
};
}
if (attribute.type === 'relation' && 'target' in attribute) {
const subAlias = join.createJoin(ctx, {
alias: alias || qb.alias,
attributeName: key,
attribute
});
return processOrderBy(value, {
db,
qb,
alias: subAlias,
uid: attribute.target
});
}
throw new Error(`You cannot order on ${attribute.type} types`);
});
}
throw new Error('Invalid orderBy syntax');
};
const getStrapiOrderColumnAlias = (column)=>{
const trimmedColumnName = column.replaceAll('.', '_');
return `${COL_STRAPI_ORDER_BY_PREFIX}__${trimmedColumnName}`;
};
/**
* Wraps the original Knex query with deep sorting functionality.
*
* The function takes an original query and an OrderByCtx object as parameters and returns a new Knex query with deep sorting applied.
*/ const wrapWithDeepSort = (originalQuery, ctx)=>{
/**
* Notes:
* - The generated query has the following flow: baseQuery (filtered unsorted data) -> T (partitioned/sorted data) --> resultQuery (distinct, paginated, sorted data)
* - Pagination and selection are transferred from the original query to the outer one to avoid pruning rows too early
* - Filtering (where) has to be done in the deepest sub query possible to avoid processing invalid rows and corrupting the final results
* - We assume that all necessary joins are done in the original query (`originalQuery`), and every needed column is available with the right name and alias.
*/ const { db, qb, uid } = ctx;
const { tableName } = db.metadata.get(uid);
// The orderBy is cloned to avoid unwanted mutations of the original object
const orderBy = _.cloneDeep(qb.state.orderBy);
// 0. Init a new Knex query instance (referenced as resultQuery) using the DB connection
// The connection reuse the original table name (aliased if needed)
const resultQueryAlias = qb.getAlias();
const aliasedTableName = qb.mustUseAlias() ? alias(resultQueryAlias, tableName) : tableName;
const resultQuery = db.getConnection(aliasedTableName);
// 1. Clone the original query to create the sub-query (referenced as baseQuery) and avoid any mutation on the initial object
const baseQuery = originalQuery.clone();
const baseQueryAlias = qb.getAlias();
// Clear unwanted statements from the sub-query 'baseQuery'
// Note: `first()` is cleared through the combination of `baseQuery.clear('limit')` and calling `baseQuery.select(...)` again
// Note: Those statements will be re-applied when duplicates are removed from the final selection
baseQuery// Columns selection
.clear('select')// Pagination and sorting
.clear('order').clear('limit').clear('offset');
// Override the initial select and return only the columns needed for the partitioning.
baseQuery.select(// Always select the row id for future manipulation
prefix(qb.alias, 'id'), // Select every column used in an order by clause, but alias it for future reference
// i.e. if t2.name is present in an order by clause:
// Then, "t2.name" will become "t2.name as __strapi_order_by__t2_name"
...orderBy.map((orderByClause)=>alias(getStrapiOrderColumnAlias(orderByClause.column), orderByClause.column)));
// 2. Create a sub-query callback to extract and sort the partitions using row number
const partitionedQueryAlias = qb.getAlias();
const selectRowsAsNumberedPartitions = (partitionedQuery)=>{
// Transform order by clause to their alias to reference them from baseQuery
const prefixedOrderBy = orderBy.map((orderByClause)=>({
column: prefix(baseQueryAlias, getStrapiOrderColumnAlias(orderByClause.column)),
order: orderByClause.order
}));
// partitionedQuery select must contain every column used for sorting
const orderByColumns = prefixedOrderBy.map(_.prop('column'));
partitionedQuery.select(// Always select baseQuery.id
prefix(baseQueryAlias, 'id'), // Sort columns
...orderByColumns)// The row number is used to assign an index to every row in every partition
.rowNumber(COL_STRAPI_ROW_NUMBER, (subQuery)=>{
for (const orderByClause of prefixedOrderBy){
subQuery.orderBy(orderByClause.column, orderByClause.order, 'last');
}
// And each partition/group is created based on baseQuery.id
subQuery.partitionBy(`${baseQueryAlias}.id`);
}).from(baseQuery.as(baseQueryAlias)).as(partitionedQueryAlias);
};
// 3. Create the final resultQuery query, that select and sort the wanted data using T
const originalSelect = _.difference(qb.state.select, // Remove order by columns from the initial select
qb.state.orderBy.map(_.prop('column')))// Alias everything in resultQuery
.map(prefix(resultQueryAlias));
resultQuery.select(originalSelect)// Join T to resultQuery to access sorted data
// Notes:
// - Only select the first row for each partition
// - Since we're applying the "where" statement directly on baseQuery (and not on resultQuery), we're using an inner join to avoid unwanted rows
.innerJoin(selectRowsAsNumberedPartitions, function() {
this// Only select rows that are returned by T
.on(`${partitionedQueryAlias}.id`, `${resultQueryAlias}.id`)// By only selecting the rows number equal to 1, we make sure we don't have duplicate, and that
// we're selecting rows in the correct order amongst the groups created by the "partition by"
.andOnVal(`${partitionedQueryAlias}.${COL_STRAPI_ROW_NUMBER}`, '=', 1);
});
// Re-apply pagination params
if (qb.state.limit) {
resultQuery.limit(qb.state.limit);
}
if (qb.state.offset) {
resultQuery.offset(qb.state.offset);
}
if (qb.state.first) {
resultQuery.first();
}
// Re-apply the sort using T values
resultQuery.orderBy([
// Transform "order by" clause to their T alias and prefix them with T alias
...orderBy.map((orderByClause)=>({
column: prefix(partitionedQueryAlias, getStrapiOrderColumnAlias(orderByClause.column)),
order: orderByClause.order
})),
// Add T.id to the order by clause to get consistent results in case several rows have the exact same order
{
column: `${partitionedQueryAlias}.id`,
order: 'asc'
}
]);
return resultQuery;
};
// Utils
const alias = _.curry((alias, value)=>`${value} as ${alias}`);
const prefix = _.curry((prefix, value)=>`${prefix}.${value}`);
exports.getStrapiOrderColumnAlias = getStrapiOrderColumnAlias;
exports.processOrderBy = processOrderBy;
exports.wrapWithDeepSort = wrapWithDeepSort;
//# sourceMappingURL=order-by.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,163 @@
import _ from 'lodash/fp';
import { isScalar } from '../../utils/types.mjs';
import { createJoin } from './join.mjs';
import { toColumnName } from './transform.mjs';
const COL_STRAPI_ROW_NUMBER = '__strapi_row_number';
const COL_STRAPI_ORDER_BY_PREFIX = '__strapi_order_by';
const processOrderBy = (orderBy, ctx)=>{
const { db, uid, qb, alias } = ctx;
const meta = db.metadata.get(uid);
const { attributes } = meta;
if (typeof orderBy === 'string') {
const attribute = attributes[orderBy];
if (!attribute) {
throw new Error(`Attribute ${orderBy} not found on model ${uid}`);
}
const columnName = toColumnName(meta, orderBy);
return [
{
column: qb.aliasColumn(columnName, alias)
}
];
}
if (Array.isArray(orderBy)) {
return orderBy.flatMap((value)=>processOrderBy(value, ctx));
}
if (_.isPlainObject(orderBy)) {
return Object.entries(orderBy).flatMap(([key, direction])=>{
const value = orderBy[key];
const attribute = attributes[key];
if (!attribute) {
throw new Error(`Attribute ${key} not found on model ${uid}`);
}
if (isScalar(attribute.type)) {
const columnName = toColumnName(meta, key);
return {
column: qb.aliasColumn(columnName, alias),
order: direction
};
}
if (attribute.type === 'relation' && 'target' in attribute) {
const subAlias = createJoin(ctx, {
alias: alias || qb.alias,
attributeName: key,
attribute
});
return processOrderBy(value, {
db,
qb,
alias: subAlias,
uid: attribute.target
});
}
throw new Error(`You cannot order on ${attribute.type} types`);
});
}
throw new Error('Invalid orderBy syntax');
};
const getStrapiOrderColumnAlias = (column)=>{
const trimmedColumnName = column.replaceAll('.', '_');
return `${COL_STRAPI_ORDER_BY_PREFIX}__${trimmedColumnName}`;
};
/**
* Wraps the original Knex query with deep sorting functionality.
*
* The function takes an original query and an OrderByCtx object as parameters and returns a new Knex query with deep sorting applied.
*/ const wrapWithDeepSort = (originalQuery, ctx)=>{
/**
* Notes:
* - The generated query has the following flow: baseQuery (filtered unsorted data) -> T (partitioned/sorted data) --> resultQuery (distinct, paginated, sorted data)
* - Pagination and selection are transferred from the original query to the outer one to avoid pruning rows too early
* - Filtering (where) has to be done in the deepest sub query possible to avoid processing invalid rows and corrupting the final results
* - We assume that all necessary joins are done in the original query (`originalQuery`), and every needed column is available with the right name and alias.
*/ const { db, qb, uid } = ctx;
const { tableName } = db.metadata.get(uid);
// The orderBy is cloned to avoid unwanted mutations of the original object
const orderBy = _.cloneDeep(qb.state.orderBy);
// 0. Init a new Knex query instance (referenced as resultQuery) using the DB connection
// The connection reuse the original table name (aliased if needed)
const resultQueryAlias = qb.getAlias();
const aliasedTableName = qb.mustUseAlias() ? alias(resultQueryAlias, tableName) : tableName;
const resultQuery = db.getConnection(aliasedTableName);
// 1. Clone the original query to create the sub-query (referenced as baseQuery) and avoid any mutation on the initial object
const baseQuery = originalQuery.clone();
const baseQueryAlias = qb.getAlias();
// Clear unwanted statements from the sub-query 'baseQuery'
// Note: `first()` is cleared through the combination of `baseQuery.clear('limit')` and calling `baseQuery.select(...)` again
// Note: Those statements will be re-applied when duplicates are removed from the final selection
baseQuery// Columns selection
.clear('select')// Pagination and sorting
.clear('order').clear('limit').clear('offset');
// Override the initial select and return only the columns needed for the partitioning.
baseQuery.select(// Always select the row id for future manipulation
prefix(qb.alias, 'id'), // Select every column used in an order by clause, but alias it for future reference
// i.e. if t2.name is present in an order by clause:
// Then, "t2.name" will become "t2.name as __strapi_order_by__t2_name"
...orderBy.map((orderByClause)=>alias(getStrapiOrderColumnAlias(orderByClause.column), orderByClause.column)));
// 2. Create a sub-query callback to extract and sort the partitions using row number
const partitionedQueryAlias = qb.getAlias();
const selectRowsAsNumberedPartitions = (partitionedQuery)=>{
// Transform order by clause to their alias to reference them from baseQuery
const prefixedOrderBy = orderBy.map((orderByClause)=>({
column: prefix(baseQueryAlias, getStrapiOrderColumnAlias(orderByClause.column)),
order: orderByClause.order
}));
// partitionedQuery select must contain every column used for sorting
const orderByColumns = prefixedOrderBy.map(_.prop('column'));
partitionedQuery.select(// Always select baseQuery.id
prefix(baseQueryAlias, 'id'), // Sort columns
...orderByColumns)// The row number is used to assign an index to every row in every partition
.rowNumber(COL_STRAPI_ROW_NUMBER, (subQuery)=>{
for (const orderByClause of prefixedOrderBy){
subQuery.orderBy(orderByClause.column, orderByClause.order, 'last');
}
// And each partition/group is created based on baseQuery.id
subQuery.partitionBy(`${baseQueryAlias}.id`);
}).from(baseQuery.as(baseQueryAlias)).as(partitionedQueryAlias);
};
// 3. Create the final resultQuery query, that select and sort the wanted data using T
const originalSelect = _.difference(qb.state.select, // Remove order by columns from the initial select
qb.state.orderBy.map(_.prop('column')))// Alias everything in resultQuery
.map(prefix(resultQueryAlias));
resultQuery.select(originalSelect)// Join T to resultQuery to access sorted data
// Notes:
// - Only select the first row for each partition
// - Since we're applying the "where" statement directly on baseQuery (and not on resultQuery), we're using an inner join to avoid unwanted rows
.innerJoin(selectRowsAsNumberedPartitions, function() {
this// Only select rows that are returned by T
.on(`${partitionedQueryAlias}.id`, `${resultQueryAlias}.id`)// By only selecting the rows number equal to 1, we make sure we don't have duplicate, and that
// we're selecting rows in the correct order amongst the groups created by the "partition by"
.andOnVal(`${partitionedQueryAlias}.${COL_STRAPI_ROW_NUMBER}`, '=', 1);
});
// Re-apply pagination params
if (qb.state.limit) {
resultQuery.limit(qb.state.limit);
}
if (qb.state.offset) {
resultQuery.offset(qb.state.offset);
}
if (qb.state.first) {
resultQuery.first();
}
// Re-apply the sort using T values
resultQuery.orderBy([
// Transform "order by" clause to their T alias and prefix them with T alias
...orderBy.map((orderByClause)=>({
column: prefix(partitionedQueryAlias, getStrapiOrderColumnAlias(orderByClause.column)),
order: orderByClause.order
})),
// Add T.id to the order by clause to get consistent results in case several rows have the exact same order
{
column: `${partitionedQueryAlias}.id`,
order: 'asc'
}
]);
return resultQuery;
};
// Utils
const alias = _.curry((alias, value)=>`${value} as ${alias}`);
const prefix = _.curry((prefix, value)=>`${prefix}.${value}`);
export { getStrapiOrderColumnAlias, processOrderBy, wrapWithDeepSort };
//# sourceMappingURL=order-by.mjs.map

File diff suppressed because one or more lines are too long

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

View File

@@ -0,0 +1,4 @@
import type { Knex } from 'knex';
import type { Ctx } from '../types';
export declare const applySearch: (knex: Knex.QueryBuilder, query: string, ctx: Ctx) => void;
//# sourceMappingURL=search.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/query/helpers/search.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAIjC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC,eAAO,MAAM,WAAW,SAAU,KAAK,YAAY,SAAS,MAAM,OAAO,GAAG,SAoE3E,CAAC"}

View File

@@ -0,0 +1,67 @@
'use strict';
var _ = require('lodash/fp');
var types = require('../../utils/types.js');
var transform = require('./transform.js');
const applySearch = (knex, query, ctx)=>{
const { qb, uid, db } = ctx;
const meta = db.metadata.get(uid);
const { attributes } = meta;
const searchColumns = [
'id'
];
const stringColumns = Object.keys(attributes).filter((attributeName)=>{
const attribute = attributes[attributeName];
return types.isScalarAttribute(attribute) && types.isString(attribute.type) && attribute.searchable !== false;
});
searchColumns.push(...stringColumns);
if (!_.isNaN(_.toNumber(query))) {
const numberColumns = Object.keys(attributes).filter((attributeName)=>{
const attribute = attributes[attributeName];
return types.isScalarAttribute(attribute) && types.isNumber(attribute.type) && attribute.searchable !== false;
});
searchColumns.push(...numberColumns);
}
switch(db.dialect.client){
case 'postgres':
{
searchColumns.forEach((attr)=>{
const columnName = transform.toColumnName(meta, attr);
return knex.orWhereRaw(`??::text ILIKE ?`, [
qb.aliasColumn(columnName),
`%${escapeQuery(query, '*%\\')}%`
]);
});
break;
}
case 'sqlite':
{
searchColumns.forEach((attr)=>{
const columnName = transform.toColumnName(meta, attr);
return knex.orWhereRaw(`?? LIKE ? ESCAPE '\\'`, [
qb.aliasColumn(columnName),
`%${escapeQuery(query, '*%\\')}%`
]);
});
break;
}
case 'mysql':
{
searchColumns.forEach((attr)=>{
const columnName = transform.toColumnName(meta, attr);
return knex.orWhereRaw(`?? LIKE ?`, [
qb.aliasColumn(columnName),
`%${escapeQuery(query, '*%\\')}%`
]);
});
break;
}
}
};
const escapeQuery = (query, charsToEscape, escapeChar = '\\')=>{
return query.split('').reduce((escapedQuery, char)=>charsToEscape.includes(char) ? `${escapedQuery}${escapeChar}${char}` : `${escapedQuery}${char}`, '');
};
exports.applySearch = applySearch;
//# sourceMappingURL=search.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,65 @@
import _ from 'lodash/fp';
import { isScalarAttribute, isString, isNumber } from '../../utils/types.mjs';
import { toColumnName } from './transform.mjs';
const applySearch = (knex, query, ctx)=>{
const { qb, uid, db } = ctx;
const meta = db.metadata.get(uid);
const { attributes } = meta;
const searchColumns = [
'id'
];
const stringColumns = Object.keys(attributes).filter((attributeName)=>{
const attribute = attributes[attributeName];
return isScalarAttribute(attribute) && isString(attribute.type) && attribute.searchable !== false;
});
searchColumns.push(...stringColumns);
if (!_.isNaN(_.toNumber(query))) {
const numberColumns = Object.keys(attributes).filter((attributeName)=>{
const attribute = attributes[attributeName];
return isScalarAttribute(attribute) && isNumber(attribute.type) && attribute.searchable !== false;
});
searchColumns.push(...numberColumns);
}
switch(db.dialect.client){
case 'postgres':
{
searchColumns.forEach((attr)=>{
const columnName = toColumnName(meta, attr);
return knex.orWhereRaw(`??::text ILIKE ?`, [
qb.aliasColumn(columnName),
`%${escapeQuery(query, '*%\\')}%`
]);
});
break;
}
case 'sqlite':
{
searchColumns.forEach((attr)=>{
const columnName = toColumnName(meta, attr);
return knex.orWhereRaw(`?? LIKE ? ESCAPE '\\'`, [
qb.aliasColumn(columnName),
`%${escapeQuery(query, '*%\\')}%`
]);
});
break;
}
case 'mysql':
{
searchColumns.forEach((attr)=>{
const columnName = toColumnName(meta, attr);
return knex.orWhereRaw(`?? LIKE ?`, [
qb.aliasColumn(columnName),
`%${escapeQuery(query, '*%\\')}%`
]);
});
break;
}
}
};
const escapeQuery = (query, charsToEscape, escapeChar = '\\')=>{
return query.split('').reduce((escapedQuery, char)=>charsToEscape.includes(char) ? `${escapedQuery}${escapeChar}${char}` : `${escapedQuery}${char}`, '');
};
export { applySearch };
//# sourceMappingURL=search.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export { default as ReadableQuery } from './readable';
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/query/helpers/streams/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,YAAY,CAAC"}

View File

@@ -0,0 +1,39 @@
/// <reference types="node" />
import { Readable } from 'stream';
import type { Knex } from 'knex';
import type { QueryBuilder } from '../../query-builder';
import type { Database } from '../../..';
import { Meta } from '../../../metadata';
declare const knexPerformingQuery: unique symbol;
interface ReadableStrapiQueryOptions {
qb: QueryBuilder;
uid: string;
db: Database;
mapResults?: boolean;
batchSize?: number;
}
declare class ReadableStrapiQuery extends Readable {
_offset: number;
_limit: number | null;
_fetched: number;
_query: Knex.QueryBuilder;
_qb: QueryBuilder;
_db: Database;
_uid: string;
_meta: Meta;
_batchSize: number;
_mapResults: boolean;
[knexPerformingQuery]: boolean;
constructor({ qb, db, uid, mapResults, batchSize }: ReadableStrapiQueryOptions);
_destroy(err: Error, cb: (err?: Error) => void): void;
/**
* Custom ._read() implementation
*
* NOTE: Here "size" means the number of entities to be read from the database.
* Not the actual byte size, as it would mean that we need to return partial entities.
*
*/
_read(size: number): Promise<void>;
}
export default ReadableStrapiQuery;
//# sourceMappingURL=readable.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"readable.d.ts","sourceRoot":"","sources":["../../../../src/query/helpers/streams/readable.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAIzC,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAGzC,QAAA,MAAM,mBAAmB,eAAgC,CAAC;AAE1D,UAAU,0BAA0B;IAClC,EAAE,EAAE,YAAY,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,QAAQ,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,cAAM,mBAAoB,SAAQ,QAAQ;IACxC,OAAO,EAAE,MAAM,CAAC;IAEhB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAEtB,QAAQ,EAAE,MAAM,CAAC;IAEjB,MAAM,EAAE,IAAI,CAAC,YAAY,CAAC;IAE1B,GAAG,EAAE,YAAY,CAAC;IAElB,GAAG,EAAE,QAAQ,CAAC;IAEd,IAAI,EAAE,MAAM,CAAC;IAEb,KAAK,EAAE,IAAI,CAAC;IAEZ,UAAU,EAAE,MAAM,CAAC;IAEnB,WAAW,EAAE,OAAO,CAAC;IAErB,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;gBAEnB,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,UAAiB,EAAE,SAAe,EAAE,EAAE,0BAA0B;IAoC3F,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI;IAU9C;;;;;;OAMG;IACG,KAAK,CAAC,IAAI,EAAE,MAAM;CAgGzB;AAED,eAAe,mBAAmB,CAAC"}

View File

@@ -0,0 +1,131 @@
'use strict';
var stream = require('stream');
var _ = require('lodash/fp');
var apply = require('../populate/apply.js');
var transform = require('../transform.js');
const knexQueryDone = Symbol('knexQueryDone');
const knexPerformingQuery = Symbol('knexPerformingQuery');
class ReadableStrapiQuery extends stream.Readable {
_destroy(err, cb) {
// If the stream is destroyed while a query is being made, then wait for a
// kQueryDone event to be emitted before actually destroying the stream
if (this[knexPerformingQuery]) {
this.once(knexQueryDone, (er)=>cb(err || er));
} else {
cb(err);
}
}
/**
* Custom ._read() implementation
*
* NOTE: Here "size" means the number of entities to be read from the database.
* Not the actual byte size, as it would mean that we need to return partial entities.
*
*/ async _read(size) {
const query = this._query;
// Remove the original offset & limit properties from the query
// Theoretically, they would be replaced by calling them again, but this is just to be sure
query.clear('limit').clear('offset');
// Define the maximum read size based on the limit and the requested size
// NOTE: size is equal to _batchSize by default. Since we want to allow customizing it on
// the fly, we need to use its value instead of batchSize when computing the maxReadSize value
const maxReadSize = // if no limit is defined in the query, use the given size,
// otherwise, use the smallest value between the two
this._limit === null ? size : Math.min(size, this._limit);
// Compute the limit for the next query
const limit = // If a limit is defined
this._limit !== null && // And reading `maxReadSize` would fetch too many entities (> _limit)
this._fetched + maxReadSize > this._limit ? this._limit - this._fetched : maxReadSize;
// If we don't have anything left to read (_limit === _fetched),
// don't bother making the query and end the stream by pushing null
if (limit <= 0) {
this.push(null);
return;
}
// Compute the offset (base offset + number of entities already fetched)
const offset = this._offset + this._fetched;
// Update the query with the new values (offset + limit)
query.offset(offset).limit(limit);
// Lock the ._destroy()
this[knexPerformingQuery] = true;
let results;
let count;
let err;
try {
// Execute the query and store the results & count
results = await query;
const { populate } = this._qb.state;
// Applies the populate if needed
if (populate) {
await apply(results, populate, {
qb: this._qb,
uid: this._uid,
db: this._db
});
}
// Map results if asked to
if (this._mapResults) {
results = transform.fromRow(this._meta, results);
}
count = results.length;
} catch (e) {
err = e;
}
// Unlock the ._destroy()
this[knexPerformingQuery] = false;
// Tell ._destroy() that it's now safe to close the db connection
if (this.destroyed) {
this.emit(knexQueryDone);
return;
}
// If there is an error, destroy with the given error
if (err) {
this.destroy(err);
return;
}
// Update the amount of fetched entities
this._fetched += count;
// While there is at least one value to unpack
for (const result of results){
this.push(result);
}
// If the amount of fetched entities is smaller than the
// maximum read size, Then push null to close the stream
if (this._fetched === this._limit || count < this._batchSize) {
this.push(null);
}
}
constructor({ qb, db, uid, mapResults = true, batchSize = 500 }){
super({
objectMode: true,
highWaterMark: batchSize
});
// Extract offset & limit from the query-builder's state
const { offset, limit } = qb.state;
// Original offset value
this._offset = _.isFinite(offset) ? Number(offset) : 0;
// Max amount of entities to fetch, force null as undefined value
this._limit = _.isFinite(limit) ? Number(limit) : null;
// Total amount of entities fetched
this._fetched = 0;
/**
* Original query
*/ this._query = qb.getKnexQuery();
// Query Builder instance
this._qb = qb;
// Database related properties
this._db = db;
this._uid = uid;
this._meta = db.metadata.get(uid);
// Stream params
this._batchSize = batchSize;
this._mapResults = mapResults;
// States
this[knexPerformingQuery] = false;
}
}
module.exports = ReadableStrapiQuery;
//# sourceMappingURL=readable.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,129 @@
import { Readable } from 'stream';
import { isFinite } from 'lodash/fp';
import applyPopulate from '../populate/apply.mjs';
import { fromRow } from '../transform.mjs';
const knexQueryDone = Symbol('knexQueryDone');
const knexPerformingQuery = Symbol('knexPerformingQuery');
class ReadableStrapiQuery extends Readable {
_destroy(err, cb) {
// If the stream is destroyed while a query is being made, then wait for a
// kQueryDone event to be emitted before actually destroying the stream
if (this[knexPerformingQuery]) {
this.once(knexQueryDone, (er)=>cb(err || er));
} else {
cb(err);
}
}
/**
* Custom ._read() implementation
*
* NOTE: Here "size" means the number of entities to be read from the database.
* Not the actual byte size, as it would mean that we need to return partial entities.
*
*/ async _read(size) {
const query = this._query;
// Remove the original offset & limit properties from the query
// Theoretically, they would be replaced by calling them again, but this is just to be sure
query.clear('limit').clear('offset');
// Define the maximum read size based on the limit and the requested size
// NOTE: size is equal to _batchSize by default. Since we want to allow customizing it on
// the fly, we need to use its value instead of batchSize when computing the maxReadSize value
const maxReadSize = // if no limit is defined in the query, use the given size,
// otherwise, use the smallest value between the two
this._limit === null ? size : Math.min(size, this._limit);
// Compute the limit for the next query
const limit = // If a limit is defined
this._limit !== null && // And reading `maxReadSize` would fetch too many entities (> _limit)
this._fetched + maxReadSize > this._limit ? this._limit - this._fetched : maxReadSize;
// If we don't have anything left to read (_limit === _fetched),
// don't bother making the query and end the stream by pushing null
if (limit <= 0) {
this.push(null);
return;
}
// Compute the offset (base offset + number of entities already fetched)
const offset = this._offset + this._fetched;
// Update the query with the new values (offset + limit)
query.offset(offset).limit(limit);
// Lock the ._destroy()
this[knexPerformingQuery] = true;
let results;
let count;
let err;
try {
// Execute the query and store the results & count
results = await query;
const { populate } = this._qb.state;
// Applies the populate if needed
if (populate) {
await applyPopulate(results, populate, {
qb: this._qb,
uid: this._uid,
db: this._db
});
}
// Map results if asked to
if (this._mapResults) {
results = fromRow(this._meta, results);
}
count = results.length;
} catch (e) {
err = e;
}
// Unlock the ._destroy()
this[knexPerformingQuery] = false;
// Tell ._destroy() that it's now safe to close the db connection
if (this.destroyed) {
this.emit(knexQueryDone);
return;
}
// If there is an error, destroy with the given error
if (err) {
this.destroy(err);
return;
}
// Update the amount of fetched entities
this._fetched += count;
// While there is at least one value to unpack
for (const result of results){
this.push(result);
}
// If the amount of fetched entities is smaller than the
// maximum read size, Then push null to close the stream
if (this._fetched === this._limit || count < this._batchSize) {
this.push(null);
}
}
constructor({ qb, db, uid, mapResults = true, batchSize = 500 }){
super({
objectMode: true,
highWaterMark: batchSize
});
// Extract offset & limit from the query-builder's state
const { offset, limit } = qb.state;
// Original offset value
this._offset = isFinite(offset) ? Number(offset) : 0;
// Max amount of entities to fetch, force null as undefined value
this._limit = isFinite(limit) ? Number(limit) : null;
// Total amount of entities fetched
this._fetched = 0;
/**
* Original query
*/ this._query = qb.getKnexQuery();
// Query Builder instance
this._qb = qb;
// Database related properties
this._db = db;
this._uid = uid;
this._meta = db.metadata.get(uid);
// Stream params
this._batchSize = batchSize;
this._mapResults = mapResults;
// States
this[knexPerformingQuery] = false;
}
}
export { ReadableStrapiQuery as default };
//# sourceMappingURL=readable.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
import type { Meta } from '../../metadata';
type Row = Record<string, unknown> | null;
export type Rec = Record<string, unknown> | null;
declare const fromRow: (meta: Meta, row: Row | Row[] | undefined) => Rec | Rec[];
declare function toRow<TData extends Rec | Rec[] | null>(meta: Meta, data: TData): TData extends null ? null : TData extends Rec[] ? Row[] : Rec;
declare const toColumnName: (meta: Meta, name: null | string) => string;
export { toRow, fromRow, toColumnName };
//# sourceMappingURL=transform.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../../src/query/helpers/transform.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAE3C,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;AAC1C,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;AAmCjD,QAAA,MAAM,OAAO,SAAU,IAAI,OAAO,GAAG,GAAG,GAAG,EAAE,GAAG,SAAS,gBAUxD,CAAC;AA4BF,iBAAS,KAAK,CAAC,KAAK,SAAS,GAAG,GAAG,GAAG,EAAE,GAAG,IAAI,EAC7C,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,KAAK,GACV,KAAK,SAAS,IAAI,GAAG,IAAI,GAAG,KAAK,SAAS,GAAG,EAAE,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC;AAajE,QAAA,MAAM,YAAY,SAAU,IAAI,QAAQ,IAAI,GAAG,MAAM,WAYpD,CAAC;AAEF,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC"}

View File

@@ -0,0 +1,77 @@
'use strict';
var _ = require('lodash/fp');
var types = require('../../utils/types.js');
var index = require('../../fields/index.js');
const fromSingleRow = (meta, row)=>{
const { attributes } = meta;
if (_.isNil(row)) {
return null;
}
const obj = {};
for(const column in row){
if (!_.has(column, meta.columnToAttribute)) {
continue;
}
const attributeName = meta.columnToAttribute[column];
const attribute = attributes[attributeName];
if (types.isScalar(attribute.type)) {
const field = index.createField(attribute);
const val = row[column] === null ? null : field.fromDB(row[column]);
obj[attributeName] = val;
}
if (types.isRelation(attribute.type)) {
obj[attributeName] = row[column];
}
}
return obj;
};
const fromRow = (meta, row)=>{
if (_.isNil(row)) {
return null;
}
if (Array.isArray(row)) {
return row.map((singleRow)=>fromSingleRow(meta, singleRow));
}
return fromSingleRow(meta, row);
};
const toSingleRow = (meta, data = {})=>{
if (_.isNil(data)) {
return data;
}
const { attributes } = meta;
for (const key of Object.keys(data)){
const attribute = attributes[key];
if (!attribute || !('columnName' in attribute) || !attribute.columnName || attribute.columnName === key) {
continue;
}
data[attribute.columnName] = data[key];
delete data[key];
}
return data;
};
function toRow(meta, data) {
if (_.isNil(data)) {
return data;
}
if (_.isArray(data)) {
return data.map((datum)=>toSingleRow(meta, datum));
}
return toSingleRow(meta, data);
}
const toColumnName = (meta, name)=>{
if (!name) {
throw new Error('Name cannot be null');
}
const attribute = meta.attributes[name];
if (!attribute) {
return name;
}
return 'columnName' in attribute && attribute.columnName || name;
};
exports.fromRow = fromRow;
exports.toColumnName = toColumnName;
exports.toRow = toRow;
//# sourceMappingURL=transform.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,73 @@
import _ from 'lodash/fp';
import { isScalar, isRelation } from '../../utils/types.mjs';
import { createField } from '../../fields/index.mjs';
const fromSingleRow = (meta, row)=>{
const { attributes } = meta;
if (_.isNil(row)) {
return null;
}
const obj = {};
for(const column in row){
if (!_.has(column, meta.columnToAttribute)) {
continue;
}
const attributeName = meta.columnToAttribute[column];
const attribute = attributes[attributeName];
if (isScalar(attribute.type)) {
const field = createField(attribute);
const val = row[column] === null ? null : field.fromDB(row[column]);
obj[attributeName] = val;
}
if (isRelation(attribute.type)) {
obj[attributeName] = row[column];
}
}
return obj;
};
const fromRow = (meta, row)=>{
if (_.isNil(row)) {
return null;
}
if (Array.isArray(row)) {
return row.map((singleRow)=>fromSingleRow(meta, singleRow));
}
return fromSingleRow(meta, row);
};
const toSingleRow = (meta, data = {})=>{
if (_.isNil(data)) {
return data;
}
const { attributes } = meta;
for (const key of Object.keys(data)){
const attribute = attributes[key];
if (!attribute || !('columnName' in attribute) || !attribute.columnName || attribute.columnName === key) {
continue;
}
data[attribute.columnName] = data[key];
delete data[key];
}
return data;
};
function toRow(meta, data) {
if (_.isNil(data)) {
return data;
}
if (_.isArray(data)) {
return data.map((datum)=>toSingleRow(meta, datum));
}
return toSingleRow(meta, data);
}
const toColumnName = (meta, name)=>{
if (!name) {
throw new Error('Name cannot be null');
}
const attribute = meta.attributes[name];
if (!attribute) {
return name;
}
return 'columnName' in attribute && attribute.columnName || name;
};
export { fromRow, toColumnName, toRow };
//# sourceMappingURL=transform.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
import type { Knex } from 'knex';
import type { Ctx } from '../types';
type WhereCtx = Ctx & {
alias?: string;
isGroupRoot?: boolean;
};
/**
* Process where parameter
*/
declare function processWhere(where: Record<string, unknown>, ctx: WhereCtx): Record<string, unknown>;
declare function processWhere(where: Record<string, unknown>[], ctx: WhereCtx): Record<string, unknown>[];
type Where = {
$and?: Where[];
$or?: Where[];
$not?: Where;
[key: string]: any;
} | Array<Where>;
declare const applyWhere: (qb: Knex.QueryBuilder, where: Where) => Knex.QueryBuilder<any, any> | undefined;
export { applyWhere, processWhere };
//# sourceMappingURL=where.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"where.d.ts","sourceRoot":"","sources":["../../../src/query/helpers/where.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AASjC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAGpC,KAAK,QAAQ,GAAG,GAAG,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAgGhE;;GAEG;AACH,iBAAS,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC9F,iBAAS,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;AAkSlG,KAAK,KAAK,GACN;IACE,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC;IACf,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;IACd,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,GACD,KAAK,CAAC,KAAK,CAAC,CAAC;AAEjB,QAAA,MAAM,UAAU,OAAQ,KAAK,YAAY,SAAS,KAAK,4CAoCtD,CAAC;AAWF,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC"}

View File

@@ -0,0 +1,372 @@
'use strict';
var _ = require('lodash/fp');
var utils = require('@strapi/utils');
var types = require('../../utils/types.js');
var index = require('../../fields/index.js');
var join = require('./join.js');
var transform = require('./transform.js');
var knex = require('../../utils/knex.js');
const isRecord = (value)=>_.isPlainObject(value);
const castValue = (value, attribute)=>{
if (!attribute) {
return value;
}
if (types.isScalar(attribute.type) && !knex.isKnexQuery(value)) {
const field = index.createField(attribute);
return value === null ? null : field.toDB(value);
}
return value;
};
const processSingleAttributeWhere = (attribute, where, operator = '$eq')=>{
if (!isRecord(where)) {
if (utils.isOperatorOfType('cast', operator)) {
return castValue(where, attribute);
}
return where;
}
const filters = {};
for (const key of Object.keys(where)){
const value = where[key];
if (!utils.isOperatorOfType('where', key)) {
throw new Error(`Undefined attribute level operator ${key}`);
}
filters[key] = processAttributeWhere(attribute, value, key);
}
return filters;
};
const processAttributeWhere = (attribute, where, operator = '$eq')=>{
if (_.isArray(where)) {
return where.map((sub)=>processSingleAttributeWhere(attribute, sub, operator));
}
return processSingleAttributeWhere(attribute, where, operator);
};
const processNested = (where, ctx)=>{
if (!isRecord(where)) {
return where;
}
return processWhere(where, ctx);
};
const processRelationWhere = (where, ctx)=>{
const { qb, alias } = ctx;
const idAlias = qb.aliasColumn('id', alias);
if (!isRecord(where)) {
return {
[idAlias]: where
};
}
const keys = Object.keys(where);
const operatorKeys = keys.filter((key)=>utils.isOperator(key));
if (operatorKeys.length > 0 && operatorKeys.length !== keys.length) {
throw new Error(`Operator and non-operator keys cannot be mixed in a relation where clause`);
}
if (operatorKeys.length > 1) {
throw new Error(`Only one operator key is allowed in a relation where clause, but found: ${operatorKeys}`);
}
if (operatorKeys.length === 1) {
const operator = operatorKeys[0];
if (utils.isOperatorOfType('group', operator)) {
return processWhere(where, ctx);
}
return {
[idAlias]: {
[operator]: processNested(where[operator], ctx)
}
};
}
return processWhere(where, ctx);
};
function processWhere(where, ctx) {
if (!_.isArray(where) && !isRecord(where)) {
throw new Error('Where must be an array or an object');
}
if (_.isArray(where)) {
return where.map((sub)=>processWhere(sub, ctx));
}
const { db, uid, qb, alias } = ctx;
const meta = db.metadata.get(uid);
const filters = {};
// for each key in where
for (const key of Object.keys(where)){
const value = where[key];
// if operator $and $or -> process recursively
if (utils.isOperatorOfType('group', key)) {
if (!Array.isArray(value)) {
throw new Error(`Operator ${key} must be an array`);
}
filters[key] = value.map((sub)=>processNested(sub, ctx));
continue;
}
if (key === '$not') {
filters[key] = processNested(value, ctx);
continue;
}
if (utils.isOperatorOfType('where', key)) {
throw new Error(`Only $and, $or and $not can only be used as root level operators. Found ${key}.`);
}
const attribute = meta.attributes[key];
if (!attribute) {
filters[qb.aliasColumn(key, alias)] = processAttributeWhere(null, value);
continue;
}
if (types.isRelation(attribute.type) && 'target' in attribute) {
// attribute
const subAlias = join.createJoin(ctx, {
alias: alias || qb.alias,
attributeName: key,
attribute
});
const nestedWhere = processRelationWhere(value, {
db,
qb,
alias: subAlias,
uid: attribute.target
});
// TODO: use a better merge logic (push to $and when collisions)
Object.assign(filters, nestedWhere);
continue;
}
if (types.isScalar(attribute.type)) {
const columnName = transform.toColumnName(meta, key);
const aliasedColumnName = qb.aliasColumn(columnName, alias);
filters[aliasedColumnName] = processAttributeWhere(attribute, value);
continue;
}
throw new Error(`You cannot filter on ${attribute.type} types`);
}
return filters;
}
// TODO: add type casting per operator at some point
const applyOperator = (qb, column, operator, value)=>{
if (Array.isArray(value) && !utils.isOperatorOfType('array', operator)) {
return qb.where((subQB)=>{
value.forEach((subValue)=>subQB.orWhere((innerQB)=>{
applyOperator(innerQB, column, operator, subValue);
}));
});
}
switch(operator){
case '$not':
{
qb.whereNot((qb)=>applyWhereToColumn(qb, column, value));
break;
}
case '$in':
{
// @ts-ignore
// TODO: fix in v5
qb.whereIn(column, knex.isKnexQuery(value) ? value : _.castArray(value));
break;
}
case '$notIn':
{
// @ts-ignore
// TODO: fix in v5
qb.whereNotIn(column, knex.isKnexQuery(value) ? value : _.castArray(value));
break;
}
case '$eq':
{
if (value === null) {
qb.whereNull(column);
break;
}
qb.where(column, value);
break;
}
case '$eqi':
{
if (value === null) {
qb.whereNull(column);
break;
}
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [
column,
`${value}`
]);
break;
}
case '$ne':
{
if (value === null) {
qb.whereNotNull(column);
break;
}
qb.where(column, '<>', value);
break;
}
case '$nei':
{
if (value === null) {
qb.whereNotNull(column);
break;
}
qb.whereRaw(`${fieldLowerFn(qb)} NOT LIKE LOWER(?)`, [
column,
`${value}`
]);
break;
}
case '$gt':
{
qb.where(column, '>', value);
break;
}
case '$gte':
{
qb.where(column, '>=', value);
break;
}
case '$lt':
{
qb.where(column, '<', value);
break;
}
case '$lte':
{
qb.where(column, '<=', value);
break;
}
case '$null':
{
if (value) {
qb.whereNull(column);
} else {
qb.whereNotNull(column);
}
break;
}
case '$notNull':
{
if (value) {
qb.whereNotNull(column);
} else {
qb.whereNull(column);
}
break;
}
case '$between':
{
qb.whereBetween(column, value);
break;
}
case '$startsWith':
{
qb.where(column, 'like', `${value}%`);
break;
}
case '$startsWithi':
{
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [
column,
`${value}%`
]);
break;
}
case '$endsWith':
{
qb.where(column, 'like', `%${value}`);
break;
}
case '$endsWithi':
{
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [
column,
`%${value}`
]);
break;
}
case '$contains':
{
qb.where(column, 'like', `%${value}%`);
break;
}
case '$notContains':
{
qb.whereNot(column, 'like', `%${value}%`);
break;
}
case '$containsi':
{
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [
column,
`%${value}%`
]);
break;
}
case '$notContainsi':
{
qb.whereRaw(`${fieldLowerFn(qb)} NOT LIKE LOWER(?)`, [
column,
`%${value}%`
]);
break;
}
// Experimental, only for internal use
// Only on MySQL, PostgreSQL and CockroachDB.
// https://knexjs.org/guide/query-builder.html#wherejsonsupersetof
case '$jsonSupersetOf':
{
qb.whereJsonSupersetOf(column, value);
break;
}
// TODO: Add more JSON operators: whereJsonObject, whereJsonPath, whereJsonSubsetOf
// TODO: relational operators every/some/exists/size ...
default:
{
throw new Error(`Undefined attribute level operator ${operator}`);
}
}
};
const applyWhereToColumn = (qb, column, columnWhere)=>{
if (!isRecord(columnWhere)) {
if (Array.isArray(columnWhere)) {
return qb.whereIn(column, columnWhere);
}
return qb.where(column, columnWhere);
}
const keys = Object.keys(columnWhere);
keys.forEach((operator)=>{
const value = columnWhere[operator];
applyOperator(qb, column, operator, value);
});
};
const applyWhere = (qb, where)=>{
if (!_.isArray(where) && !isRecord(where)) {
throw new Error('Where must be an array or an object');
}
if (_.isArray(where)) {
return qb.where((subQB)=>where.forEach((subWhere)=>applyWhere(subQB, subWhere)));
}
Object.keys(where).forEach((key)=>{
if (key === '$and') {
const value = where[key] ?? [];
return qb.where((subQB)=>{
value.forEach((v)=>applyWhere(subQB, v));
});
}
if (key === '$or') {
const value = where[key] ?? [];
return qb.where((subQB)=>{
value.forEach((v)=>subQB.orWhere((inner)=>applyWhere(inner, v)));
});
}
if (key === '$not') {
const value = where[key] ?? {};
return qb.whereNot((qb)=>applyWhere(qb, value));
}
applyWhereToColumn(qb, key, where[key]);
});
};
const fieldLowerFn = (qb)=>{
// Postgres requires string to be passed
if (qb.client.dialect === 'postgresql') {
return 'LOWER(CAST(?? AS VARCHAR))';
}
return 'LOWER(??)';
};
exports.applyWhere = applyWhere;
exports.processWhere = processWhere;
//# sourceMappingURL=where.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,369 @@
import { isArray, isPlainObject, castArray } from 'lodash/fp';
import { isOperatorOfType, isOperator } from '@strapi/utils';
import { isRelation, isScalar } from '../../utils/types.mjs';
import { createField } from '../../fields/index.mjs';
import { createJoin } from './join.mjs';
import { toColumnName } from './transform.mjs';
import { isKnexQuery } from '../../utils/knex.mjs';
const isRecord = (value)=>isPlainObject(value);
const castValue = (value, attribute)=>{
if (!attribute) {
return value;
}
if (isScalar(attribute.type) && !isKnexQuery(value)) {
const field = createField(attribute);
return value === null ? null : field.toDB(value);
}
return value;
};
const processSingleAttributeWhere = (attribute, where, operator = '$eq')=>{
if (!isRecord(where)) {
if (isOperatorOfType('cast', operator)) {
return castValue(where, attribute);
}
return where;
}
const filters = {};
for (const key of Object.keys(where)){
const value = where[key];
if (!isOperatorOfType('where', key)) {
throw new Error(`Undefined attribute level operator ${key}`);
}
filters[key] = processAttributeWhere(attribute, value, key);
}
return filters;
};
const processAttributeWhere = (attribute, where, operator = '$eq')=>{
if (isArray(where)) {
return where.map((sub)=>processSingleAttributeWhere(attribute, sub, operator));
}
return processSingleAttributeWhere(attribute, where, operator);
};
const processNested = (where, ctx)=>{
if (!isRecord(where)) {
return where;
}
return processWhere(where, ctx);
};
const processRelationWhere = (where, ctx)=>{
const { qb, alias } = ctx;
const idAlias = qb.aliasColumn('id', alias);
if (!isRecord(where)) {
return {
[idAlias]: where
};
}
const keys = Object.keys(where);
const operatorKeys = keys.filter((key)=>isOperator(key));
if (operatorKeys.length > 0 && operatorKeys.length !== keys.length) {
throw new Error(`Operator and non-operator keys cannot be mixed in a relation where clause`);
}
if (operatorKeys.length > 1) {
throw new Error(`Only one operator key is allowed in a relation where clause, but found: ${operatorKeys}`);
}
if (operatorKeys.length === 1) {
const operator = operatorKeys[0];
if (isOperatorOfType('group', operator)) {
return processWhere(where, ctx);
}
return {
[idAlias]: {
[operator]: processNested(where[operator], ctx)
}
};
}
return processWhere(where, ctx);
};
function processWhere(where, ctx) {
if (!isArray(where) && !isRecord(where)) {
throw new Error('Where must be an array or an object');
}
if (isArray(where)) {
return where.map((sub)=>processWhere(sub, ctx));
}
const { db, uid, qb, alias } = ctx;
const meta = db.metadata.get(uid);
const filters = {};
// for each key in where
for (const key of Object.keys(where)){
const value = where[key];
// if operator $and $or -> process recursively
if (isOperatorOfType('group', key)) {
if (!Array.isArray(value)) {
throw new Error(`Operator ${key} must be an array`);
}
filters[key] = value.map((sub)=>processNested(sub, ctx));
continue;
}
if (key === '$not') {
filters[key] = processNested(value, ctx);
continue;
}
if (isOperatorOfType('where', key)) {
throw new Error(`Only $and, $or and $not can only be used as root level operators. Found ${key}.`);
}
const attribute = meta.attributes[key];
if (!attribute) {
filters[qb.aliasColumn(key, alias)] = processAttributeWhere(null, value);
continue;
}
if (isRelation(attribute.type) && 'target' in attribute) {
// attribute
const subAlias = createJoin(ctx, {
alias: alias || qb.alias,
attributeName: key,
attribute
});
const nestedWhere = processRelationWhere(value, {
db,
qb,
alias: subAlias,
uid: attribute.target
});
// TODO: use a better merge logic (push to $and when collisions)
Object.assign(filters, nestedWhere);
continue;
}
if (isScalar(attribute.type)) {
const columnName = toColumnName(meta, key);
const aliasedColumnName = qb.aliasColumn(columnName, alias);
filters[aliasedColumnName] = processAttributeWhere(attribute, value);
continue;
}
throw new Error(`You cannot filter on ${attribute.type} types`);
}
return filters;
}
// TODO: add type casting per operator at some point
const applyOperator = (qb, column, operator, value)=>{
if (Array.isArray(value) && !isOperatorOfType('array', operator)) {
return qb.where((subQB)=>{
value.forEach((subValue)=>subQB.orWhere((innerQB)=>{
applyOperator(innerQB, column, operator, subValue);
}));
});
}
switch(operator){
case '$not':
{
qb.whereNot((qb)=>applyWhereToColumn(qb, column, value));
break;
}
case '$in':
{
// @ts-ignore
// TODO: fix in v5
qb.whereIn(column, isKnexQuery(value) ? value : castArray(value));
break;
}
case '$notIn':
{
// @ts-ignore
// TODO: fix in v5
qb.whereNotIn(column, isKnexQuery(value) ? value : castArray(value));
break;
}
case '$eq':
{
if (value === null) {
qb.whereNull(column);
break;
}
qb.where(column, value);
break;
}
case '$eqi':
{
if (value === null) {
qb.whereNull(column);
break;
}
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [
column,
`${value}`
]);
break;
}
case '$ne':
{
if (value === null) {
qb.whereNotNull(column);
break;
}
qb.where(column, '<>', value);
break;
}
case '$nei':
{
if (value === null) {
qb.whereNotNull(column);
break;
}
qb.whereRaw(`${fieldLowerFn(qb)} NOT LIKE LOWER(?)`, [
column,
`${value}`
]);
break;
}
case '$gt':
{
qb.where(column, '>', value);
break;
}
case '$gte':
{
qb.where(column, '>=', value);
break;
}
case '$lt':
{
qb.where(column, '<', value);
break;
}
case '$lte':
{
qb.where(column, '<=', value);
break;
}
case '$null':
{
if (value) {
qb.whereNull(column);
} else {
qb.whereNotNull(column);
}
break;
}
case '$notNull':
{
if (value) {
qb.whereNotNull(column);
} else {
qb.whereNull(column);
}
break;
}
case '$between':
{
qb.whereBetween(column, value);
break;
}
case '$startsWith':
{
qb.where(column, 'like', `${value}%`);
break;
}
case '$startsWithi':
{
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [
column,
`${value}%`
]);
break;
}
case '$endsWith':
{
qb.where(column, 'like', `%${value}`);
break;
}
case '$endsWithi':
{
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [
column,
`%${value}`
]);
break;
}
case '$contains':
{
qb.where(column, 'like', `%${value}%`);
break;
}
case '$notContains':
{
qb.whereNot(column, 'like', `%${value}%`);
break;
}
case '$containsi':
{
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [
column,
`%${value}%`
]);
break;
}
case '$notContainsi':
{
qb.whereRaw(`${fieldLowerFn(qb)} NOT LIKE LOWER(?)`, [
column,
`%${value}%`
]);
break;
}
// Experimental, only for internal use
// Only on MySQL, PostgreSQL and CockroachDB.
// https://knexjs.org/guide/query-builder.html#wherejsonsupersetof
case '$jsonSupersetOf':
{
qb.whereJsonSupersetOf(column, value);
break;
}
// TODO: Add more JSON operators: whereJsonObject, whereJsonPath, whereJsonSubsetOf
// TODO: relational operators every/some/exists/size ...
default:
{
throw new Error(`Undefined attribute level operator ${operator}`);
}
}
};
const applyWhereToColumn = (qb, column, columnWhere)=>{
if (!isRecord(columnWhere)) {
if (Array.isArray(columnWhere)) {
return qb.whereIn(column, columnWhere);
}
return qb.where(column, columnWhere);
}
const keys = Object.keys(columnWhere);
keys.forEach((operator)=>{
const value = columnWhere[operator];
applyOperator(qb, column, operator, value);
});
};
const applyWhere = (qb, where)=>{
if (!isArray(where) && !isRecord(where)) {
throw new Error('Where must be an array or an object');
}
if (isArray(where)) {
return qb.where((subQB)=>where.forEach((subWhere)=>applyWhere(subQB, subWhere)));
}
Object.keys(where).forEach((key)=>{
if (key === '$and') {
const value = where[key] ?? [];
return qb.where((subQB)=>{
value.forEach((v)=>applyWhere(subQB, v));
});
}
if (key === '$or') {
const value = where[key] ?? [];
return qb.where((subQB)=>{
value.forEach((v)=>subQB.orWhere((inner)=>applyWhere(inner, v)));
});
}
if (key === '$not') {
const value = where[key] ?? {};
return qb.whereNot((qb)=>applyWhere(qb, value));
}
applyWhereToColumn(qb, key, where[key]);
});
};
const fieldLowerFn = (qb)=>{
// Postgres requires string to be passed
if (qb.client.dialect === 'postgresql') {
return 'LOWER(CAST(?? AS VARCHAR))';
}
return 'LOWER(??)';
};
export { applyWhere, processWhere };
//# sourceMappingURL=where.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import createQueryBuilder from './query-builder';
export { createQueryBuilder };
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/query/index.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,kBAAkB,EAAE,CAAC"}

View File

@@ -0,0 +1,81 @@
import type { Knex } from 'knex';
import type { Database } from '..';
import * as helpers from './helpers';
import type { Join } from './helpers/join';
interface State {
type: 'select' | 'insert' | 'update' | 'delete' | 'count' | 'max' | 'truncate';
select: Array<string | Knex.Raw>;
count: string | null;
max: string | null;
first: boolean;
data: Record<string, unknown> | (null | Record<string, unknown>)[] | null;
where: Record<string, unknown>[];
joins: Join[];
populate: object | null;
limit: number | null;
offset: number | null;
transaction: any;
forUpdate: boolean;
onConflict: any;
merge: any;
ignore: boolean;
orderBy: any[];
groupBy: any[];
increments: any[];
decrements: any[];
aliasCounter: number;
filters: any;
search: string;
processed: boolean;
}
export interface QueryBuilder {
alias: string;
state: State;
raw: Knex.RawBuilder;
getAlias(): string;
clone(): QueryBuilder;
select(args: string | Array<string | Knex.Raw>): QueryBuilder;
addSelect(args: string | string[]): QueryBuilder;
insert<TData extends Record<string, unknown> | Record<string, unknown>[]>(data: TData): QueryBuilder;
onConflict(args: any): QueryBuilder;
merge(args: any): QueryBuilder;
ignore(): QueryBuilder;
delete(): QueryBuilder;
ref(name: string): any;
update<TData extends Record<string, unknown>>(data: TData): QueryBuilder;
increment(column: string, amount?: number): QueryBuilder;
decrement(column: string, amount?: number): QueryBuilder;
count(count?: string): QueryBuilder;
max(column: string): QueryBuilder;
where(where?: object): QueryBuilder;
limit(limit: number): QueryBuilder;
offset(offset: number): QueryBuilder;
orderBy(orderBy: any): QueryBuilder;
groupBy(groupBy: any): QueryBuilder;
populate(populate: any): QueryBuilder;
search(query: string): QueryBuilder;
transacting(transaction: any): QueryBuilder;
forUpdate(): QueryBuilder;
init(params?: any): QueryBuilder;
filters(filters: any): void;
first(): QueryBuilder;
join(join: any): QueryBuilder;
mustUseAlias(): boolean;
aliasColumn(key: any, alias?: string): any;
shouldUseSubQuery(): boolean;
runSubQuery(): any;
processState(): void;
shouldUseDistinct(): boolean;
shouldUseDeepSort(): boolean;
processSelect(): void;
getKnexQuery(): Knex.QueryBuilder;
execute<T>(options?: {
mapResults?: boolean;
}): Promise<T>;
stream(options?: {
mapResults?: boolean;
}): helpers.ReadableQuery;
}
declare const createQueryBuilder: (uid: string, db: Database, initialState?: Partial<State>) => QueryBuilder;
export default createQueryBuilder;
//# sourceMappingURL=query-builder.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"query-builder.d.ts","sourceRoot":"","sources":["../../src/query/query-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAGjC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAKnC,OAAO,KAAK,OAAO,MAAM,WAAW,CAAC;AACrC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAE3C,UAAU,KAAK;IACb,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,UAAU,CAAC;IAC/E,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACjC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC;IAC1E,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACjC,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,EAAE,GAAG,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,GAAG,CAAC;IAChB,KAAK,EAAE,GAAG,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,GAAG,EAAE,CAAC;IACf,OAAO,EAAE,GAAG,EAAE,CAAC;IACf,UAAU,EAAE,GAAG,EAAE,CAAC;IAClB,UAAU,EAAE,GAAG,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,GAAG,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC;IAErB,QAAQ,IAAI,MAAM,CAAC;IAEnB,KAAK,IAAI,YAAY,CAAC;IAEtB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC;IAE9D,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,YAAY,CAAC;IAEjD,MAAM,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACtE,IAAI,EAAE,KAAK,GACV,YAAY,CAAC;IAEhB,UAAU,CAAC,IAAI,EAAE,GAAG,GAAG,YAAY,CAAC;IAEpC,KAAK,CAAC,IAAI,EAAE,GAAG,GAAG,YAAY,CAAC;IAE/B,MAAM,IAAI,YAAY,CAAC;IAEvB,MAAM,IAAI,YAAY,CAAC;IAEvB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC;IAEvB,MAAM,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,GAAG,YAAY,CAAC;IAEzE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAEzD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAEzD,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAEpC,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC;IAElC,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IAEpC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC;IAEnC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC;IAErC,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,YAAY,CAAC;IAEpC,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,YAAY,CAAC;IAEpC,QAAQ,CAAC,QAAQ,EAAE,GAAG,GAAG,YAAY,CAAC;IAEtC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAAC;IAEpC,WAAW,CAAC,WAAW,EAAE,GAAG,GAAG,YAAY,CAAC;IAE5C,SAAS,IAAI,YAAY,CAAC;IAE1B,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,GAAG,YAAY,CAAC;IAEjC,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC;IAE5B,KAAK,IAAI,YAAY,CAAC;IAEtB,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,YAAY,CAAC;IAE9B,YAAY,IAAI,OAAO,CAAC;IAExB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAE3C,iBAAiB,IAAI,OAAO,CAAC;IAE7B,WAAW,IAAI,GAAG,CAAC;IAEnB,YAAY,IAAI,IAAI,CAAC;IAErB,iBAAiB,IAAI,OAAO,CAAC;IAE7B,iBAAiB,IAAI,OAAO,CAAC;IAE7B,aAAa,IAAI,IAAI,CAAC;IAEtB,YAAY,IAAI,IAAI,CAAC,YAAY,CAAC;IAElC,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAE3D,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC;CACnE;AAED,QAAA,MAAM,kBAAkB,QACjB,MAAM,MACP,QAAQ,iBACE,QAAQ,KAAK,CAAC,KAC3B,YA+iBF,CAAC;AAEF,eAAe,kBAAkB,CAAC"}

View File

@@ -0,0 +1,507 @@
'use strict';
var _ = require('lodash/fp');
var database = require('../errors/database.js');
var transactionContext = require('../transaction-context.js');
var knex = require('../utils/knex.js');
var search = require('./helpers/search.js');
var orderBy = require('./helpers/order-by.js');
var join = require('./helpers/join.js');
var apply = require('./helpers/populate/apply.js');
var process = require('./helpers/populate/process.js');
var where = require('./helpers/where.js');
var transform = require('./helpers/transform.js');
var readable = require('./helpers/streams/readable.js');
const createQueryBuilder = (uid, db, initialState = {})=>{
const meta = db.metadata.get(uid);
const { tableName } = meta;
const state = _.defaults({
type: 'select',
select: [],
count: null,
max: null,
first: false,
data: null,
where: [],
joins: [],
populate: null,
limit: null,
offset: null,
transaction: null,
forUpdate: false,
onConflict: null,
merge: null,
ignore: false,
orderBy: [],
groupBy: [],
increments: [],
decrements: [],
aliasCounter: 0,
filters: null,
search: null,
processed: false
}, initialState);
const getAlias = ()=>{
const alias = `t${state.aliasCounter}`;
state.aliasCounter += 1;
return alias;
};
return {
alias: getAlias(),
getAlias,
state,
clone () {
return createQueryBuilder(uid, db, state);
},
select (args) {
state.type = 'select';
state.select = _.uniq(_.castArray(args));
return this;
},
addSelect (args) {
state.select = _.uniq([
...state.select,
..._.castArray(args)
]);
return this;
},
insert (data) {
state.type = 'insert';
state.data = data;
return this;
},
onConflict (args) {
state.onConflict = args;
return this;
},
merge (args) {
state.merge = args;
return this;
},
ignore () {
state.ignore = true;
return this;
},
delete () {
state.type = 'delete';
return this;
},
ref (name) {
return db.connection.ref(transform.toColumnName(meta, name));
},
update (data) {
state.type = 'update';
state.data = data;
return this;
},
increment (column, amount = 1) {
state.type = 'update';
state.increments.push({
column,
amount
});
return this;
},
decrement (column, amount = 1) {
state.type = 'update';
state.decrements.push({
column,
amount
});
return this;
},
count (count = 'id') {
state.type = 'count';
state.count = count;
return this;
},
max (column) {
state.type = 'max';
state.max = column;
return this;
},
where (where = {}) {
if (!_.isPlainObject(where)) {
throw new Error('Where must be an object');
}
state.where.push(where);
return this;
},
limit (limit) {
state.limit = limit;
return this;
},
offset (offset) {
state.offset = offset;
return this;
},
orderBy (orderBy) {
state.orderBy = orderBy;
return this;
},
groupBy (groupBy) {
state.groupBy = groupBy;
return this;
},
populate (populate) {
state.populate = populate;
return this;
},
search (query) {
state.search = query;
return this;
},
transacting (transaction) {
state.transaction = transaction;
return this;
},
forUpdate () {
state.forUpdate = true;
return this;
},
init (params = {}) {
const { _q, filters, where, select, limit, offset, orderBy, groupBy, populate } = params;
if (!_.isNil(where)) {
this.where(where);
}
if (!_.isNil(_q)) {
this.search(_q);
}
if (!_.isNil(select)) {
this.select(select);
} else {
this.select('*');
}
if (!_.isNil(limit)) {
this.limit(limit);
}
if (!_.isNil(offset)) {
this.offset(offset);
}
if (!_.isNil(orderBy)) {
this.orderBy(orderBy);
}
if (!_.isNil(groupBy)) {
this.groupBy(groupBy);
}
if (!_.isNil(populate)) {
this.populate(populate);
}
if (!_.isNil(filters)) {
this.filters(filters);
}
return this;
},
filters (filters) {
state.filters = filters;
},
first () {
state.first = true;
return this;
},
join (join$1) {
if (!join$1.targetField) {
state.joins.push(join$1);
return this;
}
const model = db.metadata.get(uid);
const attribute = model.attributes[join$1.targetField];
join.createJoin({
db,
qb: this,
uid
}, {
alias: this.alias,
refAlias: join$1.alias,
attributeName: join$1.targetField,
attribute
});
return this;
},
mustUseAlias () {
return [
'select',
'count'
].includes(state.type);
},
aliasColumn (key, alias) {
if (typeof key !== 'string') {
return key;
}
if (key.indexOf('.') >= 0) {
return key;
}
if (!_.isNil(alias)) {
return `${alias}.${key}`;
}
return this.mustUseAlias() ? `${this.alias}.${key}` : key;
},
raw: db.connection.raw.bind(db.connection),
shouldUseSubQuery () {
return [
'delete',
'update'
].includes(state.type) && state.joins.length > 0;
},
runSubQuery () {
const originalType = state.type;
this.select('id');
const subQB = this.getKnexQuery();
const nestedSubQuery = db.getConnection().select('id').from(subQB.as('subQuery'));
const connection = db.getConnection(tableName);
return connection[originalType]().whereIn('id', nestedSubQuery);
},
processState () {
if (this.state.processed) {
return;
}
state.orderBy = orderBy.processOrderBy(state.orderBy, {
qb: this,
uid,
db
});
if (!_.isNil(state.filters)) {
if (_.isFunction(state.filters)) {
const filters = state.filters({
qb: this,
uid,
meta,
db
});
if (!_.isNil(filters)) {
state.where.push(filters);
}
} else {
state.where.push(state.filters);
}
}
state.where = where.processWhere(state.where, {
qb: this,
uid,
db
});
state.populate = process(state.populate, {
qb: this,
uid,
db
});
state.data = transform.toRow(meta, state.data);
this.processSelect();
this.state.processed = true;
},
shouldUseDistinct () {
return state.joins.length > 0 && _.isEmpty(state.groupBy);
},
shouldUseDeepSort () {
return state.orderBy.filter(({ column })=>column.indexOf('.') >= 0).filter(({ column })=>{
const col = column.split('.');
for(let i = 0; i < col.length - 1; i += 1){
const el = col[i];
// order by "rel"."xxx"
const isRelationAttribute = meta.attributes[el]?.type === 'relation';
// order by "t2"."xxx"
const isAliasedRelation = Object.values(state.joins).map((join)=>join.alias).includes(el);
if (isRelationAttribute || isAliasedRelation) {
return true;
}
}
return false;
}).length > 0;
},
processSelect () {
state.select = state.select.map((field)=>{
if (knex.isKnexQuery(field)) {
return field;
}
return transform.toColumnName(meta, field);
});
if (this.shouldUseDistinct()) {
const joinsOrderByColumns = state.joins.flatMap((join)=>{
return _.keys(join.orderBy).map((key)=>this.aliasColumn(key, join.alias));
});
const orderByColumns = state.orderBy.map(({ column })=>column);
state.select = _.uniq([
...joinsOrderByColumns,
...orderByColumns,
...state.select
]);
}
},
getKnexQuery () {
if (!state.type) {
this.select('*');
}
const aliasedTableName = this.mustUseAlias() ? `${tableName} as ${this.alias}` : tableName;
const qb = db.getConnection(aliasedTableName);
// The state should always be processed before calling shouldUseSubQuery as it
// relies on the presence or absence of joins to determine the need of a subquery
this.processState();
if (this.shouldUseSubQuery()) {
return this.runSubQuery();
}
switch(state.type){
case 'select':
{
qb.select(state.select.map((column)=>this.aliasColumn(column)));
if (this.shouldUseDistinct()) {
qb.distinct();
}
break;
}
case 'count':
{
const dbColumnName = this.aliasColumn(transform.toColumnName(meta, state.count));
if (this.shouldUseDistinct()) {
qb.countDistinct({
count: dbColumnName
});
} else {
qb.count({
count: dbColumnName
});
}
break;
}
case 'max':
{
const dbColumnName = this.aliasColumn(transform.toColumnName(meta, state.max));
qb.max({
max: dbColumnName
});
break;
}
case 'insert':
{
qb.insert(state.data);
if (db.dialect.useReturning() && _.has('id', meta.attributes)) {
qb.returning('id');
}
break;
}
case 'update':
{
if (state.data) {
qb.update(state.data);
}
break;
}
case 'delete':
{
qb.delete();
break;
}
case 'truncate':
{
qb.truncate();
break;
}
default:
{
throw new Error('Unknown query type');
}
}
if (state.transaction) {
qb.transacting(state.transaction);
}
if (state.forUpdate) {
qb.forUpdate();
}
if (!_.isEmpty(state.increments)) {
state.increments.forEach((incr)=>qb.increment(incr.column, incr.amount));
}
if (!_.isEmpty(state.decrements)) {
state.decrements.forEach((decr)=>qb.decrement(decr.column, decr.amount));
}
if (state.onConflict) {
if (state.merge) {
qb.onConflict(state.onConflict).merge(state.merge);
} else if (state.ignore) {
qb.onConflict(state.onConflict).ignore();
}
}
if (state.limit) {
qb.limit(state.limit);
}
if (state.offset) {
qb.offset(state.offset);
}
if (state.orderBy.length > 0) {
qb.orderBy(state.orderBy);
}
if (state.first) {
qb.first();
}
if (state.groupBy.length > 0) {
qb.groupBy(state.groupBy);
}
// if there are joins and it is a delete or update use a sub query
if (state.where) {
where.applyWhere(qb, state.where);
}
// if there are joins and it is a delete or update use a sub query
if (state.search) {
qb.where((subQb)=>{
search.applySearch(subQb, state.search, {
qb: this,
db,
uid
});
});
}
if (state.joins.length > 0) {
join.applyJoins(qb, state.joins);
}
if (this.shouldUseDeepSort()) {
return orderBy.wrapWithDeepSort(qb, {
qb: this,
db,
uid
});
}
return qb;
},
async execute ({ mapResults = true } = {}) {
try {
const qb = this.getKnexQuery();
const transaction = transactionContext.transactionCtx.get();
if (transaction) {
qb.transacting(transaction);
}
const rows = await qb;
if (state.populate && !_.isNil(rows)) {
await apply(_.castArray(rows), state.populate, {
qb: this,
uid,
db
});
}
let results = rows;
if (mapResults && state.type === 'select') {
results = transform.fromRow(meta, rows);
}
return results;
} catch (error) {
if (error instanceof Error) {
db.dialect.transformErrors(error);
} else {
throw error;
}
}
},
stream ({ mapResults = true } = {}) {
if (state.type === 'select') {
return new readable({
qb: this,
db,
uid,
mapResults
});
}
throw new database(`query-builder.stream() has been called with an unsupported query type: "${state.type}"`);
}
};
};
module.exports = createQueryBuilder;
//# sourceMappingURL=query-builder.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,505 @@
import _ from 'lodash/fp';
import DatabaseError from '../errors/database.mjs';
import { transactionCtx } from '../transaction-context.mjs';
import { isKnexQuery } from '../utils/knex.mjs';
import { applySearch } from './helpers/search.mjs';
import { processOrderBy, wrapWithDeepSort } from './helpers/order-by.mjs';
import { createJoin, applyJoins } from './helpers/join.mjs';
import applyPopulate from './helpers/populate/apply.mjs';
import processPopulate from './helpers/populate/process.mjs';
import { processWhere, applyWhere } from './helpers/where.mjs';
import { toColumnName, toRow, fromRow } from './helpers/transform.mjs';
import ReadableStrapiQuery from './helpers/streams/readable.mjs';
const createQueryBuilder = (uid, db, initialState = {})=>{
const meta = db.metadata.get(uid);
const { tableName } = meta;
const state = _.defaults({
type: 'select',
select: [],
count: null,
max: null,
first: false,
data: null,
where: [],
joins: [],
populate: null,
limit: null,
offset: null,
transaction: null,
forUpdate: false,
onConflict: null,
merge: null,
ignore: false,
orderBy: [],
groupBy: [],
increments: [],
decrements: [],
aliasCounter: 0,
filters: null,
search: null,
processed: false
}, initialState);
const getAlias = ()=>{
const alias = `t${state.aliasCounter}`;
state.aliasCounter += 1;
return alias;
};
return {
alias: getAlias(),
getAlias,
state,
clone () {
return createQueryBuilder(uid, db, state);
},
select (args) {
state.type = 'select';
state.select = _.uniq(_.castArray(args));
return this;
},
addSelect (args) {
state.select = _.uniq([
...state.select,
..._.castArray(args)
]);
return this;
},
insert (data) {
state.type = 'insert';
state.data = data;
return this;
},
onConflict (args) {
state.onConflict = args;
return this;
},
merge (args) {
state.merge = args;
return this;
},
ignore () {
state.ignore = true;
return this;
},
delete () {
state.type = 'delete';
return this;
},
ref (name) {
return db.connection.ref(toColumnName(meta, name));
},
update (data) {
state.type = 'update';
state.data = data;
return this;
},
increment (column, amount = 1) {
state.type = 'update';
state.increments.push({
column,
amount
});
return this;
},
decrement (column, amount = 1) {
state.type = 'update';
state.decrements.push({
column,
amount
});
return this;
},
count (count = 'id') {
state.type = 'count';
state.count = count;
return this;
},
max (column) {
state.type = 'max';
state.max = column;
return this;
},
where (where = {}) {
if (!_.isPlainObject(where)) {
throw new Error('Where must be an object');
}
state.where.push(where);
return this;
},
limit (limit) {
state.limit = limit;
return this;
},
offset (offset) {
state.offset = offset;
return this;
},
orderBy (orderBy) {
state.orderBy = orderBy;
return this;
},
groupBy (groupBy) {
state.groupBy = groupBy;
return this;
},
populate (populate) {
state.populate = populate;
return this;
},
search (query) {
state.search = query;
return this;
},
transacting (transaction) {
state.transaction = transaction;
return this;
},
forUpdate () {
state.forUpdate = true;
return this;
},
init (params = {}) {
const { _q, filters, where, select, limit, offset, orderBy, groupBy, populate } = params;
if (!_.isNil(where)) {
this.where(where);
}
if (!_.isNil(_q)) {
this.search(_q);
}
if (!_.isNil(select)) {
this.select(select);
} else {
this.select('*');
}
if (!_.isNil(limit)) {
this.limit(limit);
}
if (!_.isNil(offset)) {
this.offset(offset);
}
if (!_.isNil(orderBy)) {
this.orderBy(orderBy);
}
if (!_.isNil(groupBy)) {
this.groupBy(groupBy);
}
if (!_.isNil(populate)) {
this.populate(populate);
}
if (!_.isNil(filters)) {
this.filters(filters);
}
return this;
},
filters (filters) {
state.filters = filters;
},
first () {
state.first = true;
return this;
},
join (join) {
if (!join.targetField) {
state.joins.push(join);
return this;
}
const model = db.metadata.get(uid);
const attribute = model.attributes[join.targetField];
createJoin({
db,
qb: this,
uid
}, {
alias: this.alias,
refAlias: join.alias,
attributeName: join.targetField,
attribute
});
return this;
},
mustUseAlias () {
return [
'select',
'count'
].includes(state.type);
},
aliasColumn (key, alias) {
if (typeof key !== 'string') {
return key;
}
if (key.indexOf('.') >= 0) {
return key;
}
if (!_.isNil(alias)) {
return `${alias}.${key}`;
}
return this.mustUseAlias() ? `${this.alias}.${key}` : key;
},
raw: db.connection.raw.bind(db.connection),
shouldUseSubQuery () {
return [
'delete',
'update'
].includes(state.type) && state.joins.length > 0;
},
runSubQuery () {
const originalType = state.type;
this.select('id');
const subQB = this.getKnexQuery();
const nestedSubQuery = db.getConnection().select('id').from(subQB.as('subQuery'));
const connection = db.getConnection(tableName);
return connection[originalType]().whereIn('id', nestedSubQuery);
},
processState () {
if (this.state.processed) {
return;
}
state.orderBy = processOrderBy(state.orderBy, {
qb: this,
uid,
db
});
if (!_.isNil(state.filters)) {
if (_.isFunction(state.filters)) {
const filters = state.filters({
qb: this,
uid,
meta,
db
});
if (!_.isNil(filters)) {
state.where.push(filters);
}
} else {
state.where.push(state.filters);
}
}
state.where = processWhere(state.where, {
qb: this,
uid,
db
});
state.populate = processPopulate(state.populate, {
qb: this,
uid,
db
});
state.data = toRow(meta, state.data);
this.processSelect();
this.state.processed = true;
},
shouldUseDistinct () {
return state.joins.length > 0 && _.isEmpty(state.groupBy);
},
shouldUseDeepSort () {
return state.orderBy.filter(({ column })=>column.indexOf('.') >= 0).filter(({ column })=>{
const col = column.split('.');
for(let i = 0; i < col.length - 1; i += 1){
const el = col[i];
// order by "rel"."xxx"
const isRelationAttribute = meta.attributes[el]?.type === 'relation';
// order by "t2"."xxx"
const isAliasedRelation = Object.values(state.joins).map((join)=>join.alias).includes(el);
if (isRelationAttribute || isAliasedRelation) {
return true;
}
}
return false;
}).length > 0;
},
processSelect () {
state.select = state.select.map((field)=>{
if (isKnexQuery(field)) {
return field;
}
return toColumnName(meta, field);
});
if (this.shouldUseDistinct()) {
const joinsOrderByColumns = state.joins.flatMap((join)=>{
return _.keys(join.orderBy).map((key)=>this.aliasColumn(key, join.alias));
});
const orderByColumns = state.orderBy.map(({ column })=>column);
state.select = _.uniq([
...joinsOrderByColumns,
...orderByColumns,
...state.select
]);
}
},
getKnexQuery () {
if (!state.type) {
this.select('*');
}
const aliasedTableName = this.mustUseAlias() ? `${tableName} as ${this.alias}` : tableName;
const qb = db.getConnection(aliasedTableName);
// The state should always be processed before calling shouldUseSubQuery as it
// relies on the presence or absence of joins to determine the need of a subquery
this.processState();
if (this.shouldUseSubQuery()) {
return this.runSubQuery();
}
switch(state.type){
case 'select':
{
qb.select(state.select.map((column)=>this.aliasColumn(column)));
if (this.shouldUseDistinct()) {
qb.distinct();
}
break;
}
case 'count':
{
const dbColumnName = this.aliasColumn(toColumnName(meta, state.count));
if (this.shouldUseDistinct()) {
qb.countDistinct({
count: dbColumnName
});
} else {
qb.count({
count: dbColumnName
});
}
break;
}
case 'max':
{
const dbColumnName = this.aliasColumn(toColumnName(meta, state.max));
qb.max({
max: dbColumnName
});
break;
}
case 'insert':
{
qb.insert(state.data);
if (db.dialect.useReturning() && _.has('id', meta.attributes)) {
qb.returning('id');
}
break;
}
case 'update':
{
if (state.data) {
qb.update(state.data);
}
break;
}
case 'delete':
{
qb.delete();
break;
}
case 'truncate':
{
qb.truncate();
break;
}
default:
{
throw new Error('Unknown query type');
}
}
if (state.transaction) {
qb.transacting(state.transaction);
}
if (state.forUpdate) {
qb.forUpdate();
}
if (!_.isEmpty(state.increments)) {
state.increments.forEach((incr)=>qb.increment(incr.column, incr.amount));
}
if (!_.isEmpty(state.decrements)) {
state.decrements.forEach((decr)=>qb.decrement(decr.column, decr.amount));
}
if (state.onConflict) {
if (state.merge) {
qb.onConflict(state.onConflict).merge(state.merge);
} else if (state.ignore) {
qb.onConflict(state.onConflict).ignore();
}
}
if (state.limit) {
qb.limit(state.limit);
}
if (state.offset) {
qb.offset(state.offset);
}
if (state.orderBy.length > 0) {
qb.orderBy(state.orderBy);
}
if (state.first) {
qb.first();
}
if (state.groupBy.length > 0) {
qb.groupBy(state.groupBy);
}
// if there are joins and it is a delete or update use a sub query
if (state.where) {
applyWhere(qb, state.where);
}
// if there are joins and it is a delete or update use a sub query
if (state.search) {
qb.where((subQb)=>{
applySearch(subQb, state.search, {
qb: this,
db,
uid
});
});
}
if (state.joins.length > 0) {
applyJoins(qb, state.joins);
}
if (this.shouldUseDeepSort()) {
return wrapWithDeepSort(qb, {
qb: this,
db,
uid
});
}
return qb;
},
async execute ({ mapResults = true } = {}) {
try {
const qb = this.getKnexQuery();
const transaction = transactionCtx.get();
if (transaction) {
qb.transacting(transaction);
}
const rows = await qb;
if (state.populate && !_.isNil(rows)) {
await applyPopulate(_.castArray(rows), state.populate, {
qb: this,
uid,
db
});
}
let results = rows;
if (mapResults && state.type === 'select') {
results = fromRow(meta, rows);
}
return results;
} catch (error) {
if (error instanceof Error) {
db.dialect.transformErrors(error);
} else {
throw error;
}
}
},
stream ({ mapResults = true } = {}) {
if (state.type === 'select') {
return new ReadableStrapiQuery({
qb: this,
db,
uid,
mapResults
});
}
throw new DatabaseError(`query-builder.stream() has been called with an unsupported query type: "${state.type}"`);
}
};
};
export { createQueryBuilder as default };
//# sourceMappingURL=query-builder.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
import type { Database } from '..';
import type { QueryBuilder } from './query-builder';
export interface Ctx {
qb: QueryBuilder;
uid: string;
db: Database;
}
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/query/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,YAAY,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,QAAQ,CAAC;CACd"}