Files
klz-cables.com/.pnpm-store/v10/files/d6/76dbcb8783b407ce2b6583c6e134a482536d776dc9c7a22d30488e3fd7e80a6dba9cd90e2a836499ad797269bb680bc85233d41bc3b7cef6a816e24c5fce43
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

666 lines
28 KiB
Plaintext

import { and, desc, eq, isNull, or } from 'drizzle-orm';
import { buildFindManyArgs } from '../find/buildFindManyArgs.js';
import { transform } from '../transform/read/index.js';
import { transformForWrite } from '../transform/write/index.js';
import { deleteExistingArrayRows } from './deleteExistingArrayRows.js';
import { deleteExistingRowsByPath } from './deleteExistingRowsByPath.js';
import { handleUpsertError } from './handleUpsertError.js';
import { insertArrays } from './insertArrays.js';
import { shouldUseOptimizedUpsertRow } from './shouldUseOptimizedUpsertRow.js';
/**
* If `id` is provided, it will update the row with that ID.
* If `where` is provided, it will update the row that matches the `where`
* If neither `id` nor `where` is provided, it will create a new row.
*
* adapter function replaces the entire row and does not support partial updates.
*/ export const upsertRow = async ({ id, adapter, collectionSlug, data, db, fields, globalSlug, ignoreResult, // TODO:
// When we support joins for write operations (create/update) - pass collectionSlug to the buildFindManyArgs
// Make a new argument in upsertRow.ts and pass the slug from every operation.
customID, joinQuery: _joinQuery, operation, path = '', req, select, tableName, upsertTarget, where })=>{
if (operation === 'create' && !data.createdAt) {
data.createdAt = new Date().toISOString();
}
let insertedRow = {
id
};
if (id && shouldUseOptimizedUpsertRow({
data,
fields
})) {
try {
const transformedForWrite = transformForWrite({
adapter,
data,
enableAtomicWrites: true,
fields,
tableName
});
const { row } = transformedForWrite;
const { arraysToPush } = transformedForWrite;
const drizzle = db;
// First, handle $push arrays
if (arraysToPush && Object.keys(arraysToPush)?.length) {
await insertArrays({
adapter,
arrays: [
arraysToPush
],
db,
parentRows: [
insertedRow
],
uuidMap: {}
});
}
// If row.updatedAt is not set, delete it to avoid triggering hasDataToUpdate. `updatedAt` may be explicitly set to null to
// disable triggering hasDataToUpdate.
if (typeof row.updatedAt === 'undefined' || row.updatedAt === null) {
delete row.updatedAt;
}
const hasDataToUpdate = row && Object.keys(row)?.length;
// Then, handle regular row update
if (ignoreResult) {
if (hasDataToUpdate) {
// Only update row if there is something to update.
// Example: if the data only consists of a single $push, calling insertArrays is enough - we don't need to update the row.
await drizzle.update(adapter.tables[tableName]).set(row).where(eq(adapter.tables[tableName].id, id));
}
return ignoreResult === 'idOnly' ? {
id
} : null;
}
const findManyArgs = buildFindManyArgs({
adapter,
depth: 0,
fields,
joinQuery: false,
select,
tableName
});
const findManyKeysLength = Object.keys(findManyArgs).length;
const hasOnlyColumns = Object.keys(findManyArgs.columns || {}).length > 0;
if (!hasDataToUpdate) {
// Nothing to update => just fetch current row and return
findManyArgs.where = eq(adapter.tables[tableName].id, insertedRow.id);
const doc = await db.query[tableName].findFirst(findManyArgs);
return transform({
adapter,
config: adapter.payload.config,
data: doc,
fields,
joinQuery: false,
tableName
});
}
if (findManyKeysLength === 0 || hasOnlyColumns) {
// Optimization - No need for joins => can simply use returning(). This is optimal for very simple collections
// without complex fields that live in separate tables like blocks, arrays, relationships, etc.
const selectedFields = {};
if (hasOnlyColumns) {
for (const [column, enabled] of Object.entries(findManyArgs.columns)){
if (enabled) {
selectedFields[column] = adapter.tables[tableName][column];
}
}
}
const docs = await drizzle.update(adapter.tables[tableName]).set(row).where(eq(adapter.tables[tableName].id, id)).returning(Object.keys(selectedFields).length ? selectedFields : undefined);
return transform({
adapter,
config: adapter.payload.config,
data: docs[0],
fields,
joinQuery: false,
tableName
});
}
// DB Update that needs the result, potentially with joins => need to update first, then find. returning() does not work with joins.
await drizzle.update(adapter.tables[tableName]).set(row).where(eq(adapter.tables[tableName].id, id));
findManyArgs.where = eq(adapter.tables[tableName].id, insertedRow.id);
const doc = await db.query[tableName].findFirst(findManyArgs);
return transform({
adapter,
config: adapter.payload.config,
data: doc,
fields,
joinQuery: false,
tableName
});
} catch (error) {
handleUpsertError({
id,
adapter,
collectionSlug,
error,
globalSlug,
req,
tableName
});
}
}
// Split out the incoming data into the corresponding:
// base row, locales, relationships, blocks, and arrays
const rowToInsert = transformForWrite({
adapter,
data,
enableAtomicWrites: false,
fields,
path,
tableName
});
if (customID) {
rowToInsert.row.id = customID;
}
// First, we insert the main row
try {
if (operation === 'update') {
const target = upsertTarget || adapter.tables[tableName].id;
// Check if we only have relationship operations and no main row data to update
// Exclude timestamp-only updates when we only have relationship operations
const rowKeys = Object.keys(rowToInsert.row);
const hasMainRowData = rowKeys.length > 0 && !rowKeys.every((key)=>key === 'updatedAt' || key === 'createdAt');
if (hasMainRowData) {
if (id) {
rowToInsert.row.id = id;
[insertedRow] = await adapter.insert({
db,
onConflictDoUpdate: {
set: rowToInsert.row,
target
},
tableName,
values: rowToInsert.row
});
} else {
;
[insertedRow] = await adapter.insert({
db,
onConflictDoUpdate: {
set: rowToInsert.row,
target,
where
},
tableName,
values: rowToInsert.row
});
}
} else {
// No main row data to update, just use the existing ID
insertedRow = {
id
};
}
} else {
if (adapter.allowIDOnCreate && data.id) {
rowToInsert.row.id = data.id;
}
;
[insertedRow] = await adapter.insert({
db,
tableName,
values: rowToInsert.row
});
}
const localesToInsert = [];
const relationsToInsert = [];
const textsToInsert = [];
const numbersToInsert = [];
const blocksToInsert = {};
const selectsToInsert = {};
// If there are locale rows with data, add the parent and locale to each
if (Object.keys(rowToInsert.locales).length > 0) {
Object.entries(rowToInsert.locales).forEach(([locale, localeRow])=>{
localeRow._parentID = insertedRow.id;
localeRow._locale = locale;
localesToInsert.push(localeRow);
});
}
// If there are relationships, add parent to each
if (rowToInsert.relationships.length > 0) {
rowToInsert.relationships.forEach((relation)=>{
relation.parent = insertedRow.id;
relationsToInsert.push(relation);
});
}
// If there are texts, add parent to each
if (rowToInsert.texts.length > 0) {
rowToInsert.texts.forEach((textRow)=>{
textRow.parent = insertedRow.id;
textsToInsert.push(textRow);
});
}
// If there are numbers, add parent to each
if (rowToInsert.numbers.length > 0) {
rowToInsert.numbers.forEach((numberRow)=>{
numberRow.parent = insertedRow.id;
numbersToInsert.push(numberRow);
});
}
// If there are selects, add parent to each, and then
// store by table name and rows
if (Object.keys(rowToInsert.selects).length > 0) {
Object.entries(rowToInsert.selects).forEach(([selectTableName, selectRows])=>{
selectsToInsert[selectTableName] = [];
selectRows.forEach((row)=>{
if (typeof row.parent === 'undefined') {
row.parent = insertedRow.id;
}
selectsToInsert[selectTableName].push(row);
});
});
}
// If there are blocks, add parent to each, and then
// store by table name and rows
Object.keys(rowToInsert.blocks).forEach((tableName)=>{
rowToInsert.blocks[tableName].forEach((blockRow)=>{
blockRow.row._parentID = insertedRow.id;
if (!blocksToInsert[tableName]) {
blocksToInsert[tableName] = [];
}
if (blockRow.row.uuid) {
delete blockRow.row.uuid;
}
blocksToInsert[tableName].push(blockRow);
});
});
// //////////////////////////////////
// INSERT LOCALES
// //////////////////////////////////
if (localesToInsert.length > 0) {
const localeTableName = `${tableName}${adapter.localesSuffix}`;
const localeTable = adapter.tables[`${tableName}${adapter.localesSuffix}`];
if (operation === 'update') {
await adapter.deleteWhere({
db,
tableName: localeTableName,
where: eq(localeTable._parentID, insertedRow.id)
});
}
await adapter.insert({
db,
tableName: localeTableName,
values: localesToInsert
});
}
// //////////////////////////////////
// INSERT RELATIONSHIPS
// //////////////////////////////////
const relationshipsTableName = `${tableName}${adapter.relationshipsSuffix}`;
if (operation === 'update') {
// Filter out specific item deletions (those with itemToRemove) from general path deletions
const generalRelationshipDeletes = rowToInsert.relationshipsToDelete.filter((rel)=>!('itemToRemove' in rel));
await deleteExistingRowsByPath({
adapter,
db,
localeColumnName: 'locale',
parentColumnName: 'parent',
parentID: insertedRow.id,
pathColumnName: 'path',
rows: [
...relationsToInsert,
...generalRelationshipDeletes
],
tableName: relationshipsTableName
});
}
if (relationsToInsert.length > 0) {
await adapter.insert({
db,
tableName: relationshipsTableName,
values: relationsToInsert
});
}
// //////////////////////////////////
// HANDLE RELATIONSHIP $push OPERATIONS
// //////////////////////////////////
if (rowToInsert.relationshipsToAppend.length > 0) {
// Prepare all relationships for batch insert (order will be set after max query)
const relationshipsToInsert = rowToInsert.relationshipsToAppend.map((rel)=>{
const parentId = id || insertedRow.id;
const row = {
parent: parentId,
path: rel.path
};
// Only add locale if this relationship table has a locale column
const relationshipTable = adapter.rawTables[relationshipsTableName];
if (rel.locale && relationshipTable && relationshipTable.columns.locale) {
row.locale = rel.locale;
}
if (rel.relationTo) {
// Use camelCase key for Drizzle table (e.g., categoriesID not categories_id)
row[`${rel.relationTo}ID`] = rel.value;
}
return row;
});
if (relationshipsToInsert.length > 0) {
// Check for potential duplicates
const relationshipTable = adapter.tables[relationshipsTableName];
if (relationshipTable) {
// Build conditions only if we have relationships to check
if (relationshipsToInsert.length === 0) {
return; // No relationships to insert
}
const conditions = relationshipsToInsert.map((row)=>{
const parts = [
eq(relationshipTable.parent, row.parent),
eq(relationshipTable.path, row.path)
];
// Add locale condition
if (row.locale !== undefined && relationshipTable.locale) {
parts.push(eq(relationshipTable.locale, row.locale));
} else if (relationshipTable.locale) {
parts.push(isNull(relationshipTable.locale));
}
// Add all relationship ID matches using schema fields
for (const [key, value] of Object.entries(row)){
if (key.endsWith('ID') && value != null) {
const column = relationshipTable[key];
if (column && typeof column === 'object') {
parts.push(eq(column, value));
}
}
}
return and(...parts);
});
// Get both existing relationships AND max order in a single query
let existingRels = [];
let maxOrder = 0;
if (conditions.length > 0) {
// Query for existing relationships
existingRels = await db.select().from(relationshipTable).where(or(...conditions));
}
// Get max order for this parent across all paths in a single query
const parentId = id || insertedRow.id;
const maxOrderResult = await db.select({
maxOrder: relationshipTable.order
}).from(relationshipTable).where(eq(relationshipTable.parent, parentId)).orderBy(desc(relationshipTable.order)).limit(1);
if (maxOrderResult.length > 0 && maxOrderResult[0].maxOrder) {
maxOrder = maxOrderResult[0].maxOrder;
}
// Set order values for all relationships based on max order
relationshipsToInsert.forEach((row, index)=>{
row.order = maxOrder + index + 1;
});
// Filter out relationships that already exist
const relationshipsToActuallyInsert = relationshipsToInsert.filter((newRow)=>{
return !existingRels.some((existingRow)=>{
// Check if this relationship already exists
let matches = existingRow.parent === newRow.parent && existingRow.path === newRow.path;
if (newRow.locale !== undefined) {
matches = matches && existingRow.locale === newRow.locale;
}
// Check relationship value matches - convert to camelCase for comparison
for (const key of Object.keys(newRow)){
if (key.endsWith('ID')) {
// Now using camelCase keys
matches = matches && existingRow[key] === newRow[key];
}
}
return matches;
});
});
// Insert only non-duplicate relationships
if (relationshipsToActuallyInsert.length > 0) {
await adapter.insert({
db,
tableName: relationshipsTableName,
values: relationshipsToActuallyInsert
});
}
}
}
}
// //////////////////////////////////
// HANDLE RELATIONSHIP $remove OPERATIONS
// //////////////////////////////////
if (rowToInsert.relationshipsToDelete.some((rel)=>'itemToRemove' in rel)) {
const relationshipTable = adapter.tables[relationshipsTableName];
if (relationshipTable) {
for (const relToDelete of rowToInsert.relationshipsToDelete){
if ('itemToRemove' in relToDelete && relToDelete.itemToRemove) {
const item = relToDelete.itemToRemove;
const parentId = id || insertedRow.id;
const conditions = [
eq(relationshipTable.parent, parentId),
eq(relationshipTable.path, relToDelete.path)
];
// Add locale condition if this relationship table has a locale column
if (adapter.rawTables[relationshipsTableName]?.columns.locale) {
if (relToDelete.locale) {
conditions.push(eq(relationshipTable.locale, relToDelete.locale));
} else {
conditions.push(isNull(relationshipTable.locale));
}
}
// Handle polymorphic vs simple relationships
if (typeof item === 'object' && 'relationTo' in item) {
// Polymorphic relationship - convert to camelCase key
const camelKey = `${item.relationTo}ID`;
if (relationshipTable[camelKey]) {
conditions.push(eq(relationshipTable[camelKey], item.value));
}
} else if (relToDelete.relationTo) {
// Simple relationship - convert to camelCase key
const camelKey = `${relToDelete.relationTo}ID`;
if (relationshipTable[camelKey]) {
conditions.push(eq(relationshipTable[camelKey], item));
}
}
// Execute DELETE using Drizzle query builder
await adapter.deleteWhere({
db,
tableName: relationshipsTableName,
where: and(...conditions)
});
}
}
}
}
// //////////////////////////////////
// INSERT hasMany TEXTS
// //////////////////////////////////
const textsTableName = `${tableName}_texts`;
if (operation === 'update') {
await deleteExistingRowsByPath({
adapter,
db,
localeColumnName: 'locale',
parentColumnName: 'parent',
parentID: insertedRow.id,
pathColumnName: 'path',
rows: [
...textsToInsert,
...rowToInsert.textsToDelete
],
tableName: textsTableName
});
}
if (textsToInsert.length > 0) {
await adapter.insert({
db,
tableName: textsTableName,
values: textsToInsert
});
}
// //////////////////////////////////
// INSERT hasMany NUMBERS
// //////////////////////////////////
const numbersTableName = `${tableName}_numbers`;
if (operation === 'update') {
await deleteExistingRowsByPath({
adapter,
db,
localeColumnName: 'locale',
parentColumnName: 'parent',
parentID: insertedRow.id,
pathColumnName: 'path',
rows: [
...numbersToInsert,
...rowToInsert.numbersToDelete
],
tableName: numbersTableName
});
}
if (numbersToInsert.length > 0) {
await adapter.insert({
db,
tableName: numbersTableName,
values: numbersToInsert
});
}
// //////////////////////////////////
// INSERT BLOCKS
// //////////////////////////////////
const insertedBlockRows = {};
if (operation === 'update') {
for (const tableName of rowToInsert.blocksToDelete){
const blockTable = adapter.tables[tableName];
await adapter.deleteWhere({
db,
tableName,
where: eq(blockTable._parentID, insertedRow.id)
});
}
}
// When versions are enabled, adapter is used to track mapping between blocks/arrays ObjectID to their numeric generated representation, then we use it for nested to arrays/blocks select hasMany in versions.
const arraysBlocksUUIDMap = {};
for (const [tableName, blockRows] of Object.entries(blocksToInsert)){
insertedBlockRows[tableName] = await adapter.insert({
db,
tableName,
values: blockRows.map(({ row })=>row)
});
insertedBlockRows[tableName].forEach((row, i)=>{
blockRows[i].row = row;
if (typeof row._uuid === 'string' && (typeof row.id === 'string' || typeof row.id === 'number')) {
arraysBlocksUUIDMap[row._uuid] = row.id;
}
});
const blockLocaleIndexMap = [];
const blockLocaleRowsToInsert = blockRows.reduce((acc, blockRow, i)=>{
if (Object.entries(blockRow.locales).length > 0) {
Object.entries(blockRow.locales).forEach(([blockLocale, blockLocaleData])=>{
if (Object.keys(blockLocaleData).length > 0) {
blockLocaleData._parentID = blockRow.row.id;
blockLocaleData._locale = blockLocale;
acc.push(blockLocaleData);
blockLocaleIndexMap.push(i);
}
});
}
return acc;
}, []);
if (blockLocaleRowsToInsert.length > 0) {
await adapter.insert({
db,
tableName: `${tableName}${adapter.localesSuffix}`,
values: blockLocaleRowsToInsert
});
}
await insertArrays({
adapter,
arrays: blockRows.map(({ arrays })=>arrays),
db,
parentRows: insertedBlockRows[tableName],
uuidMap: arraysBlocksUUIDMap
});
}
// //////////////////////////////////
// INSERT ARRAYS RECURSIVELY
// //////////////////////////////////
if (operation === 'update') {
for (const arrayTableName of Object.keys(rowToInsert.arrays)){
await deleteExistingArrayRows({
adapter,
db,
parentID: insertedRow.id,
tableName: arrayTableName
});
}
}
await insertArrays({
adapter,
arrays: [
rowToInsert.arrays,
rowToInsert.arraysToPush
],
db,
parentRows: [
insertedRow,
insertedRow
],
uuidMap: arraysBlocksUUIDMap
});
// //////////////////////////////////
// INSERT hasMany SELECTS
// //////////////////////////////////
for (const [selectTableName, tableRows] of Object.entries(selectsToInsert)){
const selectTable = adapter.tables[selectTableName];
if (operation === 'update') {
await adapter.deleteWhere({
db,
tableName: selectTableName,
where: eq(selectTable.parent, insertedRow.id)
});
}
if (Object.keys(arraysBlocksUUIDMap).length > 0) {
tableRows.forEach((row)=>{
if (row.parent in arraysBlocksUUIDMap) {
row.parent = arraysBlocksUUIDMap[row.parent];
}
});
}
if (tableRows.length) {
await adapter.insert({
db,
tableName: selectTableName,
values: tableRows
});
}
}
} catch (error) {
handleUpsertError({
id,
adapter,
collectionSlug,
error,
globalSlug,
req,
tableName
});
}
if (ignoreResult === 'idOnly') {
return {
id: insertedRow.id
};
}
if (ignoreResult) {
return data;
}
// //////////////////////////////////
// RETRIEVE NEWLY UPDATED ROW
// //////////////////////////////////
const findManyArgs = buildFindManyArgs({
adapter,
depth: 0,
fields,
joinQuery: false,
select,
tableName
});
findManyArgs.where = eq(adapter.tables[tableName].id, insertedRow.id);
const doc = await db.query[tableName].findFirst(findManyArgs);
// //////////////////////////////////
// TRANSFORM DATA
// //////////////////////////////////
const result = transform({
adapter,
config: adapter.payload.config,
data: doc,
fields,
joinQuery: false,
tableName
});
return result;
};
//# sourceMappingURL=index.js.map