Files
klz-cables.com/.pnpm-store/v10/files/9d/e8221c92d0e655176693ebe65b93d9027cfed3794f712377418491c6e039eb07cc0931620710154d9077665715945e4de549650d02e582452936959d8e77d4
Marc Mintel 5397309103
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
fix(products): fix breadcrumbs and product filtering (backport from main)
2026-02-24 16:04:21 +01:00

631 lines
34 KiB
Plaintext

import { and, asc, count, desc, eq, getTableName, or, sql } from 'drizzle-orm';
import { appendVersionToQueryKey, buildVersionCollectionFields, combineQueries, getFieldByPath, getQueryDraftsSort } from 'payload';
import { fieldIsVirtual, fieldShouldBeLocalized, hasDraftsEnabled } from 'payload/shared';
import toSnakeCase from 'to-snake-case';
import { buildQuery } from '../queries/buildQuery.js';
import { getTableAlias } from '../queries/getTableAlias.js';
import { operatorMap } from '../queries/operatorMap.js';
import { getArrayRelationName } from '../utilities/getArrayRelationName.js';
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js';
import { jsonAggBuildObject } from '../utilities/json.js';
import { rawConstraint } from '../utilities/rawConstraint.js';
import { InternalBlockTableNameIndex, resolveBlockTableName } from '../utilities/validateExistingBlockIsIdentical.js';
const flattenAllWherePaths = (where, paths)=>{
for(const k in where){
if ([
'AND',
'OR'
].includes(k.toUpperCase())) {
if (Array.isArray(where[k])) {
for (const whereField of where[k]){
flattenAllWherePaths(whereField, paths);
}
}
} else {
// TODO: explore how to support arrays/relationship querying.
paths.push({
path: k.split('.').join('_'),
ref: where
});
}
}
};
const buildSQLWhere = (where, alias)=>{
for(const k in where){
if ([
'AND',
'OR'
].includes(k.toUpperCase())) {
if (Array.isArray(where[k])) {
const op = 'AND' === k.toUpperCase() ? and : or;
const accumulated = [];
for (const whereField of where[k]){
accumulated.push(buildSQLWhere(whereField, alias));
}
return op(...accumulated);
}
} else {
let payloadOperator = Object.keys(where[k])[0];
const value = where[k][payloadOperator];
if (payloadOperator === '$raw') {
return sql.raw(value);
}
// Handle exists: false -> use isNull instead of isNotNull
// This logic is duplicated from sanitizeQueryValue.ts because buildSQLWhere
// is a simplified WHERE builder for polymorphic joins that doesn't have access
// to field definitions needed by sanitizeQueryValue
if (payloadOperator === 'exists' && value === false) {
payloadOperator = 'isNull';
}
return operatorMap[payloadOperator](sql.raw(`"${alias}"."${k.split('.').join('_')}"`), value);
}
}
};
export const traverseFields = ({ _locales, adapter, collectionSlug, currentArgs, currentTableName, depth, draftsEnabled, fields, forceWithFields, joinQuery = {}, joins, locale, parentIsLocalized = false, path, select, selectAllOnCurrentLevel = false, selectMode, tablePath, topLevelArgs, topLevelTableName, versions, withTabledFields })=>{
fields.forEach((field)=>{
if (fieldIsVirtual(field)) {
return;
}
const isFieldLocalized = fieldShouldBeLocalized({
field,
parentIsLocalized
});
// handle simple relationship
if (depth > 0 && (field.type === 'upload' || field.type === 'relationship') && !field.hasMany && typeof field.relationTo === 'string') {
if (isFieldLocalized) {
_locales.with[`${path}${field.name}`] = true;
} else {
currentArgs.with[`${path}${field.name}`] = true;
}
}
switch(field.type){
case 'array':
{
const arraySelect = selectAllOnCurrentLevel ? true : select?.[field.name];
if (select) {
if (selectMode === 'include' && typeof arraySelect === 'undefined' || selectMode === 'exclude' && arraySelect === false) {
break;
}
}
const withArray = {
columns: typeof arraySelect === 'object' ? {
id: true,
_order: true
} : {
_parentID: false
},
orderBy: ({ _order }, { asc })=>[
asc(_order)
],
with: {}
};
const arrayTableName = adapter.tableNameMap.get(`${currentTableName}_${tablePath}${toSnakeCase(field.name)}`);
if (typeof arraySelect === 'object') {
if (adapter.tables[arrayTableName]._locale) {
withArray.columns._locale = true;
}
if (adapter.tables[arrayTableName]._uuid) {
withArray.columns._uuid = true;
}
}
const arrayTableNameWithLocales = `${arrayTableName}${adapter.localesSuffix}`;
if (adapter.tables[arrayTableNameWithLocales]) {
withArray.with._locales = {
columns: typeof arraySelect === 'object' ? {
_locale: true
} : {
id: false,
_parentID: false
},
with: {}
};
}
const relationName = getArrayRelationName({
field,
path: `${path}${field.name}`,
tableName: arrayTableName
});
currentArgs.with[relationName] = withArray;
traverseFields({
_locales: withArray.with._locales,
adapter,
currentArgs: withArray,
currentTableName: arrayTableName,
depth,
draftsEnabled,
fields: field.flattenedFields,
forceWithFields,
joinQuery,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
path: '',
select: typeof arraySelect === 'object' ? arraySelect : undefined,
selectMode,
tablePath: '',
topLevelArgs,
topLevelTableName,
withTabledFields
});
if (typeof arraySelect === 'object' && withArray.with._locales && Object.keys(withArray.with._locales).length === 1) {
delete withArray.with._locales;
}
break;
}
case 'blocks':
{
const blocksSelect = selectAllOnCurrentLevel ? true : select?.[field.name];
if (select) {
if (selectMode === 'include' && !blocksSelect || selectMode === 'exclude' && blocksSelect === false) {
break;
}
}
if (adapter.blocksAsJSON) {
if (select || selectAllOnCurrentLevel) {
const fieldPath = `${path}${field.name}`;
if ((isFieldLocalized || parentIsLocalized) && _locales) {
_locales.columns[fieldPath] = true;
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
currentArgs.columns[fieldPath] = true;
}
}
break;
}
;
(field.blockReferences ?? field.blocks).forEach((_block)=>{
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block;
const blockKey = `_blocks_${block.slug}${!block[InternalBlockTableNameIndex] ? '' : `_${block[InternalBlockTableNameIndex]}`}`;
let blockSelect;
let blockSelectMode = selectMode;
if (selectMode === 'include' && blocksSelect === true) {
blockSelect = true;
}
if (typeof blocksSelect === 'object') {
if (typeof blocksSelect[block.slug] === 'object') {
blockSelect = blocksSelect[block.slug];
} else if (selectMode === 'include' && typeof blocksSelect[block.slug] === 'undefined' || selectMode === 'exclude' && blocksSelect[block.slug] === false) {
blockSelect = {};
blockSelectMode = 'include';
} else if (selectMode === 'include' && Boolean(blocksSelect[block.slug])) {
blockSelect = true;
}
}
if (!topLevelArgs[blockKey]) {
const withBlock = {
columns: typeof blockSelect === 'object' ? {
id: true,
_order: true,
_path: true
} : {
_parentID: false
},
orderBy: ({ _order }, { asc })=>[
asc(_order)
],
with: {}
};
const tableName = resolveBlockTableName(block, adapter.tableNameMap.get(`${topLevelTableName}_blocks_${toSnakeCase(block.slug)}`));
if (typeof blockSelect === 'object') {
if (adapter.tables[tableName]._locale) {
withBlock.columns._locale = true;
}
if (adapter.tables[tableName]._uuid) {
withBlock.columns._uuid = true;
}
}
if (adapter.tables[`${tableName}${adapter.localesSuffix}`]) {
withBlock.with._locales = {
with: {}
};
if (typeof blockSelect === 'object') {
withBlock.with._locales.columns = {
_locale: true
};
}
}
topLevelArgs.with[blockKey] = withBlock;
traverseFields({
_locales: withBlock.with._locales,
adapter,
currentArgs: withBlock,
currentTableName: tableName,
depth,
draftsEnabled,
fields: block.flattenedFields,
forceWithFields: blockSelect === true,
joinQuery,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
path: '',
select: typeof blockSelect === 'object' ? blockSelect : undefined,
selectMode: blockSelectMode,
tablePath: '',
topLevelArgs,
topLevelTableName,
withTabledFields
});
if (typeof blockSelect === 'object' && withBlock.with._locales && Object.keys(withBlock.with._locales.columns).length === 1) {
delete withBlock.with._locales;
}
}
});
break;
}
case 'group':
case 'tab':
{
const fieldSelect = select?.[field.name];
if (fieldSelect === false) {
break;
}
traverseFields({
_locales,
adapter,
collectionSlug,
currentArgs,
currentTableName,
depth,
draftsEnabled,
fields: field.flattenedFields,
forceWithFields,
joinQuery,
joins,
locale,
parentIsLocalized: parentIsLocalized || field.localized,
path: `${path}${field.name}_`,
select: typeof fieldSelect === 'object' ? fieldSelect : undefined,
selectAllOnCurrentLevel: selectAllOnCurrentLevel || fieldSelect === true || selectMode === 'exclude' && typeof fieldSelect === 'undefined',
selectMode,
tablePath: `${tablePath}${toSnakeCase(field.name)}_`,
topLevelArgs,
topLevelTableName,
versions,
withTabledFields
});
break;
}
case 'join':
{
// when `joinsQuery` is false, do not join
if (joinQuery === false) {
break;
}
if (select && selectMode === 'include' && !select[field.name] || selectMode === 'exclude' && select[field.name] === false) {
break;
}
const joinSchemaPath = `${path.replaceAll('_', '.')}${field.name}`;
if (joinQuery[joinSchemaPath] === false) {
break;
}
const { count: shouldCount = false, limit: limitArg = field.defaultLimit ?? 10, page, sort = field.defaultSort, where } = joinQuery[joinSchemaPath] || {};
let limit = limitArg;
if (limit !== 0) {
// get an additional document and slice it later to determine if there is a next page
limit += 1;
}
const columnName = `${path.replaceAll('.', '_')}${field.name}`;
const db = adapter.drizzle;
if (Array.isArray(field.collection)) {
let currentQuery = null;
const onPath = field.on.split('.').join('_');
if (Array.isArray(sort)) {
throw new Error('Not implemented');
}
let sanitizedSort = sort;
if (!sanitizedSort) {
if (field.collection.some((collection)=>adapter.payload.collections[collection].config.fields.some((f)=>f.type === 'date' && f.name === 'createdAt'))) {
sanitizedSort = '-createdAt';
} else {
sanitizedSort = 'id';
}
}
const sortOrder = sanitizedSort.startsWith('-') ? desc : asc;
sanitizedSort = sanitizedSort.replace('-', '');
const sortPath = sanitizedSort.split('.').join('_');
const wherePaths = [];
if (where) {
flattenAllWherePaths(where, wherePaths);
}
for (const collection of field.collection){
const joinCollectionTableName = adapter.tableNameMap.get(toSnakeCase(collection));
const table = adapter.tables[joinCollectionTableName];
const sortColumn = table[sortPath];
const selectFields = {
id: adapter.tables[joinCollectionTableName].id,
parent: sql`${adapter.tables[joinCollectionTableName][onPath]}`.as(onPath),
relationTo: sql`${collection}`.as('relationTo'),
sortPath: sql`${sortColumn ? sortColumn : null}`.as('sortPath')
};
const collectionQueryWhere = [];
// Select for WHERE and Fallback NULL
for (const { path, ref } of wherePaths){
const collectioConfig = adapter.payload.collections[collection].config;
const field = getFieldByPath({
fields: collectioConfig.flattenedFields,
path
});
if (field && field.field.type === 'select' && field.field.hasMany) {
let tableName = adapter.tableNameMap.get(`${toSnakeCase(collection)}_${toSnakeCase(path)}`);
let parentTable = getTableName(table);
if (adapter.schemaName) {
tableName = `"${adapter.schemaName}"."${tableName}"`;
parentTable = `"${adapter.schemaName}"."${parentTable}"`;
}
if (adapter.name === 'postgres') {
selectFields[path] = sql.raw(`(select jsonb_agg(${tableName}.value) from ${tableName} where ${tableName}.parent_id = ${parentTable}.id)`).as(path);
} else {
selectFields[path] = sql.raw(`(select json_group_array(${tableName}.value) from ${tableName} where ${tableName}.parent_id = ${parentTable}.id)`).as(path);
}
const constraint = ref[path];
const operator = Object.keys(constraint)[0];
const value = Object.values(constraint)[0];
const query = adapter.createJSONQuery({
column: `"${path}"`,
operator,
pathSegments: [
field.field.name
],
table: parentTable,
value
});
ref[path] = {
$raw: query
};
} else if (adapter.tables[joinCollectionTableName][path]) {
selectFields[path] = sql`${adapter.tables[joinCollectionTableName][path]}`.as(path);
// Allow to filter by collectionSlug
} else if (path !== 'relationTo') {
// For timestamp fields like deletedAt, we need to cast to timestamp in Postgres
// SQLite doesn't require explicit type casting for UNION queries
if (path === 'deletedAt' && adapter.name === 'postgres') {
selectFields[path] = sql`null::timestamp with time zone`.as(path);
} else {
selectFields[path] = sql`null`.as(path);
}
}
}
let query = db.select(selectFields).from(adapter.tables[joinCollectionTableName]);
if (collectionQueryWhere.length) {
query = query.where(and(...collectionQueryWhere));
}
if (currentQuery === null) {
currentQuery = query;
} else {
currentQuery = currentQuery.unionAll(query);
}
}
const subQueryAlias = `${columnName}_subquery`;
let sqlWhere = eq(sql.raw(`"${currentTableName}"."id"`), sql.raw(`"${subQueryAlias}"."${onPath}"`));
if (where && Object.keys(where).length > 0) {
sqlWhere = and(sqlWhere, buildSQLWhere(where, subQueryAlias));
}
if (shouldCount) {
currentArgs.extras[`${columnName}_count`] = sql`${db.select({
count: count()
}).from(sql`${currentQuery.as(subQueryAlias)}`).where(sqlWhere)}`.as(`${columnName}_count`);
}
currentQuery = currentQuery.orderBy(sortOrder(sql`"sortPath"`));
if (page && limit !== 0) {
const offset = (page - 1) * limit;
if (offset > 0) {
currentQuery = currentQuery.offset(offset);
}
}
if (limit) {
currentQuery = currentQuery.limit(limit);
}
currentArgs.extras[columnName] = sql`${db.select({
id: jsonAggBuildObject(adapter, {
id: sql.raw(`"${subQueryAlias}"."id"`),
relationTo: sql.raw(`"${subQueryAlias}"."relationTo"`)
})
}).from(sql`${currentQuery.as(subQueryAlias)}`).where(sqlWhere)}`.as(columnName);
} else {
const useDrafts = (versions || draftsEnabled) && hasDraftsEnabled(adapter.payload.collections[field.collection].config);
const fields = useDrafts ? buildVersionCollectionFields(adapter.payload.config, adapter.payload.collections[field.collection].config, true) : adapter.payload.collections[field.collection].config.flattenedFields;
const joinCollectionTableName = adapter.tableNameMap.get(useDrafts ? `_${toSnakeCase(field.collection)}${adapter.versionsSuffix}` : toSnakeCase(field.collection));
const joins = [];
const currentIDColumn = versions ? adapter.tables[currentTableName].parent : adapter.tables[currentTableName].id;
let joinQueryWhere;
const currentIDRaw = sql.raw(`"${getNameFromDrizzleTable(currentIDColumn.table)}"."${currentIDColumn.name}"`);
if (Array.isArray(field.targetField.relationTo)) {
joinQueryWhere = {
[field.on]: {
equals: {
relationTo: collectionSlug,
value: rawConstraint(currentIDRaw)
}
}
};
} else {
joinQueryWhere = {
[field.on]: {
equals: rawConstraint(currentIDRaw)
}
};
}
if (where && Object.keys(where).length) {
joinQueryWhere = {
and: [
joinQueryWhere,
where
]
};
}
if (useDrafts) {
joinQueryWhere = combineQueries(appendVersionToQueryKey(joinQueryWhere), {
latest: {
equals: true
}
});
}
const columnName = `${path.replaceAll('.', '_')}${field.name}`;
const subQueryAlias = `${columnName}_alias`;
const { newAliasTable } = getTableAlias({
adapter,
tableName: joinCollectionTableName
});
const { orderBy, selectFields, where: subQueryWhere } = buildQuery({
adapter,
aliasTable: newAliasTable,
fields,
joins,
locale,
parentIsLocalized,
selectLocale: true,
sort: useDrafts ? getQueryDraftsSort({
collectionConfig: adapter.payload.collections[field.collection].config,
sort
}) : sort,
tableName: joinCollectionTableName,
where: joinQueryWhere
});
for(let key in selectFields){
const val = selectFields[key];
if (val.table && getNameFromDrizzleTable(val.table) === joinCollectionTableName) {
delete selectFields[key];
key = key.split('.').pop();
selectFields[key] = newAliasTable[key];
}
}
if (useDrafts) {
selectFields.parent = newAliasTable.parent;
}
let query = db.select(selectFields).from(newAliasTable).where(subQueryWhere).orderBy(()=>orderBy.map(({ column, order })=>order(column))).$dynamic();
joins.forEach(({ type, condition, table })=>{
query = query[type ?? 'leftJoin'](table, condition);
});
if (page && limit !== 0) {
const offset = (page - 1) * limit - 1;
if (offset > 0) {
query = query.offset(offset);
}
}
if (limit !== 0) {
query = query.limit(limit);
}
const subQuery = query.as(subQueryAlias);
if (shouldCount) {
let countSubquery = db.select(selectFields).from(newAliasTable).where(subQueryWhere).$dynamic();
joins.forEach(({ type, condition, table })=>{
countSubquery = countSubquery[type ?? 'leftJoin'](table, condition);
});
currentArgs.extras[`${columnName}_count`] = sql`${db.select({
count: count()
}).from(sql`${countSubquery.as(`${subQueryAlias}_count_subquery`)}`)}`.as(`${subQueryAlias}_count`);
}
currentArgs.extras[columnName] = sql`${db.select({
result: jsonAggBuildObject(adapter, {
id: sql.raw(`"${subQueryAlias}".${useDrafts ? 'parent_id' : 'id'}`),
...selectFields._locale && {
locale: sql.raw(`"${subQueryAlias}".${selectFields._locale.name}`)
}
})
}).from(sql`${subQuery}`)}`.as(subQueryAlias);
}
break;
}
case 'point':
{
if (adapter.name === 'sqlite') {
break;
}
const args = isFieldLocalized ? _locales : currentArgs;
if (!args.columns) {
args.columns = {};
}
if (!args.extras) {
args.extras = {};
}
const name = `${path}${field.name}`;
// Drizzle handles that poorly. See https://github.com/drizzle-team/drizzle-orm/issues/2526
// Additionally, this way we format the column value straight in the database using ST_AsGeoJSON
args.columns[name] = false;
let shouldSelect = false;
if (select || selectAllOnCurrentLevel) {
if (selectAllOnCurrentLevel || selectMode === 'include' && Boolean(select[field.name]) || selectMode === 'exclude' && typeof select[field.name] === 'undefined') {
shouldSelect = true;
}
} else {
shouldSelect = true;
}
const tableName = fieldShouldBeLocalized({
field,
parentIsLocalized
}) ? `${currentTableName}${adapter.localesSuffix}` : currentTableName;
if (shouldSelect) {
args.extras[name] = sql.raw(`ST_AsGeoJSON("${adapter.tables[tableName][name].name}")::jsonb`).as(name);
}
break;
}
case 'select':
{
if (select && !selectAllOnCurrentLevel) {
if (selectMode === 'include' && !select[field.name] || selectMode === 'exclude' && select[field.name] === false) {
break;
}
}
if (field.hasMany) {
const withSelect = {
columns: {
id: false,
order: false,
parent: false
},
orderBy: ({ order }, { asc })=>[
asc(order)
]
};
currentArgs.with[`${path}${field.name}`] = withSelect;
break;
}
if (select || selectAllOnCurrentLevel) {
const fieldPath = `${path}${field.name}`;
if ((isFieldLocalized || parentIsLocalized) && _locales) {
_locales.columns[fieldPath] = true;
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
currentArgs.columns[fieldPath] = true;
}
}
break;
}
default:
{
if (forceWithFields) {
if ((field.type === 'relationship' || field.type === 'upload') && (field.hasMany || Array.isArray(field.relationTo))) {
withTabledFields.rels = true;
}
if (field.type === 'number' && field.hasMany) {
withTabledFields.numbers = true;
}
if (field.type === 'text' && field.hasMany) {
withTabledFields.texts = true;
}
}
if (!select && !selectAllOnCurrentLevel) {
break;
}
if (selectAllOnCurrentLevel || selectMode === 'include' && Boolean(select[field.name]) || selectMode === 'exclude' && typeof select[field.name] === 'undefined') {
const fieldPath = `${path}${field.name}`;
if ((isFieldLocalized || parentIsLocalized) && _locales) {
_locales.columns[fieldPath] = true;
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
currentArgs.columns[fieldPath] = true;
}
if (!withTabledFields.rels && (field.type === 'relationship' || field.type === 'upload') && (field.hasMany || Array.isArray(field.relationTo))) {
withTabledFields.rels = true;
}
if (!withTabledFields.numbers && field.type === 'number' && field.hasMany) {
withTabledFields.numbers = true;
}
if (!withTabledFields.texts && field.type === 'text' && field.hasMany) {
withTabledFields.texts = true;
}
}
break;
}
}
});
return topLevelArgs;
};
//# sourceMappingURL=traverseFields.js.map