mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
253 lines
7.9 KiB
TypeScript
253 lines
7.9 KiB
TypeScript
import { dateTimeSchema } from '@n8n/api-types';
|
|
import type {
|
|
AddDataStoreColumnDto,
|
|
CreateDataStoreDto,
|
|
ListDataStoreContentQueryDto,
|
|
MoveDataStoreColumnDto,
|
|
DataStoreListOptions,
|
|
DataStoreRows,
|
|
UpsertDataStoreRowsDto,
|
|
UpdateDataStoreDto,
|
|
} from '@n8n/api-types';
|
|
import { Logger } from '@n8n/backend-common';
|
|
import { Service } from '@n8n/di';
|
|
|
|
import { DataStoreColumnRepository } from './data-store-column.repository';
|
|
import { DataStoreRowsRepository } from './data-store-rows.repository';
|
|
import { DataStoreRepository } from './data-store.repository';
|
|
import { DataStoreColumnNotFoundError } from './errors/data-store-column-not-found.error';
|
|
import { DataStoreNameConflictError } from './errors/data-store-name-conflict.error';
|
|
import { DataStoreNotFoundError } from './errors/data-store-not-found.error';
|
|
import { DataStoreValidationError } from './errors/data-store-validation.error';
|
|
import { toTableName, normalizeRows } from './utils/sql-utils';
|
|
|
|
@Service()
|
|
export class DataStoreService {
|
|
constructor(
|
|
private readonly dataStoreRepository: DataStoreRepository,
|
|
private readonly dataStoreColumnRepository: DataStoreColumnRepository,
|
|
private readonly dataStoreRowsRepository: DataStoreRowsRepository,
|
|
private readonly logger: Logger,
|
|
) {
|
|
this.logger = this.logger.scoped('data-store');
|
|
}
|
|
|
|
async start() {}
|
|
async shutdown() {}
|
|
|
|
async createDataStore(projectId: string, dto: CreateDataStoreDto) {
|
|
await this.validateUniqueName(dto.name, projectId);
|
|
|
|
return await this.dataStoreRepository.createDataStore(projectId, dto.name, dto.columns);
|
|
}
|
|
|
|
// Currently only renames data stores
|
|
async updateDataStore(dataStoreId: string, projectId: string, dto: UpdateDataStoreDto) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
await this.validateUniqueName(dto.name, projectId);
|
|
|
|
await this.dataStoreRepository.update({ id: dataStoreId }, { name: dto.name });
|
|
|
|
return true;
|
|
}
|
|
|
|
async deleteDataStoreByProjectId(projectId: string) {
|
|
return await this.dataStoreRepository.deleteDataStoreByProjectId(projectId);
|
|
}
|
|
|
|
async deleteDataStoreAll() {
|
|
return await this.dataStoreRepository.deleteDataStoreAll();
|
|
}
|
|
|
|
async deleteDataStore(dataStoreId: string, projectId: string) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
|
|
await this.dataStoreRepository.deleteDataStore(dataStoreId);
|
|
|
|
return true;
|
|
}
|
|
|
|
async addColumn(dataStoreId: string, projectId: string, dto: AddDataStoreColumnDto) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
|
|
return await this.dataStoreColumnRepository.addColumn(dataStoreId, dto);
|
|
}
|
|
|
|
async moveColumn(
|
|
dataStoreId: string,
|
|
projectId: string,
|
|
columnId: string,
|
|
dto: MoveDataStoreColumnDto,
|
|
) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
const existingColumn = await this.validateColumnExists(dataStoreId, columnId);
|
|
|
|
await this.dataStoreColumnRepository.moveColumn(dataStoreId, existingColumn, dto.targetIndex);
|
|
|
|
return true;
|
|
}
|
|
|
|
async deleteColumn(dataStoreId: string, projectId: string, columnId: string) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
const existingColumn = await this.validateColumnExists(dataStoreId, columnId);
|
|
|
|
await this.dataStoreColumnRepository.deleteColumn(dataStoreId, existingColumn);
|
|
|
|
return true;
|
|
}
|
|
|
|
async getManyAndCount(options: DataStoreListOptions) {
|
|
return await this.dataStoreRepository.getManyAndCount(options);
|
|
}
|
|
|
|
async getManyRowsAndCount(
|
|
dataStoreId: string,
|
|
projectId: string,
|
|
dto: ListDataStoreContentQueryDto,
|
|
) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
|
|
// unclear if we should validate here, only use case would be to reduce the chance of
|
|
// a renamed/removed column appearing here (or added column missing) if the store was
|
|
// modified between when the frontend sent the request and we received it
|
|
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
|
const result = await this.dataStoreRowsRepository.getManyAndCount(
|
|
toTableName(dataStoreId),
|
|
dto,
|
|
);
|
|
return {
|
|
count: result.count,
|
|
data: normalizeRows(result.data, columns),
|
|
};
|
|
}
|
|
|
|
async getColumns(dataStoreId: string, projectId: string) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
|
|
return await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
|
}
|
|
|
|
async insertRows(dataStoreId: string, projectId: string, rows: DataStoreRows) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
await this.validateRows(dataStoreId, rows);
|
|
|
|
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
|
return await this.dataStoreRowsRepository.insertRows(toTableName(dataStoreId), rows, columns);
|
|
}
|
|
|
|
async upsertRows(dataStoreId: string, projectId: string, dto: UpsertDataStoreRowsDto) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
await this.validateRows(dataStoreId, dto.rows);
|
|
|
|
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
|
|
|
return await this.dataStoreRowsRepository.upsertRows(toTableName(dataStoreId), dto, columns);
|
|
}
|
|
|
|
async deleteRows(dataStoreId: string, projectId: string, ids: number[]) {
|
|
await this.validateDataStoreExists(dataStoreId, projectId);
|
|
|
|
return await this.dataStoreRowsRepository.deleteRows(toTableName(dataStoreId), ids);
|
|
}
|
|
|
|
private async validateRows(dataStoreId: string, rows: DataStoreRows): Promise<void> {
|
|
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
|
|
if (columns.length === 0) {
|
|
throw new DataStoreValidationError(
|
|
'No columns found for this data store or data store not found',
|
|
);
|
|
}
|
|
|
|
const columnNames = new Set(columns.map((x) => x.name));
|
|
const columnTypeMap = new Map(columns.map((x) => [x.name, x.type]));
|
|
for (const row of rows) {
|
|
const keys = Object.keys(row);
|
|
if (columns.length !== keys.length) {
|
|
throw new DataStoreValidationError('mismatched key count');
|
|
}
|
|
for (const key of keys) {
|
|
if (!columnNames.has(key)) {
|
|
throw new DataStoreValidationError('unknown column name');
|
|
}
|
|
const cell = row[key];
|
|
if (cell === null) continue;
|
|
switch (columnTypeMap.get(key)) {
|
|
case 'boolean':
|
|
if (typeof cell !== 'boolean') {
|
|
throw new DataStoreValidationError(
|
|
`value '${cell.toString()}' does not match column type 'boolean'`,
|
|
);
|
|
}
|
|
break;
|
|
case 'date':
|
|
if (typeof cell === 'string') {
|
|
const validated = dateTimeSchema.safeParse(cell);
|
|
if (validated.success) {
|
|
row[key] = validated.data.toISOString();
|
|
break;
|
|
}
|
|
} else if (cell instanceof Date) {
|
|
row[key] = cell.toISOString();
|
|
break;
|
|
}
|
|
|
|
throw new DataStoreValidationError(`value '${cell}' does not match column type 'date'`);
|
|
case 'string':
|
|
if (typeof cell !== 'string') {
|
|
throw new DataStoreValidationError(
|
|
`value '${cell.toString()}' does not match column type 'string'`,
|
|
);
|
|
}
|
|
break;
|
|
case 'number':
|
|
if (typeof cell !== 'number') {
|
|
throw new DataStoreValidationError(
|
|
`value '${cell.toString()}' does not match column type 'number'`,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async validateDataStoreExists(dataStoreId: string, projectId: string) {
|
|
const existingTable = await this.dataStoreRepository.findOneBy({
|
|
id: dataStoreId,
|
|
project: {
|
|
id: projectId,
|
|
},
|
|
});
|
|
|
|
if (!existingTable) {
|
|
throw new DataStoreNotFoundError(dataStoreId);
|
|
}
|
|
|
|
return existingTable;
|
|
}
|
|
|
|
private async validateColumnExists(dataStoreId: string, columnId: string) {
|
|
const existingColumn = await this.dataStoreColumnRepository.findOneBy({
|
|
id: columnId,
|
|
dataStoreId,
|
|
});
|
|
|
|
if (existingColumn === null) {
|
|
throw new DataStoreColumnNotFoundError(dataStoreId, columnId);
|
|
}
|
|
|
|
return existingColumn;
|
|
}
|
|
|
|
private async validateUniqueName(name: string, projectId: string) {
|
|
const hasNameClash = await this.dataStoreRepository.existsBy({
|
|
name,
|
|
projectId,
|
|
});
|
|
|
|
if (hasNameClash) {
|
|
throw new DataStoreNameConflictError(name);
|
|
}
|
|
}
|
|
}
|