diff --git a/packages/nc-gui/composables/useData.ts b/packages/nc-gui/composables/useData.ts index 6139ddd2a9..a75e8502fd 100644 --- a/packages/nc-gui/composables/useData.ts +++ b/packages/nc-gui/composables/useData.ts @@ -1,6 +1,7 @@ import type { ColumnType, LinkToAnotherRecordType, PaginatedType, RelationTypes, TableType, ViewType } from 'nocodb-sdk' import { UITypes, isCreatedOrLastModifiedByCol, isCreatedOrLastModifiedTimeCol } from 'nocodb-sdk' import type { ComputedRef, Ref } from 'vue' +import { extractPkFromRow } from '../utils/dataUtils' import type { CellRange } from '#imports' export function useData(args: { @@ -550,10 +551,7 @@ export function useData(args: { try { const row = formattedData.value[rowIndex] if (!row.rowMeta.new) { - const id = meta?.value?.columns - ?.filter((c) => c.pk) - .map((c) => row.row[c.title!]) - .join('___') + const id = extractPkFromRow(row.row, meta?.value?.columns) const fullRecord = await $api.dbTableRow.read( NOCO, diff --git a/packages/nc-gui/composables/useKanbanViewStore.ts b/packages/nc-gui/composables/useKanbanViewStore.ts index cf8e78cd2d..3195996dca 100644 --- a/packages/nc-gui/composables/useKanbanViewStore.ts +++ b/packages/nc-gui/composables/useKanbanViewStore.ts @@ -659,10 +659,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState( } if (!row.rowMeta.new) { - const id = (meta?.value?.columns as ColumnType[]) - ?.filter((c) => c.pk) - .map((c) => row.row[c.title!]) - .join('___') + const id = extractPkFromRow(row.row, meta?.value?.columns) const deleted = await deleteRowById(id as string) if (!deleted) { diff --git a/packages/nc-gui/composables/useLTARStore.ts b/packages/nc-gui/composables/useLTARStore.ts index 5f13d2720a..0b2a3f82ca 100644 --- a/packages/nc-gui/composables/useLTARStore.ts +++ b/packages/nc-gui/composables/useLTARStore.ts @@ -7,6 +7,7 @@ import type { } from 'nocodb-sdk' import { RelationTypes, UITypes, dateFormats, parseStringDateTime, timeFormats } from 'nocodb-sdk' import type { ComputedRef, Ref } from 'vue' +import { extractPkFromRow } from '../utils/dataUtils' interface DataApiResponse { list: Record @@ -88,18 +89,12 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState( return metas.value?.[colOptions.value?.fk_related_model_id as string] }) - const rowId = computed(() => - meta.value.columns - .filter((c: Required) => c.pk) - .map((c: Required) => row?.value?.row?.[c.title]) - .join('___'), - ) + const rowId = computed(() => { + extractPkFromRow(row.value.row, meta.value) + }) const getRelatedTableRowId = (row: Record) => { - return relatedTableMeta.value?.columns - ?.filter((c) => c.pk) - .map((c) => row?.[c.title as string] ?? row?.[c.id as string]) - .join('___') + return extractPkFromRow(row, relatedTableMeta.value?.columns) } // actions diff --git a/packages/nc-gui/utils/dataUtils.ts b/packages/nc-gui/utils/dataUtils.ts index bbe2a7369e..0451161908 100644 --- a/packages/nc-gui/utils/dataUtils.ts +++ b/packages/nc-gui/utils/dataUtils.ts @@ -26,11 +26,13 @@ export const isValidValue = (val: unknown) => { export const extractPkFromRow = (row: Record, columns: ColumnType[]) => { if (!row || !columns) return null - const pkColumns = columns.filter((c) => c.pk) - - if (pkColumns.every((c) => row?.[c.title as string] === null || row?.[c.title as string] === undefined)) return null - - return pkColumns.map((c) => row?.[c.title as string]).join('___') + const pkCols = columns.filter((c: Required) => c.pk) + // if multiple pk columns, join them with ___ and escape _ in id values with \_ to avoid conflicts + if (pkCols.length > 1) { + pkCols.map((c: Required) => row?.[c.title]?.toString?.().replaceAll('_', '\\_') ?? null).join('___') + } else if (pkCols.length) { + return row?.[pkCols[0].title] ?? null + } } export const rowPkData = (row: Record, columns: ColumnType[]) => { diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 738491da4a..cef6a5a490 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -6517,7 +6517,11 @@ class BaseModelSqlv2 { for (const pk of this.model.primaryKeys) { pkValues[pk.title] = data[pk.title] ?? data[pk.column_name]; } - return asString ? Object.values(pkValues).join('___') : pkValues; + return asString + ? Object.values(pkValues) + .map((val) => val?.toString?.().replaceAll('_', '\\_')) + .join('___') + : pkValues; } else if (this.model.primaryKey) { return ( data[this.model.primaryKey.title] ?? @@ -10225,7 +10229,16 @@ export function getCompositePkValue(primaryKeys: Column[], row) { if (typeof row !== 'object') return row; - return primaryKeys.map((c) => row[c.title] ?? row[c.column_name]).join('___'); + if (primaryKeys.length > 1) { + primaryKeys.map((c) => + (row[c.title] ?? row[c.column_name])?.toString?.().replaceAll('_', '\\_'), + ); + } + + return ( + primaryKeys[0] && + (row[primaryKeys[0].title] ?? row[primaryKeys[0].column_name]) + ); } export function haveFormulaColumn(columns: Column[]) { diff --git a/packages/nocodb/src/helpers/catchError.ts b/packages/nocodb/src/helpers/catchError.ts index 1cf89641b6..0eee5ddca9 100644 --- a/packages/nocodb/src/helpers/catchError.ts +++ b/packages/nocodb/src/helpers/catchError.ts @@ -795,22 +795,43 @@ export class NcError { id: string | string[] | Record | Record[], args?: NcErrorArgs, ) { + let formatedId: string | string[] = ''; if (!id) { - id = 'unknown'; + formatedId = 'unknown'; } else if (typeof id === 'string') { - id = [id]; + formatedId = [id]; } else if (Array.isArray(id)) { if (id.every((i) => typeof i === 'string')) { - id = id as string[]; + formatedId = id as string[]; } else { - id = id.map((i) => Object.values(i).join('___')); + formatedId = id.map((val) => { + const idsArr = Object.values(val); + if (idsArr.length > 1) { + return idsArr + .map((idVal) => idVal?.toString?.().replaceAll('_', '\\_')) + .join('___'); + } else if (idsArr.length) { + return idsArr[0]; + } else { + return 'unknown'; + } + }); } } else { - id = Object.values(id).join('___'); + const idsArr = Object.values(id); + if (idsArr.length > 1) { + idsArr + .map((idVal) => idVal?.toString?.().replaceAll('_', '\\_')) + .join('___'); + } else if (idsArr.length) { + formatedId = idsArr[0]; + } else { + formatedId = 'unknown'; + } } throw new NcBaseErrorv2(NcErrorType.RECORD_NOT_FOUND, { - params: id, + params: formatedId, ...args, }); } diff --git a/packages/nocodb/src/services/data-table.service.ts b/packages/nocodb/src/services/data-table.service.ts index 7229885660..f3e09463d2 100644 --- a/packages/nocodb/src/services/data-table.service.ts +++ b/packages/nocodb/src/services/data-table.service.ts @@ -315,7 +315,11 @@ export class DataTableService { // if composite primary key then join the values with ___ else pk = model.primaryKeys - .map((pk) => row[pk.title] ?? row[pk.column_name]) + .map((pk) => + (row[pk.title] ?? row[pk.column_name]) + ?.toString?.() + ?.replaceAll('_', '\\_'), + ) .join('___'); // if duplicate then throw error if (keys.has(pk)) {