import { AddDataTableRowsDto, AddDataTableColumnDto, CreateDataTableDto, DeleteDataTableRowsDto, DownloadDataTableCsvQueryDto, ImportCsvToDataTableDto, ListDataTableContentQueryDto, ListDataTableQueryDto, MoveDataTableColumnDto, RenameDataTableColumnDto, UpdateDataTableDto, UpdateDataTableRowDto, UpsertDataTableRowDto, } from '@n8n/api-types'; import { AuthenticatedRequest } from '@n8n/db'; import { Body, Delete, Get, Middleware, Param, Patch, Post, ProjectScope, Query, RestController, } from '@n8n/decorators'; import { NextFunction, Response } from 'express'; import { DataTableRowReturn } from 'n8n-workflow'; import { ResponseError } from '@/errors/response-errors/abstract/response.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ConflictError } from '@/errors/response-errors/conflict.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { DataTableService } from './data-table.service'; import { DataTableColumnNameConflictError } from './errors/data-table-column-name-conflict.error'; import { DataTableNameConflictError } from './errors/data-table-name-conflict.error'; import { DataTableNotFoundError } from './errors/data-table-not-found.error'; import { DataTableSystemColumnNameConflictError } from './errors/data-table-system-column-name-conflict.error'; import { DataTableValidationError } from './errors/data-table-validation.error'; import { FileUploadError } from './errors/data-table-file-upload.error'; import { ProjectService } from '@/services/project.service.ee'; import { SourceControlPreferencesService } from '@/modules/source-control.ee/source-control-preferences.service.ee'; @RestController('/projects/:projectId/data-tables') export class DataTableController { constructor( private readonly dataTableService: DataTableService, private readonly projectService: ProjectService, private readonly sourceControlPreferencesService: SourceControlPreferencesService, ) {} private handleDataTableColumnOperationError(e: unknown): never { if ( e instanceof DataTableColumnNameConflictError || e instanceof DataTableSystemColumnNameConflictError || e instanceof DataTableNameConflictError ) { throw new ConflictError(e.message); } if (e instanceof DataTableValidationError) { throw new BadRequestError(e.message); } if (e instanceof ResponseError) { throw e; } if (e instanceof Error) { throw new InternalServerError(e.message, e); } throw e; } private checkInstanceWriteAccess(): void { const preferences = this.sourceControlPreferencesService.getPreferences(); if (preferences.branchReadOnly) { throw new ForbiddenError( 'Cannot modify data tables on a protected instance. This instance is in read-only mode.', ); } } @Middleware() async validateProjectExists( req: AuthenticatedRequest<{ projectId: string }>, res: Response, next: NextFunction, ) { try { const { projectId } = req.params; await this.projectService.getProject(projectId); next(); } catch (e) { res.status(404).send('Project not found'); return; } } @Post('/') @ProjectScope('dataTable:create') async createDataTable( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Body dto: CreateDataTableDto, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.createDataTable(req.params.projectId, dto); } catch (e: unknown) { if (!(e instanceof Error)) { throw e; } else if (e instanceof DataTableNameConflictError) { throw new ConflictError(e.message); } else if (e instanceof DataTableValidationError) { throw new BadRequestError(e.message); } else if (e instanceof FileUploadError) { throw new BadRequestError(e.message); } else { throw new InternalServerError(e.message, e); } } } @Get('/') @ProjectScope('dataTable:listProject') async listProjectDataTables( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Query payload: ListDataTableQueryDto, ) { const providedFilter = payload?.filter ?? {}; return await this.dataTableService.getManyAndCount({ ...payload, filter: { ...providedFilter, projectId: req.params.projectId }, }); } @Patch('/:dataTableId') @ProjectScope('dataTable:update') async updateDataTable( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Body dto: UpdateDataTableDto, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.updateDataTable(dataTableId, req.params.projectId, dto); } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof DataTableNameConflictError) { throw new ConflictError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } @Delete('/:dataTableId') @ProjectScope('dataTable:delete') async deleteDataTable( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.deleteDataTable(dataTableId, req.params.projectId); } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } @Get('/:dataTableId/columns') @ProjectScope('dataTable:read') async getColumns( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, ) { try { return await this.dataTableService.getColumns(dataTableId, req.params.projectId); } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } @Post('/:dataTableId/columns') @ProjectScope('dataTable:update') async addColumn( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Body dto: AddDataTableColumnDto, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.addColumn(dataTableId, req.params.projectId, dto); } catch (e: unknown) { this.handleDataTableColumnOperationError(e); } } @Delete('/:dataTableId/columns/:columnId') @ProjectScope('dataTable:update') async deleteColumn( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Param('columnId') columnId: string, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.deleteColumn(dataTableId, req.params.projectId, columnId); } catch (e: unknown) { this.handleDataTableColumnOperationError(e); } } @Patch('/:dataTableId/columns/:columnId/move') @ProjectScope('dataTable:update') async moveColumn( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Param('columnId') columnId: string, @Body dto: MoveDataTableColumnDto, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.moveColumn( dataTableId, req.params.projectId, columnId, dto, ); } catch (e: unknown) { this.handleDataTableColumnOperationError(e); } } @Patch('/:dataTableId/columns/:columnId/rename') @ProjectScope('dataTable:update') async renameColumn( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Param('columnId') columnId: string, @Body dto: RenameDataTableColumnDto, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.renameColumn( dataTableId, req.params.projectId, columnId, dto, ); } catch (e: unknown) { this.handleDataTableColumnOperationError(e); } } @Get('/:dataTableId/rows') @ProjectScope('dataTable:readRow') async getDataTableRows( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Query dto: ListDataTableContentQueryDto, ) { try { return await this.dataTableService.getManyRowsAndCount( dataTableId, req.params.projectId, dto, ); } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } @Get('/:dataTableId/download-csv') @ProjectScope('dataTable:read') async downloadDataTableCsv( req: AuthenticatedRequest<{ projectId: string; dataTableId: string }>, _res: Response, @Query query: DownloadDataTableCsvQueryDto, ) { try { const { projectId, dataTableId } = req.params; // Generate CSV content - this will validate that the table exists const { csvContent, dataTableName } = await this.dataTableService.generateDataTableCsv( dataTableId, projectId, query.includeSystemColumns, ); return { csvContent, dataTableName, }; } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } /** * @returns the IDs of the inserted rows */ async appendDataTableRows( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, dataTableId: string, dto: AddDataTableRowsDto & { returnType?: T }, ): Promise>>; @Post('/:dataTableId/insert') @ProjectScope('dataTable:writeRow') async appendDataTableRows( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Body dto: AddDataTableRowsDto, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.insertRows( dataTableId, req.params.projectId, dto.data, dto.returnType, ); } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof DataTableValidationError) { throw new BadRequestError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } @Post('/:dataTableId/import-csv') @ProjectScope('dataTable:writeRow') async importCsvToDataTable( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Body dto: ImportCsvToDataTableDto, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.importCsvToExistingTable( dataTableId, req.params.projectId, dto.fileId, ); } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof DataTableValidationError) { throw new BadRequestError(e.message); } else if (e instanceof FileUploadError) { throw new BadRequestError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } @Post('/:dataTableId/upsert') @ProjectScope('dataTable:writeRow') async upsertDataTableRow( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Body dto: UpsertDataTableRowDto, ) { this.checkInstanceWriteAccess(); try { // because of strict overloads, we need separate paths const dryRun = dto.dryRun; if (dryRun) { return await this.dataTableService.upsertRow( dataTableId, req.params.projectId, dto, true, // we want to always return data for dry runs dryRun, ); } const returnData = dto.returnData; if (returnData) { return await this.dataTableService.upsertRow( dataTableId, req.params.projectId, dto, returnData, dryRun, ); } return await this.dataTableService.upsertRow( dataTableId, req.params.projectId, dto, returnData, dryRun, ); } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof DataTableValidationError) { throw new BadRequestError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } @Patch('/:dataTableId/rows') @ProjectScope('dataTable:writeRow') async updateDataTableRows( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Body dto: UpdateDataTableRowDto, ) { this.checkInstanceWriteAccess(); try { // because of strict overloads, we need separate paths const dryRun = dto.dryRun; if (dryRun) { return await this.dataTableService.updateRows( dataTableId, req.params.projectId, dto, true, // we want to always return data for dry runs dryRun, ); } const returnData = dto.returnData; if (returnData) { return await this.dataTableService.updateRows( dataTableId, req.params.projectId, dto, returnData, dryRun, ); } return await this.dataTableService.updateRows( dataTableId, req.params.projectId, dto, returnData, dryRun, ); } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof DataTableValidationError) { throw new BadRequestError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } @Delete('/:dataTableId/rows') @ProjectScope('dataTable:writeRow') async deleteDataTableRows( req: AuthenticatedRequest<{ projectId: string }>, _res: Response, @Param('dataTableId') dataTableId: string, @Query dto: DeleteDataTableRowsDto, ) { this.checkInstanceWriteAccess(); try { return await this.dataTableService.deleteRows( dataTableId, req.params.projectId, dto, dto.returnData, ); } catch (e: unknown) { if (e instanceof DataTableNotFoundError) { throw new NotFoundError(e.message); } else if (e instanceof DataTableValidationError) { throw new BadRequestError(e.message); } else if (e instanceof Error) { throw new InternalServerError(e.message, e); } else { throw e; } } } }