strapi/packages/core/database/src/query/query-builder.ts

606 lines
13 KiB
TypeScript
Raw Normal View History

2023-07-19 16:35:50 +02:00
import _ from 'lodash/fp';
import type { Knex } from 'knex';
import { DatabaseError } from '../errors';
import * as helpers from './helpers';
import { transactionCtx } from '../transaction-context';
2023-09-14 15:21:32 +02:00
import type { Join } from './helpers/join';
2023-07-19 16:35:50 +02:00
import type { Database } from '..';
2023-09-14 17:35:36 +02:00
import { isKnexQuery } from '../utils/knex';
2023-07-19 16:35:50 +02:00
interface State {
type: 'select' | 'insert' | 'update' | 'delete' | 'count' | 'max' | 'truncate';
2023-09-14 17:35:36 +02:00
select: Array<string | Knex.Raw>;
2023-07-19 16:35:50 +02:00
count: string | null;
max: string | null;
first: boolean;
2023-09-14 15:21:32 +02:00
data: Record<string, unknown> | (null | Record<string, unknown>)[] | null;
where: Record<string, unknown>[];
joins: Join[];
2023-07-19 16:35:50 +02:00
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;
}
export interface QueryBuilder {
alias: string;
state: State;
getAlias(): string;
clone(): QueryBuilder;
2023-09-14 17:35:36 +02:00
select(args: string | Array<string | Knex.Raw>): QueryBuilder;
2023-07-19 16:35:50 +02:00
addSelect(args: string | string[]): QueryBuilder;
2023-09-14 15:21:32 +02:00
insert<T extends Record<string, unknown> | Record<string, unknown>[]>(data: T): QueryBuilder;
2023-07-19 16:35:50 +02:00
onConflict(args: any): QueryBuilder;
merge(args: any): QueryBuilder;
ignore(): QueryBuilder;
delete(): QueryBuilder;
ref(name: string): any;
2023-09-14 15:21:32 +02:00
update<T extends Record<string, unknown>>(data: T): QueryBuilder;
2023-07-19 16:35:50 +02:00
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;
2023-09-14 15:21:32 +02:00
aliasColumn(key: any, alias?: string): any;
2023-07-19 16:35:50 +02:00
raw: Knex.RawBuilder;
shouldUseSubQuery(): boolean;
runSubQuery(): any;
processState(): void;
shouldUseDistinct(): boolean;
processSelect(): void;
2023-09-14 15:21:32 +02:00
getKnexQuery(): Knex.QueryBuilder;
2023-07-19 16:35:50 +02:00
execute<T>({ mapResults }?: { mapResults?: boolean }): Promise<T>;
2023-09-14 15:21:32 +02:00
stream({ mapResults }?: { mapResults?: boolean }): helpers.ReadableQuery;
2023-07-19 16:35:50 +02:00
}
2023-09-13 09:43:58 +02:00
const createQueryBuilder = <TResult>(
2023-07-19 16:35:50 +02:00
uid: string,
db: Database,
initialState: Partial<State> = {}
): QueryBuilder => {
2021-06-17 16:17:15 +02:00
const meta = db.metadata.get(uid);
const { tableName } = meta;
2023-07-19 16:35:50 +02:00
const state: State = _.defaults(
2022-08-05 16:05:52 +02:00
{
type: 'select',
select: [],
count: null,
max: null,
first: false,
data: null,
where: [],
joins: [],
populate: null,
limit: null,
offset: null,
transaction: null,
forUpdate: false,
2022-09-20 15:53:17 +02:00
onConflict: null,
merge: null,
ignore: false,
2022-08-05 16:05:52 +02:00
orderBy: [],
groupBy: [],
increments: [],
decrements: [],
2022-08-05 16:05:52 +02:00
aliasCounter: 0,
2023-07-19 16:35:50 +02:00
filters: null,
search: null,
2022-08-05 16:05:52 +02:00
},
initialState
);
2021-06-17 16:17:15 +02:00
2022-09-05 19:44:58 +02:00
const getAlias = () => {
const alias = `t${state.aliasCounter}`;
2022-09-05 19:44:58 +02:00
state.aliasCounter += 1;
2022-09-05 19:44:58 +02:00
return alias;
};
2021-06-17 16:17:15 +02:00
return {
alias: getAlias(),
getAlias,
state,
2021-06-17 16:17:15 +02:00
2022-08-05 16:05:52 +02:00
clone() {
return createQueryBuilder(uid, db, state);
},
2021-08-06 10:51:34 +02:00
select(args) {
state.type = 'select';
state.select = _.uniq(_.castArray(args));
2021-08-11 09:34:55 +02:00
return this;
},
addSelect(args) {
state.select = _.uniq([...state.select, ..._.castArray(args)]);
2021-08-06 10:51:34 +02:00
return this;
},
2021-06-17 16:17:15 +02:00
insert(data) {
state.type = 'insert';
state.data = data;
return this;
},
2022-09-20 15:53:17 +02:00
onConflict(args) {
state.onConflict = args;
return this;
},
merge(args) {
state.merge = args;
return this;
},
ignore() {
state.ignore = true;
return this;
},
2021-06-17 16:17:15 +02:00
delete() {
state.type = 'delete';
return this;
},
ref(name) {
return db.connection.ref(helpers.toColumnName(meta, name));
},
2021-06-17 16:17:15 +02:00
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;
},
2022-08-09 10:45:58 +03:00
count(count = 'id') {
2021-06-17 16:17:15 +02:00
state.type = 'count';
state.count = count;
return this;
},
2023-07-19 16:35:50 +02:00
max(column: string) {
2022-06-03 16:21:52 +02:00
state.type = 'max';
state.max = column;
return this;
},
2023-09-14 15:21:32 +02:00
where(where: Record<string, unknown> = {}) {
if (!_.isPlainObject(where)) {
throw new Error('Where must be an object');
}
2021-06-17 16:17:15 +02:00
state.where.push(where);
2021-06-17 16:17:15 +02:00
return this;
},
limit(limit) {
state.limit = limit;
return this;
},
offset(offset) {
state.offset = offset;
return this;
},
orderBy(orderBy) {
state.orderBy = orderBy;
2021-06-17 16:17:15 +02:00
return this;
},
groupBy(groupBy) {
state.groupBy = groupBy;
2021-06-24 18:28:36 +02:00
return this;
2021-06-17 16:17:15 +02:00
},
populate(populate) {
state.populate = populate;
2021-07-28 21:03:32 +02:00
return this;
},
2021-06-17 16:17:15 +02:00
2021-07-28 21:03:32 +02:00
search(query) {
state.search = query;
2021-06-17 16:17:15 +02:00
return this;
},
2022-05-18 19:00:43 +02:00
transacting(transaction) {
state.transaction = transaction;
return this;
},
forUpdate() {
state.forUpdate = true;
return this;
},
2021-06-17 16:17:15 +02:00
init(params = {}) {
const { _q, filters, where, select, limit, offset, orderBy, groupBy, populate } = params;
2021-06-17 16:17:15 +02:00
2021-08-09 18:20:27 +02:00
if (!_.isNil(where)) {
2021-06-17 16:17:15 +02:00
this.where(where);
}
2021-08-09 18:20:27 +02:00
if (!_.isNil(_q)) {
2021-07-28 21:03:32 +02:00
this.search(_q);
}
2021-08-09 18:20:27 +02:00
if (!_.isNil(select)) {
2021-06-17 16:17:15 +02:00
this.select(select);
} else {
this.select('*');
}
2021-08-09 18:20:27 +02:00
if (!_.isNil(limit)) {
2021-06-17 16:17:15 +02:00
this.limit(limit);
}
2021-08-09 18:20:27 +02:00
if (!_.isNil(offset)) {
2021-06-17 16:17:15 +02:00
this.offset(offset);
}
2021-08-09 18:20:27 +02:00
if (!_.isNil(orderBy)) {
2021-06-17 16:17:15 +02:00
this.orderBy(orderBy);
}
2021-08-09 18:20:27 +02:00
if (!_.isNil(groupBy)) {
2021-06-17 16:17:15 +02:00
this.groupBy(groupBy);
}
2021-08-09 18:20:27 +02:00
if (!_.isNil(populate)) {
2021-06-17 16:17:15 +02:00
this.populate(populate);
}
if (!_.isNil(filters)) {
this.filters(filters);
}
2021-06-17 16:17:15 +02:00
return this;
},
filters(filters) {
state.filters = filters;
},
2021-06-17 16:17:15 +02:00
first() {
state.first = true;
return this;
},
join(join) {
2022-08-16 17:01:52 +02:00
if (!join.targetField) {
state.joins.push(join);
return this;
}
const model = db.metadata.get(uid);
const attribute = model.attributes[join.targetField];
helpers.createJoin(
2023-09-14 15:21:32 +02:00
{ db, qb: this, uid },
2022-08-16 17:01:52 +02:00
{
alias: this.alias,
refAlias: join.alias,
attributeName: join.targetField,
attribute,
}
);
2021-06-17 16:17:15 +02:00
return this;
},
mustUseAlias() {
return ['select', 'count'].includes(state.type);
},
2023-09-14 15:21:32 +02:00
aliasColumn(key: string | unknown, alias: string): string | unknown {
if (typeof key !== 'string') {
return key;
2021-07-08 18:15:32 +02:00
}
if (key.indexOf('.') >= 0) {
return key;
2021-09-16 23:29:25 +02:00
}
if (!_.isNil(alias)) {
return `${alias}.${key}`;
}
return this.mustUseAlias() ? `${this.alias}.${key}` : key;
2021-06-17 16:17:15 +02:00
},
2023-09-14 15:21:32 +02:00
raw: db.connection.raw.bind(db.connection),
2021-07-08 18:15:32 +02:00
shouldUseSubQuery() {
return ['delete', 'update'].includes(state.type) && state.joins.length > 0;
},
2021-06-24 18:28:36 +02:00
runSubQuery() {
this.select('id');
const subQB = this.getKnexQuery();
2021-06-17 16:17:15 +02:00
2022-08-08 23:33:39 +02:00
const nestedSubQuery = db.getConnection().select('id').from(subQB.as('subQuery'));
2023-09-14 15:21:32 +02:00
const connection = db.getConnection(tableName);
2022-08-08 23:33:39 +02:00
2023-09-14 15:21:32 +02:00
return (connection[state.type] as Knex)().whereIn('id', nestedSubQuery);
},
processState() {
state.orderBy = helpers.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 = helpers.processWhere(state.where, { qb: this, uid, db });
state.populate = helpers.processPopulate(state.populate, { qb: this, uid, db });
state.data = helpers.toRow(meta, state.data);
2021-09-22 18:49:04 +02:00
this.processSelect();
},
2022-08-09 10:45:58 +03:00
shouldUseDistinct() {
return state.joins.length > 0 && _.isEmpty(state.groupBy);
2021-09-22 18:49:04 +02:00
},
processSelect() {
2023-09-14 17:35:36 +02:00
state.select = state.select.map((field) => {
if (isKnexQuery(field)) {
return field;
}
return helpers.toColumnName(meta, field);
});
2021-09-22 18:49:04 +02:00
if (this.shouldUseDistinct()) {
2022-08-08 23:33:39 +02:00
const joinsOrderByColumns = state.joins.flatMap((join) => {
return _.keys(join.orderBy).map((key) => this.aliasColumn(key, join.alias));
2021-09-22 18:49:04 +02:00
});
const orderByColumns = state.orderBy.map(({ column }) => column);
2021-09-22 20:10:48 +02:00
state.select = _.uniq([...joinsOrderByColumns, ...orderByColumns, ...state.select]);
2021-09-22 18:49:04 +02:00
}
},
getKnexQuery() {
2021-07-08 18:15:32 +02:00
if (!state.type) {
this.select('*');
}
const aliasedTableName = this.mustUseAlias() ? `${tableName} as ${this.alias}` : tableName;
const qb = db.getConnection(aliasedTableName);
if (this.shouldUseSubQuery()) {
return this.runSubQuery();
}
this.processState();
2021-07-05 18:35:16 +02:00
switch (state.type) {
case 'select': {
2022-08-08 23:33:39 +02:00
qb.select(state.select.map((column) => this.aliasColumn(column)));
2021-09-21 19:16:25 +02:00
2021-09-22 18:49:04 +02:00
if (this.shouldUseDistinct()) {
qb.distinct();
2021-06-17 16:17:15 +02:00
}
2021-07-05 18:35:16 +02:00
break;
2021-06-24 18:28:36 +02:00
}
2021-07-05 18:35:16 +02:00
case 'count': {
2022-08-09 10:45:58 +03:00
const dbColumnName = this.aliasColumn(helpers.toColumnName(meta, state.count));
2022-08-03 12:59:30 +03:00
2022-08-09 10:45:58 +03:00
if (this.shouldUseDistinct()) {
2022-08-03 14:46:07 +03:00
qb.countDistinct({ count: dbColumnName });
} else {
qb.count({ count: dbColumnName });
}
2021-07-05 18:35:16 +02:00
break;
2022-06-03 16:21:52 +02:00
}
case 'max': {
2022-06-07 11:16:53 +02:00
const dbColumnName = this.aliasColumn(helpers.toColumnName(meta, state.max));
qb.max({ max: dbColumnName });
2022-06-03 16:21:52 +02:00
break;
2021-06-17 16:17:15 +02:00
}
2021-07-05 18:35:16 +02:00
case 'insert': {
qb.insert(state.data);
2021-06-17 16:17:15 +02:00
2021-07-05 18:35:16 +02:00
if (db.dialect.useReturning() && _.has('id', meta.attributes)) {
qb.returning('id');
}
2021-06-17 16:17:15 +02:00
2021-07-05 18:35:16 +02:00
break;
2021-06-24 18:28:36 +02:00
}
2021-07-05 18:35:16 +02:00
case 'update': {
if (state.data) {
qb.update(state.data);
}
2021-07-05 18:35:16 +02:00
break;
2021-06-24 18:28:36 +02:00
}
2021-07-05 18:35:16 +02:00
case 'delete': {
2021-09-16 23:29:25 +02:00
qb.delete();
break;
}
case 'truncate': {
2023-09-14 15:21:32 +02:00
qb.truncate();
2021-07-05 18:35:16 +02:00
break;
2021-06-24 18:28:36 +02:00
}
2022-08-08 23:33:39 +02:00
default: {
throw new Error('Unknown query type');
}
2021-07-05 18:35:16 +02:00
}
2021-06-17 16:17:15 +02:00
2022-05-18 19:00:43 +02:00
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));
}
2022-09-20 15:53:17 +02:00
if (state.onConflict) {
if (state.merge) {
qb.onConflict(state.onConflict).merge(state.merge);
} else if (state.ignore) {
qb.onConflict(state.onConflict).ignore();
}
}
2021-07-05 18:35:16 +02:00
if (state.limit) {
qb.limit(state.limit);
}
2021-06-17 16:17:15 +02:00
2021-07-05 18:35:16 +02:00
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);
}
2021-09-16 15:32:27 +02:00
// if there are joins and it is a delete or update use a sub query
2021-07-05 18:35:16 +02:00
if (state.where) {
helpers.applyWhere(qb, state.where);
}
2021-09-16 15:32:27 +02:00
// if there are joins and it is a delete or update use a sub query
2021-07-28 21:03:32 +02:00
if (state.search) {
2022-08-08 23:33:39 +02:00
qb.where((subQb) => {
helpers.applySearch(subQb, state.search, { qb: this, db, uid });
2021-07-28 21:03:32 +02:00
});
}
2021-07-05 18:35:16 +02:00
if (state.joins.length > 0) {
helpers.applyJoins(qb, state.joins);
}
return qb;
},
async execute({ mapResults = true } = {}) {
try {
const qb = this.getKnexQuery();
2021-06-17 16:17:15 +02:00
2023-09-14 15:21:32 +02:00
const transaction = transactionCtx.get();
if (transaction) {
qb.transacting(transaction);
2022-09-12 15:24:53 +02:00
}
2021-06-28 21:37:44 +02:00
const rows = await qb;
2021-06-17 16:17:15 +02:00
2021-06-30 20:00:03 +02:00
if (state.populate && !_.isNil(rows)) {
2022-09-12 15:24:53 +02:00
await helpers.applyPopulate(_.castArray(rows), state.populate, {
qb: this,
uid,
db,
});
2021-06-28 21:37:44 +02:00
}
2021-06-17 16:17:15 +02:00
2021-06-30 20:00:03 +02:00
let results = rows;
if (mapResults && state.type === 'select') {
2021-06-30 21:17:32 +02:00
results = helpers.fromRow(meta, rows);
2021-06-24 18:28:36 +02:00
}
2021-06-17 16:17:15 +02:00
2021-06-24 18:28:36 +02:00
return results;
} catch (error) {
2023-09-14 15:21:32 +02:00
if (error instanceof Error) {
db.dialect.transformErrors(error);
} else {
throw error;
}
2021-06-24 18:28:36 +02:00
}
2021-06-17 16:17:15 +02:00
},
stream({ mapResults = true } = {}) {
if (state.type === 'select') {
return new helpers.ReadableQuery({ qb: this, db, uid, mapResults });
}
throw new DatabaseError(
`query-builder.stream() has been called with an unsupported query type: "${state.type}"`
);
},
2021-06-17 16:17:15 +02:00
};
};
2023-07-19 16:35:50 +02:00
export default createQueryBuilder;