diff --git a/packages/cli/src/modules/data-table/__tests__/data-table.controller.integration.test.ts b/packages/cli/src/modules/data-table/__tests__/data-table.controller.integration.test.ts index c14ed0897ec..339bcb07481 100644 --- a/packages/cli/src/modules/data-table/__tests__/data-table.controller.integration.test.ts +++ b/packages/cli/src/modules/data-table/__tests__/data-table.controller.integration.test.ts @@ -100,6 +100,24 @@ describe('POST /projects/:projectId/data-tables', () => { await authOwnerAgent.post(`/projects/${project.id}/data-tables`).send(payload).expect(400); }); + test('should return 409 when column name conflicts with system columns', async () => { + const project = await createTeamProject(undefined, owner); + + for (const systemColumnName of ['id', 'ID', 'createdAt', 'updatedAt']) { + const payload = { + name: `Table with ${systemColumnName}`, + columns: [{ name: systemColumnName, type: 'string' }], + }; + + const response = await authOwnerAgent + .post(`/projects/${project.id}/data-tables`) + .send(payload) + .expect(409); + + expect(response.body.message).toContain('reserved'); + } + }); + test('should not create data table if user has project:viewer role in team project', async () => { const project = await createTeamProject(undefined, owner); await linkUserToProject(member, project, 'project:viewer'); diff --git a/packages/cli/src/modules/data-table/__tests__/data-table.service.integration.test.ts b/packages/cli/src/modules/data-table/__tests__/data-table.service.integration.test.ts index 770516e1ba1..c9677ade2bc 100644 --- a/packages/cli/src/modules/data-table/__tests__/data-table.service.integration.test.ts +++ b/packages/cli/src/modules/data-table/__tests__/data-table.service.integration.test.ts @@ -10,6 +10,7 @@ import { DataTableRepository } from '../data-table.repository'; import { DataTableService } from '../data-table.service'; import { mockDataTableSizeValidator } from './test-helpers'; import { DataTableColumnNameConflictError } from '../errors/data-table-column-name-conflict.error'; +import { DataTableSystemColumnNameConflictError } from '../errors/data-table-system-column-name-conflict.error'; import { DataTableColumnNotFoundError } from '../errors/data-table-column-not-found.error'; import { DataTableNameConflictError } from '../errors/data-table-name-conflict.error'; import { DataTableNotFoundError } from '../errors/data-table-not-found.error'; @@ -140,6 +141,25 @@ describe('dataTable', () => { expect(project.name).toBe(project1.name); }); + it('should reject columns named after system columns (id, createdAt, updatedAt)', async () => { + for (const systemColumnName of [ + 'id', + 'ID', + 'Id', + 'createdAt', + 'CreatedAt', + 'updatedAt', + 'UpdatedAt', + ]) { + await expect( + dataTableService.createDataTable(project1.id, { + name: `table_with_${systemColumnName}`, + columns: [{ name: systemColumnName, type: 'string' }], + }), + ).rejects.toThrow(DataTableSystemColumnNameConflictError); + } + }); + it('should return an error if name/project combination already exists', async () => { const name = 'dataTable'; diff --git a/packages/cli/src/modules/data-table/data-table-column.repository.ts b/packages/cli/src/modules/data-table/data-table-column.repository.ts index 7f229e467d6..3ae66283f13 100644 --- a/packages/cli/src/modules/data-table/data-table-column.repository.ts +++ b/packages/cli/src/modules/data-table/data-table-column.repository.ts @@ -29,10 +29,11 @@ export class DataTableColumnRepository extends Repository { * Validates that a column name is not reserved as a system column */ private validateNotSystemColumn(columnName: string): void { - if (DATA_TABLE_SYSTEM_COLUMNS.includes(columnName)) { + const lowerName = columnName.toLowerCase(); + if (DATA_TABLE_SYSTEM_COLUMNS.some((sc) => sc.toLowerCase() === lowerName)) { throw new DataTableSystemColumnNameConflictError(columnName); } - if (columnName === DATA_TABLE_SYSTEM_TESTING_COLUMN) { + if (lowerName === DATA_TABLE_SYSTEM_TESTING_COLUMN.toLowerCase()) { throw new DataTableSystemColumnNameConflictError(columnName, 'testing'); } } diff --git a/packages/cli/src/modules/data-table/data-table.controller.ts b/packages/cli/src/modules/data-table/data-table.controller.ts index f94361a267f..bc77ab8a8cf 100644 --- a/packages/cli/src/modules/data-table/data-table.controller.ts +++ b/packages/cli/src/modules/data-table/data-table.controller.ts @@ -112,7 +112,10 @@ export class DataTableController { } catch (e: unknown) { if (!(e instanceof Error)) { throw e; - } else if (e instanceof DataTableNameConflictError) { + } else if ( + e instanceof DataTableNameConflictError || + e instanceof DataTableSystemColumnNameConflictError + ) { throw new ConflictError(e.message); } else if (e instanceof DataTableValidationError) { throw new BadRequestError(e.message); diff --git a/packages/cli/src/modules/data-table/data-table.repository.ts b/packages/cli/src/modules/data-table/data-table.repository.ts index 0e6bbb9379c..a9e803168ec 100644 --- a/packages/cli/src/modules/data-table/data-table.repository.ts +++ b/packages/cli/src/modules/data-table/data-table.repository.ts @@ -8,7 +8,11 @@ import { GlobalConfig } from '@n8n/config'; import { Project, withTransaction } from '@n8n/db'; import { Service } from '@n8n/di'; import { DataSource, EntityManager, Repository, SelectQueryBuilder } from '@n8n/typeorm'; -import { UnexpectedError } from 'n8n-workflow'; +import { + DATA_TABLE_SYSTEM_COLUMNS, + DATA_TABLE_SYSTEM_TESTING_COLUMN, + UnexpectedError, +} from 'n8n-workflow'; import type { DataTableInfo, DataTablesSizeData } from 'n8n-workflow'; import { DataTableColumn } from './data-table-column.entity'; @@ -16,6 +20,7 @@ import { DataTableDDLService } from './data-table-ddl.service'; import { DataTable } from './data-table.entity'; import { DataTableUserTableName } from './data-table.types'; import { DataTableNameConflictError } from './errors/data-table-name-conflict.error'; +import { DataTableSystemColumnNameConflictError } from './errors/data-table-system-column-name-conflict.error'; import { DataTableValidationError } from './errors/data-table-validation.error'; import { isValidColumnName, toTableId, toTableName } from './utils/sql-utils'; @@ -56,6 +61,16 @@ export class DataTableRepository extends Repository { throw new DataTableValidationError(DATA_TABLE_COLUMN_ERROR_MESSAGE); } + for (const col of columns) { + const lowerName = col.name.toLowerCase(); + if (DATA_TABLE_SYSTEM_COLUMNS.some((sc) => sc.toLowerCase() === lowerName)) { + throw new DataTableSystemColumnNameConflictError(col.name); + } + if (lowerName === DATA_TABLE_SYSTEM_TESTING_COLUMN.toLowerCase()) { + throw new DataTableSystemColumnNameConflictError(col.name, 'testing'); + } + } + const dataTable = em.create(DataTable, { name, columns, projectId }); await em.insert(DataTable, dataTable); diff --git a/packages/frontend/editor-ui/src/features/core/dataTable/components/AddDataTableModal.vue b/packages/frontend/editor-ui/src/features/core/dataTable/components/AddDataTableModal.vue index 52908e99a6e..5302b433912 100644 --- a/packages/frontend/editor-ui/src/features/core/dataTable/components/AddDataTableModal.vue +++ b/packages/frontend/editor-ui/src/features/core/dataTable/components/AddDataTableModal.vue @@ -86,7 +86,7 @@ const getColumnTypeOptions = (compatibleTypes: ColumnType[]) => { }; const validateColumnName = (columnName: string): string | undefined => { - if (DATA_TABLE_SYSTEM_COLUMNS.includes(columnName)) { + if (DATA_TABLE_SYSTEM_COLUMNS.some((sc) => sc.toLowerCase() === columnName.toLowerCase())) { return i18n.baseText('dataTable.import.systemColumnName', { interpolate: { columnName }, });