fix(editor): Use DB_TABLE_PREFIX on data store tables and refactor data store user table repo (no-changelog) (#18604)

This commit is contained in:
Jaakko Husso
2025-08-21 16:25:17 +02:00
committed by GitHub
parent f7a663172b
commit d01616cf3d
11 changed files with 90 additions and 113 deletions

View File

@@ -53,6 +53,5 @@ export {
type DataStoreCreateColumnSchema, type DataStoreCreateColumnSchema,
type DataStoreListFilter, type DataStoreListFilter,
type DataStoreListOptions, type DataStoreListOptions,
type DataStoreUserTableName,
dateTimeSchema, dateTimeSchema,
} from './schemas/data-store.schema'; } from './schemas/data-store.schema';

View File

@@ -39,8 +39,6 @@ export const dataStoreSchema = z.object({
export type DataStore = z.infer<typeof dataStoreSchema>; export type DataStore = z.infer<typeof dataStoreSchema>;
export type DataStoreColumn = z.infer<typeof dataStoreColumnSchema>; export type DataStoreColumn = z.infer<typeof dataStoreColumnSchema>;
export type DataStoreUserTableName = `data_store_user_${string}`;
export type DataStoreListFilter = { export type DataStoreListFilter = {
id?: string | string[]; id?: string | string[];
projectId?: string | string[]; projectId?: string | string[];

View File

@@ -18,7 +18,6 @@ import * as utils from '@test-integration/utils';
import { DataStoreColumnRepository } from '../data-store-column.repository'; import { DataStoreColumnRepository } from '../data-store-column.repository';
import { DataStoreRowsRepository } from '../data-store-rows.repository'; import { DataStoreRowsRepository } from '../data-store-rows.repository';
import { DataStoreRepository } from '../data-store.repository'; import { DataStoreRepository } from '../data-store.repository';
import { toTableName } from '../utils/sql-utils';
let owner: User; let owner: User;
let member: User; let member: User;
@@ -781,9 +780,9 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId', () => {
}); });
expect(dataStoreColumnInDb).toBeNull(); expect(dataStoreColumnInDb).toBeNull();
await expect( await expect(dataStoreRowsRepository.getManyAndCount(dataStore.id, {})).rejects.toThrow(
dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}), QueryFailedError,
).rejects.toThrow(QueryFailedError); );
}); });
}); });
@@ -1839,7 +1838,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
data: [1], data: [1],
}); });
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
expect(rowsInDb.data[0]).toMatchObject(payload.data[0]); expect(rowsInDb.data[0]).toMatchObject(payload.data[0]);
}); });
@@ -1879,7 +1878,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
data: [1], data: [1],
}); });
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
expect(rowsInDb.data[0]).toMatchObject(payload.data[0]); expect(rowsInDb.data[0]).toMatchObject(payload.data[0]);
}); });
@@ -1916,7 +1915,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
data: [1], data: [1],
}); });
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
expect(rowsInDb.data[0]).toMatchObject(payload.data[0]); expect(rowsInDb.data[0]).toMatchObject(payload.data[0]);
}); });
@@ -1950,7 +1949,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/insert', () => {
.expect(400); .expect(400);
expect(response.body.message).toContain('unknown column'); expect(response.body.message).toContain('unknown column');
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(0); expect(rowsInDb.count).toBe(0);
}); });
@@ -2287,7 +2286,7 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
.query({ ids: '1' }) .query({ ids: '1' })
.expect(403); .expect(403);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
}); });
@@ -2318,7 +2317,7 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
.query({ ids: '1' }) .query({ ids: '1' })
.expect(403); .expect(403);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
}); });
@@ -2358,7 +2357,7 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
.query({ ids: '1,3' }) .query({ ids: '1,3' })
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
expect(rowsInDb.data[0]).toMatchObject({ expect(rowsInDb.data[0]).toMatchObject({
first: 'test value 2', first: 'test value 2',
@@ -2398,7 +2397,7 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
.query({ ids: '2' }) .query({ ids: '2' })
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
expect(rowsInDb.data[0]).toMatchObject({ expect(rowsInDb.data[0]).toMatchObject({
first: 'test value 1', first: 'test value 1',
@@ -2437,7 +2436,7 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
.query({ ids: '1,2' }) .query({ ids: '1,2' })
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(0); expect(rowsInDb.count).toBe(0);
}); });
@@ -2474,7 +2473,7 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
.query({ ids: '2' }) .query({ ids: '2' })
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(2); expect(rowsInDb.count).toBe(2);
expect(rowsInDb.data.map((r) => r.first).sort()).toEqual(['test value 1', 'test value 3']); expect(rowsInDb.data.map((r) => r.first).sort()).toEqual(['test value 1', 'test value 3']);
}); });
@@ -2501,7 +2500,7 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
expect(response.body.data).toBe(true); expect(response.body.data).toBe(true);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
}); });
@@ -2525,7 +2524,7 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
.query({ ids: '999,1000' }) .query({ ids: '999,1000' })
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
}); });
@@ -2552,7 +2551,7 @@ describe('DELETE /projects/:projectId/data-stores/:dataStoreId/rows', () => {
.query({ ids: '1,999,2,1000' }) .query({ ids: '1,999,2,1000' })
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(0); expect(rowsInDb.count).toBe(0);
}); });
}); });
@@ -2687,7 +2686,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/upsert', () => {
.send(payload) .send(payload)
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
expect(rowsInDb.data[0]).toMatchObject(payload.rows[0]); expect(rowsInDb.data[0]).toMatchObject(payload.rows[0]);
}); });
@@ -2724,7 +2723,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/upsert', () => {
.send(payload) .send(payload)
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
expect(rowsInDb.data[0]).toMatchObject(payload.rows[0]); expect(rowsInDb.data[0]).toMatchObject(payload.rows[0]);
}); });
@@ -2758,7 +2757,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/upsert', () => {
.send(payload) .send(payload)
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(1); expect(rowsInDb.count).toBe(1);
expect(rowsInDb.data[0]).toMatchObject(payload.rows[0]); expect(rowsInDb.data[0]).toMatchObject(payload.rows[0]);
}); });
@@ -2793,7 +2792,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/upsert', () => {
.expect(400); .expect(400);
expect(response.body.message).toContain('unknown column'); expect(response.body.message).toContain('unknown column');
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), {}); const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {});
expect(rowsInDb.count).toBe(0); expect(rowsInDb.count).toBe(0);
}); });
@@ -2840,7 +2839,7 @@ describe('POST /projects/:projectId/data-stores/:dataStoreId/upsert', () => {
.send(payload) .send(payload)
.expect(200); .expect(200);
const rowsInDb = await dataStoreRowsRepository.getManyAndCount(toTableName(dataStore.id), { const rowsInDb = await dataStoreRowsRepository.getManyAndCount(dataStore.id, {
sortBy: ['id', 'ASC'], sortBy: ['id', 'ASC'],
}); });
expect(rowsInDb.count).toBe(3); expect(rowsInDb.count).toBe(3);

View File

@@ -11,7 +11,6 @@ import { DataStoreColumnNotFoundError } from '../errors/data-store-column-not-fo
import { DataStoreNameConflictError } from '../errors/data-store-name-conflict.error'; import { DataStoreNameConflictError } from '../errors/data-store-name-conflict.error';
import { DataStoreNotFoundError } from '../errors/data-store-not-found.error'; import { DataStoreNotFoundError } from '../errors/data-store-not-found.error';
import { DataStoreValidationError } from '../errors/data-store-validation.error'; import { DataStoreValidationError } from '../errors/data-store-validation.error';
import { toTableName } from '../utils/sql-utils';
beforeAll(async () => { beforeAll(async () => {
await testModules.loadModules(['data-store']); await testModules.loadModules(['data-store']);
@@ -73,7 +72,7 @@ describe('dataStore', () => {
]); ]);
// Select the column from user table to check for its existence // Select the column from user table to check for its existence
const userTableName = toTableName(dataStoreId); const userTableName = dataStoreRowsRepository.toTableName(dataStoreId);
const rows = await dataStoreRepository.manager const rows = await dataStoreRepository.manager
.createQueryBuilder() .createQueryBuilder()
.select('foo') .select('foo')
@@ -96,7 +95,7 @@ describe('dataStore', () => {
await expect(dataStoreService.getColumns(dataStoreId, project1.id)).resolves.toEqual([]); await expect(dataStoreService.getColumns(dataStoreId, project1.id)).resolves.toEqual([]);
const userTableName = toTableName(dataStoreId); const userTableName = dataStoreRowsRepository.toTableName(dataStoreId);
const queryRunner = dataStoreRepository.manager.connection.createQueryRunner(); const queryRunner = dataStoreRepository.manager.connection.createQueryRunner();
try { try {
const table = await queryRunner.getTable(userTableName); const table = await queryRunner.getTable(userTableName);
@@ -225,7 +224,7 @@ describe('dataStore', () => {
// ACT // ACT
const result = await dataStoreService.deleteDataStore(dataStoreId, project1.id); const result = await dataStoreService.deleteDataStore(dataStoreId, project1.id);
const userTableName = toTableName(dataStoreId); const userTableName = dataStoreRowsRepository.toTableName(dataStoreId);
// ASSERT // ASSERT
expect(result).toEqual(true); expect(result).toEqual(true);
@@ -335,7 +334,7 @@ describe('dataStore', () => {
}, },
]); ]);
const userTableName = toTableName(dataStoreId); const userTableName = dataStoreRowsRepository.toTableName(dataStoreId);
const queryRunner = dataStoreRepository.manager.connection.createQueryRunner(); const queryRunner = dataStoreRepository.manager.connection.createQueryRunner();
try { try {
const table = await queryRunner.getTable(userTableName); const table = await queryRunner.getTable(userTableName);
@@ -402,7 +401,7 @@ describe('dataStore', () => {
}, },
]); ]);
const userTableName = toTableName(dataStoreId); const userTableName = dataStoreRowsRepository.toTableName(dataStoreId);
const queryRunner = dataStoreRepository.manager.connection.createQueryRunner(); const queryRunner = dataStoreRepository.manager.connection.createQueryRunner();
try { try {
const table = await queryRunner.getTable(userTableName); const table = await queryRunner.getTable(userTableName);
@@ -1020,10 +1019,7 @@ describe('dataStore', () => {
// ASSERT // ASSERT
expect(result).toEqual([2]); expect(result).toEqual([2]);
const { count, data } = await dataStoreRowsRepository.getManyAndCount( const { count, data } = await dataStoreRowsRepository.getManyAndCount(dataStoreId, {});
toTableName(dataStoreId),
{},
);
expect(count).toEqual(2); expect(count).toEqual(2);
expect(data).toEqual([ expect(data).toEqual([
@@ -1060,10 +1056,7 @@ describe('dataStore', () => {
// ASSERT // ASSERT
expect(result).toEqual([3, 4]); expect(result).toEqual([3, 4]);
const { count, data } = await dataStoreRowsRepository.getManyAndCount( const { count, data } = await dataStoreRowsRepository.getManyAndCount(dataStoreId, {});
toTableName(dataStoreId),
{},
);
expect(count).toEqual(3); expect(count).toEqual(3);
expect(data).toEqual([ expect(data).toEqual([
@@ -1246,10 +1239,7 @@ describe('dataStore', () => {
// ASSERT // ASSERT
expect(result).toBe(true); expect(result).toBe(true);
const { count, data } = await dataStoreRowsRepository.getManyAndCount( const { count, data } = await dataStoreRowsRepository.getManyAndCount(dataStoreId, {});
toTableName(dataStoreId),
{},
);
expect(count).toEqual(1); expect(count).toEqual(1);
expect(data).toEqual([{ fullName: 'Alicia', age: 31, id: 1, pid: '1995-111a' }]); expect(data).toEqual([{ fullName: 'Alicia', age: 31, id: 1, pid: '1995-111a' }]);
@@ -1281,10 +1271,7 @@ describe('dataStore', () => {
// ASSERT // ASSERT
expect(result).toBe(true); expect(result).toBe(true);
const { count, data } = await dataStoreRowsRepository.getManyAndCount( const { count, data } = await dataStoreRowsRepository.getManyAndCount(dataStoreId, {});
toTableName(dataStoreId),
{},
);
expect(count).toEqual(2); expect(count).toEqual(2);
expect(data).toEqual([ expect(data).toEqual([

View File

@@ -76,10 +76,16 @@ export class DataStoreColumnRepository extends Repository<DataStoreColumn> {
async deleteColumn(dataStoreId: string, column: DataStoreColumn) { async deleteColumn(dataStoreId: string, column: DataStoreColumn) {
await this.manager.transaction(async (em) => { await this.manager.transaction(async (em) => {
await em.remove(DataStoreColumn, column); await em.remove(DataStoreColumn, column);
const queryRunner = em.queryRunner;
if (!queryRunner) {
throw new UnexpectedError('QueryRunner is not available');
}
await this.dataStoreRowsRepository.dropColumnFromTable( await this.dataStoreRowsRepository.dropColumnFromTable(
dataStoreId, dataStoreId,
column.name, column.name,
em, queryRunner,
em.connection.options.type, em.connection.options.type,
); );
await this.shiftColumns(dataStoreId, column.index, -1, em); await this.shiftColumns(dataStoreId, column.index, -1, em);

View File

@@ -1,21 +1,16 @@
import type { import type {
ListDataStoreContentQueryDto, ListDataStoreContentQueryDto,
ListDataStoreContentFilter, ListDataStoreContentFilter,
DataStoreUserTableName,
UpsertDataStoreRowsDto, UpsertDataStoreRowsDto,
} from '@n8n/api-types'; } from '@n8n/api-types';
import { GlobalConfig } from '@n8n/config';
import { CreateTable, DslColumn } from '@n8n/db'; import { CreateTable, DslColumn } from '@n8n/db';
import { Service } from '@n8n/di'; import { Service } from '@n8n/di';
import { import { DataSource, DataSourceOptions, QueryRunner, SelectQueryBuilder } from '@n8n/typeorm';
DataSource,
DataSourceOptions,
EntityManager,
QueryRunner,
SelectQueryBuilder,
} from '@n8n/typeorm';
import { DataStoreColumnJsType, DataStoreRows } from 'n8n-workflow'; import { DataStoreColumnJsType, DataStoreRows } from 'n8n-workflow';
import { DataStoreColumn } from './data-store-column.entity'; import { DataStoreColumn } from './data-store-column.entity';
import { DataStoreUserTableName } from './data-store.types';
import { import {
addColumnQuery, addColumnQuery,
buildColumnTypeMap, buildColumnTypeMap,
@@ -26,7 +21,6 @@ import {
quoteIdentifier, quoteIdentifier,
splitRowsByExistence, splitRowsByExistence,
toDslColumns, toDslColumns,
toTableName,
} from './utils/sql-utils'; } from './utils/sql-utils';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -50,14 +44,17 @@ function getConditionAndParams(
@Service() @Service()
export class DataStoreRowsRepository { export class DataStoreRowsRepository {
constructor(private dataSource: DataSource) {} constructor(
private dataSource: DataSource,
private readonly globalConfig: GlobalConfig,
) {}
// TypeORM cannot infer the columns for a dynamic table name, so we use a raw query toTableName(dataStoreId: string): DataStoreUserTableName {
async insertRows( const { tablePrefix } = this.globalConfig.database;
tableName: DataStoreUserTableName, return `${tablePrefix}data_store_user_${dataStoreId}`;
rows: DataStoreRows, }
columns: DataStoreColumn[],
) { async insertRows(dataStoreId: string, rows: DataStoreRows, columns: DataStoreColumn[]) {
const insertedIds: number[] = []; const insertedIds: number[] = [];
// We insert one by one as the default behavior of returning the last inserted ID // We insert one by one as the default behavior of returning the last inserted ID
@@ -75,7 +72,7 @@ export class DataStoreRowsRepository {
.createQueryBuilder() .createQueryBuilder()
.insert() .insert()
.into( .into(
tableName, this.toTableName(dataStoreId),
columns.map((c) => c.name), columns.map((c) => c.name),
) )
.values(row); .values(row);
@@ -92,21 +89,18 @@ export class DataStoreRowsRepository {
return insertedIds; return insertedIds;
} }
async upsertRows( // TypeORM cannot infer the columns for a dynamic table name, so we use a raw query
tableName: DataStoreUserTableName, async upsertRows(dataStoreId: string, dto: UpsertDataStoreRowsDto, columns: DataStoreColumn[]) {
dto: UpsertDataStoreRowsDto,
columns: DataStoreColumn[],
) {
const { rows, matchFields } = dto; const { rows, matchFields } = dto;
const { rowsToInsert, rowsToUpdate } = await this.fetchAndSplitRowsByExistence( const { rowsToInsert, rowsToUpdate } = await this.fetchAndSplitRowsByExistence(
tableName, dataStoreId,
matchFields, matchFields,
rows, rows,
); );
if (rowsToInsert.length > 0) { if (rowsToInsert.length > 0) {
await this.insertRows(tableName, rowsToInsert, columns); await this.insertRows(dataStoreId, rowsToInsert, columns);
} }
if (rowsToUpdate.length > 0) { if (rowsToUpdate.length > 0) {
@@ -119,7 +113,7 @@ export class DataStoreRowsRepository {
const setData = Object.fromEntries(updateKeys.map((key) => [key, row[key]])); const setData = Object.fromEntries(updateKeys.map((key) => [key, row[key]]));
const whereData = Object.fromEntries(matchFields.map((key) => [key, row[key]])); const whereData = Object.fromEntries(matchFields.map((key) => [key, row[key]]));
await this.updateRow(tableName, setData, whereData, columns); await this.updateRow(dataStoreId, setData, whereData, columns);
} }
} }
@@ -127,7 +121,7 @@ export class DataStoreRowsRepository {
} }
async updateRow( async updateRow(
tableName: DataStoreUserTableName, dataStoreId: string,
setData: Record<string, DataStoreColumnJsType | null>, setData: Record<string, DataStoreColumnJsType | null>,
whereData: Record<string, DataStoreColumnJsType | null>, whereData: Record<string, DataStoreColumnJsType | null>,
columns: DataStoreColumn[], columns: DataStoreColumn[],
@@ -135,7 +129,7 @@ export class DataStoreRowsRepository {
const dbType = this.dataSource.options.type; const dbType = this.dataSource.options.type;
const columnTypeMap = buildColumnTypeMap(columns); const columnTypeMap = buildColumnTypeMap(columns);
const queryBuilder = this.dataSource.createQueryBuilder().update(tableName); const queryBuilder = this.dataSource.createQueryBuilder().update(this.toTableName(dataStoreId));
const setValues: Record<string, DataStoreColumnJsType | null> = {}; const setValues: Record<string, DataStoreColumnJsType | null> = {};
for (const [key, value] of Object.entries(setData)) { for (const [key, value] of Object.entries(setData)) {
@@ -153,13 +147,13 @@ export class DataStoreRowsRepository {
await queryBuilder.execute(); await queryBuilder.execute();
} }
async deleteRows(tableName: DataStoreUserTableName, ids: number[]) { async deleteRows(dataStoreId: string, ids: number[]) {
if (ids.length === 0) { if (ids.length === 0) {
return true; return true;
} }
const dbType = this.dataSource.options.type; const dbType = this.dataSource.options.type;
const quotedTableName = quoteIdentifier(tableName, dbType); const quotedTableName = quoteIdentifier(this.toTableName(dataStoreId), dbType);
const placeholders = ids.map((_, index) => getPlaceholder(index + 1, dbType)).join(', '); const placeholders = ids.map((_, index) => getPlaceholder(index + 1, dbType)).join(', ');
const query = `DELETE FROM ${quotedTableName} WHERE id IN (${placeholders})`; const query = `DELETE FROM ${quotedTableName} WHERE id IN (${placeholders})`;
@@ -168,36 +162,40 @@ export class DataStoreRowsRepository {
} }
async createTableWithColumns( async createTableWithColumns(
tableName: string, dataStoreId: string,
columns: DataStoreColumn[], columns: DataStoreColumn[],
queryRunner: QueryRunner, queryRunner: QueryRunner,
) { ) {
const dslColumns = [new DslColumn('id').int.autoGenerate2.primary, ...toDslColumns(columns)]; const dslColumns = [new DslColumn('id').int.autoGenerate2.primary, ...toDslColumns(columns)];
const createTable = new CreateTable(tableName, '', queryRunner); const createTable = new CreateTable(this.toTableName(dataStoreId), '', queryRunner);
createTable.withColumns.apply(createTable, dslColumns); createTable.withColumns.apply(createTable, dslColumns);
await createTable.execute(queryRunner); await createTable.execute(queryRunner);
} }
async dropTable(dataStoreId: string, queryRunner: QueryRunner) {
await queryRunner.dropTable(this.toTableName(dataStoreId), true);
}
async addColumn( async addColumn(
dataStoreId: string, dataStoreId: string,
column: DataStoreColumn, column: DataStoreColumn,
queryRunner: QueryRunner, queryRunner: QueryRunner,
dbType: DataSourceOptions['type'], dbType: DataSourceOptions['type'],
) { ) {
const tableName = toTableName(dataStoreId); await queryRunner.query(addColumnQuery(this.toTableName(dataStoreId), column, dbType));
await queryRunner.manager.query(addColumnQuery(tableName, column, dbType));
} }
async dropColumnFromTable( async dropColumnFromTable(
dataStoreId: string, dataStoreId: string,
columnName: string, columnName: string,
em: EntityManager, queryRunner: QueryRunner,
dbType: DataSourceOptions['type'], dbType: DataSourceOptions['type'],
) { ) {
await em.query(deleteColumnQuery(toTableName(dataStoreId), columnName, dbType)); await queryRunner.query(deleteColumnQuery(this.toTableName(dataStoreId), columnName, dbType));
} }
async getManyAndCount(dataStoreId: DataStoreUserTableName, dto: ListDataStoreContentQueryDto) { async getManyAndCount(dataStoreId: string, dto: ListDataStoreContentQueryDto) {
const [countQuery, query] = this.getManyQuery(dataStoreId, dto); const [countQuery, query] = this.getManyQuery(dataStoreId, dto);
const data: DataStoreRows = await query.select('*').getRawMany(); const data: DataStoreRows = await query.select('*').getRawMany();
const countResult = await countQuery.select('COUNT(*) as count').getRawOne<{ const countResult = await countQuery.select('COUNT(*) as count').getRawOne<{
@@ -208,19 +206,19 @@ export class DataStoreRowsRepository {
return { count: count ?? -1, data }; return { count: count ?? -1, data };
} }
async getRowIds(dataStoreId: DataStoreUserTableName, dto: ListDataStoreContentQueryDto) { async getRowIds(dataStoreId: string, dto: ListDataStoreContentQueryDto) {
const [_, query] = this.getManyQuery(dataStoreId, dto); const [_, query] = this.getManyQuery(dataStoreId, dto);
const result = await query.select('dataStore.id').getRawMany<number>(); const result = await query.select('dataStore.id').getRawMany<number>();
return result; return result;
} }
private getManyQuery( private getManyQuery(
dataStoreTableName: DataStoreUserTableName, dataStoreId: string,
dto: ListDataStoreContentQueryDto, dto: ListDataStoreContentQueryDto,
): [QueryBuilder, QueryBuilder] { ): [QueryBuilder, QueryBuilder] {
const query = this.dataSource.createQueryBuilder(); const query = this.dataSource.createQueryBuilder();
query.from(dataStoreTableName, 'dataStore'); query.from(this.toTableName(dataStoreId), 'dataStore');
this.applyFilters(query, dto); this.applyFilters(query, dto);
const countQuery = query.clone().select('COUNT(*)'); const countQuery = query.clone().select('COUNT(*)');
this.applySorting(query, dto); this.applySorting(query, dto);
@@ -268,7 +266,7 @@ export class DataStoreRowsRepository {
} }
private async fetchAndSplitRowsByExistence( private async fetchAndSplitRowsByExistence(
tableName: string, dataStoreId: string,
matchFields: string[], matchFields: string[],
rows: DataStoreRows, rows: DataStoreRows,
): Promise<{ rowsToInsert: DataStoreRows; rowsToUpdate: DataStoreRows }> { ): Promise<{ rowsToInsert: DataStoreRows; rowsToUpdate: DataStoreRows }> {
@@ -287,7 +285,7 @@ export class DataStoreRowsRepository {
} }
const quotedFields = matchFields.map((field) => quoteIdentifier(field, dbType)).join(', '); const quotedFields = matchFields.map((field) => quoteIdentifier(field, dbType)).join(', ');
const quotedTableName = quoteIdentifier(tableName, dbType); const quotedTableName = quoteIdentifier(this.toTableName(dataStoreId), dbType);
const query = ` const query = `
SELECT ${quotedFields} SELECT ${quotedFields}

View File

@@ -10,7 +10,6 @@ import { UnexpectedError } from 'n8n-workflow';
import { DataStoreColumn } from './data-store-column.entity'; import { DataStoreColumn } from './data-store-column.entity';
import { DataStoreRowsRepository } from './data-store-rows.repository'; import { DataStoreRowsRepository } from './data-store-rows.repository';
import { DataStore } from './data-store.entity'; import { DataStore } from './data-store.entity';
import { toTableName } from './utils/sql-utils';
@Service() @Service()
export class DataStoreRepository extends Repository<DataStore> { export class DataStoreRepository extends Repository<DataStore> {
@@ -32,7 +31,6 @@ export class DataStoreRepository extends Repository<DataStore> {
await em.insert(DataStore, dataStore); await em.insert(DataStore, dataStore);
dataStoreId = dataStore.id; dataStoreId = dataStore.id;
const tableName = toTableName(dataStore.id);
const queryRunner = em.queryRunner; const queryRunner = em.queryRunner;
if (!queryRunner) { if (!queryRunner) {
throw new UnexpectedError('QueryRunner is not available'); throw new UnexpectedError('QueryRunner is not available');
@@ -41,9 +39,9 @@ export class DataStoreRepository extends Repository<DataStore> {
// insert columns // insert columns
const columnEntities = columns.map((col, index) => const columnEntities = columns.map((col, index) =>
em.create(DataStoreColumn, { em.create(DataStoreColumn, {
dataStoreId,
name: col.name, name: col.name,
type: col.type, type: col.type,
dataStoreId: dataStore.id,
index: col.index ?? index, index: col.index ?? index,
}), }),
); );
@@ -54,7 +52,7 @@ export class DataStoreRepository extends Repository<DataStore> {
// create user table (will create empty table with just id column if no columns) // create user table (will create empty table with just id column if no columns)
await this.dataStoreRowsRepository.createTableWithColumns( await this.dataStoreRowsRepository.createTableWithColumns(
tableName, dataStoreId,
columnEntities, columnEntities,
queryRunner, queryRunner,
); );
@@ -81,7 +79,7 @@ export class DataStoreRepository extends Repository<DataStore> {
} }
await em.delete(DataStore, { id: dataStoreId }); await em.delete(DataStore, { id: dataStoreId });
await queryRunner.dropTable(toTableName(dataStoreId), true); await this.dataStoreRowsRepository.dropTable(dataStoreId, queryRunner);
return true; return true;
}); });
@@ -113,7 +111,7 @@ export class DataStoreRepository extends Repository<DataStore> {
let changed = false; let changed = false;
for (const match of existingTables) { for (const match of existingTables) {
const result = await em.delete(DataStore, { id: match.id }); const result = await em.delete(DataStore, { id: match.id });
await queryRunner.dropTable(toTableName(match.id), true); await this.dataStoreRowsRepository.dropTable(match.id, queryRunner);
changed = changed || (result.affected ?? 0) > 0; changed = changed || (result.affected ?? 0) > 0;
} }

View File

@@ -20,7 +20,7 @@ import { DataStoreColumnNotFoundError } from './errors/data-store-column-not-fou
import { DataStoreNameConflictError } from './errors/data-store-name-conflict.error'; import { DataStoreNameConflictError } from './errors/data-store-name-conflict.error';
import { DataStoreNotFoundError } from './errors/data-store-not-found.error'; import { DataStoreNotFoundError } from './errors/data-store-not-found.error';
import { DataStoreValidationError } from './errors/data-store-validation.error'; import { DataStoreValidationError } from './errors/data-store-validation.error';
import { toTableName, normalizeRows } from './utils/sql-utils'; import { normalizeRows } from './utils/sql-utils';
@Service() @Service()
export class DataStoreService { export class DataStoreService {
@@ -112,10 +112,7 @@ export class DataStoreService {
// a renamed/removed column appearing here (or added column missing) if the store was // 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 // modified between when the frontend sent the request and we received it
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId); const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
const result = await this.dataStoreRowsRepository.getManyAndCount( const result = await this.dataStoreRowsRepository.getManyAndCount(dataStoreId, dto);
toTableName(dataStoreId),
dto,
);
return { return {
count: result.count, count: result.count,
data: normalizeRows(result.data, columns), data: normalizeRows(result.data, columns),
@@ -133,7 +130,7 @@ export class DataStoreService {
await this.validateRows(dataStoreId, rows); await this.validateRows(dataStoreId, rows);
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId); const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
return await this.dataStoreRowsRepository.insertRows(toTableName(dataStoreId), rows, columns); return await this.dataStoreRowsRepository.insertRows(dataStoreId, rows, columns);
} }
async upsertRows(dataStoreId: string, projectId: string, dto: UpsertDataStoreRowsDto) { async upsertRows(dataStoreId: string, projectId: string, dto: UpsertDataStoreRowsDto) {
@@ -146,7 +143,7 @@ export class DataStoreService {
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId); const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId);
return await this.dataStoreRowsRepository.upsertRows(toTableName(dataStoreId), dto, columns); return await this.dataStoreRowsRepository.upsertRows(dataStoreId, dto, columns);
} }
async updateRow(dataStoreId: string, projectId: string, dto: UpdateDataStoreRowDto) { async updateRow(dataStoreId: string, projectId: string, dto: UpdateDataStoreRowDto) {
@@ -170,14 +167,14 @@ export class DataStoreService {
this.validateRowsWithColumns([filter], columns, true, true); this.validateRowsWithColumns([filter], columns, true, true);
this.validateRowsWithColumns([data], columns, true, false); this.validateRowsWithColumns([data], columns, true, false);
await this.dataStoreRowsRepository.updateRow(toTableName(dataStoreId), data, filter, columns); await this.dataStoreRowsRepository.updateRow(dataStoreId, data, filter, columns);
return true; return true;
} }
async deleteRows(dataStoreId: string, projectId: string, ids: number[]) { async deleteRows(dataStoreId: string, projectId: string, ids: number[]) {
await this.validateDataStoreExists(dataStoreId, projectId); await this.validateDataStoreExists(dataStoreId, projectId);
return await this.dataStoreRowsRepository.deleteRows(toTableName(dataStoreId), ids); return await this.dataStoreRowsRepository.deleteRows(dataStoreId, ids);
} }
private validateRowsWithColumns( private validateRowsWithColumns(

View File

@@ -1 +1 @@
export type DataStoreUserTableName = `data_store_user_${string}`; export type DataStoreUserTableName = `${string}data_store_user_${string}`;

View File

@@ -8,10 +8,10 @@ import type { DataSourceOptions } from '@n8n/typeorm';
import type { DataStoreColumnJsType, DataStoreRows } from 'n8n-workflow'; import type { DataStoreColumnJsType, DataStoreRows } from 'n8n-workflow';
import { UnexpectedError } from 'n8n-workflow'; import { UnexpectedError } from 'n8n-workflow';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import type { DataStoreUserTableName } from '../data-store.types'; import type { DataStoreUserTableName } from '../data-store.types';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
export function toDslColumns(columns: DataStoreCreateColumnSchema[]): DslColumn[] { export function toDslColumns(columns: DataStoreCreateColumnSchema[]): DslColumn[] {
return columns.map((col) => { return columns.map((col) => {
const name = new DslColumn(col.name.trim()); const name = new DslColumn(col.name.trim());
@@ -137,10 +137,6 @@ export function quoteIdentifier(name: string, dbType: DataSourceOptions['type'])
} }
} }
export function toTableName(dataStoreId: string): DataStoreUserTableName {
return `data_store_user_${dataStoreId}`;
}
type WithInsertId = { insertId: number }; type WithInsertId = { insertId: number };
type WithRowId = { id: number }; type WithRowId = { id: number };

View File

@@ -7,7 +7,6 @@ import type { DataStoreRows } from 'n8n-workflow';
import { DataStoreColumnRepository } from '@/modules/data-store/data-store-column.repository'; import { DataStoreColumnRepository } from '@/modules/data-store/data-store-column.repository';
import { DataStoreRowsRepository } from '@/modules/data-store/data-store-rows.repository'; import { DataStoreRowsRepository } from '@/modules/data-store/data-store-rows.repository';
import { DataStoreRepository } from '@/modules/data-store/data-store.repository'; import { DataStoreRepository } from '@/modules/data-store/data-store.repository';
import { toTableName } from '@/modules/data-store/utils/sql-utils';
export const createDataStore = async ( export const createDataStore = async (
project: Project, project: Project,
@@ -37,7 +36,7 @@ export const createDataStore = async (
const columns = await dataStoreColumnRepository.getColumns(dataStore.id); const columns = await dataStoreColumnRepository.getColumns(dataStore.id);
const dataStoreRowsRepository = Container.get(DataStoreRowsRepository); const dataStoreRowsRepository = Container.get(DataStoreRowsRepository);
await dataStoreRowsRepository.insertRows(toTableName(dataStore.id), options.data, columns); await dataStoreRowsRepository.insertRows(dataStore.id, options.data, columns);
} }
return dataStore; return dataStore;