Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
356 lines
22 KiB
Plaintext
356 lines
22 KiB
Plaintext
import { and, getTableName, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm';
|
|
import { PgUUID } from 'drizzle-orm/pg-core';
|
|
import { APIError, QueryError } from 'payload';
|
|
import { validOperatorSet } from 'payload/shared';
|
|
import { escapeSQLValue } from '../utilities/escapeSQLValue.js';
|
|
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js';
|
|
import { isValidStringID } from '../utilities/isValidStringID.js';
|
|
import { DistinctSymbol } from '../utilities/rawConstraint.js';
|
|
import { buildAndOrConditions } from './buildAndOrConditions.js';
|
|
import { getTableColumnFromPath } from './getTableColumnFromPath.js';
|
|
import { sanitizeQueryValue } from './sanitizeQueryValue.js';
|
|
export function parseParams({ adapter, aliasTable, context, fields, joins, locale, parentIsLocalized, selectFields, selectLocale, tableName, where }) {
|
|
let result;
|
|
const constraints = [];
|
|
if (typeof where === 'object' && Object.keys(where).length > 0) {
|
|
// We need to determine if the whereKey is an AND, OR, or a schema path
|
|
for (const relationOrPath of Object.keys(where)){
|
|
if (relationOrPath) {
|
|
const condition = where[relationOrPath];
|
|
let conditionOperator;
|
|
if (relationOrPath.toLowerCase() === 'and') {
|
|
conditionOperator = and;
|
|
} else if (relationOrPath.toLowerCase() === 'or') {
|
|
conditionOperator = or;
|
|
}
|
|
if (Array.isArray(condition)) {
|
|
const builtConditions = buildAndOrConditions({
|
|
adapter,
|
|
aliasTable,
|
|
context,
|
|
fields,
|
|
joins,
|
|
locale,
|
|
parentIsLocalized,
|
|
selectFields,
|
|
selectLocale,
|
|
tableName,
|
|
where: condition
|
|
});
|
|
if (builtConditions.length > 0) {
|
|
result = conditionOperator(...builtConditions);
|
|
}
|
|
} else {
|
|
// It's a path - and there can be multiple comparisons on a single path.
|
|
// For example - title like 'test' and title not equal to 'tester'
|
|
// So we need to loop on keys again here to handle each operator independently
|
|
const pathOperators = where[relationOrPath];
|
|
if (typeof pathOperators === 'object') {
|
|
for (let operator of Object.keys(pathOperators)){
|
|
if (validOperatorSet.has(operator)) {
|
|
const val = where[relationOrPath][operator];
|
|
const { columnName, columns, constraints: queryConstraints, field, getNotNullColumnByValue, pathSegments, rawColumn, table } = getTableColumnFromPath({
|
|
adapter,
|
|
aliasTable,
|
|
collectionPath: relationOrPath,
|
|
fields,
|
|
joins,
|
|
locale,
|
|
parentIsLocalized,
|
|
pathSegments: relationOrPath.replace(/__/g, '.').split('.'),
|
|
selectFields,
|
|
selectLocale,
|
|
tableName,
|
|
value: val
|
|
});
|
|
const resolvedColumn = rawColumn || (aliasTable && tableName === getNameFromDrizzleTable(table) ? aliasTable[columnName] : table[columnName]);
|
|
if (val === DistinctSymbol) {
|
|
selectFields['_selected'] = resolvedColumn;
|
|
break;
|
|
}
|
|
queryConstraints.forEach(({ columnName: col, table: constraintTable, value })=>{
|
|
if (typeof value === 'string' && value.indexOf('%') > -1) {
|
|
constraints.push(adapter.operators.like(constraintTable[col], value));
|
|
} else {
|
|
constraints.push(adapter.operators.equals(constraintTable[col], value));
|
|
}
|
|
});
|
|
if (([
|
|
'json',
|
|
'richText'
|
|
].includes(field.type) || field.type === 'blocks' && adapter.blocksAsJSON) && Array.isArray(pathSegments) && pathSegments.length > 1) {
|
|
if (adapter.name === 'postgres') {
|
|
const constraint = adapter.createJSONQuery({
|
|
column: rawColumn || table[columnName],
|
|
operator,
|
|
pathSegments,
|
|
value: val
|
|
});
|
|
constraints.push(sql.raw(constraint));
|
|
break;
|
|
}
|
|
const segments = pathSegments.slice(1);
|
|
segments.unshift(table[columnName].name);
|
|
if (field.type === 'richText') {
|
|
// use the table name from the nearest join to handle blocks, arrays, etc. or use the tableName arg
|
|
const jsonTable = joins.length === 0 ? tableName : joins[joins.length - 1].table[Object.getOwnPropertySymbols(joins[joins.length - 1].table)[0]];
|
|
const jsonQuery = adapter.createJSONQuery({
|
|
operator,
|
|
pathSegments: segments,
|
|
table: jsonTable,
|
|
treatAsArray: [
|
|
'children'
|
|
],
|
|
treatRootAsArray: true,
|
|
value: val
|
|
});
|
|
constraints.push(sql.raw(jsonQuery));
|
|
break;
|
|
}
|
|
const jsonQuery = adapter.convertPathToJSONTraversal(pathSegments);
|
|
const operatorKeys = {
|
|
contains: {
|
|
operator: 'like',
|
|
wildcard: '%'
|
|
},
|
|
equals: {
|
|
operator: '=',
|
|
wildcard: ''
|
|
},
|
|
exists: {
|
|
operator: val === true ? 'is not null' : 'is null',
|
|
wildcard: ''
|
|
},
|
|
in: {
|
|
operator: 'in',
|
|
wildcard: ''
|
|
},
|
|
like: {
|
|
operator: 'like',
|
|
wildcard: '%'
|
|
},
|
|
not_equals: {
|
|
operator: '<>',
|
|
wildcard: ''
|
|
},
|
|
not_in: {
|
|
operator: 'not in',
|
|
wildcard: ''
|
|
},
|
|
not_like: {
|
|
operator: 'not like',
|
|
wildcard: '%'
|
|
}
|
|
};
|
|
let formattedValue = val;
|
|
if (adapter.name === 'sqlite' && operator === 'equals' && !isNaN(val)) {
|
|
formattedValue = val;
|
|
} else if ([
|
|
'in',
|
|
'not_in'
|
|
].includes(operator) && Array.isArray(val)) {
|
|
formattedValue = `(${val.map((v)=>`${escapeSQLValue(v)}`).join(',')})`;
|
|
} else {
|
|
formattedValue = `'${operatorKeys[operator].wildcard}${escapeSQLValue(val)}${operatorKeys[operator].wildcard}'`;
|
|
}
|
|
if (operator === 'exists') {
|
|
formattedValue = '';
|
|
}
|
|
let jsonQuerySelector = `${table[columnName].name}${jsonQuery}`;
|
|
if (adapter.name === 'sqlite' && operator === 'not_like') {
|
|
jsonQuerySelector = `COALESCE(${table[columnName].name}${jsonQuery}, '')`;
|
|
}
|
|
const rawSQLQuery = `${jsonQuerySelector} ${operatorKeys[operator].operator} ${formattedValue}`;
|
|
constraints.push(sql.raw(rawSQLQuery));
|
|
break;
|
|
}
|
|
if (getNotNullColumnByValue) {
|
|
const columnName = getNotNullColumnByValue(val);
|
|
if (columnName) {
|
|
constraints.push(isNotNull(table[columnName]));
|
|
} else {
|
|
throw new QueryError([
|
|
{
|
|
path: relationOrPath
|
|
}
|
|
]);
|
|
}
|
|
break;
|
|
}
|
|
if (operator === 'like' && (field.type === 'number' || field.type === 'relationship' || field.type === 'upload' || table[columnName].columnType === 'PgUUID')) {
|
|
operator = 'equals';
|
|
}
|
|
if (operator === 'like') {
|
|
constraints.push(and(...val.split(' ').map((word)=>adapter.operators.like(table[columnName], `%${word}%`))));
|
|
break;
|
|
}
|
|
const sanitizedQueryValue = sanitizeQueryValue({
|
|
adapter,
|
|
columns,
|
|
field,
|
|
isUUID: table?.[columnName] instanceof PgUUID,
|
|
operator,
|
|
relationOrPath,
|
|
val
|
|
});
|
|
if (sanitizedQueryValue === null) {
|
|
break;
|
|
}
|
|
const { columns: queryColumns, operator: queryOperator, value: queryValue } = sanitizedQueryValue;
|
|
// Handle polymorphic relationships by value
|
|
if (queryColumns) {
|
|
if (!queryColumns.length) {
|
|
break;
|
|
}
|
|
let wrapOperator = or;
|
|
if (queryValue === null && [
|
|
'equals',
|
|
'not_equals'
|
|
].includes(operator)) {
|
|
if (operator === 'equals') {
|
|
wrapOperator = and;
|
|
}
|
|
constraints.push(wrapOperator(...queryColumns.map(({ rawColumn })=>operator === 'equals' ? isNull(rawColumn) : isNotNull(rawColumn))));
|
|
break;
|
|
}
|
|
if ([
|
|
'not_equals',
|
|
'not_in'
|
|
].includes(operator)) {
|
|
wrapOperator = and;
|
|
}
|
|
constraints.push(wrapOperator(...queryColumns.map(({ rawColumn, value })=>adapter.operators[queryOperator](rawColumn, value))));
|
|
break;
|
|
}
|
|
if (queryOperator === 'not_equals' && queryValue !== null) {
|
|
constraints.push(or(isNull(resolvedColumn), /* eslint-disable @typescript-eslint/no-explicit-any */ ne(resolvedColumn, queryValue)));
|
|
break;
|
|
}
|
|
if ((field.type === 'relationship' || field.type === 'upload') && Array.isArray(queryValue) && operator === 'not_in') {
|
|
constraints.push(sql`(${notInArray(table[columnName], queryValue)} OR
|
|
${table[columnName]}
|
|
IS
|
|
NULL)`);
|
|
break;
|
|
}
|
|
if (operator === 'equals' && queryValue === null) {
|
|
constraints.push(isNull(resolvedColumn));
|
|
break;
|
|
}
|
|
if (operator === 'not_equals' && queryValue === null) {
|
|
constraints.push(isNotNull(resolvedColumn));
|
|
break;
|
|
}
|
|
if (field.type === 'point' && adapter.name === 'postgres') {
|
|
switch(operator){
|
|
case 'intersects':
|
|
{
|
|
constraints.push(sql`ST_Intersects(${table[columnName]}, ST_GeomFromGeoJSON(${JSON.stringify(queryValue)}))`);
|
|
break;
|
|
}
|
|
case 'near':
|
|
{
|
|
const [lng, lat, maxDistance, minDistance] = queryValue;
|
|
const geoConstraints = [];
|
|
if (typeof maxDistance === 'number' && !Number.isNaN(maxDistance)) {
|
|
geoConstraints.push(sql`ST_DWithin(ST_Transform(${table[columnName]}, 3857), ST_Transform(ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326), 3857), ${maxDistance})`);
|
|
}
|
|
if (typeof minDistance === 'number' && !Number.isNaN(minDistance)) {
|
|
geoConstraints.push(sql`ST_Distance(ST_Transform(${table[columnName]}, 3857), ST_Transform(ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326), 3857)) >= ${minDistance}`);
|
|
}
|
|
if (geoConstraints.length) {
|
|
context.sort = relationOrPath;
|
|
context.rawSort = sql`${table[columnName]} <-> ST_SetSRID(ST_MakePoint(${lng}, ${lat}), 4326)`;
|
|
constraints.push(and(...geoConstraints));
|
|
}
|
|
break;
|
|
}
|
|
case 'within':
|
|
{
|
|
constraints.push(sql`ST_Within(${table[columnName]}, ST_GeomFromGeoJSON(${JSON.stringify(queryValue)}))`);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
const orConditions = [];
|
|
let resolvedQueryValue = queryValue;
|
|
if (operator === 'in' && Array.isArray(queryValue) && queryValue.some((v)=>v === null)) {
|
|
orConditions.push(isNull(resolvedColumn));
|
|
resolvedQueryValue = queryValue.filter((v)=>v !== null);
|
|
}
|
|
if (operator === 'contains' && Array.isArray(queryValue) && 'hasMany' in field && field.hasMany && [
|
|
'number',
|
|
'select',
|
|
'text'
|
|
].includes(field.type)) {
|
|
// Create OR conditions for each value in the array
|
|
orConditions.push(...queryValue.map((val)=>adapter.operators[queryOperator](resolvedColumn, val)));
|
|
// Set constraint to combine all OR conditions
|
|
const constraint = orConditions.length > 0 ? or(...orConditions) : undefined;
|
|
if (constraint) {
|
|
constraints.push(constraint);
|
|
}
|
|
break;
|
|
}
|
|
let constraint = adapter.operators[queryOperator](resolvedColumn, resolvedQueryValue);
|
|
if (adapter.limitedBoundParameters && (operator === 'in' || operator === 'not_in') && relationOrPath === 'id' && Array.isArray(queryValue)) {
|
|
let isInvalid = false;
|
|
for (const val of queryValue){
|
|
if (typeof val === 'number' || val === null) {
|
|
continue;
|
|
}
|
|
if (typeof val === 'string') {
|
|
if (!isValidStringID(val)) {
|
|
isInvalid = true;
|
|
break;
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
isInvalid = true;
|
|
break;
|
|
}
|
|
if (isInvalid) {
|
|
throw new APIError(`Invalid ID value in ${JSON.stringify(queryValue)}`);
|
|
}
|
|
constraints.push(sql.raw(`"${getTableName(resolvedColumn.table)}"."${resolvedColumn.name}" ${operator === 'in' ? 'IN' : 'NOT IN'} (${queryValue.map((e)=>{
|
|
if (e === null) {
|
|
return `NULL`;
|
|
}
|
|
if (typeof e === 'number') {
|
|
return e;
|
|
}
|
|
return `'${e}'`;
|
|
}).join(',')})`));
|
|
break;
|
|
}
|
|
if (orConditions.length) {
|
|
orConditions.push(constraint);
|
|
constraint = or(...orConditions);
|
|
}
|
|
constraints.push(constraint);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (constraints.length > 0) {
|
|
if (result) {
|
|
result = and(result, ...constraints);
|
|
} else {
|
|
result = and(...constraints);
|
|
}
|
|
}
|
|
if (constraints.length === 1 && !result) {
|
|
;
|
|
[result] = constraints;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
//# sourceMappingURL=parseParams.js.map |