refactor(core): Rename data-store to data-table in the BE (no-changelog) (#20424)

This commit is contained in:
Ricardo Espinoza 2025-10-07 04:10:30 -04:00 committed by GitHub
parent 123a742685
commit a2aca5e7a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 2498 additions and 2499 deletions

View File

@ -1,5 +0,0 @@
import { Z } from 'zod-class';
import { dataStoreCreateColumnSchema } from '../../schemas/data-store.schema';
export class AddDataStoreColumnDto extends Z.class(dataStoreCreateColumnSchema.shape) {}

View File

@ -1,13 +0,0 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import {
dataStoreColumnNameSchema,
dataStoreColumnValueSchema,
insertRowReturnType,
} from '../../schemas/data-store.schema';
export class AddDataStoreRowsDto extends Z.class({
data: z.array(z.record(dataStoreColumnNameSchema, dataStoreColumnValueSchema)),
returnType: insertRowReturnType,
}) {}

View File

@ -1,11 +0,0 @@
import { Z } from 'zod-class';
import {
dataStoreColumnNameSchema,
dataStoreColumnTypeSchema,
} from '../../schemas/data-store.schema';
export class CreateDataStoreColumnDto extends Z.class({
name: dataStoreColumnNameSchema,
type: dataStoreColumnTypeSchema,
}) {}

View File

@ -1,10 +0,0 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import { CreateDataStoreColumnDto } from './create-data-store-column.dto';
import { dataStoreNameSchema } from '../../schemas/data-store.schema';
export class CreateDataStoreDto extends Z.class({
name: dataStoreNameSchema,
columns: z.array(CreateDataStoreColumnDto),
}) {}

View File

@ -1,7 +0,0 @@
import { Z } from 'zod-class';
import { dataStoreNameSchema } from '../../schemas/data-store.schema';
export class UpdateDataStoreDto extends Z.class({
name: dataStoreNameSchema,
}) {}

View File

@ -0,0 +1,5 @@
import { Z } from 'zod-class';
import { dataTableCreateColumnSchema } from '../../schemas/data-table.schema';
export class AddDataTableColumnDto extends Z.class(dataTableCreateColumnSchema.shape) {}

View File

@ -0,0 +1,13 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import {
dataTableColumnNameSchema,
dataTableColumnValueSchema,
insertRowReturnType,
} from '../../schemas/data-table.schema';
export class AddDataTableRowsDto extends Z.class({
data: z.array(z.record(dataTableColumnNameSchema, dataTableColumnValueSchema)),
returnType: insertRowReturnType,
}) {}

View File

@ -0,0 +1,11 @@
import { Z } from 'zod-class';
import {
dataTableColumnNameSchema,
dataTableColumnTypeSchema,
} from '../../schemas/data-table.schema';
export class CreateDataTableColumnDto extends Z.class({
name: dataTableColumnNameSchema,
type: dataTableColumnTypeSchema,
}) {}

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import { CreateDataTableColumnDto } from './create-data-table-column.dto';
import { dataTableNameSchema } from '../../schemas/data-table.schema';
export class CreateDataTableDto extends Z.class({
name: dataTableNameSchema,
columns: z.array(CreateDataTableColumnDto),
}) {}

View File

@ -2,8 +2,8 @@ import { jsonParse } from 'n8n-workflow';
import { z } from 'zod';
import { Z } from 'zod-class';
import { dataStoreColumnNameSchema } from '../../schemas/data-store.schema';
import { dataTableFilterSchema } from '../../schemas/data-table-filter.schema';
import { dataTableColumnNameSchema } from '../../schemas/data-table.schema';
import { paginationSchema } from '../pagination/pagination.dto';
const filterValidator = z
@ -51,7 +51,7 @@ const sortByValidator = z
let [column, direction] = val.split(':');
try {
column = dataStoreColumnNameSchema.parse(column);
column = dataTableColumnNameSchema.parse(column);
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@ -74,7 +74,7 @@ const sortByValidator = z
return [column, direction] as const;
});
export class ListDataStoreContentQueryDto extends Z.class({
export class ListDataTableContentQueryDto extends Z.class({
take: paginationSchema.take.optional(),
skip: paginationSchema.skip.optional(),
filter: filterValidator.optional(),

View File

@ -15,7 +15,7 @@ const VALID_SORT_OPTIONS = [
'sizeBytes:desc',
] as const;
export type ListDataStoreQuerySortOptions = (typeof VALID_SORT_OPTIONS)[number];
export type ListDataTableQuerySortOptions = (typeof VALID_SORT_OPTIONS)[number];
const FILTER_OPTIONS = {
id: z.union([z.string(), z.array(z.string())]).optional(),
@ -63,7 +63,7 @@ const sortByValidator = z
.enum(VALID_SORT_OPTIONS, { message: `sortBy must be one of: ${VALID_SORT_OPTIONS.join(', ')}` })
.optional();
export class ListDataStoreQueryDto extends Z.class({
export class ListDataTableQueryDto extends Z.class({
...paginationSchema,
filter: filterValidator,
sortBy: sortByValidator,

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { Z } from 'zod-class';
export class MoveDataStoreColumnDto extends Z.class({
export class MoveDataTableColumnDto extends Z.class({
targetIndex: z.number().int().nonnegative(),
}) {}

View File

@ -1,11 +1,11 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import {
dataStoreColumnNameSchema,
dataStoreColumnValueSchema,
} from '../../schemas/data-store.schema';
import { dataTableFilterSchema } from '../../schemas/data-table-filter.schema';
import {
dataTableColumnNameSchema,
dataTableColumnValueSchema,
} from '../../schemas/data-table.schema';
const updateFilterSchema = dataTableFilterSchema.refine((filter) => filter.filters.length > 0, {
message: 'filter must not be empty',
@ -14,7 +14,7 @@ const updateFilterSchema = dataTableFilterSchema.refine((filter) => filter.filte
const updateDataTableRowShape = {
filter: updateFilterSchema,
data: z
.record(dataStoreColumnNameSchema, dataStoreColumnValueSchema)
.record(dataTableColumnNameSchema, dataTableColumnValueSchema)
.refine((obj) => Object.keys(obj).length > 0, {
message: 'data must not be empty',
}),

View File

@ -0,0 +1,7 @@
import { Z } from 'zod-class';
import { dataTableNameSchema } from '../../schemas/data-table.schema';
export class UpdateDataTableDto extends Z.class({
name: dataTableNameSchema,
}) {}

View File

@ -1,20 +1,20 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import {
dataStoreColumnNameSchema,
dataStoreColumnValueSchema,
} from '../../schemas/data-store.schema';
import { dataTableFilterSchema } from '../../schemas/data-table-filter.schema';
import {
dataTableColumnNameSchema,
dataTableColumnValueSchema,
} from '../../schemas/data-table.schema';
const upsertFilterSchema = dataTableFilterSchema.refine((filter) => filter.filters.length > 0, {
message: 'filter must not be empty',
});
const upsertDataStoreRowShape = {
const upsertDataTableRowShape = {
filter: upsertFilterSchema,
data: z
.record(dataStoreColumnNameSchema, dataStoreColumnValueSchema)
.record(dataTableColumnNameSchema, dataTableColumnValueSchema)
.refine((obj) => Object.keys(obj).length > 0, {
message: 'data must not be empty',
}),
@ -22,4 +22,4 @@ const upsertDataStoreRowShape = {
dryRun: z.boolean().optional().default(false),
};
export class UpsertDataStoreRowDto extends Z.class(upsertDataStoreRowShape) {}
export class UpsertDataTableRowDto extends Z.class(upsertDataTableRowShape) {}

View File

@ -86,14 +86,14 @@ export { RoleGetQueryDto } from './roles/role-get-query.dto';
export { OidcConfigDto } from './oidc/config.dto';
export { CreateDataStoreDto } from './data-store/create-data-store.dto';
export { UpdateDataStoreDto } from './data-store/update-data-store.dto';
export { UpdateDataTableRowDto } from './data-store/update-data-store-row.dto';
export { DeleteDataTableRowsDto } from './data-store/delete-data-table-rows.dto';
export { UpsertDataStoreRowDto } from './data-store/upsert-data-store-row.dto';
export { ListDataStoreQueryDto } from './data-store/list-data-store-query.dto';
export { ListDataStoreContentQueryDto } from './data-store/list-data-store-content-query.dto';
export { CreateDataStoreColumnDto } from './data-store/create-data-store-column.dto';
export { AddDataStoreRowsDto } from './data-store/add-data-store-rows.dto';
export { AddDataStoreColumnDto } from './data-store/add-data-store-column.dto';
export { MoveDataStoreColumnDto } from './data-store/move-data-store-column.dto';
export { CreateDataTableDto } from './data-table/create-data-table.dto';
export { UpdateDataTableDto } from './data-table/update-data-table.dto';
export { UpdateDataTableRowDto } from './data-table/update-data-table-row.dto';
export { DeleteDataTableRowsDto } from './data-table/delete-data-table-rows.dto';
export { UpsertDataTableRowDto } from './data-table/upsert-data-table-row.dto';
export { ListDataTableQueryDto } from './data-table/list-data-table-query.dto';
export { ListDataTableContentQueryDto } from './data-table/list-data-table-content-query.dto';
export { CreateDataTableColumnDto } from './data-table/create-data-table-column.dto';
export { AddDataTableRowsDto } from './data-table/add-data-table-rows.dto';
export { AddDataTableColumnDto } from './data-table/add-data-table-column.dto';
export { MoveDataTableColumnDto } from './data-table/move-data-table-column.dto';

View File

@ -50,17 +50,17 @@ export {
} from './schemas/user.schema';
export {
DATA_STORE_COLUMN_REGEX,
DATA_STORE_COLUMN_MAX_LENGTH,
DATA_STORE_COLUMN_ERROR_MESSAGE,
type DataStore,
type DataStoreColumn,
type DataStoreCreateColumnSchema,
type DataStoreListFilter,
type DataStoreListOptions,
DATA_TABLE_COLUMN_REGEX,
DATA_TABLE_COLUMN_MAX_LENGTH,
DATA_TABLE_COLUMN_ERROR_MESSAGE,
type DataTable,
type DataTableColumn,
type DataTableCreateColumnSchema,
type DataTableListFilter,
type DataTableListOptions,
dateTimeSchema,
dataStoreColumnNameSchema,
} from './schemas/data-store.schema';
dataTableColumnNameSchema,
} from './schemas/data-table.schema';
export type {
DataTableFilter,

View File

@ -1,67 +0,0 @@
import { z } from 'zod';
import type { ListDataStoreQueryDto } from '../dto';
export const insertRowReturnType = z.union([z.literal('all'), z.literal('count'), z.literal('id')]);
export const dataStoreNameSchema = z.string().trim().min(1).max(128);
export const dataStoreIdSchema = z.string().max(36);
// Postgres does not allow leading numbers or -
export const DATA_STORE_COLUMN_REGEX = /^[a-zA-Z][a-zA-Z0-9_]*$/;
export const DATA_STORE_COLUMN_MAX_LENGTH = 63; // Postgres has a maximum of 63 characters
export const DATA_STORE_COLUMN_ERROR_MESSAGE =
'Only alphabetical characters and non-leading numbers and underscores are allowed for column names, and the maximum length is 63 characters.';
export const dataStoreColumnNameSchema = z
.string()
.trim()
.min(1)
.max(DATA_STORE_COLUMN_MAX_LENGTH) // Postgres has a maximum of 63 characters
.regex(DATA_STORE_COLUMN_REGEX, DATA_STORE_COLUMN_ERROR_MESSAGE);
export const dataStoreColumnTypeSchema = z.enum(['string', 'number', 'boolean', 'date']);
export const dataStoreCreateColumnSchema = z.object({
name: dataStoreColumnNameSchema,
type: dataStoreColumnTypeSchema,
index: z.number().optional(),
});
export type DataStoreCreateColumnSchema = z.infer<typeof dataStoreCreateColumnSchema>;
export const dataStoreColumnSchema = dataStoreCreateColumnSchema.extend({
dataStoreId: dataStoreIdSchema,
});
export const dataStoreSchema = z.object({
id: dataStoreIdSchema,
name: dataStoreNameSchema,
columns: z.array(dataStoreColumnSchema),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
export type DataStore = z.infer<typeof dataStoreSchema>;
export type DataStoreColumn = z.infer<typeof dataStoreColumnSchema>;
export type DataStoreListFilter = {
id?: string | string[];
projectId?: string | string[];
name?: string;
};
export type DataStoreListOptions = Partial<ListDataStoreQueryDto> & {
filter: { projectId: string };
};
export const dateTimeSchema = z
.string()
.datetime({ offset: true })
.transform((s) => new Date(s))
.pipe(z.date());
export const dataStoreColumnValueSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.date(),
]);

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { dataStoreColumnNameSchema } from './data-store.schema';
import { dataTableColumnNameSchema } from './data-table.schema';
export const FilterConditionSchema = z.union([
z.literal('eq'),
@ -16,7 +16,7 @@ export const FilterConditionSchema = z.union([
export type DataTableFilterConditionType = z.infer<typeof FilterConditionSchema>;
export const dataTableFilterRecordSchema = z.object({
columnName: dataStoreColumnNameSchema,
columnName: dataTableColumnNameSchema,
condition: FilterConditionSchema.default('eq'),
value: z.union([z.string(), z.number(), z.boolean(), z.date(), z.null()]),
});

View File

@ -0,0 +1,67 @@
import { z } from 'zod';
import type { ListDataTableQueryDto } from '../dto';
export const insertRowReturnType = z.union([z.literal('all'), z.literal('count'), z.literal('id')]);
export const dataTableNameSchema = z.string().trim().min(1).max(128);
export const dataTableIdSchema = z.string().max(36);
// Postgres does not allow leading numbers or -
export const DATA_TABLE_COLUMN_REGEX = /^[a-zA-Z][a-zA-Z0-9_]*$/;
export const DATA_TABLE_COLUMN_MAX_LENGTH = 63; // Postgres has a maximum of 63 characters
export const DATA_TABLE_COLUMN_ERROR_MESSAGE =
'Only alphabetical characters and non-leading numbers and underscores are allowed for column names, and the maximum length is 63 characters.';
export const dataTableColumnNameSchema = z
.string()
.trim()
.min(1)
.max(DATA_TABLE_COLUMN_MAX_LENGTH) // Postgres has a maximum of 63 characters
.regex(DATA_TABLE_COLUMN_REGEX, DATA_TABLE_COLUMN_ERROR_MESSAGE);
export const dataTableColumnTypeSchema = z.enum(['string', 'number', 'boolean', 'date']);
export const dataTableCreateColumnSchema = z.object({
name: dataTableColumnNameSchema,
type: dataTableColumnTypeSchema,
index: z.number().optional(),
});
export type DataTableCreateColumnSchema = z.infer<typeof dataTableCreateColumnSchema>;
export const dataTableColumnSchema = dataTableCreateColumnSchema.extend({
dataTableId: dataTableIdSchema,
});
export const dataTableSchema = z.object({
id: dataTableIdSchema,
name: dataTableNameSchema,
columns: z.array(dataTableColumnSchema),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
});
export type DataTable = z.infer<typeof dataTableSchema>;
export type DataTableColumn = z.infer<typeof dataTableColumnSchema>;
export type DataTableListFilter = {
id?: string | string[];
projectId?: string | string[];
name?: string;
};
export type DataTableListOptions = Partial<ListDataTableQueryDto> & {
filter: { projectId: string };
};
export const dateTimeSchema = z
.string()
.datetime({ offset: true })
.transform((s) => new Date(s))
.pipe(z.date());
export const dataTableColumnValueSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.date(),
]);

View File

@ -24,7 +24,7 @@ import {
SubworkflowPolicyChecker,
} from '@/executions/pre-execution-checks';
import { ExternalHooks } from '@/external-hooks';
import { DataStoreProxyService } from '@/modules/data-table/data-store-proxy.service';
import { DataTableProxyService } from '@/modules/data-table/data-table-proxy.service';
import { UrlService } from '@/services/url.service';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
import { Telemetry } from '@/telemetry';
@ -99,7 +99,7 @@ describe('WorkflowExecuteAdditionalData', () => {
mockInstance(CredentialsPermissionChecker);
mockInstance(SubworkflowPolicyChecker);
mockInstance(WorkflowStatisticsService);
mockInstance(DataStoreProxyService);
mockInstance(DataTableProxyService);
const urlService = mockInstance(UrlService);
Container.set(UrlService, urlService);

View File

@ -1,4 +1,4 @@
import type { DataStore } from '@n8n/api-types';
import type { DataTable } from '@n8n/api-types';
import {
createTeamProject,
getPersonalProject,
@ -8,7 +8,7 @@ import {
import type { Project, User } from '@n8n/db';
import { DateTime } from 'luxon';
import { createDataStore } from '@test-integration/db/data-stores';
import { createDataTable } from '@test-integration/db/data-tables';
import { createOwner, createMember, createAdmin } from '@test-integration/db/users';
import type { SuperAgentTest } from '@test-integration/types';
import * as utils from '@test-integration/utils';
@ -51,67 +51,66 @@ afterAll(async () => {
});
describe('GET /data-tables-global', () => {
test('should not list data stores when no data stores exist', async () => {
test('should not list data tables when no data tables exist', async () => {
const response = await authOwnerAgent.get('/data-tables-global').expect(200);
expect(response.body.data.count).toBe(0);
expect(response.body.data.data).toHaveLength(0);
});
test('should not list data stores from projects member has no access to', async () => {
test('should not list data tables from projects member has no access to', async () => {
const project = await createTeamProject('test project', owner);
await createDataStore(project, { name: 'Test Data Store' });
await createDataTable(project, { name: 'Test Data Table' });
const response = await authMemberAgent.get('/data-tables-global').expect(200);
expect(response.body.data.count).toBe(0);
expect(response.body.data.data).toHaveLength(0);
});
test('should not list data stores from projects admin has no access to', async () => {
test('should not list data tables from projects admin has no access to', async () => {
const project = await createTeamProject('test project', owner);
await createDataStore(project, { name: 'Test Data Store' });
await createDataTable(project, { name: 'Test Data Table' });
const response = await authAdminAgent.get('/data-tables-global').expect(200);
expect(response.body.data.count).toBe(0);
expect(response.body.data.data).toHaveLength(0);
});
test("should not list data stores from another user's personal project", async () => {
await createDataStore(ownerProject, { name: 'Personal Data Store' });
test("should not list data tables from another user's personal project", async () => {
await createDataTable(ownerProject, { name: 'Personal Data Table' });
const response = await authAdminAgent.get('/data-tables-global').expect(200);
expect(response.body.data.count).toBe(0);
expect(response.body.data.data).toHaveLength(0);
});
test('should list data stores from team projects where user has project:viewer role', async () => {
test('should list data tables from team projects where user has project:viewer role', async () => {
const project = await createTeamProject('test project', owner);
await linkUserToProject(member, project, 'project:viewer');
await createDataStore(project, { name: 'Test Data Store' });
await createDataTable(project, { name: 'Test Data Table' });
const response = await authMemberAgent.get('/data-tables-global').expect(200);
expect(response.body.data.count).toBe(1);
expect(response.body.data.data).toHaveLength(1);
expect(response.body.data.data[0].name).toBe('Test Data Store');
expect(response.body.data.data[0].name).toBe('Test Data Table');
});
test("should list data stores from user's own personal project", async () => {
await createDataStore(ownerProject, { name: 'Personal Data Store 1' });
await createDataStore(ownerProject, { name: 'Personal Data Store 2' });
test("should list data tables from user's own personal project", async () => {
await createDataTable(ownerProject, { name: 'Personal Data Table 1' });
await createDataTable(ownerProject, { name: 'Personal Data Table 2' });
const response = await authOwnerAgent.get('/data-tables-global').expect(200);
expect(response.body.data.count).toBe(2);
expect(response.body.data.data).toHaveLength(2);
expect(response.body.data.data.map((f: DataStore) => f.name).sort()).toEqual(
['Personal Data Store 1', 'Personal Data Store 2'].sort(),
expect(response.body.data.data.map((f: DataTable) => f.name).sort()).toEqual(
['Personal Data Table 1', 'Personal Data Table 2'].sort(),
);
});
test('should filter data stores by projectId', async () => {
await createDataStore(ownerProject, { name: 'Test Data Store 1' });
await createDataStore(ownerProject, { name: 'Test Data Store 2' });
await createDataStore(memberProject, { name: 'Another Data Store' });
test('should filter data tables by projectId', async () => {
await createDataTable(ownerProject, { name: 'Test Data Table 1' });
await createDataTable(ownerProject, { name: 'Test Data Table 2' });
await createDataTable(memberProject, { name: 'Another Data Table' });
const response = await authOwnerAgent
.get('/data-tables-global')
@ -120,15 +119,15 @@ describe('GET /data-tables-global', () => {
expect(response.body.data.count).toBe(2);
expect(response.body.data.data).toHaveLength(2);
expect(response.body.data.data.map((f: DataStore) => f.name).sort()).toEqual(
['Test Data Store 1', 'Test Data Store 2'].sort(),
expect(response.body.data.data.map((f: DataTable) => f.name).sort()).toEqual(
['Test Data Table 1', 'Test Data Table 2'].sort(),
);
});
test('should not list projects the user cant access even with project filters', async () => {
await createDataStore(ownerProject, { name: 'Test Data Store 1' });
await createDataStore(ownerProject, { name: 'Test Data Store 2' });
await createDataStore(memberProject, { name: 'Another Data Store' });
await createDataTable(ownerProject, { name: 'Test Data Table 1' });
await createDataTable(ownerProject, { name: 'Test Data Table 2' });
await createDataTable(memberProject, { name: 'Another Data Table' });
const response = await authMemberAgent
.get('/data-tables-global')
@ -139,12 +138,12 @@ describe('GET /data-tables-global', () => {
expect(response.body.data.data).toHaveLength(0);
});
test('should filter data stores by name', async () => {
test('should filter data tables by name', async () => {
const project = await createTeamProject('test project', owner);
await createDataStore(ownerProject, { name: 'Test Data Store' });
await createDataStore(ownerProject, { name: 'Another Data Store' });
await createDataStore(project, { name: 'Test Something Else' });
await createDataTable(ownerProject, { name: 'Test Data Table' });
await createDataTable(ownerProject, { name: 'Another Data Table' });
await createDataTable(project, { name: 'Test Something Else' });
const response = await authOwnerAgent
.get('/data-tables-global')
@ -154,14 +153,14 @@ describe('GET /data-tables-global', () => {
expect(response.body.data.count).toBe(2);
expect(response.body.data.data).toHaveLength(2);
expect(response.body.data.data.map((f: any) => f.name).sort()).toEqual(
['Test Data Store', 'Test Something Else'].sort(),
['Test Data Table', 'Test Something Else'].sort(),
);
});
test('should filter data stores by id', async () => {
const dataStore1 = await createDataStore(ownerProject, { name: 'Data Store 1' });
await createDataStore(ownerProject, { name: 'Data Store 2' });
await createDataStore(ownerProject, { name: 'Data Store 3' });
test('should filter data tables by id', async () => {
const dataStore1 = await createDataTable(ownerProject, { name: 'Data Table 1' });
await createDataTable(ownerProject, { name: 'Data Table 2' });
await createDataTable(ownerProject, { name: 'Data Table 3' });
const response = await authOwnerAgent
.get('/data-tables-global')
@ -170,15 +169,15 @@ describe('GET /data-tables-global', () => {
expect(response.body.data.count).toBe(1);
expect(response.body.data.data).toHaveLength(1);
expect(response.body.data.data[0].name).toBe('Data Store 1');
expect(response.body.data.data[0].name).toBe('Data Table 1');
});
test('should filter data stores by multiple names (AND operator)', async () => {
test('should filter data tables by multiple names (AND operator)', async () => {
const project = await createTeamProject('test project', owner);
await createDataStore(ownerProject, { name: 'Data Store' });
await createDataStore(ownerProject, { name: 'Test Store' });
await createDataStore(project, { name: 'Another Store' });
await createDataTable(ownerProject, { name: 'Data Table' });
await createDataTable(ownerProject, { name: 'Test Store' });
await createDataTable(project, { name: 'Another Store' });
const response = await authOwnerAgent
.get('/data-tables-global')
@ -193,8 +192,8 @@ describe('GET /data-tables-global', () => {
test('should apply pagination with take parameter', async () => {
const project = await createTeamProject('test project', owner);
for (let i = 1; i <= 5; i++) {
await createDataStore(i % 2 ? ownerProject : project, {
name: `Data Store ${i}`,
await createDataTable(i % 2 ? ownerProject : project, {
name: `Data Table ${i}`,
updatedAt: DateTime.now()
.minus({ minutes: 6 - i })
.toJSDate(),
@ -205,18 +204,18 @@ describe('GET /data-tables-global', () => {
expect(response.body.data.count).toBe(5); // Total count should be 5
expect(response.body.data.data).toHaveLength(3); // But only 3 returned
expect(response.body.data.data.map((store: DataStore) => store.name)).toEqual([
'Data Store 5',
'Data Store 4',
'Data Store 3',
expect(response.body.data.data.map((store: DataTable) => store.name)).toEqual([
'Data Table 5',
'Data Table 4',
'Data Table 3',
]);
});
test('should apply pagination with skip parameter', async () => {
const project = await createTeamProject('test project', owner);
for (let i = 1; i <= 5; i++) {
await createDataStore(i % 2 ? ownerProject : project, {
name: `Data Store ${i}`,
await createDataTable(i % 2 ? ownerProject : project, {
name: `Data Table ${i}`,
updatedAt: DateTime.now()
.minus({ minutes: 6 - i })
.toJSDate(),
@ -227,18 +226,18 @@ describe('GET /data-tables-global', () => {
expect(response.body.data.count).toBe(5);
expect(response.body.data.data).toHaveLength(3);
expect(response.body.data.data.map((store: DataStore) => store.name)).toEqual([
'Data Store 3',
'Data Store 2',
'Data Store 1',
expect(response.body.data.data.map((store: DataTable) => store.name)).toEqual([
'Data Table 3',
'Data Table 2',
'Data Table 1',
]);
});
test('should apply combined skip and take parameters', async () => {
const project = await createTeamProject('test project', owner);
for (let i = 1; i <= 5; i++) {
await createDataStore(i % 2 ? ownerProject : project, {
name: `Data Store ${i}`,
await createDataTable(i % 2 ? ownerProject : project, {
name: `Data Table ${i}`,
updatedAt: DateTime.now()
.minus({ minutes: 6 - i })
.toJSDate(),
@ -252,57 +251,57 @@ describe('GET /data-tables-global', () => {
expect(response.body.data.count).toBe(5);
expect(response.body.data.data).toHaveLength(2);
expect(response.body.data.data.map((store: DataStore) => store.name)).toEqual([
'Data Store 4',
'Data Store 3',
expect(response.body.data.data.map((store: DataTable) => store.name)).toEqual([
'Data Table 4',
'Data Table 3',
]);
});
test('should sort data stores by name ascending', async () => {
await createDataStore(ownerProject, { name: 'Z Data Store' });
await createDataStore(ownerProject, { name: 'A Data Store' });
await createDataStore(ownerProject, { name: 'M Data Store' });
test('should sort data tables by name ascending', async () => {
await createDataTable(ownerProject, { name: 'Z Data Table' });
await createDataTable(ownerProject, { name: 'A Data Table' });
await createDataTable(ownerProject, { name: 'M Data Table' });
const response = await authOwnerAgent
.get('/data-tables-global')
.query({ sortBy: 'name:asc' })
.expect(200);
expect(response.body.data.data.map((store: DataStore) => store.name)).toEqual([
'A Data Store',
'M Data Store',
'Z Data Store',
expect(response.body.data.data.map((store: DataTable) => store.name)).toEqual([
'A Data Table',
'M Data Table',
'Z Data Table',
]);
});
test('should sort data stores by name descending', async () => {
await createDataStore(ownerProject, { name: 'Z Data Store' });
await createDataStore(ownerProject, { name: 'A Data Store' });
await createDataStore(ownerProject, { name: 'M Data Store' });
test('should sort data tables by name descending', async () => {
await createDataTable(ownerProject, { name: 'Z Data Table' });
await createDataTable(ownerProject, { name: 'A Data Table' });
await createDataTable(ownerProject, { name: 'M Data Table' });
const response = await authOwnerAgent
.get('/data-tables-global')
.query({ sortBy: 'name:desc' })
.expect(200);
expect(response.body.data.data.map((f: DataStore) => f.name)).toEqual([
'Z Data Store',
'M Data Store',
'A Data Store',
expect(response.body.data.data.map((f: DataTable) => f.name)).toEqual([
'Z Data Table',
'M Data Table',
'A Data Table',
]);
});
test('should sort data stores by updatedAt', async () => {
await createDataStore(ownerProject, {
name: 'Older Data Store',
test('should sort data tables by updatedAt', async () => {
await createDataTable(ownerProject, {
name: 'Older Data Table',
updatedAt: DateTime.now().minus({ days: 2 }).toJSDate(),
});
await createDataStore(ownerProject, {
name: 'Newest Data Store',
await createDataTable(ownerProject, {
name: 'Newest Data Table',
updatedAt: DateTime.now().toJSDate(),
});
await createDataStore(ownerProject, {
name: 'Middle Data Store',
await createDataTable(ownerProject, {
name: 'Middle Data Table',
updatedAt: DateTime.now().minus({ days: 1 }).toJSDate(),
});
@ -311,16 +310,16 @@ describe('GET /data-tables-global', () => {
.query({ sortBy: 'updatedAt:desc' })
.expect(200);
expect(response.body.data.data.map((f: DataStore) => f.name)).toEqual([
'Newest Data Store',
'Middle Data Store',
'Older Data Store',
expect(response.body.data.data.map((f: DataTable) => f.name)).toEqual([
'Newest Data Table',
'Middle Data Table',
'Older Data Table',
]);
});
test('should combine multiple query parameters correctly', async () => {
const dataStore1 = await createDataStore(ownerProject, { name: 'Test Data Store' });
await createDataStore(ownerProject, { name: 'Another Data Store' });
const dataStore1 = await createDataTable(ownerProject, { name: 'Test Data Table' });
await createDataTable(ownerProject, { name: 'Another Data Table' });
const response = await authOwnerAgent
.get('/data-tables-global')
@ -329,12 +328,12 @@ describe('GET /data-tables-global', () => {
expect(response.body.data.count).toBe(1);
expect(response.body.data.data).toHaveLength(1);
expect(response.body.data.data[0].name).toBe('Test Data Store');
expect(response.body.data.data[0].name).toBe('Test Data Table');
});
test('should include columns', async () => {
await createDataStore(ownerProject, {
name: 'Test Data Store',
await createDataTable(ownerProject, {
name: 'Test Data Table',
columns: [
{
name: 'test_column_1',

View File

@ -14,8 +14,8 @@ import { mock } from 'jest-mock-extended';
import { createUser } from '@test-integration/db/users';
import { DataStoreAggregateService } from '../data-store-aggregate.service';
import { DataStoreService } from '../data-store.service';
import { DataTableAggregateService } from '../data-table-aggregate.service';
import { DataTableService } from '../data-table.service';
beforeAll(async () => {
await testModules.loadModules(['data-table']);
@ -31,15 +31,15 @@ afterAll(async () => {
});
describe('dataStoreAggregate', () => {
let dataStoreService: DataStoreService;
let dataStoreAggregateService: DataStoreAggregateService;
let dataStoreService: DataTableService;
let dataStoreAggregateService: DataTableAggregateService;
const manager = mock<EntityManager>();
const projectRelationRepository = mock<ProjectRelationRepository>({ manager });
beforeAll(() => {
Container.set(ProjectRelationRepository, projectRelationRepository);
dataStoreAggregateService = Container.get(DataStoreAggregateService);
dataStoreService = Container.get(DataStoreService);
dataStoreAggregateService = Container.get(DataTableAggregateService);
dataStoreService = Container.get(DataTableService);
});
let user: User;
@ -53,18 +53,18 @@ describe('dataStoreAggregate', () => {
});
afterEach(async () => {
// Clean up any created user data stores
await dataStoreService.deleteDataStoreAll();
// Clean up any created user data tables
await dataStoreService.deleteDataTableAll();
});
describe('getManyAndCount', () => {
it('should return the correct data stores for the user', async () => {
it('should return the correct data tables for the user', async () => {
// ARRANGE
const ds1 = await dataStoreService.createDataStore(project1.id, {
const ds1 = await dataStoreService.createDataTable(project1.id, {
name: 'store1',
columns: [],
});
const ds2 = await dataStoreService.createDataStore(project1.id, {
const ds2 = await dataStoreService.createDataTable(project1.id, {
name: 'store2',
columns: [],
});
@ -92,7 +92,7 @@ describe('dataStoreAggregate', () => {
},
]);
await dataStoreService.createDataStore(project2.id, {
await dataStoreService.createDataTable(project2.id, {
name: 'store3',
columns: [],
});
@ -118,7 +118,7 @@ describe('dataStoreAggregate', () => {
// ARRANGE
const currentUser = await createUser({ role: GLOBAL_MEMBER_ROLE });
await dataStoreService.createDataStore(project1.id, {
await dataStoreService.createDataTable(project1.id, {
name: 'store1',
columns: [],
});
@ -135,13 +135,13 @@ describe('dataStoreAggregate', () => {
expect(result.count).toBe(0);
});
it('should return only the data store matching the given data store id filter', async () => {
it('should return only the data table matching the given data table id filter', async () => {
// ARRANGE
await dataStoreService.createDataStore(project1.id, {
await dataStoreService.createDataTable(project1.id, {
name: 'store1',
columns: [],
});
const ds2 = await dataStoreService.createDataStore(project1.id, {
const ds2 = await dataStoreService.createDataTable(project1.id, {
name: 'store2',
columns: [],
});
@ -182,15 +182,15 @@ describe('dataStoreAggregate', () => {
it('should respect pagination (skip/take)', async () => {
// ARRANGE
const ds1 = await dataStoreService.createDataStore(project1.id, {
const ds1 = await dataStoreService.createDataTable(project1.id, {
name: 'store1',
columns: [],
});
const ds2 = await dataStoreService.createDataStore(project1.id, {
const ds2 = await dataStoreService.createDataTable(project1.id, {
name: 'store2',
columns: [],
});
const ds3 = await dataStoreService.createDataStore(project1.id, {
const ds3 = await dataStoreService.createDataTable(project1.id, {
name: 'store3',
columns: [],
});

View File

@ -3,18 +3,18 @@ import { testDb, testModules } from '@n8n/backend-test-utils';
import type { Project } from '@n8n/db';
import { mock } from 'jest-mock-extended';
import type {
AddDataStoreColumnOptions,
AddDataTableColumnOptions,
INode,
ListDataStoreRowsOptions,
MoveDataStoreColumnOptions,
UpsertDataStoreRowOptions,
ListDataTableRowsOptions,
MoveDataTableColumnOptions,
UpsertDataTableRowOptions,
Workflow,
} from 'n8n-workflow';
import type { OwnershipService } from '@/services/ownership.service';
import { DataStoreProxyService } from '../data-store-proxy.service';
import type { DataStoreService } from '../data-store.service';
import { DataTableProxyService } from '../data-table-proxy.service';
import type { DataTableService } from '../data-table.service';
const PROJECT_ID = 'project-id';
@ -22,23 +22,23 @@ beforeAll(async () => {
await testModules.loadModules(['data-table']);
await testDb.init();
});
describe('DataStoreProxyService', () => {
let dataStoreServiceMock = mock<DataStoreService>();
describe('DataTableProxyService', () => {
let dataTableServiceMock = mock<DataTableService>();
let ownershipServiceMock = mock<OwnershipService>();
let loggerMock = mock<Logger>();
let dataStoreProxyService: DataStoreProxyService;
let dataStoreProxyService: DataTableProxyService;
let workflow: Workflow;
let node: INode;
let project: Project;
beforeEach(() => {
dataStoreServiceMock = mock<DataStoreService>();
dataTableServiceMock = mock<DataTableService>();
ownershipServiceMock = mock<OwnershipService>();
loggerMock = mock<Logger>();
dataStoreProxyService = new DataStoreProxyService(
dataStoreServiceMock,
dataStoreProxyService = new DataTableProxyService(
dataTableServiceMock,
ownershipServiceMock,
loggerMock,
);
@ -60,103 +60,103 @@ describe('DataStoreProxyService', () => {
it('should call getManyAndCount with correct parameters', async () => {
const options = { filter: { name: 'test' } };
const aggregateOperations = await dataStoreProxyService.getDataStoreAggregateProxy(
const aggregateOperations = await dataStoreProxyService.getDataTableAggregateProxy(
workflow,
node,
);
await aggregateOperations.getManyAndCount(options);
expect(dataStoreServiceMock.getManyAndCount).toBeCalledWith({
expect(dataTableServiceMock.getManyAndCount).toBeCalledWith({
filter: { name: 'test', projectId: PROJECT_ID },
});
});
it('should call createDataStore with correct parameters', async () => {
const options = { name: 'newDataStore', columns: [] };
it('should call createDataTable with correct parameters', async () => {
const options = { name: 'newDataTable', columns: [] };
const aggregateOperations = await dataStoreProxyService.getDataStoreAggregateProxy(
const aggregateOperations = await dataStoreProxyService.getDataTableAggregateProxy(
workflow,
node,
);
await aggregateOperations.createDataStore(options);
await aggregateOperations.createDataTable(options);
expect(dataStoreServiceMock.createDataStore).toBeCalledWith(PROJECT_ID, options);
expect(dataTableServiceMock.createDataTable).toBeCalledWith(PROJECT_ID, options);
});
it('should call deleteDataStoreByProject when proxy calls deleteDataStoreAll', async () => {
const aggregateOperations = await dataStoreProxyService.getDataStoreAggregateProxy(
it('should call deleteDataTableByProject when proxy calls deleteDataTableAll', async () => {
const aggregateOperations = await dataStoreProxyService.getDataTableAggregateProxy(
workflow,
node,
);
await aggregateOperations.deleteDataStoreAll();
await aggregateOperations.deleteDataTableAll();
expect(dataStoreServiceMock.deleteDataStoreByProjectId).toBeCalledWith(PROJECT_ID);
expect(dataTableServiceMock.deleteDataTableByProjectId).toBeCalledWith(PROJECT_ID);
});
});
it('should call updateDataStore with correct parameters', async () => {
const options = { name: 'updatedDataStore' };
it('should call updateDataTable with correct parameters', async () => {
const options = { name: 'updatedDataTable' };
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.updateDataStore(options);
await dataStoreOperations.updateDataTable(options);
expect(dataStoreServiceMock.updateDataStore).toBeCalledWith(
expect(dataTableServiceMock.updateDataTable).toBeCalledWith(
'dataStore-id',
PROJECT_ID,
options,
);
});
it('should call deleteDataStore with correct parameters', async () => {
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
it('should call deleteDataTable with correct parameters', async () => {
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.deleteDataStore();
await dataStoreOperations.deleteDataTable();
expect(dataStoreServiceMock.deleteDataStore).toBeCalledWith('dataStore-id', PROJECT_ID);
expect(dataTableServiceMock.deleteDataTable).toBeCalledWith('dataStore-id', PROJECT_ID);
});
it('should call getColumns with correct parameters', async () => {
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.getColumns();
expect(dataStoreServiceMock.getColumns).toBeCalledWith('dataStore-id', PROJECT_ID);
expect(dataTableServiceMock.getColumns).toBeCalledWith('dataStore-id', PROJECT_ID);
});
it('should call addColumn with correct parameters', async () => {
const options: AddDataStoreColumnOptions = { name: 'newColumn', type: 'string' };
const options: AddDataTableColumnOptions = { name: 'newColumn', type: 'string' };
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.addColumn(options);
expect(dataStoreServiceMock.addColumn).toBeCalledWith('dataStore-id', PROJECT_ID, options);
expect(dataTableServiceMock.addColumn).toBeCalledWith('dataStore-id', PROJECT_ID, options);
});
it('should call moveColumn with correct parameters', async () => {
const columnId = 'column-id';
const options: MoveDataStoreColumnOptions = { targetIndex: 1 };
const options: MoveDataTableColumnOptions = { targetIndex: 1 };
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.moveColumn(columnId, options);
expect(dataStoreServiceMock.moveColumn).toBeCalledWith(
expect(dataTableServiceMock.moveColumn).toBeCalledWith(
'dataStore-id',
PROJECT_ID,
columnId,
@ -167,32 +167,32 @@ describe('DataStoreProxyService', () => {
it('should call deleteColumn with correct parameters', async () => {
const columnId = 'column-id';
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.deleteColumn(columnId);
expect(dataStoreServiceMock.deleteColumn).toBeCalledWith('dataStore-id', PROJECT_ID, columnId);
expect(dataTableServiceMock.deleteColumn).toBeCalledWith('dataStore-id', PROJECT_ID, columnId);
});
it('should call getManyRowsAndCount with correct parameters', async () => {
const options: ListDataStoreRowsOptions = {
const options: ListDataTableRowsOptions = {
filter: {
filters: [{ columnName: 'x', condition: 'eq', value: 'testRow' }],
type: 'and',
},
};
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.getManyRowsAndCount(options);
expect(dataStoreServiceMock.getManyRowsAndCount).toBeCalledWith(
expect(dataTableServiceMock.getManyRowsAndCount).toBeCalledWith(
'dataStore-id',
PROJECT_ID,
options,
@ -202,14 +202,14 @@ describe('DataStoreProxyService', () => {
it('should call insertRows with correct parameters', async () => {
const rows = [{ id: 1, name: 'row1' }];
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.insertRows(rows, 'count');
expect(dataStoreServiceMock.insertRows).toBeCalledWith(
expect(dataTableServiceMock.insertRows).toBeCalledWith(
'dataStore-id',
PROJECT_ID,
rows,
@ -218,7 +218,7 @@ describe('DataStoreProxyService', () => {
});
it('should call upsertRow with correct parameters', async () => {
const options: UpsertDataStoreRowOptions = {
const options: UpsertDataTableRowOptions = {
filter: {
filters: [{ columnName: 'name', condition: 'eq', value: 'test' }],
type: 'and',
@ -226,14 +226,14 @@ describe('DataStoreProxyService', () => {
data: { name: 'newName' },
};
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.upsertRow(options);
expect(dataStoreServiceMock.upsertRow).toBeCalledWith(
expect(dataTableServiceMock.upsertRow).toBeCalledWith(
'dataStore-id',
PROJECT_ID,
options,
@ -243,7 +243,7 @@ describe('DataStoreProxyService', () => {
});
it('should call upsertRow dry run with correct parameters', async () => {
const options: UpsertDataStoreRowOptions = {
const options: UpsertDataTableRowOptions = {
filter: {
filters: [{ columnName: 'name', condition: 'eq', value: 'test' }],
type: 'and',
@ -252,14 +252,14 @@ describe('DataStoreProxyService', () => {
dryRun: true,
};
const dataStoreOperations = await dataStoreProxyService.getDataStoreProxy(
const dataStoreOperations = await dataStoreProxyService.getDataTableProxy(
workflow,
node,
'dataStore-id',
);
await dataStoreOperations.upsertRow(options);
expect(dataStoreServiceMock.upsertRow).toBeCalledWith(
expect(dataTableServiceMock.upsertRow).toBeCalledWith(
'dataStore-id',
PROJECT_ID,
options,

View File

@ -1,10 +1,10 @@
import { mockInstance } from '@n8n/backend-test-utils';
import { GlobalConfig } from '@n8n/config';
import { DataStoreSizeValidator } from '../data-store-size-validator.service';
import { DataTableSizeValidator } from '../data-table-size-validator.service';
describe('DataStoreSizeValidator', () => {
let validator: DataStoreSizeValidator;
describe('DataTableSizeValidator', () => {
let validator: DataTableSizeValidator;
let fetchSizeFn: jest.Mock;
const globalConfig = mockInstance(GlobalConfig, {
dataTable: {
@ -14,7 +14,7 @@ describe('DataStoreSizeValidator', () => {
},
});
beforeEach(() => {
validator = new DataStoreSizeValidator(globalConfig);
validator = new DataTableSizeValidator(globalConfig);
fetchSizeFn = jest.fn();
});
@ -40,7 +40,7 @@ describe('DataStoreSizeValidator', () => {
await expect(
validator.validateSize(fetchSizeFn, new Date('2024-01-01T00:00:00Z')),
).rejects.toThrow('Data store size limit exceeded: 150MB used, limit is 100MB');
).rejects.toThrow('Data table size limit exceeded: 150MB used, limit is 100MB');
});
it('should throw error when size equals limit', async () => {
@ -48,7 +48,7 @@ describe('DataStoreSizeValidator', () => {
await expect(
validator.validateSize(fetchSizeFn, new Date('2024-01-01T00:00:00Z')),
).rejects.toThrow('Data store size limit exceeded: 100MB used, limit is 100MB');
).rejects.toThrow('Data table size limit exceeded: 100MB used, limit is 100MB');
});
});
@ -95,13 +95,13 @@ describe('DataStoreSizeValidator', () => {
const time1 = new Date('2024-01-01T00:00:00Z');
await expect(validator.validateSize(fetchSizeFn, time1)).rejects.toThrow(
'Data store size limit exceeded: 100MB used, limit is 100MB',
'Data table size limit exceeded: 100MB used, limit is 100MB',
);
// Subsequent calls within cache duration should also fail
const time2 = new Date('2024-01-01T00:00:00.500Z');
await expect(validator.validateSize(fetchSizeFn, time2)).rejects.toThrow(
'Data store size limit exceeded: 100MB used, limit is 100MB',
'Data table size limit exceeded: 100MB used, limit is 100MB',
);
// Size was only fetched once
@ -161,13 +161,13 @@ describe('DataStoreSizeValidator', () => {
// All should fail with the same error
await expect(promise1).rejects.toThrow(
'Data store size limit exceeded: 150MB used, limit is 100MB',
'Data table size limit exceeded: 150MB used, limit is 100MB',
);
await expect(promise2).rejects.toThrow(
'Data store size limit exceeded: 150MB used, limit is 100MB',
'Data table size limit exceeded: 150MB used, limit is 100MB',
);
await expect(promise3).rejects.toThrow(
'Data store size limit exceeded: 150MB used, limit is 100MB',
'Data table size limit exceeded: 150MB used, limit is 100MB',
);
// Should only fetch once
@ -219,13 +219,13 @@ describe('DataStoreSizeValidator', () => {
fetchSizeFn.mockResolvedValueOnce({ totalBytes: 100 * 1024 * 1024, dataTables: {} });
const time3 = new Date('2024-01-01T00:00:01.001Z');
await expect(validator.validateSize(fetchSizeFn, time3)).rejects.toThrow(
'Data store size limit exceeded: 100MB used, limit is 100MB',
'Data table size limit exceeded: 100MB used, limit is 100MB',
);
// Subsequent calls use the cached "full" state and continue to fail correctly
const time4 = new Date('2024-01-01T00:00:01.500Z');
await expect(validator.validateSize(fetchSizeFn, time4)).rejects.toThrow(
'Data store size limit exceeded: 100MB used, limit is 100MB',
'Data table size limit exceeded: 100MB used, limit is 100MB',
);
expect(fetchSizeFn).toHaveBeenCalledTimes(2);

View File

@ -5,10 +5,10 @@ import { Container } from '@n8n/di';
import { createUser } from '@test-integration/db/users';
import { DataStoreSizeValidator } from '../data-store-size-validator.service';
import { DataStoreRepository } from '../data-store.repository';
import { DataStoreService } from '../data-store.service';
import { DataStoreValidationError } from '../errors/data-store-validation.error';
import { DataTableSizeValidator } from '../data-table-size-validator.service';
import { DataTableRepository } from '../data-table.repository';
import { DataTableService } from '../data-table.service';
import { DataTableValidationError } from '../errors/data-table-validation.error';
beforeAll(async () => {
await testModules.loadModules(['data-table']);
@ -16,11 +16,11 @@ beforeAll(async () => {
});
beforeEach(async () => {
const dataStoreService = Container.get(DataStoreService);
await dataStoreService.deleteDataStoreAll();
const dataStoreService = Container.get(DataTableService);
await dataStoreService.deleteDataTableAll();
await testDb.truncate(['DataTable', 'DataTableColumn']);
const dataStoreSizeValidator = Container.get(DataStoreSizeValidator);
const dataStoreSizeValidator = Container.get(DataTableSizeValidator);
dataStoreSizeValidator.reset();
});
@ -28,13 +28,13 @@ afterAll(async () => {
await testDb.terminate();
});
describe('Data Store Size Tests', () => {
let dataStoreService: DataStoreService;
let dataStoreRepository: DataStoreRepository;
describe('Data Table Size Tests', () => {
let dataStoreService: DataTableService;
let dataStoreRepository: DataTableRepository;
beforeAll(() => {
dataStoreService = Container.get(DataStoreService);
dataStoreRepository = Container.get(DataStoreRepository);
dataStoreService = Container.get(DataTableService);
dataStoreRepository = Container.get(DataTableRepository);
});
let project1: Project;
@ -46,12 +46,12 @@ describe('Data Store Size Tests', () => {
describe('size validation', () => {
it('should prevent insertRows when size limit exceeded', async () => {
// ARRANGE
const dataStoreSizeValidator = Container.get(DataStoreSizeValidator);
const dataStoreSizeValidator = Container.get(DataTableSizeValidator);
dataStoreSizeValidator.reset();
const maxSize = Container.get(GlobalConfig).dataTable.maxSize;
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
const { id: dataStoreId } = await dataStoreService.createDataTable(project1.id, {
name: 'dataStore',
columns: [{ name: 'data', type: 'string' }],
});
@ -63,7 +63,7 @@ describe('Data Store Size Tests', () => {
// ACT & ASSERT
await expect(
dataStoreService.insertRows(dataStoreId, project1.id, [{ data: 'test' }]),
).rejects.toThrow(DataStoreValidationError);
).rejects.toThrow(DataTableValidationError);
expect(mockFindDataTablesSize).toHaveBeenCalled();
mockFindDataTablesSize.mockRestore();
@ -71,12 +71,12 @@ describe('Data Store Size Tests', () => {
it('should prevent updateRows when size limit exceeded', async () => {
// ARRANGE
const dataStoreSizeValidator = Container.get(DataStoreSizeValidator);
const dataStoreSizeValidator = Container.get(DataTableSizeValidator);
dataStoreSizeValidator.reset();
const maxSize = Container.get(GlobalConfig).dataTable.maxSize;
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
const { id: dataStoreId } = await dataStoreService.createDataTable(project1.id, {
name: 'dataStore',
columns: [{ name: 'data', type: 'string' }],
});
@ -95,7 +95,7 @@ describe('Data Store Size Tests', () => {
},
data: { data: 'updated' },
}),
).rejects.toThrow(DataStoreValidationError);
).rejects.toThrow(DataTableValidationError);
expect(mockFindDataTablesSize).toHaveBeenCalled();
mockFindDataTablesSize.mockRestore();
@ -106,7 +106,7 @@ describe('Data Store Size Tests', () => {
const maxSize = Container.get(GlobalConfig).dataTable.maxSize;
const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, {
const { id: dataStoreId } = await dataStoreService.createDataTable(project1.id, {
name: 'dataStore',
columns: [{ name: 'data', type: 'string' }],
});
@ -124,7 +124,7 @@ describe('Data Store Size Tests', () => {
},
data: { data: 'new' },
}),
).rejects.toThrow(DataStoreValidationError);
).rejects.toThrow(DataTableValidationError);
expect(mockFindDataTablesSize).toHaveBeenCalled();
mockFindDataTablesSize.mockRestore();
@ -150,12 +150,12 @@ describe('Data Store Size Tests', () => {
it('should return all data tables for admin user', async () => {
// ARRANGE
const dataStore1 = await dataStoreService.createDataStore(project1.id, {
const dataStore1 = await dataStoreService.createDataTable(project1.id, {
name: 'project1-dataStore',
columns: [{ name: 'data', type: 'string' }],
});
const dataStore2 = await dataStoreService.createDataStore(project2.id, {
const dataStore2 = await dataStoreService.createDataTable(project2.id, {
name: 'project2-dataStore',
columns: [{ name: 'data', type: 'string' }],
});
@ -201,12 +201,12 @@ describe('Data Store Size Tests', () => {
// ARRANGE
await linkUserToProject(regularUser, project1, 'project:viewer');
const dataStore1 = await dataStoreService.createDataStore(project1.id, {
const dataStore1 = await dataStoreService.createDataTable(project1.id, {
name: 'accessible-dataStore',
columns: [{ name: 'data', type: 'string' }],
});
const dataStore2 = await dataStoreService.createDataStore(project2.id, {
const dataStore2 = await dataStoreService.createDataTable(project2.id, {
name: 'inaccessible-dataStore',
columns: [{ name: 'data', type: 'string' }],
});
@ -233,12 +233,12 @@ describe('Data Store Size Tests', () => {
it('should return empty dataTables but full totalBytes when user has no project access', async () => {
// ARRANGE
const dataStore1 = await dataStoreService.createDataStore(project1.id, {
const dataStore1 = await dataStoreService.createDataTable(project1.id, {
name: 'inaccessible-dataStore1',
columns: [{ name: 'data', type: 'string' }],
});
const dataStore2 = await dataStoreService.createDataStore(project2.id, {
const dataStore2 = await dataStoreService.createDataTable(project2.id, {
name: 'inaccessible-dataStore2',
columns: [{ name: 'data', type: 'string' }],
});
@ -271,8 +271,8 @@ describe('Data Store Size Tests', () => {
it('should handle data tables with no rows', async () => {
// ARRANGE
const dataStore = await dataStoreService.createDataStore(project1.id, {
name: 'emptyDataStore',
const dataStore = await dataStoreService.createDataTable(project1.id, {
name: 'emptyDataTable',
columns: [{ name: 'data', type: 'string' }],
});
@ -285,7 +285,7 @@ describe('Data Store Size Tests', () => {
expect(result.dataTables).toBeDefined();
expect(result.dataTables[dataStore.id]).toBeDefined();
expect(result.dataTables[dataStore.id].sizeBytes).toBeGreaterThanOrEqual(0);
expect(result.dataTables[dataStore.id].name).toBe('emptyDataStore');
expect(result.dataTables[dataStore.id].name).toBe('emptyDataTable');
});
});
@ -303,7 +303,7 @@ describe('Data Store Size Tests', () => {
it('should cache data globally and filter based on user permissions', async () => {
// ARRANGE
const dataStore = await dataStoreService.createDataStore(project1.id, {
const dataStore = await dataStoreService.createDataTable(project1.id, {
name: 'test-dataStore',
columns: [{ name: 'data', type: 'string' }],
});
@ -332,8 +332,8 @@ describe('Data Store Size Tests', () => {
it('should use global cache for both data fetching and size validation', async () => {
// ARRANGE
const dataStoreSizeValidator = Container.get(DataStoreSizeValidator);
const dataStore = await dataStoreService.createDataStore(project1.id, {
const dataStoreSizeValidator = Container.get(DataTableSizeValidator);
const dataStore = await dataStoreService.createDataTable(project1.id, {
name: 'test-dataStore',
columns: [{ name: 'data', type: 'string' }],
});
@ -366,8 +366,8 @@ describe('Data Store Size Tests', () => {
it('should invalidate cache on data modifications', async () => {
// ARRANGE
const dataStoreSizeValidator = Container.get(DataStoreSizeValidator);
const dataStore = await dataStoreService.createDataStore(project1.id, {
const dataStoreSizeValidator = Container.get(DataTableSizeValidator);
const dataStore = await dataStoreService.createDataTable(project1.id, {
name: 'test-dataStore',
columns: [{ name: 'data', type: 'string' }],
});

View File

@ -1,9 +1,9 @@
import { Container } from '@n8n/di';
import { DataStoreSizeValidator } from '../data-store-size-validator.service';
import { DataTableSizeValidator } from '../data-table-size-validator.service';
export function mockDataStoreSizeValidator() {
const sizeValidator = Container.get(DataStoreSizeValidator);
export function mockDataTableSizeValidator() {
const sizeValidator = Container.get(DataTableSizeValidator);
jest.spyOn(sizeValidator, 'validateSize').mockResolvedValue();
jest.spyOn(sizeValidator, 'getCachedSizeData').mockResolvedValue({
totalBytes: 50 * 1024 * 1024, // 50MB - under the default limit

View File

@ -1,23 +1,23 @@
import { ListDataStoreQueryDto } from '@n8n/api-types';
import { ListDataTableQueryDto } from '@n8n/api-types';
import { AuthenticatedRequest } from '@n8n/db';
import { Get, GlobalScope, Query, RestController } from '@n8n/decorators';
import { DataStoreAggregateService } from './data-store-aggregate.service';
import { DataStoreService } from './data-store.service';
import { DataTableAggregateService } from './data-table-aggregate.service';
import { DataTableService } from './data-table.service';
@RestController('/data-tables-global')
export class DataStoreAggregateController {
export class DataTableAggregateController {
constructor(
private readonly dataStoreAggregateService: DataStoreAggregateService,
private readonly dataStoreService: DataStoreService,
private readonly dataStoreAggregateService: DataTableAggregateService,
private readonly dataStoreService: DataTableService,
) {}
@Get('/')
@GlobalScope('dataStore:list')
async listDataStores(
async listDataTables(
req: AuthenticatedRequest,
_res: Response,
@Query payload: ListDataStoreQueryDto,
@Query payload: ListDataTableQueryDto,
) {
return await this.dataStoreAggregateService.getManyAndCount(req.user, payload);
}

View File

@ -1,16 +1,16 @@
import type { ListDataStoreQueryDto } from '@n8n/api-types';
import type { ListDataTableQueryDto } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import { User } from '@n8n/db';
import { Service } from '@n8n/di';
import { ProjectService } from '@/services/project.service.ee';
import { DataStoreRepository } from './data-store.repository';
import { DataTableRepository } from './data-table.repository';
@Service()
export class DataStoreAggregateService {
export class DataTableAggregateService {
constructor(
private readonly dataStoreRepository: DataStoreRepository,
private readonly dataTableRepository: DataTableRepository,
private readonly projectService: ProjectService,
private readonly logger: Logger,
) {
@ -19,7 +19,7 @@ export class DataStoreAggregateService {
async start() {}
async shutdown() {}
async getManyAndCount(user: User, options: ListDataStoreQueryDto) {
async getManyAndCount(user: User, options: ListDataTableQueryDto) {
const projects = await this.projectService.getProjectRelationsForUser(user);
let projectIds = projects.map((x) => x.projectId);
if (options.filter?.projectId) {
@ -31,7 +31,7 @@ export class DataStoreAggregateService {
return { count: 0, data: [] };
}
return await this.dataStoreRepository.getManyAndCount({
return await this.dataTableRepository.getManyAndCount({
...options,
filter: {
...options.filter,

View File

@ -1,4 +1,4 @@
import { DataStoreCreateColumnSchema } from '@n8n/api-types';
import { DataTableCreateColumnSchema } from '@n8n/api-types';
import { withTransaction } from '@n8n/db';
import { Service } from '@n8n/di';
import { DataSource, EntityManager, Repository } from '@n8n/typeorm';
@ -8,18 +8,18 @@ import {
UnexpectedError,
} from 'n8n-workflow';
import { DataStoreRowsRepository } from './data-store-rows.repository';
import { DataTableColumn } from './data-table-column.entity';
import { DataTableRowsRepository } from './data-table-rows.repository';
import { DataTable } from './data-table.entity';
import { DataStoreColumnNameConflictError } from './errors/data-store-column-name-conflict.error';
import { DataStoreSystemColumnNameConflictError } from './errors/data-store-system-column-name-conflict.error';
import { DataStoreValidationError } from './errors/data-store-validation.error';
import { DataTableColumnNameConflictError } from './errors/data-table-column-name-conflict.error';
import { DataTableSystemColumnNameConflictError } from './errors/data-table-system-column-name-conflict.error';
import { DataTableValidationError } from './errors/data-table-validation.error';
@Service()
export class DataStoreColumnRepository extends Repository<DataTableColumn> {
export class DataTableColumnRepository extends Repository<DataTableColumn> {
constructor(
dataSource: DataSource,
private dataStoreRowsRepository: DataStoreRowsRepository,
private dataTableRowsRepository: DataTableRowsRepository,
) {
super(DataTableColumn, dataSource.manager);
}
@ -44,13 +44,13 @@ export class DataStoreColumnRepository extends Repository<DataTableColumn> {
);
}
async addColumn(dataTableId: string, schema: DataStoreCreateColumnSchema, trx?: EntityManager) {
async addColumn(dataTableId: string, schema: DataTableCreateColumnSchema, trx?: EntityManager) {
return await withTransaction(this.manager, trx, async (em) => {
if (DATA_TABLE_SYSTEM_COLUMNS.includes(schema.name)) {
throw new DataStoreSystemColumnNameConflictError(schema.name);
throw new DataTableSystemColumnNameConflictError(schema.name);
}
if (schema.name === DATA_TABLE_SYSTEM_TESTING_COLUMN) {
throw new DataStoreSystemColumnNameConflictError(schema.name, 'testing');
throw new DataTableSystemColumnNameConflictError(schema.name, 'testing');
}
const existingColumnMatch = await em.existsBy(DataTableColumn, {
@ -61,9 +61,9 @@ export class DataStoreColumnRepository extends Repository<DataTableColumn> {
if (existingColumnMatch) {
const dataTable = await em.findOneBy(DataTable, { id: dataTableId });
if (!dataTable) {
throw new UnexpectedError('Data store not found');
throw new UnexpectedError('Data table not found');
}
throw new DataStoreColumnNameConflictError(schema.name, dataTable.name);
throw new DataTableColumnNameConflictError(schema.name, dataTable.name);
}
if (schema.index === undefined) {
@ -81,7 +81,7 @@ export class DataStoreColumnRepository extends Repository<DataTableColumn> {
// @ts-ignore Workaround for intermittent typecheck issue with _QueryDeepPartialEntity
await em.insert(DataTableColumn, column);
await this.dataStoreRowsRepository.addColumn(
await this.dataTableRowsRepository.addColumn(
dataTableId,
column,
em.connection.options.type,
@ -92,17 +92,17 @@ export class DataStoreColumnRepository extends Repository<DataTableColumn> {
});
}
async deleteColumn(dataStoreId: string, column: DataTableColumn, trx?: EntityManager) {
async deleteColumn(dataTableId: string, column: DataTableColumn, trx?: EntityManager) {
await withTransaction(this.manager, trx, async (em) => {
await em.remove(DataTableColumn, column);
await this.dataStoreRowsRepository.dropColumnFromTable(
dataStoreId,
await this.dataTableRowsRepository.dropColumnFromTable(
dataTableId,
column.name,
em.connection.options.type,
em,
);
await this.shiftColumns(dataStoreId, column.index, -1, em);
await this.shiftColumns(dataTableId, column.index, -1, em);
});
}
@ -116,11 +116,11 @@ export class DataStoreColumnRepository extends Repository<DataTableColumn> {
const columnCount = await em.countBy(DataTableColumn, { dataTableId });
if (targetIndex < 0) {
throw new DataStoreValidationError('tried to move column to negative index');
throw new DataTableValidationError('tried to move column to negative index');
}
if (targetIndex >= columnCount) {
throw new DataStoreValidationError(
throw new DataTableValidationError(
'tried to move column to an index larger than column count',
);
}

View File

@ -1,30 +1,30 @@
import type { DataStoreListOptions } from '@n8n/api-types';
import type { DataTableListOptions } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import { Service } from '@n8n/di';
import {
AddDataStoreColumnOptions,
CreateDataStoreOptions,
DataStore,
DataStoreColumn,
DataStoreProxyProvider,
DataStoreRows,
AddDataTableColumnOptions,
CreateDataTableOptions,
DataTable,
DataTableColumn,
DataTableProxyProvider,
DataTableRows,
DeleteDataTableRowsOptions,
IDataStoreProjectAggregateService,
IDataStoreProjectService,
IDataTableProjectAggregateService,
IDataTableProjectService,
DataTableInsertRowsReturnType,
INode,
ListDataStoreOptions,
ListDataStoreRowsOptions,
MoveDataStoreColumnOptions,
UpdateDataStoreOptions,
UpdateDataStoreRowOptions,
UpsertDataStoreRowOptions,
ListDataTableOptions,
ListDataTableRowsOptions,
MoveDataTableColumnOptions,
UpdateDataTableOptions,
UpdateDataTableRowOptions,
UpsertDataTableRowOptions,
Workflow,
} from 'n8n-workflow';
import { OwnershipService } from '@/services/ownership.service';
import { DataStoreService } from './data-store.service';
import { DataTableService } from './data-table.service';
const ALLOWED_NODES = [
'n8n-nodes-base.dataTable',
@ -40,9 +40,9 @@ export function isAllowedNode(s: string): s is AllowedNode {
}
@Service()
export class DataStoreProxyService implements DataStoreProxyProvider {
export class DataTableProxyService implements DataTableProxyProvider {
constructor(
private readonly dataStoreService: DataStoreService,
private readonly dataStoreService: DataTableService,
private readonly ownershipService: OwnershipService,
private readonly logger: Logger,
) {
@ -60,80 +60,80 @@ export class DataStoreProxyService implements DataStoreProxyProvider {
return homeProject.id;
}
async getDataStoreAggregateProxy(
async getDataTableAggregateProxy(
workflow: Workflow,
node: INode,
projectId?: string,
): Promise<IDataStoreProjectAggregateService> {
): Promise<IDataTableProjectAggregateService> {
this.validateRequest(node);
projectId = projectId ?? (await this.getProjectId(workflow));
return this.makeAggregateOperations(projectId);
}
async getDataStoreProxy(
async getDataTableProxy(
workflow: Workflow,
node: INode,
dataStoreId: string,
projectId?: string,
): Promise<IDataStoreProjectService> {
): Promise<IDataTableProjectService> {
this.validateRequest(node);
projectId = projectId ?? (await this.getProjectId(workflow));
return this.makeDataStoreOperations(projectId, dataStoreId);
return this.makeDataTableOperations(projectId, dataStoreId);
}
private makeAggregateOperations(projectId: string): IDataStoreProjectAggregateService {
private makeAggregateOperations(projectId: string): IDataTableProjectAggregateService {
const dataStoreService = this.dataStoreService;
return {
getProjectId() {
return projectId;
},
async getManyAndCount(options: ListDataStoreOptions = {}) {
const serviceOptions: DataStoreListOptions = {
async getManyAndCount(options: ListDataTableOptions = {}) {
const serviceOptions: DataTableListOptions = {
...options,
filter: { projectId, ...(options.filter ?? {}) },
};
return await dataStoreService.getManyAndCount(serviceOptions);
},
async createDataStore(options: CreateDataStoreOptions): Promise<DataStore> {
return await dataStoreService.createDataStore(projectId, options);
async createDataTable(options: CreateDataTableOptions): Promise<DataTable> {
return await dataStoreService.createDataTable(projectId, options);
},
async deleteDataStoreAll(): Promise<boolean> {
return await dataStoreService.deleteDataStoreByProjectId(projectId);
async deleteDataTableAll(): Promise<boolean> {
return await dataStoreService.deleteDataTableByProjectId(projectId);
},
};
}
private makeDataStoreOperations(
private makeDataTableOperations(
projectId: string,
dataStoreId: string,
): Omit<IDataStoreProjectService, keyof IDataStoreProjectAggregateService> {
): Omit<IDataTableProjectService, keyof IDataTableProjectAggregateService> {
const dataStoreService = this.dataStoreService;
return {
// DataStore management
async updateDataStore(options: UpdateDataStoreOptions): Promise<boolean> {
return await dataStoreService.updateDataStore(dataStoreId, projectId, options);
// DataTable management
async updateDataTable(options: UpdateDataTableOptions): Promise<boolean> {
return await dataStoreService.updateDataTable(dataStoreId, projectId, options);
},
async deleteDataStore(): Promise<boolean> {
return await dataStoreService.deleteDataStore(dataStoreId, projectId);
async deleteDataTable(): Promise<boolean> {
return await dataStoreService.deleteDataTable(dataStoreId, projectId);
},
// Column operations
async getColumns(): Promise<DataStoreColumn[]> {
async getColumns(): Promise<DataTableColumn[]> {
return await dataStoreService.getColumns(dataStoreId, projectId);
},
async addColumn(options: AddDataStoreColumnOptions): Promise<DataStoreColumn> {
async addColumn(options: AddDataTableColumnOptions): Promise<DataTableColumn> {
return await dataStoreService.addColumn(dataStoreId, projectId, options);
},
async moveColumn(columnId: string, options: MoveDataStoreColumnOptions): Promise<boolean> {
async moveColumn(columnId: string, options: MoveDataTableColumnOptions): Promise<boolean> {
return await dataStoreService.moveColumn(dataStoreId, projectId, columnId, options);
},
@ -142,18 +142,18 @@ export class DataStoreProxyService implements DataStoreProxyProvider {
},
// Row operations
async getManyRowsAndCount(options: Partial<ListDataStoreRowsOptions>) {
async getManyRowsAndCount(options: Partial<ListDataTableRowsOptions>) {
return await dataStoreService.getManyRowsAndCount(dataStoreId, projectId, options);
},
async insertRows<T extends DataTableInsertRowsReturnType>(
rows: DataStoreRows,
rows: DataTableRows,
returnType: T,
) {
return await dataStoreService.insertRows(dataStoreId, projectId, rows, returnType);
},
async updateRows(options: UpdateDataStoreRowOptions) {
async updateRows(options: UpdateDataTableRowOptions) {
return await dataStoreService.updateRows(
dataStoreId,
projectId,
@ -163,7 +163,7 @@ export class DataStoreProxyService implements DataStoreProxyProvider {
);
},
async upsertRow(options: UpsertDataStoreRowOptions) {
async upsertRow(options: UpsertDataTableRowOptions) {
return await dataStoreService.upsertRow(
dataStoreId,
projectId,

View File

@ -1,4 +1,4 @@
import { ListDataStoreContentQueryDto, DataTableFilter } from '@n8n/api-types';
import { DataTableFilter, ListDataTableContentQueryDto } from '@n8n/api-types';
import { CreateTable, DslColumn, withTransaction } from '@n8n/db';
import { Service } from '@n8n/di';
import {
@ -12,18 +12,17 @@ import {
DeleteQueryBuilder,
} from '@n8n/typeorm';
import {
DataStoreColumnJsType,
DataStoreRows,
DataStoreRowReturn,
DataTableColumnJsType,
DataTableRows,
DataTableRowReturn,
UnexpectedError,
DataStoreRowsReturn,
DataTableRowsReturn,
DATA_TABLE_SYSTEM_COLUMNS,
DataTableInsertRowsReturnType,
DataTableInsertRowsResult,
DataStoreRowReturnWithState,
DataTableRowReturnWithState,
} from 'n8n-workflow';
import { DataStoreUserTableName } from './data-store.types';
import { DataTableColumn } from './data-table-column.entity';
import {
addColumnQuery,
@ -38,6 +37,7 @@ import {
toSqliteGlobFromPercent,
toTableName,
} from './utils/sql-utils';
import { DataTableUserTableName } from './data-table.types';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type QueryBuilder = SelectQueryBuilder<any>;
@ -153,12 +153,12 @@ function getConditionAndParams(
}
@Service()
export class DataStoreRowsRepository {
export class DataTableRowsRepository {
constructor(private dataSource: DataSource) {}
async insertRowsBulk(
table: DataStoreUserTableName,
rows: DataStoreRows,
table: DataTableUserTableName,
rows: DataTableRows,
columns: DataTableColumn[],
trx?: EntityManager,
) {
@ -192,9 +192,9 @@ export class DataStoreRowsRepository {
if (endExclusive <= start) break;
const completeRows = new Array<DataStoreColumnJsType[]>(endExclusive - start);
const completeRows = new Array<DataTableColumnJsType[]>(endExclusive - start);
for (let j = start; j < endExclusive; ++j) {
const insertArray: DataStoreColumnJsType[] = [];
const insertArray: DataTableColumnJsType[] = [];
for (let h = 0; h < columnNames.length; ++h) {
const column = columns[h];
@ -218,25 +218,25 @@ export class DataStoreRowsRepository {
}
async insertRows<T extends DataTableInsertRowsReturnType>(
dataStoreId: string,
rows: DataStoreRows,
dataTableId: string,
rows: DataTableRows,
columns: DataTableColumn[],
returnType: T,
trx?: EntityManager,
): Promise<DataTableInsertRowsResult<T>>;
async insertRows<T extends DataTableInsertRowsReturnType>(
dataStoreId: string,
rows: DataStoreRows,
dataTableId: string,
rows: DataTableRows,
columns: DataTableColumn[],
returnType: T,
trx?: EntityManager,
): Promise<DataTableInsertRowsResult> {
return await withTransaction(this.dataSource.manager, trx, async (em) => {
const inserted: Array<Pick<DataStoreRowReturn, 'id'>> = [];
const inserted: Array<Pick<DataTableRowReturn, 'id'>> = [];
const dbType = this.dataSource.options.type;
const useReturning = dbType === 'postgres' || dbType === 'mariadb';
const table = toTableName(dataStoreId);
const table = toTableName(dataTableId);
const escapedColumns = columns.map((c) => this.dataSource.driver.escape(c.name));
const escapedSystemColumns = DATA_TABLE_SYSTEM_COLUMNS.map((x) =>
this.dataSource.driver.escape(x),
@ -289,7 +289,7 @@ export class DataStoreRowsRepository {
continue;
}
const insertedRows = await this.getManyByIds(dataStoreId, ids, columns, em);
const insertedRows = await this.getManyByIds(dataTableId, ids, columns, em);
inserted.push(...insertedRows);
}
@ -299,16 +299,16 @@ export class DataStoreRowsRepository {
}
async updateRows<T extends boolean | undefined>(
dataStoreId: string,
data: Record<string, DataStoreColumnJsType | null>,
dataTableId: string,
data: Record<string, DataTableColumnJsType | null>,
filter: DataTableFilter,
columns: DataTableColumn[],
returnData?: T,
trx?: EntityManager,
): Promise<T extends true ? DataStoreRowReturn[] : true>;
): Promise<T extends true ? DataTableRowReturn[] : true>;
async updateRows(
dataStoreId: string,
data: Record<string, DataStoreColumnJsType | null>,
dataTableId: string,
data: Record<string, DataTableColumnJsType | null>,
filter: DataTableFilter,
columns: DataTableColumn[],
returnData: boolean = false,
@ -318,7 +318,7 @@ export class DataStoreRowsRepository {
const dbType = this.dataSource.options.type;
const useReturning = dbType === 'postgres';
const table = toTableName(dataStoreId);
const table = toTableName(dataTableId);
const escapedColumns = columns.map((c) => this.dataSource.driver.escape(c.name));
const escapedSystemColumns = DATA_TABLE_SYSTEM_COLUMNS.map((x) =>
this.dataSource.driver.escape(x),
@ -326,11 +326,11 @@ export class DataStoreRowsRepository {
const selectColumns = [...escapedSystemColumns, ...escapedColumns];
const setData = this.prepareUpdateData(data, columns, dbType);
let affectedRows: Array<Pick<DataStoreRowReturn, 'id'>> = [];
let affectedRows: Array<Pick<DataTableRowReturn, 'id'>> = [];
if (!useReturning && returnData) {
// Only Postgres supports RETURNING statement on updates (with our typeorm),
// on other engines we must query the list of updates rows later by ID
affectedRows = await this.getAffectedRowsForUpdate(dataStoreId, filter, columns, true, trx);
affectedRows = await this.getAffectedRowsForUpdate(dataTableId, filter, columns, true, trx);
}
setData.updatedAt = normalizeValue(new Date(), 'date', dbType);
@ -355,21 +355,21 @@ export class DataStoreRowsRepository {
}
const ids = affectedRows.map((row) => row.id);
return await this.getManyByIds(dataStoreId, ids, columns, em);
return await this.getManyByIds(dataTableId, ids, columns, em);
});
}
async dryRunUpdateRows(
dataStoreId: string,
data: Record<string, DataStoreColumnJsType | null>,
dataTableId: string,
data: Record<string, DataTableColumnJsType | null>,
filter: DataTableFilter,
columns: DataTableColumn[],
trx?: EntityManager,
): Promise<DataStoreRowReturnWithState[]> {
): Promise<DataTableRowReturnWithState[]> {
const dbType = this.dataSource.options.type;
const beforeRows = await this.getAffectedRowsForUpdate(
dataStoreId,
dataTableId,
filter,
columns,
false,
@ -385,13 +385,13 @@ export class DataStoreRowsRepository {
}
async dryRunUpsertRow(
dataStoreId: string,
data: Record<string, DataStoreColumnJsType | null>,
dataTableId: string,
data: Record<string, DataTableColumnJsType | null>,
filter: DataTableFilter,
columns: DataTableColumn[],
trx?: EntityManager,
): Promise<DataStoreRowReturnWithState[]> {
const updateResult = await this.dryRunUpdateRows(dataStoreId, data, filter, columns, trx);
): Promise<DataTableRowReturnWithState[]> {
const updateResult = await this.dryRunUpdateRows(dataTableId, data, filter, columns, trx);
if (updateResult.length > 0) {
return updateResult;
@ -401,7 +401,7 @@ export class DataStoreRowsRepository {
const dbType = this.dataSource.options.type;
const now = new Date();
const preparedData = this.prepareUpdateData(data, columns, dbType);
const insertedRow: DataStoreRowReturn = {
const insertedRow: DataTableRowReturn = {
id: 0, // Placeholder ID for dry run
createdAt: now,
updatedAt: now,
@ -446,7 +446,7 @@ export class DataStoreRowsRepository {
return true;
}
let affectedRows: DataStoreRowReturn[] = [];
let affectedRows: DataTableRowReturn[] = [];
if (!useReturning) {
const selectQuery = em.createQueryBuilder().select('*').from(table, 'dataTable');
@ -455,7 +455,7 @@ export class DataStoreRowsRepository {
this.applyFilters(selectQuery, filter, 'dataTable', columns);
}
const rawRows = await selectQuery.getRawMany<DataStoreRowReturn>();
const rawRows = await selectQuery.getRawMany<DataTableRowReturn>();
affectedRows = normalizeRows(rawRows, columns);
}
@ -490,18 +490,18 @@ export class DataStoreRowsRepository {
}
private async getAffectedRowsForUpdate<T extends boolean>(
dataStoreId: string,
dataTableId: string,
filter: DataTableFilter,
columns: DataTableColumn[],
idsOnly: T,
trx?: EntityManager,
): Promise<T extends true ? Array<Pick<DataStoreRowReturn, 'id'>> : DataStoreRowReturn[]> {
): Promise<T extends true ? Array<Pick<DataTableRowReturn, 'id'>> : DataTableRowReturn[]> {
return await withTransaction(this.dataSource.manager, trx, async (em) => {
const table = toTableName(dataStoreId);
const table = toTableName(dataTableId);
const selectColumns = idsOnly ? 'id' : '*';
const selectQuery = em.createQueryBuilder().select(selectColumns).from(table, 'dataTable');
this.applyFilters(selectQuery, filter, 'dataTable', columns);
const rawRows: DataStoreRowsReturn = await selectQuery.getRawMany();
const rawRows: DataTableRowsReturn = await selectQuery.getRawMany();
if (idsOnly) {
return rawRows;
@ -512,10 +512,10 @@ export class DataStoreRowsRepository {
}
private prepareUpdateData(
data: Record<string, DataStoreColumnJsType | null>,
data: Record<string, DataTableColumnJsType | null>,
columns: DataTableColumn[],
dbType: DataSourceOptions['type'],
): Record<string, DataStoreColumnJsType | null> {
): Record<string, DataTableColumnJsType | null> {
const setData = { ...data };
for (const column of columns) {
if (column.name in setData) {
@ -526,9 +526,9 @@ export class DataStoreRowsRepository {
}
private toDryRunRows(
beforeState: DataStoreRowReturn | null,
afterState: DataStoreRowReturn | null,
): DataStoreRowReturnWithState[] {
beforeState: DataTableRowReturn | null,
afterState: DataTableRowReturn | null,
): DataTableRowReturnWithState[] {
if (beforeState === null && afterState === null) {
throw new Error('Both before and after rows cannot be null');
}
@ -559,7 +559,7 @@ export class DataStoreRowsRepository {
}
async createTableWithColumns(
dataStoreId: string,
dataTableId: string,
columns: DataTableColumn[],
trx?: EntityManager,
) {
@ -569,7 +569,7 @@ export class DataStoreRowsRepository {
}
const dslColumns = [new DslColumn('id').int.autoGenerate2.primary, ...toDslColumns(columns)];
const createTable = new CreateTable(toTableName(dataStoreId), '', em.queryRunner).withColumns(
const createTable = new CreateTable(toTableName(dataTableId), '', em.queryRunner).withColumns(
...dslColumns,
).withTimestamps;
@ -577,40 +577,40 @@ export class DataStoreRowsRepository {
});
}
async dropTable(dataStoreId: string, trx?: EntityManager) {
async dropTable(dataTableId: string, trx?: EntityManager) {
await withTransaction(this.dataSource.manager, trx, async (em) => {
if (!em.queryRunner) {
throw new UnexpectedError('QueryRunner is not available');
}
await em.queryRunner.dropTable(toTableName(dataStoreId), true);
await em.queryRunner.dropTable(toTableName(dataTableId), true);
});
}
async addColumn(
dataStoreId: string,
dataTableId: string,
column: DataTableColumn,
dbType: DataSourceOptions['type'],
trx?: EntityManager,
) {
await withTransaction(this.dataSource.manager, trx, async (em) => {
await em.query(addColumnQuery(toTableName(dataStoreId), column, dbType));
await em.query(addColumnQuery(toTableName(dataTableId), column, dbType));
});
}
async dropColumnFromTable(
dataStoreId: string,
dataTableId: string,
columnName: string,
dbType: DataSourceOptions['type'],
trx?: EntityManager,
) {
await withTransaction(this.dataSource.manager, trx, async (em) => {
await em.query(deleteColumnQuery(toTableName(dataStoreId), columnName, dbType));
await em.query(deleteColumnQuery(toTableName(dataTableId), columnName, dbType));
});
}
async getManyAndCount(
dataStoreId: string,
dto: ListDataStoreContentQueryDto,
dataTableId: string,
dto: ListDataTableContentQueryDto,
columns?: DataTableColumn[],
trx?: EntityManager,
) {
@ -618,8 +618,8 @@ export class DataStoreRowsRepository {
this.dataSource.manager,
trx,
async (em) => {
const [countQuery, query] = this.getManyQuery(dataStoreId, dto, em, columns);
const data: DataStoreRowsReturn = await query.select('*').getRawMany();
const [countQuery, query] = this.getManyQuery(dataTableId, dto, em, columns);
const data: DataTableRowsReturn = await query.select('*').getRawMany();
const countResult = await countQuery.select('COUNT(*) as count').getRawOne<{
count: number | string | null;
}>();
@ -634,7 +634,7 @@ export class DataStoreRowsRepository {
}
async getManyByIds(
dataStoreId: string,
dataTableId: string,
ids: number[],
columns: DataTableColumn[],
trx?: EntityManager,
@ -643,7 +643,7 @@ export class DataStoreRowsRepository {
this.dataSource.manager,
trx,
async (em) => {
const table = toTableName(dataStoreId);
const table = toTableName(dataTableId);
const escapedColumns = columns.map((c) => this.dataSource.driver.escape(c.name));
const escapedSystemColumns = DATA_TABLE_SYSTEM_COLUMNS.map((x) =>
this.dataSource.driver.escape(x),
@ -659,7 +659,7 @@ export class DataStoreRowsRepository {
.select(selectColumns)
.from(table, 'dataTable')
.where({ id: In(ids) })
.getRawMany<DataStoreRowReturn>();
.getRawMany<DataTableRowReturn>();
return normalizeRows(rows, columns);
},
@ -668,15 +668,15 @@ export class DataStoreRowsRepository {
}
private getManyQuery(
dataStoreId: string,
dto: ListDataStoreContentQueryDto,
dataTableId: string,
dto: ListDataTableContentQueryDto,
em: EntityManager,
columns?: DataTableColumn[],
): [QueryBuilder, QueryBuilder] {
const query = em.createQueryBuilder();
const tableReference = 'dataTable';
query.from(toTableName(dataStoreId), tableReference);
query.from(toTableName(dataTableId), tableReference);
if (dto.filter) {
this.applyFilters(query, dto.filter, tableReference, columns);
}
@ -716,7 +716,7 @@ export class DataStoreRowsRepository {
}
}
private applySorting(query: QueryBuilder, dto: ListDataStoreContentQueryDto): void {
private applySorting(query: QueryBuilder, dto: ListDataTableContentQueryDto): void {
if (!dto.sortBy) {
return;
}
@ -731,7 +731,7 @@ export class DataStoreRowsRepository {
query.orderBy(quotedField, direction);
}
private applyPagination(query: QueryBuilder, dto: ListDataStoreContentQueryDto): void {
private applyPagination(query: QueryBuilder, dto: ListDataTableContentQueryDto): void {
query.skip(dto.skip ?? 0);
if (dto.take) query.take(dto.take);
}

View File

@ -2,10 +2,10 @@ import { GlobalConfig } from '@n8n/config';
import { Service } from '@n8n/di';
import { DataTableSizeStatus, DataTablesSizeData } from 'n8n-workflow';
import { DataStoreValidationError } from './errors/data-store-validation.error';
import { DataTableValidationError } from './errors/data-table-validation.error';
@Service()
export class DataStoreSizeValidator {
export class DataTableSizeValidator {
private lastCheck: Date | undefined;
private cachedSizeData: DataTablesSizeData | undefined;
private pendingCheck: Promise<DataTablesSizeData> | null = null;
@ -53,8 +53,8 @@ export class DataStoreSizeValidator {
): Promise<void> {
const size = await this.getCachedSizeData(fetchSizeFn, now);
if (size.totalBytes >= this.globalConfig.dataTable.maxSize) {
throw new DataStoreValidationError(
`Data store size limit exceeded: ${this.toMb(size.totalBytes)}MB used, limit is ${this.toMb(this.globalConfig.dataTable.maxSize)}MB`,
throw new DataTableValidationError(
`Data table size limit exceeded: ${this.toMb(size.totalBytes)}MB used, limit is ${this.toMb(this.globalConfig.dataTable.maxSize)}MB`,
);
}
}

View File

@ -1,14 +1,14 @@
import {
AddDataStoreRowsDto,
AddDataStoreColumnDto,
CreateDataStoreDto,
AddDataTableRowsDto,
AddDataTableColumnDto,
CreateDataTableDto,
DeleteDataTableRowsDto,
ListDataStoreContentQueryDto,
ListDataStoreQueryDto,
MoveDataStoreColumnDto,
UpdateDataStoreDto,
ListDataTableContentQueryDto,
ListDataTableQueryDto,
MoveDataTableColumnDto,
UpdateDataTableDto,
UpdateDataTableRowDto,
UpsertDataStoreRowDto,
UpsertDataTableRowDto,
} from '@n8n/api-types';
import { AuthenticatedRequest } from '@n8n/db';
import {
@ -22,38 +22,38 @@ import {
Query,
RestController,
} from '@n8n/decorators';
import { DataStoreRowReturn } from 'n8n-workflow';
import { DataTableRowReturn } from 'n8n-workflow';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ConflictError } from '@/errors/response-errors/conflict.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { DataStoreService } from './data-store.service';
import { DataStoreColumnNameConflictError } from './errors/data-store-column-name-conflict.error';
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 { DataStoreSystemColumnNameConflictError } from './errors/data-store-system-column-name-conflict.error';
import { DataStoreValidationError } from './errors/data-store-validation.error';
import { DataTableService } from './data-table.service';
import { DataTableColumnNameConflictError } from './errors/data-table-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';
import { DataTableSystemColumnNameConflictError } from './errors/data-table-system-column-name-conflict.error';
import { DataTableValidationError } from './errors/data-table-validation.error';
@RestController('/projects/:projectId/data-tables')
export class DataStoreController {
constructor(private readonly dataStoreService: DataStoreService) {}
export class DataTableController {
constructor(private readonly dataTableService: DataTableService) {}
@Post('/')
@ProjectScope('dataStore:create')
async createDataStore(
async createDataTable(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Body dto: CreateDataStoreDto,
@Body dto: CreateDataTableDto,
) {
try {
return await this.dataStoreService.createDataStore(req.params.projectId, dto);
return await this.dataTableService.createDataTable(req.params.projectId, dto);
} catch (e: unknown) {
if (!(e instanceof Error)) {
throw e;
} else if (e instanceof DataStoreNameConflictError) {
} else if (e instanceof DataTableNameConflictError) {
throw new ConflictError(e.message);
} else {
throw new InternalServerError(e.message, e);
@ -63,32 +63,32 @@ export class DataStoreController {
@Get('/')
@ProjectScope('dataStore:listProject')
async listProjectDataStores(
async listProjectDataTables(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Query payload: ListDataStoreQueryDto,
@Query payload: ListDataTableQueryDto,
) {
const providedFilter = payload?.filter ?? {};
return await this.dataStoreService.getManyAndCount({
return await this.dataTableService.getManyAndCount({
...payload,
filter: { ...providedFilter, projectId: req.params.projectId },
});
}
@Patch('/:dataStoreId')
@Patch('/:dataTableId')
@ProjectScope('dataStore:update')
async updateDataStore(
async updateDataTable(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Body dto: UpdateDataStoreDto,
@Param('dataTableId') dataTableId: string,
@Body dto: UpdateDataTableDto,
) {
try {
return await this.dataStoreService.updateDataStore(dataStoreId, req.params.projectId, dto);
return await this.dataTableService.updateDataTable(dataTableId, req.params.projectId, dto);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) {
if (e instanceof DataTableNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof DataStoreNameConflictError) {
} else if (e instanceof DataTableNameConflictError) {
throw new ConflictError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);
@ -98,17 +98,17 @@ export class DataStoreController {
}
}
@Delete('/:dataStoreId')
@Delete('/:dataTableId')
@ProjectScope('dataStore:delete')
async deleteDataStore(
async deleteDataTable(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Param('dataTableId') dataTableId: string,
) {
try {
return await this.dataStoreService.deleteDataStore(dataStoreId, req.params.projectId);
return await this.dataTableService.deleteDataTable(dataTableId, req.params.projectId);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) {
if (e instanceof DataTableNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);
@ -118,17 +118,17 @@ export class DataStoreController {
}
}
@Get('/:dataStoreId/columns')
@Get('/:dataTableId/columns')
@ProjectScope('dataStore:read')
async getColumns(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Param('dataTableId') dataTableId: string,
) {
try {
return await this.dataStoreService.getColumns(dataStoreId, req.params.projectId);
return await this.dataTableService.getColumns(dataTableId, req.params.projectId);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) {
if (e instanceof DataTableNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);
@ -138,22 +138,22 @@ export class DataStoreController {
}
}
@Post('/:dataStoreId/columns')
@Post('/:dataTableId/columns')
@ProjectScope('dataStore:update')
async addColumn(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Body dto: AddDataStoreColumnDto,
@Param('dataTableId') dataTableId: string,
@Body dto: AddDataTableColumnDto,
) {
try {
return await this.dataStoreService.addColumn(dataStoreId, req.params.projectId, dto);
return await this.dataTableService.addColumn(dataTableId, req.params.projectId, dto);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) {
if (e instanceof DataTableNotFoundError) {
throw new NotFoundError(e.message);
} else if (
e instanceof DataStoreColumnNameConflictError ||
e instanceof DataStoreSystemColumnNameConflictError
e instanceof DataTableColumnNameConflictError ||
e instanceof DataTableSystemColumnNameConflictError
) {
throw new ConflictError(e.message);
} else if (e instanceof Error) {
@ -164,18 +164,18 @@ export class DataStoreController {
}
}
@Delete('/:dataStoreId/columns/:columnId')
@Delete('/:dataTableId/columns/:columnId')
@ProjectScope('dataStore:update')
async deleteColumn(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Param('dataTableId') dataTableId: string,
@Param('columnId') columnId: string,
) {
try {
return await this.dataStoreService.deleteColumn(dataStoreId, req.params.projectId, columnId);
return await this.dataTableService.deleteColumn(dataTableId, req.params.projectId, columnId);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError || e instanceof DataStoreColumnNotFoundError) {
if (e instanceof DataTableNotFoundError || e instanceof DataTableColumnNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);
@ -185,26 +185,26 @@ export class DataStoreController {
}
}
@Patch('/:dataStoreId/columns/:columnId/move')
@Patch('/:dataTableId/columns/:columnId/move')
@ProjectScope('dataStore:update')
async moveColumn(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Param('dataTableId') dataTableId: string,
@Param('columnId') columnId: string,
@Body dto: MoveDataStoreColumnDto,
@Body dto: MoveDataTableColumnDto,
) {
try {
return await this.dataStoreService.moveColumn(
dataStoreId,
return await this.dataTableService.moveColumn(
dataTableId,
req.params.projectId,
columnId,
dto,
);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError || e instanceof DataStoreColumnNotFoundError) {
if (e instanceof DataTableNotFoundError || e instanceof DataTableColumnNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof DataStoreValidationError) {
} else if (e instanceof DataTableValidationError) {
throw new BadRequestError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);
@ -214,22 +214,22 @@ export class DataStoreController {
}
}
@Get('/:dataStoreId/rows')
@Get('/:dataTableId/rows')
@ProjectScope('dataStore:readRow')
async getDataStoreRows(
async getDataTableRows(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Query dto: ListDataStoreContentQueryDto,
@Param('dataTableId') dataTableId: string,
@Query dto: ListDataTableContentQueryDto,
) {
try {
return await this.dataStoreService.getManyRowsAndCount(
dataStoreId,
return await this.dataTableService.getManyRowsAndCount(
dataTableId,
req.params.projectId,
dto,
);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) {
if (e instanceof DataTableNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);
@ -242,31 +242,31 @@ export class DataStoreController {
/**
* @returns the IDs of the inserted rows
*/
async appendDataStoreRows<T extends DataStoreRowReturn | undefined>(
async appendDataTableRows<T extends DataTableRowReturn | undefined>(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
dataStoreId: string,
dto: AddDataStoreRowsDto & { returnType?: T },
): Promise<Array<T extends true ? DataStoreRowReturn : Pick<DataStoreRowReturn, 'id'>>>;
@Post('/:dataStoreId/insert')
dataTableId: string,
dto: AddDataTableRowsDto & { returnType?: T },
): Promise<Array<T extends true ? DataTableRowReturn : Pick<DataTableRowReturn, 'id'>>>;
@Post('/:dataTableId/insert')
@ProjectScope('dataStore:writeRow')
async appendDataStoreRows(
async appendDataTableRows(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Body dto: AddDataStoreRowsDto,
@Param('dataTableId') dataTableId: string,
@Body dto: AddDataTableRowsDto,
) {
try {
return await this.dataStoreService.insertRows(
dataStoreId,
return await this.dataTableService.insertRows(
dataTableId,
req.params.projectId,
dto.data,
dto.returnType,
);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) {
if (e instanceof DataTableNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof DataStoreValidationError) {
} else if (e instanceof DataTableValidationError) {
throw new BadRequestError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);
@ -276,20 +276,20 @@ export class DataStoreController {
}
}
@Post('/:dataStoreId/upsert')
@Post('/:dataTableId/upsert')
@ProjectScope('dataStore:writeRow')
async upsertDataStoreRow(
async upsertDataTableRow(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Body dto: UpsertDataStoreRowDto,
@Param('dataTableId') dataTableId: string,
@Body dto: UpsertDataTableRowDto,
) {
try {
// because of strict overloads, we need separate paths
const dryRun = dto.dryRun;
if (dryRun) {
return await this.dataStoreService.upsertRow(
dataStoreId,
return await this.dataTableService.upsertRow(
dataTableId,
req.params.projectId,
dto,
true, // we want to always return data for dry runs
@ -299,8 +299,8 @@ export class DataStoreController {
const returnData = dto.returnData;
if (returnData) {
return await this.dataStoreService.upsertRow(
dataStoreId,
return await this.dataTableService.upsertRow(
dataTableId,
req.params.projectId,
dto,
returnData,
@ -308,17 +308,17 @@ export class DataStoreController {
);
}
return await this.dataStoreService.upsertRow(
dataStoreId,
return await this.dataTableService.upsertRow(
dataTableId,
req.params.projectId,
dto,
returnData,
dryRun,
);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) {
if (e instanceof DataTableNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof DataStoreValidationError) {
} else if (e instanceof DataTableValidationError) {
throw new BadRequestError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);
@ -328,20 +328,20 @@ export class DataStoreController {
}
}
@Patch('/:dataStoreId/rows')
@Patch('/:dataTableId/rows')
@ProjectScope('dataStore:writeRow')
async updateDataStoreRows(
async updateDataTableRows(
req: AuthenticatedRequest<{ projectId: string }>,
_res: Response,
@Param('dataStoreId') dataStoreId: string,
@Param('dataTableId') dataTableId: string,
@Body dto: UpdateDataTableRowDto,
) {
try {
// because of strict overloads, we need separate paths
const dryRun = dto.dryRun;
if (dryRun) {
return await this.dataStoreService.updateRows(
dataStoreId,
return await this.dataTableService.updateRows(
dataTableId,
req.params.projectId,
dto,
true, // we want to always return data for dry runs
@ -351,8 +351,8 @@ export class DataStoreController {
const returnData = dto.returnData;
if (returnData) {
return await this.dataStoreService.updateRows(
dataStoreId,
return await this.dataTableService.updateRows(
dataTableId,
req.params.projectId,
dto,
returnData,
@ -360,17 +360,17 @@ export class DataStoreController {
);
}
return await this.dataStoreService.updateRows(
dataStoreId,
return await this.dataTableService.updateRows(
dataTableId,
req.params.projectId,
dto,
returnData,
dryRun,
);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) {
if (e instanceof DataTableNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof DataStoreValidationError) {
} else if (e instanceof DataTableValidationError) {
throw new BadRequestError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);
@ -389,16 +389,16 @@ export class DataStoreController {
@Query dto: DeleteDataTableRowsDto,
) {
try {
return await this.dataStoreService.deleteRows(
return await this.dataTableService.deleteRows(
dataTableId,
req.params.projectId,
dto,
dto.returnData,
);
} catch (e: unknown) {
if (e instanceof DataStoreNotFoundError) {
if (e instanceof DataTableNotFoundError) {
throw new NotFoundError(e.message);
} else if (e instanceof DataStoreValidationError) {
} else if (e instanceof DataTableValidationError) {
throw new BadRequestError(e.message);
} else if (e instanceof Error) {
throw new InternalServerError(e.message, e);

View File

@ -5,23 +5,23 @@ import { Container } from '@n8n/di';
@BackendModule({ name: 'data-table' })
export class DataStoreModule implements ModuleInterface {
async init() {
await import('./data-store.controller');
await import('./data-store-aggregate.controller');
await import('./data-table.controller');
await import('./data-table-aggregate.controller');
const { DataStoreService } = await import('./data-store.service');
await Container.get(DataStoreService).start();
const { DataTableService } = await import('./data-table.service');
await Container.get(DataTableService).start();
const { DataStoreAggregateService } = await import('./data-store-aggregate.service');
await Container.get(DataStoreAggregateService).start();
const { DataTableAggregateService } = await import('./data-table-aggregate.service');
await Container.get(DataTableAggregateService).start();
}
@OnShutdown()
async shutdown() {
const { DataStoreService } = await import('./data-store.service');
await Container.get(DataStoreService).shutdown();
const { DataTableService } = await import('./data-table.service');
await Container.get(DataTableService).shutdown();
const { DataStoreAggregateService } = await import('./data-store-aggregate.service');
await Container.get(DataStoreAggregateService).start();
const { DataTableAggregateService } = await import('./data-table-aggregate.service');
await Container.get(DataTableAggregateService).shutdown();
}
async entities() {
@ -32,8 +32,8 @@ export class DataStoreModule implements ModuleInterface {
}
async context() {
const { DataStoreProxyService } = await import('./data-store-proxy.service');
const { DataTableProxyService } = await import('./data-table-proxy.service');
return { dataStoreProxyProvider: Container.get(DataStoreProxyService) };
return { dataTableProxyProvider: Container.get(DataTableProxyService) };
}
}

View File

@ -1,7 +1,7 @@
import {
DATA_STORE_COLUMN_ERROR_MESSAGE,
type DataStoreCreateColumnSchema,
type ListDataStoreQueryDto,
DATA_TABLE_COLUMN_ERROR_MESSAGE,
type DataTableCreateColumnSchema,
type ListDataTableQueryDto,
} from '@n8n/api-types';
import { GlobalConfig } from '@n8n/config';
import { Project, withTransaction } from '@n8n/db';
@ -10,40 +10,40 @@ import { DataSource, EntityManager, Repository, SelectQueryBuilder } from '@n8n/
import { UnexpectedError } from 'n8n-workflow';
import type { DataTableInfo, DataTablesSizeData } from 'n8n-workflow';
import { DataStoreRowsRepository } from './data-store-rows.repository';
import { DataStoreUserTableName } from './data-store.types';
import { DataTableColumn } from './data-table-column.entity';
import { DataTableRowsRepository } from './data-table-rows.repository';
import { DataTable } from './data-table.entity';
import { DataStoreNameConflictError } from './errors/data-store-name-conflict.error';
import { DataStoreValidationError } from './errors/data-store-validation.error';
import { DataTableUserTableName } from './data-table.types';
import { DataTableNameConflictError } from './errors/data-table-name-conflict.error';
import { DataTableValidationError } from './errors/data-table-validation.error';
import { isValidColumnName, toTableId, toTableName } from './utils/sql-utils';
@Service()
export class DataStoreRepository extends Repository<DataTable> {
export class DataTableRepository extends Repository<DataTable> {
constructor(
dataSource: DataSource,
private dataStoreRowsRepository: DataStoreRowsRepository,
private dataTableRowsRepository: DataTableRowsRepository,
private readonly globalConfig: GlobalConfig,
) {
super(DataTable, dataSource.manager);
}
async createDataStore(
async createDataTable(
projectId: string,
name: string,
columns: DataStoreCreateColumnSchema[],
columns: DataTableCreateColumnSchema[],
trx?: EntityManager,
) {
return await withTransaction(this.manager, trx, async (em) => {
if (columns.some((c) => !isValidColumnName(c.name))) {
throw new DataStoreValidationError(DATA_STORE_COLUMN_ERROR_MESSAGE);
throw new DataTableValidationError(DATA_TABLE_COLUMN_ERROR_MESSAGE);
}
const dataStore = em.create(DataTable, { name, columns, projectId });
const dataTable = em.create(DataTable, { name, columns, projectId });
// @ts-ignore Workaround for intermittent typecheck issue with _QueryDeepPartialEntity
await em.insert(DataTable, dataStore);
const dataTableId = dataStore.id;
await em.insert(DataTable, dataTable);
const dataTableId = dataTable.id;
// insert columns
const columnEntities = columns.map((col, index) =>
@ -61,30 +61,30 @@ export class DataStoreRepository extends Repository<DataTable> {
}
// create user table (will create empty table with just id column if no columns)
await this.dataStoreRowsRepository.createTableWithColumns(dataTableId, columnEntities, em);
await this.dataTableRowsRepository.createTableWithColumns(dataTableId, columnEntities, em);
if (!dataTableId) {
throw new UnexpectedError('Data store creation failed');
throw new UnexpectedError('Data table creation failed');
}
const createdDataStore = await em.findOneOrFail(DataTable, {
const createdDataTable = await em.findOneOrFail(DataTable, {
where: { id: dataTableId },
relations: ['project', 'columns'],
});
return createdDataStore;
return createdDataTable;
});
}
async deleteDataStore(dataStoreId: string, trx?: EntityManager) {
async deleteDataTable(dataTableId: string, trx?: EntityManager) {
return await withTransaction(this.manager, trx, async (em) => {
await em.delete(DataTable, { id: dataStoreId });
await this.dataStoreRowsRepository.dropTable(dataStoreId, em);
await em.delete(DataTable, { id: dataTableId });
await this.dataTableRowsRepository.dropTable(dataTableId, em);
return true;
});
}
async transferDataStoreByProjectId(
async transferDataTableByProjectId(
fromProjectId: string,
toProjectId: string,
trx?: EntityManager,
@ -112,8 +112,8 @@ export class DataStoreRepository extends Repository<DataTable> {
});
if (stillHasNameClash) {
throw new DataStoreNameConflictError(
`Failed to transfer data store "${existing.name}" to the target project "${toProjectId}". A data table with the same name already exists in the target project.`,
throw new DataTableNameConflictError(
`Failed to transfer data table "${existing.name}" to the target project "${toProjectId}". A data table with the same name already exists in the target project.`,
);
}
}
@ -126,13 +126,13 @@ export class DataStoreRepository extends Repository<DataTable> {
});
}
async deleteDataStoreByProjectId(projectId: string, trx?: EntityManager) {
async deleteDataTableByProjectId(projectId: string, trx?: EntityManager) {
return await withTransaction(this.manager, trx, async (em) => {
const existingTables = await em.findBy(DataTable, { projectId });
let changed = false;
for (const match of existingTables) {
const result = await this.deleteDataStore(match.id, em);
const result = await this.deleteDataTable(match.id, em);
changed = changed || result;
}
@ -140,14 +140,14 @@ export class DataStoreRepository extends Repository<DataTable> {
});
}
async deleteDataStoreAll(trx?: EntityManager) {
async deleteDataTableAll(trx?: EntityManager) {
return await withTransaction(this.manager, trx, async (em) => {
const existingTables = await em.findBy(DataTable, {});
let changed = false;
for (const match of existingTables) {
const result = await em.delete(DataTable, { id: match.id });
await this.dataStoreRowsRepository.dropTable(match.id, em);
await this.dataTableRowsRepository.dropTable(match.id, em);
changed = changed || (result.affected ?? 0) > 0;
}
@ -155,19 +155,19 @@ export class DataStoreRepository extends Repository<DataTable> {
});
}
async getManyAndCount(options: Partial<ListDataStoreQueryDto>) {
async getManyAndCount(options: Partial<ListDataTableQueryDto>) {
const query = this.getManyQuery(options);
const [data, count] = await query.getManyAndCount();
return { count, data };
}
async getMany(options: Partial<ListDataStoreQueryDto>) {
async getMany(options: Partial<ListDataTableQueryDto>) {
const query = this.getManyQuery(options);
return await query.getMany();
}
private getManyQuery(options: Partial<ListDataStoreQueryDto>): SelectQueryBuilder<DataTable> {
const query = this.createQueryBuilder('dataStore');
private getManyQuery(options: Partial<ListDataTableQueryDto>): SelectQueryBuilder<DataTable> {
const query = this.createQueryBuilder('dataTable');
this.applySelections(query);
this.applyFilters(query, options.filter);
@ -183,13 +183,13 @@ export class DataStoreRepository extends Repository<DataTable> {
private applyFilters(
query: SelectQueryBuilder<DataTable>,
filter: Partial<ListDataStoreQueryDto>['filter'],
filter: Partial<ListDataTableQueryDto>['filter'],
): void {
for (const x of ['id', 'projectId'] as const) {
const content = [filter?.[x]].flat().filter((x) => x !== undefined);
if (content.length === 0) continue;
query.andWhere(`dataStore.${x} IN (:...${x}s)`, {
query.andWhere(`dataTable.${x} IN (:...${x}s)`, {
/*
* If list is empty, add a dummy value to prevent an error
* when using the IN operator with an empty array.
@ -202,7 +202,7 @@ export class DataStoreRepository extends Repository<DataTable> {
const nameFilters = Array.isArray(filter.name) ? filter.name : [filter.name];
nameFilters.forEach((name, i) => {
query.andWhere(`LOWER(dataStore.name) LIKE LOWER(:name${i})`, {
query.andWhere(`LOWER(dataTable.name) LIKE LOWER(:name${i})`, {
['name' + i]: `%${name}%`,
});
});
@ -211,7 +211,7 @@ export class DataStoreRepository extends Repository<DataTable> {
private applySorting(query: SelectQueryBuilder<DataTable>, sortBy?: string): void {
if (!sortBy) {
query.orderBy('dataStore.updatedAt', 'DESC');
query.orderBy('dataTable.updatedAt', 'DESC');
return;
}
@ -231,16 +231,16 @@ export class DataStoreRepository extends Repository<DataTable> {
): void {
if (field === 'name') {
query
.addSelect('LOWER(dataStore.name)', 'datastore_name_lower')
.orderBy('datastore_name_lower', direction);
.addSelect('LOWER(dataTable.name)', 'datatable_name_lower')
.orderBy('datatable_name_lower', direction);
} else if (['createdAt', 'updatedAt'].includes(field)) {
query.orderBy(`dataStore.${field}`, direction);
query.orderBy(`dataTable.${field}`, direction);
}
}
private applyPagination(
query: SelectQueryBuilder<DataTable>,
options: Partial<ListDataStoreQueryDto>,
options: Partial<ListDataTableQueryDto>,
): void {
query.skip(options.skip ?? 0);
if (options.take !== undefined) query.take(options.take);
@ -248,16 +248,16 @@ export class DataStoreRepository extends Repository<DataTable> {
private applyDefaultSelect(query: SelectQueryBuilder<DataTable>): void {
query
.leftJoinAndSelect('dataStore.project', 'project')
.leftJoinAndSelect('dataStore.columns', 'data_store_column')
.leftJoinAndSelect('dataTable.project', 'project')
.leftJoinAndSelect('dataTable.columns', 'data_table_column')
.select([
'dataStore',
...this.getDataStoreColumnFields('data_store_column'),
'dataTable',
...this.getDataTableColumnFields('data_table_column'),
...this.getProjectFields('project'),
]);
}
private getDataStoreColumnFields(alias: string): string[] {
private getDataTableColumnFields(alias: string): string[] {
return [
`${alias}.id`,
`${alias}.name`,
@ -377,9 +377,9 @@ export class DataStoreRepository extends Repository<DataTable> {
for (const row of result) {
if (row.table_bytes !== null && row.table_name) {
const dataStoreId = toTableId(row.table_name as DataStoreUserTableName);
const dataTableId = toTableId(row.table_name as DataTableUserTableName);
const sizeBytes = this.parseSize(row.table_bytes);
sizeMap.set(dataStoreId, (sizeMap.get(dataStoreId) ?? 0) + sizeBytes);
sizeMap.set(dataTableId, (sizeMap.get(dataTableId) ?? 0) + sizeBytes);
}
}

View File

@ -1,12 +1,12 @@
import type {
AddDataStoreColumnDto,
CreateDataStoreDto,
AddDataTableColumnDto,
CreateDataTableDto,
DeleteDataTableRowsDto,
ListDataStoreContentQueryDto,
MoveDataStoreColumnDto,
DataStoreListOptions,
UpsertDataStoreRowDto,
UpdateDataStoreDto,
ListDataTableContentQueryDto,
MoveDataTableColumnDto,
DataTableListOptions,
UpsertDataTableRowDto,
UpdateDataTableDto,
UpdateDataTableRowDto,
} from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
@ -14,42 +14,42 @@ import { ProjectRelationRepository, type User } from '@n8n/db';
import { Service } from '@n8n/di';
import { DateTime } from 'luxon';
import type {
DataStoreColumnJsType,
DataTableColumnJsType,
DataTableFilter,
DataStoreRow,
DataStoreRowReturn,
DataStoreRows,
DataTableRow,
DataTableRowReturn,
DataTableRows,
DataTableInsertRowsReturnType,
DataTableInsertRowsResult,
DataTablesSizeResult,
DataTableInfoById,
DataStoreColumnType,
DataStoreRowReturnWithState,
DataTableColumnType,
DataTableRowReturnWithState,
} from 'n8n-workflow';
import { DATA_TABLE_SYSTEM_COLUMN_TYPE_MAP, validateFieldType } from 'n8n-workflow';
import { RoleService } from '@/services/role.service';
import { DataStoreColumnRepository } from './data-store-column.repository';
import { DataStoreRowsRepository } from './data-store-rows.repository';
import { DataStoreSizeValidator } from './data-store-size-validator.service';
import { DataStoreRepository } from './data-store.repository';
import { columnTypeToFieldType } from './data-store.types';
import { DataTableColumn } from './data-table-column.entity';
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 { DataTableColumnRepository } from './data-table-column.repository';
import { DataTableRowsRepository } from './data-table-rows.repository';
import { DataTableSizeValidator } from './data-table-size-validator.service';
import { DataTableRepository } from './data-table.repository';
import { columnTypeToFieldType } from './data-table.types';
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';
import { DataTableValidationError } from './errors/data-table-validation.error';
import { normalizeRows } from './utils/sql-utils';
@Service()
export class DataStoreService {
export class DataTableService {
constructor(
private readonly dataStoreRepository: DataStoreRepository,
private readonly dataStoreColumnRepository: DataStoreColumnRepository,
private readonly dataStoreRowsRepository: DataStoreRowsRepository,
private readonly dataTableRepository: DataTableRepository,
private readonly dataTableColumnRepository: DataTableColumnRepository,
private readonly dataTableRowsRepository: DataTableRowsRepository,
private readonly logger: Logger,
private readonly dataStoreSizeValidator: DataStoreSizeValidator,
private readonly dataTableSizeValidator: DataTableSizeValidator,
private readonly projectRelationRepository: ProjectRelationRepository,
private readonly roleService: RoleService,
) {
@ -59,107 +59,107 @@ export class DataStoreService {
async start() {}
async shutdown() {}
async createDataStore(projectId: string, dto: CreateDataStoreDto) {
async createDataTable(projectId: string, dto: CreateDataTableDto) {
await this.validateUniqueName(dto.name, projectId);
const result = await this.dataStoreRepository.createDataStore(projectId, dto.name, dto.columns);
const result = await this.dataTableRepository.createDataTable(projectId, dto.name, dto.columns);
this.dataStoreSizeValidator.reset();
this.dataTableSizeValidator.reset();
return result;
}
// Updates data store properties (currently limited to renaming)
async updateDataStore(dataStoreId: string, projectId: string, dto: UpdateDataStoreDto) {
await this.validateDataStoreExists(dataStoreId, projectId);
// Updates data table properties (currently limited to renaming)
async updateDataTable(dataTableId: string, projectId: string, dto: UpdateDataTableDto) {
await this.validateDataTableExists(dataTableId, projectId);
await this.validateUniqueName(dto.name, projectId);
await this.dataStoreRepository.update({ id: dataStoreId }, { name: dto.name });
await this.dataTableRepository.update({ id: dataTableId }, { name: dto.name });
return true;
}
async transferDataStoresByProjectId(fromProjectId: string, toProjectId: string) {
return await this.dataStoreRepository.transferDataStoreByProjectId(fromProjectId, toProjectId);
async transferDataTablesByProjectId(fromProjectId: string, toProjectId: string) {
return await this.dataTableRepository.transferDataTableByProjectId(fromProjectId, toProjectId);
}
async deleteDataStoreByProjectId(projectId: string) {
const result = await this.dataStoreRepository.deleteDataStoreByProjectId(projectId);
async deleteDataTableByProjectId(projectId: string) {
const result = await this.dataTableRepository.deleteDataTableByProjectId(projectId);
if (result) {
this.dataStoreSizeValidator.reset();
this.dataTableSizeValidator.reset();
}
return result;
}
async deleteDataStoreAll() {
const result = await this.dataStoreRepository.deleteDataStoreAll();
async deleteDataTableAll() {
const result = await this.dataTableRepository.deleteDataTableAll();
if (result) {
this.dataStoreSizeValidator.reset();
this.dataTableSizeValidator.reset();
}
return result;
}
async deleteDataStore(dataStoreId: string, projectId: string) {
await this.validateDataStoreExists(dataStoreId, projectId);
async deleteDataTable(dataTableId: string, projectId: string) {
await this.validateDataTableExists(dataTableId, projectId);
await this.dataStoreRepository.deleteDataStore(dataStoreId);
await this.dataTableRepository.deleteDataTable(dataTableId);
this.dataStoreSizeValidator.reset();
this.dataTableSizeValidator.reset();
return true;
}
async addColumn(dataStoreId: string, projectId: string, dto: AddDataStoreColumnDto) {
await this.validateDataStoreExists(dataStoreId, projectId);
async addColumn(dataTableId: string, projectId: string, dto: AddDataTableColumnDto) {
await this.validateDataTableExists(dataTableId, projectId);
return await this.dataStoreColumnRepository.addColumn(dataStoreId, dto);
return await this.dataTableColumnRepository.addColumn(dataTableId, dto);
}
async moveColumn(
dataStoreId: string,
dataTableId: string,
projectId: string,
columnId: string,
dto: MoveDataStoreColumnDto,
dto: MoveDataTableColumnDto,
) {
await this.validateDataStoreExists(dataStoreId, projectId);
const existingColumn = await this.validateColumnExists(dataStoreId, columnId);
await this.validateDataTableExists(dataTableId, projectId);
const existingColumn = await this.validateColumnExists(dataTableId, columnId);
await this.dataStoreColumnRepository.moveColumn(dataStoreId, existingColumn, dto.targetIndex);
await this.dataTableColumnRepository.moveColumn(dataTableId, 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);
async deleteColumn(dataTableId: string, projectId: string, columnId: string) {
await this.validateDataTableExists(dataTableId, projectId);
const existingColumn = await this.validateColumnExists(dataTableId, columnId);
await this.dataStoreColumnRepository.deleteColumn(dataStoreId, existingColumn);
await this.dataTableColumnRepository.deleteColumn(dataTableId, existingColumn);
return true;
}
async getManyAndCount(options: DataStoreListOptions) {
return await this.dataStoreRepository.getManyAndCount(options);
async getManyAndCount(options: DataTableListOptions) {
return await this.dataTableRepository.getManyAndCount(options);
}
async getManyRowsAndCount(
dataStoreId: string,
dataTableId: string,
projectId: string,
dto: ListDataStoreContentQueryDto,
dto: ListDataTableContentQueryDto,
) {
await this.validateDataStoreExists(dataStoreId, projectId);
await this.validateDataTableExists(dataTableId, projectId);
return await this.dataStoreColumnRepository.manager.transaction(async (em) => {
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId, em);
return await this.dataTableColumnRepository.manager.transaction(async (em) => {
const columns = await this.dataTableColumnRepository.getColumns(dataTableId, em);
if (dto.filter) {
this.validateAndTransformFilters(dto.filter, columns);
}
const result = await this.dataStoreRowsRepository.getManyAndCount(
dataStoreId,
const result = await this.dataTableRowsRepository.getManyAndCount(
dataTableId,
dto,
columns,
em,
@ -171,33 +171,33 @@ export class DataStoreService {
});
}
async getColumns(dataStoreId: string, projectId: string) {
await this.validateDataStoreExists(dataStoreId, projectId);
async getColumns(dataTableId: string, projectId: string) {
await this.validateDataTableExists(dataTableId, projectId);
return await this.dataStoreColumnRepository.getColumns(dataStoreId);
return await this.dataTableColumnRepository.getColumns(dataTableId);
}
async insertRows<T extends DataTableInsertRowsReturnType = 'count'>(
dataStoreId: string,
dataTableId: string,
projectId: string,
rows: DataStoreRows,
rows: DataTableRows,
returnType?: T,
): Promise<DataTableInsertRowsResult<T>>;
async insertRows(
dataStoreId: string,
dataTableId: string,
projectId: string,
rows: DataStoreRows,
rows: DataTableRows,
returnType: DataTableInsertRowsReturnType = 'count',
) {
await this.validateDataTableSize();
await this.validateDataStoreExists(dataStoreId, projectId);
await this.validateDataTableExists(dataTableId, projectId);
const result = await this.dataStoreColumnRepository.manager.transaction(async (trx) => {
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId, trx);
const result = await this.dataTableColumnRepository.manager.transaction(async (trx) => {
const columns = await this.dataTableColumnRepository.getColumns(dataTableId, trx);
this.validateRowsWithColumns(rows, columns);
return await this.dataStoreRowsRepository.insertRows(
dataStoreId,
return await this.dataTableRowsRepository.insertRows(
dataTableId,
rows,
columns,
returnType,
@ -205,7 +205,7 @@ export class DataStoreService {
);
});
this.dataStoreSizeValidator.reset();
this.dataTableSizeValidator.reset();
return result;
}
@ -213,40 +213,40 @@ export class DataStoreService {
async upsertRow<T extends boolean | undefined>(
dataTableId: string,
projectId: string,
dto: Omit<UpsertDataStoreRowDto, 'returnData' | 'dryRun'>,
dto: Omit<UpsertDataTableRowDto, 'returnData' | 'dryRun'>,
returnData: true,
dryRun?: boolean,
): Promise<DataStoreRowReturn[] | DataStoreRowReturnWithState[]>;
): Promise<DataTableRowReturn[] | DataTableRowReturnWithState[]>;
async upsertRow(
dataTableId: string,
projectId: string,
dto: Omit<UpsertDataStoreRowDto, 'returnData' | 'dryRun'>,
dto: Omit<UpsertDataTableRowDto, 'returnData' | 'dryRun'>,
returnData?: boolean,
dryRun?: true,
): Promise<DataStoreRowReturnWithState[]>;
): Promise<DataTableRowReturnWithState[]>;
async upsertRow(
dataTableId: string,
projectId: string,
dto: Omit<UpsertDataStoreRowDto, 'returnData' | 'dryRun'>,
dto: Omit<UpsertDataTableRowDto, 'returnData' | 'dryRun'>,
returnData?: false,
dryRun?: false,
): Promise<true>;
async upsertRow(
dataTableId: string,
projectId: string,
dto: Omit<UpsertDataStoreRowDto, 'returnData' | 'dryRun'>,
dto: Omit<UpsertDataTableRowDto, 'returnData' | 'dryRun'>,
returnData: boolean = false,
dryRun: boolean = false,
) {
await this.validateDataTableSize();
await this.validateDataStoreExists(dataTableId, projectId);
await this.validateDataTableExists(dataTableId, projectId);
const result = await this.dataStoreColumnRepository.manager.transaction(async (trx) => {
const columns = await this.dataStoreColumnRepository.getColumns(dataTableId, trx);
const result = await this.dataTableColumnRepository.manager.transaction(async (trx) => {
const columns = await this.dataTableColumnRepository.getColumns(dataTableId, trx);
this.validateUpdateParams(dto, columns);
if (dryRun) {
return await this.dataStoreRowsRepository.dryRunUpsertRow(
return await this.dataTableRowsRepository.dryRunUpsertRow(
dataTableId,
dto.data,
dto.filter,
@ -255,7 +255,7 @@ export class DataStoreService {
);
}
const updated = await this.dataStoreRowsRepository.updateRows(
const updated = await this.dataTableRowsRepository.updateRows(
dataTableId,
dto.data,
dto.filter,
@ -269,7 +269,7 @@ export class DataStoreService {
}
// No rows were updated, so insert a new one
const inserted = await this.dataStoreRowsRepository.insertRows(
const inserted = await this.dataTableRowsRepository.insertRows(
dataTableId,
[dto.data],
columns,
@ -280,7 +280,7 @@ export class DataStoreService {
});
if (!dryRun) {
this.dataStoreSizeValidator.reset();
this.dataTableSizeValidator.reset();
}
return result;
@ -291,16 +291,16 @@ export class DataStoreService {
columns: DataTableColumn[],
) {
if (columns.length === 0) {
throw new DataStoreValidationError(
throw new DataTableValidationError(
'No columns found for this data table or data table not found',
);
}
if (!filter?.filters || filter.filters.length === 0) {
throw new DataStoreValidationError('Filter must not be empty');
throw new DataTableValidationError('Filter must not be empty');
}
if (!data || Object.keys(data).length === 0) {
throw new DataStoreValidationError('Data columns must not be empty');
throw new DataTableValidationError('Data columns must not be empty');
}
this.validateRowsWithColumns([data], columns, false);
@ -313,14 +313,14 @@ export class DataStoreService {
dto: Omit<UpdateDataTableRowDto, 'returnData' | 'dryRun'>,
returnData: true,
dryRun?: boolean,
): Promise<DataStoreRowReturn[] | DataStoreRowReturnWithState[]>;
): Promise<DataTableRowReturn[] | DataTableRowReturnWithState[]>;
async updateRows(
dataTableId: string,
projectId: string,
dto: Omit<UpdateDataTableRowDto, 'returnData' | 'dryRun'>,
returnData?: boolean,
dryRun?: true,
): Promise<DataStoreRowReturnWithState[]>;
): Promise<DataTableRowReturnWithState[]>;
async updateRows(
dataTableId: string,
projectId: string,
@ -336,14 +336,14 @@ export class DataStoreService {
dryRun: boolean = false,
) {
await this.validateDataTableSize();
await this.validateDataStoreExists(dataTableId, projectId);
await this.validateDataTableExists(dataTableId, projectId);
const result = await this.dataStoreColumnRepository.manager.transaction(async (trx) => {
const columns = await this.dataStoreColumnRepository.getColumns(dataTableId, trx);
const result = await this.dataTableColumnRepository.manager.transaction(async (trx) => {
const columns = await this.dataTableColumnRepository.getColumns(dataTableId, trx);
this.validateUpdateParams(dto, columns);
if (dryRun) {
return await this.dataStoreRowsRepository.dryRunUpdateRows(
return await this.dataTableRowsRepository.dryRunUpdateRows(
dataTableId,
dto.data,
dto.filter,
@ -352,7 +352,7 @@ export class DataStoreService {
);
}
return await this.dataStoreRowsRepository.updateRows(
return await this.dataTableRowsRepository.updateRows(
dataTableId,
dto.data,
dto.filter,
@ -363,55 +363,55 @@ export class DataStoreService {
});
if (!dryRun) {
this.dataStoreSizeValidator.reset();
this.dataTableSizeValidator.reset();
}
return result;
}
async deleteRows(
dataStoreId: string,
dataTableId: string,
projectId: string,
dto: Omit<DeleteDataTableRowsDto, 'returnData' | 'dryRun'>,
returnData: true,
dryRun?: boolean,
): Promise<DataStoreRowReturn[]>;
): Promise<DataTableRowReturn[]>;
async deleteRows(
dataStoreId: string,
dataTableId: string,
projectId: string,
dto: Omit<DeleteDataTableRowsDto, 'returnData' | 'dryRun'>,
returnData?: boolean,
dryRun?: true,
): Promise<DataStoreRowReturn[]>;
): Promise<DataTableRowReturn[]>;
async deleteRows(
dataStoreId: string,
dataTableId: string,
projectId: string,
dto: Omit<DeleteDataTableRowsDto, 'returnData' | 'dryRun'>,
returnData?: false,
dryRun?: false,
): Promise<true>;
async deleteRows(
dataStoreId: string,
dataTableId: string,
projectId: string,
dto: Omit<DeleteDataTableRowsDto, 'returnData' | 'dryRun'>,
returnData: boolean = false,
dryRun: boolean = false,
) {
await this.validateDataStoreExists(dataStoreId, projectId);
await this.validateDataTableExists(dataTableId, projectId);
const result = await this.dataStoreColumnRepository.manager.transaction(async (trx) => {
const columns = await this.dataStoreColumnRepository.getColumns(dataStoreId, trx);
const result = await this.dataTableColumnRepository.manager.transaction(async (trx) => {
const columns = await this.dataTableColumnRepository.getColumns(dataTableId, trx);
if (!dto.filter?.filters || dto.filter.filters.length === 0) {
throw new DataStoreValidationError(
throw new DataTableValidationError(
'Filter is required for delete operations to prevent accidental deletion of all data',
);
}
this.validateAndTransformFilters(dto.filter, columns);
return await this.dataStoreRowsRepository.deleteRows(
dataStoreId,
return await this.dataTableRowsRepository.deleteRows(
dataTableId,
columns,
dto.filter,
returnData,
@ -421,15 +421,15 @@ export class DataStoreService {
});
if (!dryRun) {
this.dataStoreSizeValidator.reset();
this.dataTableSizeValidator.reset();
}
return result;
}
private validateRowsWithColumns(
rows: DataStoreRows,
columns: Array<{ name: string; type: DataStoreColumnType }>,
rows: DataTableRows,
columns: Array<{ name: string; type: DataTableColumnType }>,
includeSystemColumns = false,
): void {
// Include system columns like 'id' if requested
@ -448,14 +448,14 @@ export class DataStoreService {
const keys = Object.keys(row);
for (const key of keys) {
if (!columnNames.has(key)) {
throw new DataStoreValidationError(`unknown column name '${key}'`);
throw new DataTableValidationError(`unknown column name '${key}'`);
}
this.validateCell(row, key, columnTypeMap);
}
}
}
private validateCell(row: DataStoreRow, key: string, columnTypeMap: Map<string, string>) {
private validateCell(row: DataTableRow, key: string, columnTypeMap: Map<string, string>) {
const cell = row[key];
if (cell === null) return;
@ -471,7 +471,7 @@ export class DataStoreService {
});
if (!validationResult.valid) {
throw new DataStoreValidationError(
throw new DataTableValidationError(
`value '${String(cell)}' does not match column type '${columnType}': ${validationResult.errorMessage}`,
);
}
@ -483,51 +483,51 @@ export class DataStoreService {
row[key] = dateInISO;
return;
} catch {
throw new DataStoreValidationError(
throw new DataTableValidationError(
`value '${String(cell)}' does not match column type 'date'`,
);
}
}
row[key] = validationResult.newValue as DataStoreColumnJsType;
row[key] = validationResult.newValue as DataTableColumnJsType;
}
private async validateDataStoreExists(dataStoreId: string, projectId: string) {
const existingTable = await this.dataStoreRepository.findOneBy({
id: dataStoreId,
private async validateDataTableExists(dataTableId: string, projectId: string) {
const existingTable = await this.dataTableRepository.findOneBy({
id: dataTableId,
project: {
id: projectId,
},
});
if (!existingTable) {
throw new DataStoreNotFoundError(dataStoreId);
throw new DataTableNotFoundError(dataTableId);
}
return existingTable;
}
private async validateColumnExists(dataTableId: string, columnId: string) {
const existingColumn = await this.dataStoreColumnRepository.findOneBy({
const existingColumn = await this.dataTableColumnRepository.findOneBy({
id: columnId,
dataTableId,
});
if (existingColumn === null) {
throw new DataStoreColumnNotFoundError(dataTableId, columnId);
throw new DataTableColumnNotFoundError(dataTableId, columnId);
}
return existingColumn;
}
private async validateUniqueName(name: string, projectId: string) {
const hasNameClash = await this.dataStoreRepository.existsBy({
const hasNameClash = await this.dataTableRepository.existsBy({
name,
projectId,
});
if (hasNameClash) {
throw new DataStoreNameConflictError(name);
throw new DataTableNameConflictError(name);
}
}
@ -548,12 +548,12 @@ export class DataStoreService {
for (const filter of filterObject.filters) {
if (['like', 'ilike'].includes(filter.condition)) {
if (filter.value === null || filter.value === undefined) {
throw new DataStoreValidationError(
throw new DataTableValidationError(
`${filter.condition.toUpperCase()} filter value cannot be null or undefined`,
);
}
if (typeof filter.value !== 'string') {
throw new DataStoreValidationError(
throw new DataTableValidationError(
`${filter.condition.toUpperCase()} filter value must be a string`,
);
}
@ -565,7 +565,7 @@ export class DataStoreService {
if (['gt', 'gte', 'lt', 'lte'].includes(filter.condition)) {
if (filter.value === null || filter.value === undefined) {
throw new DataStoreValidationError(
throw new DataTableValidationError(
`${filter.condition.toUpperCase()} filter value cannot be null or undefined`,
);
}
@ -574,14 +574,14 @@ export class DataStoreService {
}
private async validateDataTableSize() {
await this.dataStoreSizeValidator.validateSize(
async () => await this.dataStoreRepository.findDataTablesSize(),
await this.dataTableSizeValidator.validateSize(
async () => await this.dataTableRepository.findDataTablesSize(),
);
}
async getDataTablesSize(user: User): Promise<DataTablesSizeResult> {
const allSizeData = await this.dataStoreSizeValidator.getCachedSizeData(
async () => await this.dataStoreRepository.findDataTablesSize(),
const allSizeData = await this.dataTableSizeValidator.getCachedSizeData(
async () => await this.dataTableRepository.findDataTablesSize(),
);
const roles = await this.roleService.rolesWithScope('project', ['dataStore:listProject']);
@ -602,7 +602,7 @@ export class DataStoreService {
return {
totalBytes: allSizeData.totalBytes,
quotaStatus: this.dataStoreSizeValidator.sizeToState(allSizeData.totalBytes),
quotaStatus: this.dataTableSizeValidator.sizeToState(allSizeData.totalBytes),
dataTables: accessibleDataTables,
};
}

View File

@ -1,6 +1,6 @@
import type { FieldTypeMap } from 'n8n-workflow';
export type DataStoreUserTableName = `${string}data_table_user_${string}`;
export type DataTableUserTableName = `${string}data_table_user_${string}`;
export const columnTypeToFieldType: Record<string, keyof FieldTypeMap> = {
// eslint-disable-next-line id-denylist

View File

@ -1,12 +0,0 @@
import { UserError } from 'n8n-workflow';
export class DataStoreColumnNameConflictError extends UserError {
constructor(columnName: string, dataStoreName: string) {
super(
`Data store column with name '${columnName}' already exists in data store '${dataStoreName}'`,
{
level: 'warning',
},
);
}
}

View File

@ -1,9 +0,0 @@
import { UserError } from 'n8n-workflow';
export class DataStoreColumnNotFoundError extends UserError {
constructor(dataStoreId: string, columnId: string) {
super(`Could not find the column '${columnId}' in the data store: ${dataStoreId}`, {
level: 'warning',
});
}
}

View File

@ -1,9 +0,0 @@
import { UserError } from 'n8n-workflow';
export class DataStoreNameConflictError extends UserError {
constructor(name: string) {
super(`Data store with name '${name}' already exists in this project`, {
level: 'warning',
});
}
}

View File

@ -1,9 +0,0 @@
import { UserError } from 'n8n-workflow';
export class DataStoreNotFoundError extends UserError {
constructor(dataStoreId: string) {
super(`Could not find the data table: '${dataStoreId}'`, {
level: 'warning',
});
}
}

View File

@ -1,9 +0,0 @@
import { UserError } from 'n8n-workflow';
export class DataStoreValidationError extends UserError {
constructor(msg: string) {
super(`Validation error with data store request: ${msg}`, {
level: 'warning',
});
}
}

View File

@ -0,0 +1,12 @@
import { UserError } from 'n8n-workflow';
export class DataTableColumnNameConflictError extends UserError {
constructor(columnName: string, dataTableName: string) {
super(
`Data table column with name '${columnName}' already exists in data table '${dataTableName}'`,
{
level: 'warning',
},
);
}
}

View File

@ -0,0 +1,9 @@
import { UserError } from 'n8n-workflow';
export class DataTableColumnNotFoundError extends UserError {
constructor(dataTableId: string, columnId: string) {
super(`Could not find the column '${columnId}' in the data table: ${dataTableId}`, {
level: 'warning',
});
}
}

View File

@ -0,0 +1,9 @@
import { UserError } from 'n8n-workflow';
export class DataTableNameConflictError extends UserError {
constructor(name: string) {
super(`Data table with name '${name}' already exists in this project`, {
level: 'warning',
});
}
}

View File

@ -0,0 +1,9 @@
import { UserError } from 'n8n-workflow';
export class DataTableNotFoundError extends UserError {
constructor(dataTableId: string) {
super(`Could not find the data table: '${dataTableId}'`, {
level: 'warning',
});
}
}

View File

@ -1,6 +1,6 @@
import { UserError } from 'n8n-workflow';
export class DataStoreSystemColumnNameConflictError extends UserError {
export class DataTableSystemColumnNameConflictError extends UserError {
constructor(columnName: string, type: string = 'system') {
super(`Column name "${columnName}" is reserved as a ${type} column name.`, {
level: 'warning',

View File

@ -0,0 +1,9 @@
import { UserError } from 'n8n-workflow';
export class DataTableValidationError extends UserError {
constructor(msg: string) {
super(`Validation error with data table request: ${msg}`, {
level: 'warning',
});
}
}

View File

@ -1,21 +1,21 @@
import {
dataStoreColumnNameSchema,
DATA_STORE_COLUMN_ERROR_MESSAGE,
type DataStoreCreateColumnSchema,
dataTableColumnNameSchema,
DATA_TABLE_COLUMN_ERROR_MESSAGE,
type DataTableCreateColumnSchema,
} from '@n8n/api-types';
import { GlobalConfig } from '@n8n/config';
import { DslColumn } from '@n8n/db';
import { Container } from '@n8n/di';
import type { DataSourceOptions } from '@n8n/typeorm';
import type { DataStoreColumnJsType, DataStoreRowReturn, DataStoreRowsReturn } from 'n8n-workflow';
import type { DataTableColumnJsType, DataTableRowReturn, DataTableRowsReturn } 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 { DataTableColumn } from '../data-table-column.entity';
import type { DataTableUserTableName } from '../data-table.types';
export function toDslColumns(columns: DataStoreCreateColumnSchema[]): DslColumn[] {
export function toDslColumns(columns: DataTableCreateColumnSchema[]): DslColumn[] {
return columns.map((col) => {
const name = new DslColumn(col.name.trim());
@ -34,8 +34,8 @@ export function toDslColumns(columns: DataStoreCreateColumnSchema[]): DslColumn[
});
}
function dataStoreColumnTypeToSql(
type: DataStoreCreateColumnSchema['type'],
function dataTableColumnTypeToSql(
type: DataTableCreateColumnSchema['type'],
dbType: DataSourceOptions['type'],
) {
switch (type) {
@ -66,24 +66,24 @@ function dataStoreColumnTypeToSql(
}
function columnToWildcardAndType(
column: DataStoreCreateColumnSchema,
column: DataTableCreateColumnSchema,
dbType: DataSourceOptions['type'],
) {
return `${quoteIdentifier(column.name, dbType)} ${dataStoreColumnTypeToSql(column.type, dbType)}`;
return `${quoteIdentifier(column.name, dbType)} ${dataTableColumnTypeToSql(column.type, dbType)}`;
}
export function isValidColumnName(name: string) {
return dataStoreColumnNameSchema.safeParse(name).success;
return dataTableColumnNameSchema.safeParse(name).success;
}
export function addColumnQuery(
tableName: DataStoreUserTableName,
column: DataStoreCreateColumnSchema,
tableName: DataTableUserTableName,
column: DataTableCreateColumnSchema,
dbType: DataSourceOptions['type'],
) {
// API requests should already conform to this, but better safe than sorry
if (!isValidColumnName(column.name)) {
throw new UnexpectedError(DATA_STORE_COLUMN_ERROR_MESSAGE);
throw new UnexpectedError(DATA_TABLE_COLUMN_ERROR_MESSAGE);
}
const quotedTableName = quoteIdentifier(tableName, dbType);
@ -92,7 +92,7 @@ export function addColumnQuery(
}
export function deleteColumnQuery(
tableName: DataStoreUserTableName,
tableName: DataTableUserTableName,
column: string,
dbType: DataSourceOptions['type'],
): string {
@ -129,7 +129,7 @@ function hasInsertId(data: unknown): data is WithInsertId {
return typeof data === 'object' && data !== null && 'insertId' in data && isNumber(data.insertId);
}
function hasRowReturnData(data: unknown): data is DataStoreRowReturn {
function hasRowReturnData(data: unknown): data is DataTableRowReturn {
return (
typeof data === 'object' &&
data !== null &&
@ -142,11 +142,11 @@ function hasRowReturnData(data: unknown): data is DataStoreRowReturn {
);
}
function hasRowId(data: unknown): data is Pick<DataStoreRowReturn, 'id'> {
function hasRowId(data: unknown): data is Pick<DataTableRowReturn, 'id'> {
return typeof data === 'object' && data !== null && 'id' in data && isNumber(data.id);
}
export function extractReturningData(raw: unknown): DataStoreRowReturn[] {
export function extractReturningData(raw: unknown): DataTableRowReturn[] {
if (!isArrayOf(raw, hasRowReturnData)) {
throw new UnexpectedError(
`Expected INSERT INTO raw to be { id: number; createdAt: string; updatedAt: string }[] on Postgres or MariaDB. Is '${JSON.stringify(raw)}'`,
@ -183,7 +183,7 @@ export function extractInsertedIds(raw: unknown, dbType: DataSourceOptions['type
}
}
export function normalizeRows(rows: DataStoreRowsReturn, columns: DataTableColumn[]) {
export function normalizeRows(rows: DataTableRowsReturn, columns: DataTableColumn[]) {
// we need to normalize system dates as well
const systemColumns = [
{ name: 'createdAt', type: 'date' },
@ -242,10 +242,10 @@ function formatDateForDatabase(date: Date, dbType?: DataSourceOptions['type']):
}
export function normalizeValue(
value: DataStoreColumnJsType,
value: DataTableColumnJsType,
columnType: string | undefined,
dbType?: DataSourceOptions['type'],
): DataStoreColumnJsType {
): DataTableColumnJsType {
if (columnType !== 'date' || value === null || value === undefined) {
return value;
}
@ -292,11 +292,11 @@ export function escapeLikeSpecials(input: string): string {
.replace(/_/g, '\\_'); // make '_' literal ('%' stays a wildcard)
}
export function toTableName(dataStoreId: string): DataStoreUserTableName {
export function toTableName(dataTableId: string): DataTableUserTableName {
const { tablePrefix } = Container.get(GlobalConfig).database;
return `${tablePrefix}data_table_user_${dataStoreId}`;
return `${tablePrefix}data_table_user_${dataTableId}`;
}
export function toTableId(tableName: DataStoreUserTableName) {
export function toTableId(tableName: DataTableUserTableName) {
return tableName.replace(/.*data_table_user_/, '');
}

View File

@ -21,7 +21,7 @@ import { CredentialsHelper } from '@/credentials-helper';
import { VariablesService } from '@/environments.ee/variables/variables.service.ee';
import { ExternalHooks } from '@/external-hooks';
import type { ManualExecutionService } from '@/manual-execution.service';
import { DataStoreProxyService } from '@/modules/data-table/data-store-proxy.service';
import { DataTableProxyService } from '@/modules/data-table/data-table-proxy.service';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service';
@ -34,7 +34,7 @@ mockInstance(ExternalSecretsProxy);
mockInstance(WorkflowStaticDataService);
mockInstance(WorkflowStatisticsService);
mockInstance(ExternalHooks);
mockInstance(DataStoreProxyService);
mockInstance(DataTableProxyService);
const processRunExecutionDataMock = jest.fn();
jest.mock('n8n-core', () => {

View File

@ -95,8 +95,8 @@ export class ProjectService {
}
private get dataTableService() {
return import('@/modules/data-table/data-store.service').then(({ DataStoreService }) =>
Container.get(DataStoreService),
return import('@/modules/data-table/data-table.service').then(({ DataTableService }) =>
Container.get(DataTableService),
);
}
@ -189,9 +189,9 @@ export class ProjectService {
const dataTableService = await this.dataTableService;
if (targetProject) {
await dataTableService.transferDataStoresByProjectId(project.id, targetProject.id);
await dataTableService.transferDataTablesByProjectId(project.id, targetProject.id);
} else {
await dataTableService.deleteDataStoreByProjectId(project.id);
await dataTableService.deleteDataTableByProjectId(project.id);
}
}

View File

@ -1,43 +0,0 @@
import type { CreateDataStoreColumnDto } from '@n8n/api-types';
import { randomName } from '@n8n/backend-test-utils';
import type { Project } from '@n8n/db';
import { Container } from '@n8n/di';
import type { DataStoreRows } from 'n8n-workflow';
import { DataStoreColumnRepository } from '@/modules/data-table/data-store-column.repository';
import { DataStoreRowsRepository } from '@/modules/data-table/data-store-rows.repository';
import { DataStoreRepository } from '@/modules/data-table/data-store.repository';
export const createDataStore = async (
project: Project,
options: {
name?: string;
columns?: CreateDataStoreColumnDto[];
data?: DataStoreRows;
updatedAt?: Date;
} = {},
) => {
const dataStoreRepository = Container.get(DataStoreRepository);
const dataStore = await dataStoreRepository.createDataStore(
project.id,
options.name ?? randomName(),
options.columns ?? [],
);
if (options.updatedAt) {
await dataStoreRepository.update(dataStore.id, {
updatedAt: options.updatedAt,
});
dataStore.updatedAt = options.updatedAt;
}
if (options.data) {
const dataStoreColumnRepository = Container.get(DataStoreColumnRepository);
const columns = await dataStoreColumnRepository.getColumns(dataStore.id);
const dataStoreRowsRepository = Container.get(DataStoreRowsRepository);
await dataStoreRowsRepository.insertRows(dataStore.id, options.data, columns, 'count');
}
return dataStore;
};

View File

@ -0,0 +1,43 @@
import type { CreateDataTableColumnDto } from '@n8n/api-types';
import { randomName } from '@n8n/backend-test-utils';
import type { Project } from '@n8n/db';
import { Container } from '@n8n/di';
import type { DataTableRows } from 'n8n-workflow';
import { DataTableColumnRepository } from '@/modules/data-table/data-table-column.repository';
import { DataTableRowsRepository } from '@/modules/data-table/data-table-rows.repository';
import { DataTableRepository } from '@/modules/data-table/data-table.repository';
export const createDataTable = async (
project: Project,
options: {
name?: string;
columns?: CreateDataTableColumnDto[];
data?: DataTableRows;
updatedAt?: Date;
} = {},
) => {
const dataTableRepository = Container.get(DataTableRepository);
const dataTable = await dataTableRepository.createDataTable(
project.id,
options.name ?? randomName(),
options.columns ?? [],
);
if (options.updatedAt) {
await dataTableRepository.update(dataTable.id, {
updatedAt: options.updatedAt,
});
dataTable.updatedAt = options.updatedAt;
}
if (options.data) {
const dataTableColumnRepository = Container.get(DataTableColumnRepository);
const columns = await dataTableColumnRepository.getColumns(dataTable.id);
const dataTableRowsRepository = Container.get(DataTableRowsRepository);
await dataTableRowsRepository.insertRows(dataTable.id, options.data, columns, 'count');
}
return dataTable;
};

View File

@ -1,4 +1,4 @@
import type { DataStoreProxyProvider } from 'n8n-workflow';
import type { DataTableProxyProvider } from 'n8n-workflow';
import type { ExecutionLifecycleHooks } from './execution-lifecycle-hooks';
import type { ExternalSecretsProxy } from './external-secrets-proxy';
@ -7,7 +7,7 @@ declare module 'n8n-workflow' {
interface IWorkflowExecuteAdditionalData {
hooks?: ExecutionLifecycleHooks;
externalSecretsProxy: ExternalSecretsProxy;
'data-table'?: { dataStoreProxyProvider: DataStoreProxyProvider };
'data-table'?: { dataTableProxyProvider: DataTableProxyProvider };
// Project ID is currently only added on the additionalData if the user
// has data table listing permission for that project. We should consider
// that only data tables belonging to their respective projects are shown.

View File

@ -36,7 +36,7 @@ import {
} from './utils/binary-helper-functions';
import { constructExecutionMetaData } from './utils/construct-execution-metadata';
import { copyInputItems } from './utils/copy-input-items';
import { getDataStoreHelperFunctions } from './utils/data-store-helper-functions';
import { getDataTableHelperFunctions } from './utils/data-table-helper-functions';
import { getDeduplicationHelperFunctions } from './utils/deduplication-helper-functions';
import { getFileSystemHelperFunctions } from './utils/file-system-helper-functions';
import { getInputConnectionData } from './utils/get-input-connection-data';
@ -95,7 +95,7 @@ export class ExecuteContext extends BaseExecuteContext implements IExecuteFuncti
connectionInputData,
),
...getBinaryHelperFunctions(additionalData, workflow.id),
...getDataStoreHelperFunctions(additionalData, workflow, node),
...getDataTableHelperFunctions(additionalData, workflow, node),
...getSSHTunnelFunctions(),
...getFileSystemHelperFunctions(node),
...getDeduplicationHelperFunctions(workflow, node),

View File

@ -10,7 +10,7 @@ import type {
} from 'n8n-workflow';
import { NodeExecutionContext } from './node-execution-context';
import { getDataStoreHelperFunctions } from './utils/data-store-helper-functions';
import { getDataTableHelperFunctions } from './utils/data-table-helper-functions';
import { extractValue } from './utils/extract-value';
import { getRequestHelperFunctions } from './utils/request-helper-functions';
import { getSSHTunnelFunctions } from './utils/ssh-tunnel-helper-functions';
@ -29,7 +29,7 @@ export class LoadOptionsContext extends NodeExecutionContext implements ILoadOpt
this.helpers = {
...getSSHTunnelFunctions(),
...getRequestHelperFunctions(workflow, node, additionalData),
...getDataStoreHelperFunctions(additionalData, workflow, node),
...getDataTableHelperFunctions(additionalData, workflow, node),
};
}

View File

@ -29,7 +29,7 @@ import {
} from './utils/binary-helper-functions';
import { constructExecutionMetaData } from './utils/construct-execution-metadata';
import { copyInputItems } from './utils/copy-input-items';
import { getDataStoreHelperFunctions } from './utils/data-store-helper-functions';
import { getDataTableHelperFunctions } from './utils/data-table-helper-functions';
import { getDeduplicationHelperFunctions } from './utils/deduplication-helper-functions';
import { getFileSystemHelperFunctions } from './utils/file-system-helper-functions';
// eslint-disable-next-line import-x/no-cycle
@ -89,7 +89,7 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
...getSSHTunnelFunctions(),
...getFileSystemHelperFunctions(node),
...getBinaryHelperFunctions(additionalData, workflow.id),
...getDataStoreHelperFunctions(additionalData, workflow, node),
...getDataTableHelperFunctions(additionalData, workflow, node),
...getDeduplicationHelperFunctions(workflow, node),
assertBinaryData: (itemIndex, propertyName) =>
assertBinaryData(inputData, node, itemIndex, propertyName, 0),

View File

@ -1,30 +0,0 @@
import type {
DataStoreProxyFunctions,
INode,
Workflow,
IWorkflowExecuteAdditionalData,
} from 'n8n-workflow';
export function getDataStoreHelperFunctions(
additionalData: IWorkflowExecuteAdditionalData,
workflow: Workflow,
node: INode,
): Partial<DataStoreProxyFunctions> {
const dataStoreProxyProvider = additionalData['data-table']?.dataStoreProxyProvider;
if (!dataStoreProxyProvider) return {};
return {
getDataStoreAggregateProxy: async () =>
await dataStoreProxyProvider.getDataStoreAggregateProxy(
workflow,
node,
additionalData.dataTableProjectId,
),
getDataStoreProxy: async (dataStoreId: string) =>
await dataStoreProxyProvider.getDataStoreProxy(
workflow,
node,
dataStoreId,
additionalData.dataTableProjectId,
),
};
}

View File

@ -0,0 +1,30 @@
import type {
DataTableProxyFunctions,
INode,
Workflow,
IWorkflowExecuteAdditionalData,
} from 'n8n-workflow';
export function getDataTableHelperFunctions(
additionalData: IWorkflowExecuteAdditionalData,
workflow: Workflow,
node: INode,
): Partial<DataTableProxyFunctions> {
const dataTableProxyProvider = additionalData['data-table']?.dataTableProxyProvider;
if (!dataTableProxyProvider) return {};
return {
getDataTableAggregateProxy: async () =>
await dataTableProxyProvider.getDataTableAggregateProxy(
workflow,
node,
additionalData.dataTableProjectId,
),
getDataTableProxy: async (dataTableId: string) =>
await dataTableProxyProvider.getDataTableProxy(
workflow,
node,
dataTableId,
additionalData.dataTableProjectId,
),
};
}

View File

@ -1,4 +1,4 @@
import { DATA_STORE_COLUMN_REGEX } from '@n8n/api-types';
import { DATA_TABLE_COLUMN_REGEX } from '@n8n/api-types';
// Route and view identifiers
export const DATA_STORE_VIEW = 'data-stores';
@ -29,7 +29,7 @@ export const DEFAULT_ID_COLUMN_NAME = 'id';
export const MAX_COLUMN_NAME_LENGTH = 128;
export const COLUMN_NAME_REGEX = DATA_STORE_COLUMN_REGEX;
export const COLUMN_NAME_REGEX = DATA_TABLE_COLUMN_REGEX;
export const MIN_LOADING_TIME = 500; // ms

View File

@ -11,7 +11,7 @@
}
]
},
"alias": ["data", "table", "knowledge", "data store", "store", "sheet"],
"alias": ["data", "table", "knowledge", "data table", "table", "sheet"],
"subcategories": {
"Core Nodes": ["Helpers"]
}

View File

@ -1,5 +1,5 @@
import type {
IDataStoreProjectService,
IDataTableProjectService,
IDisplayOptions,
IExecuteFunctions,
INodeExecutionData,
@ -65,7 +65,7 @@ export async function execute(
export async function executeBulk(
this: IExecuteFunctions,
proxy: IDataStoreProjectService,
proxy: IDataTableProjectService,
): Promise<INodeExecutionData[]> {
const optimizeBulkEnabled = this.getNodeParameter('options.optimizeBulk', 0, false);
const rows = this.getInputData().flatMap((_, i) => [getAddRow(this, i)]);

View File

@ -1,4 +1,4 @@
import type { DataStoreColumnJsType } from 'n8n-workflow';
import type { DataTableColumnJsType } from 'n8n-workflow';
export const ANY_CONDITION = 'anyCondition';
export const ALL_CONDITIONS = 'allConditions';
@ -15,5 +15,5 @@ export type FieldEntry =
| {
keyName: string;
condition?: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
keyValue: DataStoreColumnJsType;
keyValue: DataTableColumnJsType;
};

View File

@ -1,8 +1,8 @@
import { DATA_TABLE_SYSTEM_COLUMNS, NodeOperationError } from 'n8n-workflow';
import type {
DataTableFilter,
DataStoreRowReturn,
IDataStoreProjectService,
DataTableRowReturn,
IDataTableProjectService,
IDisplayOptions,
IExecuteFunctions,
INodeProperties,
@ -144,10 +144,10 @@ export async function getSelectFilter(
export async function executeSelectMany(
ctx: IExecuteFunctions,
index: number,
dataStoreProxy: IDataStoreProjectService,
dataStoreProxy: IDataTableProjectService,
rejectEmpty = false,
limit?: number,
): Promise<Array<{ json: DataStoreRowReturn }>> {
): Promise<Array<{ json: DataTableRowReturn }>> {
const filter = await getSelectFilter(ctx, index);
if (rejectEmpty && filter.filters.length === 0) {
@ -155,7 +155,7 @@ export async function executeSelectMany(
}
const PAGE_SIZE = 1000;
const result: Array<{ json: DataStoreRowReturn }> = [];
const result: Array<{ json: DataTableRowReturn }> = [];
const returnAll = ctx.getNodeParameter('returnAll', index, false);
limit = limit ?? (!returnAll ? ctx.getNodeParameter('limit', index, ROWS_LIMIT_DEFAULT) : 0);

View File

@ -3,11 +3,11 @@ import type {
IDataObject,
INode,
DataTableFilter,
IDataStoreProjectAggregateService,
IDataStoreProjectService,
IDataTableProjectAggregateService,
IDataTableProjectService,
IExecuteFunctions,
ILoadOptionsFunctions,
DataStoreColumnJsType,
DataTableColumnJsType,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
@ -28,50 +28,50 @@ function isDateLike(v: unknown): v is DateLike {
export async function getDataTableProxyExecute(
ctx: IExecuteFunctions,
index: number = 0,
): Promise<IDataStoreProjectService> {
if (ctx.helpers.getDataStoreProxy === undefined)
): Promise<IDataTableProjectService> {
if (ctx.helpers.getDataTableProxy === undefined)
throw new NodeOperationError(
ctx.getNode(),
'Attempted to use Data table node but the module is disabled',
);
const dataStoreId = ctx.getNodeParameter(DATA_TABLE_ID_FIELD, index, undefined, {
const dataTableId = ctx.getNodeParameter(DATA_TABLE_ID_FIELD, index, undefined, {
extractValue: true,
}) as string;
return await ctx.helpers.getDataStoreProxy(dataStoreId);
return await ctx.helpers.getDataTableProxy(dataTableId);
}
export async function getDataTableProxyLoadOptions(
ctx: ILoadOptionsFunctions,
): Promise<IDataStoreProjectService | undefined> {
if (ctx.helpers.getDataStoreProxy === undefined)
): Promise<IDataTableProjectService | undefined> {
if (ctx.helpers.getDataTableProxy === undefined)
throw new NodeOperationError(
ctx.getNode(),
'Attempted to use Data table node but the module is disabled',
);
const dataStoreId = ctx.getNodeParameter(DATA_TABLE_ID_FIELD, undefined, {
const dataTableId = ctx.getNodeParameter(DATA_TABLE_ID_FIELD, undefined, {
extractValue: true,
}) as string;
if (!dataStoreId) {
if (!dataTableId) {
return;
}
return await ctx.helpers.getDataStoreProxy(dataStoreId);
return await ctx.helpers.getDataTableProxy(dataTableId);
}
export async function getDataTableAggregateProxy(
ctx: IExecuteFunctions | ILoadOptionsFunctions,
): Promise<IDataStoreProjectAggregateService> {
if (ctx.helpers.getDataStoreAggregateProxy === undefined)
): Promise<IDataTableProjectAggregateService> {
if (ctx.helpers.getDataTableAggregateProxy === undefined)
throw new NodeOperationError(
ctx.getNode(),
'Attempted to use Data table node but the module is disabled',
);
return await ctx.helpers.getDataStoreAggregateProxy();
return await ctx.helpers.getDataTableAggregateProxy();
}
export function isFieldEntry(obj: unknown): obj is FieldEntry {
@ -134,9 +134,9 @@ export function dataObjectToApiInput(
data: IDataObject,
node: INode,
row: number,
): Record<string, DataStoreColumnJsType> {
): Record<string, DataTableColumnJsType> {
return Object.fromEntries(
Object.entries(data).map(([k, v]): [string, DataStoreColumnJsType] => {
Object.entries(data).map(([k, v]): [string, DataTableColumnJsType] => {
if (v === undefined || v === null) return [k, null];
if (Array.isArray(v)) {

View File

@ -1,7 +1,7 @@
import {
type INode,
NodeOperationError,
type IDataStoreProjectService,
type IDataTableProjectService,
type IExecuteFunctions,
} from 'n8n-workflow';
@ -13,9 +13,9 @@ import { executeSelectMany, getSelectFilter } from '../../common/selectMany';
describe('selectMany utils', () => {
let mockExecuteFunctions: IExecuteFunctions;
const getManyRowsAndCount = jest.fn();
const dataStoreProxy = jest.mocked<IDataStoreProjectService>({
const dataStoreProxy = jest.mocked<IDataTableProjectService>({
getManyRowsAndCount,
} as unknown as IDataStoreProjectService);
} as unknown as IDataTableProjectService);
const dataTableId = 2345;
let filters: FieldEntry[];
const node = { id: 1 } as unknown as INode;
@ -29,7 +29,7 @@ describe('selectMany utils', () => {
},
];
const mockDataStoreProxy = {
const mockDataTableProxy = {
getColumns: jest.fn().mockResolvedValue([
{ name: 'name', type: 'string' },
{ name: 'age', type: 'number' },
@ -50,7 +50,7 @@ describe('selectMany utils', () => {
}
}),
helpers: {
getDataStoreProxy: jest.fn().mockResolvedValue(mockDataStoreProxy),
getDataTableProxy: jest.fn().mockResolvedValue(mockDataTableProxy),
},
} as unknown as IExecuteFunctions;

View File

@ -190,7 +190,7 @@ export class EvaluationTrigger implements INodeType {
? (this.getNodeParameter('maxRows', 0, MAX_ROWS) as number)
: MAX_ROWS;
if (this.helpers.getDataStoreProxy === undefined) {
if (this.helpers.getDataTableProxy === undefined) {
throw new NodeOperationError(
this.getNode(),
'Attempted to use Data table node but the module is disabled',
@ -205,7 +205,7 @@ export class EvaluationTrigger implements INodeType {
const dataTableId = this.getNodeParameter('dataTableId', 0, undefined, {
extractValue: true,
}) as string;
const dataTableProxy = await this.helpers.getDataStoreProxy(dataTableId);
const dataTableProxy = await this.helpers.getDataTableProxy(dataTableId);
const filter = await getDataTableFilter(this, 0);
@ -318,7 +318,7 @@ export class EvaluationTrigger implements INodeType {
? (this.getNodeParameter('maxRows', 0, MAX_ROWS) as number)
: MAX_ROWS;
if (this.helpers.getDataStoreProxy === undefined) {
if (this.helpers.getDataTableProxy === undefined) {
throw new NodeOperationError(
this.getNode(),
'Attempted to use Data table node but the module is disabled',
@ -328,7 +328,7 @@ export class EvaluationTrigger implements INodeType {
const dataTableId = this.getNodeParameter('dataTableId', 0, undefined, {
extractValue: true,
}) as string;
const dataTableProxy = await this.helpers.getDataStoreProxy(dataTableId);
const dataTableProxy = await this.helpers.getDataTableProxy(dataTableId);
const filter = await getDataTableFilter(this, 0);
const { data } = await dataTableProxy.getManyRowsAndCount({

View File

@ -1,6 +1,6 @@
import { mock } from 'jest-mock-extended';
import {
type IDataStoreProjectService,
type IDataTableProjectService,
NodeOperationError,
type AssignmentCollectionValue,
type IExecuteFunctions,
@ -15,14 +15,14 @@ describe('Test Evaluation', () => {
const sheetName = 'Sheet5';
const spreadsheetId = '1oqFpPgEPTGDw7BPkp1SfPXq3Cb3Hyr1SROtf-Ec4zvA';
const mockDataTable = mock<IDataStoreProjectService>({
const mockDataTable = mock<IDataTableProjectService>({
getColumns: jest.fn(),
addColumn: jest.fn(),
updateRows: jest.fn(),
});
const mockExecuteFunctions = mock<IExecuteFunctions>({
helpers: { getDataStoreProxy: jest.fn().mockResolvedValue(mockDataTable) },
helpers: { getDataTableProxy: jest.fn().mockResolvedValue(mockDataTable) },
});
beforeEach(() => {

View File

@ -338,7 +338,7 @@ describe('Evaluation Trigger Node', () => {
mockExecuteFunctions = mockDeep<IExecuteFunctions>({
getNode: jest.fn().mockReturnValue({ typeVersion: 4.7 }),
helpers: {
getDataStoreProxy: jest.fn().mockResolvedValue(mockDataTable),
getDataTableProxy: jest.fn().mockResolvedValue(mockDataTable),
},
});
});

View File

@ -175,7 +175,7 @@ describe('setOutputs', () => {
addExecutionHints: jest.fn(),
getMode: jest.fn().mockReturnValue('evaluation'),
helpers: {
getDataStoreProxy: jest.fn().mockResolvedValue(mockDataTable),
getDataTableProxy: jest.fn().mockResolvedValue(mockDataTable),
},
...options,
});

View File

@ -11,7 +11,7 @@ import type {
INodeExecutionData,
JsonObject,
JsonValue,
DataStoreColumnJsType,
DataTableColumnJsType,
} from 'n8n-workflow';
import { getGoogleSheet, getSheet } from './evaluationTriggerUtils';
@ -52,7 +52,7 @@ function isOutputsArray(
);
}
export function toDataTableValue(value: JsonValue): DataStoreColumnJsType {
export function toDataTableValue(value: JsonValue): DataTableColumnJsType {
if (
typeof value === 'string' ||
typeof value === 'number' ||
@ -139,7 +139,7 @@ export async function setOutputs(this: IExecuteFunctions): Promise<INodeExecutio
const source = this.getNodeParameter('source', 0) as string;
if (source === 'dataTable') {
if (this.helpers.getDataStoreProxy === undefined) {
if (this.helpers.getDataTableProxy === undefined) {
throw new NodeOperationError(
this.getNode(),
'Attempted to use Data table node but the module is disabled',
@ -149,7 +149,7 @@ export async function setOutputs(this: IExecuteFunctions): Promise<INodeExecutio
const dataTableId = this.getNodeParameter('dataTableId', 0, undefined, {
extractValue: true,
}) as string;
const dataTableProxy = await this.helpers.getDataStoreProxy(dataTableId);
const dataTableProxy = await this.helpers.getDataTableProxy(dataTableId);
const rowId = typeof evaluationTrigger.row_id === 'number' ? evaluationTrigger.row_id : 1;

View File

@ -1,190 +0,0 @@
export type DataStoreColumnType = 'string' | 'number' | 'boolean' | 'date';
export type DataStoreColumn = {
id: string;
name: string;
type: DataStoreColumnType;
index: number;
dataTableId: string;
};
export type DataStore = {
id: string;
name: string;
columns: DataStoreColumn[];
createdAt: Date;
updatedAt: Date;
projectId: string;
};
export type CreateDataStoreColumnOptions = Pick<DataStoreColumn, 'name' | 'type'> &
Partial<Pick<DataStoreColumn, 'index'>>;
export type CreateDataStoreOptions = Pick<DataStore, 'name'> & {
columns: CreateDataStoreColumnOptions[];
};
export type UpdateDataStoreOptions = { name: string };
export type ListDataStoreOptions = {
filter?: Record<string, string | string[]>;
sortBy?:
| 'name:asc'
| 'name:desc'
| 'createdAt:asc'
| 'createdAt:desc'
| 'updatedAt:asc'
| 'updatedAt:desc'
| 'sizeBytes:asc'
| 'sizeBytes:desc';
take?: number;
skip?: number;
};
export type DataTableFilter = {
type: 'and' | 'or';
filters: Array<{
columnName: string;
condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
value: DataStoreColumnJsType;
}>;
};
export type ListDataStoreRowsOptions = {
filter?: DataTableFilter;
sortBy?: [string, 'ASC' | 'DESC'];
take?: number;
skip?: number;
};
export type UpdateDataStoreRowOptions = {
filter: DataTableFilter;
data: DataStoreRow;
dryRun?: boolean;
};
export type UpsertDataStoreRowOptions = {
filter: DataTableFilter;
data: DataStoreRow;
dryRun?: boolean;
};
export type DeleteDataTableRowsOptions = {
filter: DataTableFilter;
dryRun?: boolean;
};
export type MoveDataStoreColumnOptions = {
targetIndex: number;
};
export type AddDataStoreColumnOptions = Pick<DataStoreColumn, 'name' | 'type'> &
Partial<Pick<DataStoreColumn, 'index'>>;
export type DataStoreColumnJsType = string | number | boolean | Date | null;
export const DATA_TABLE_SYSTEM_COLUMN_TYPE_MAP: Record<string, DataStoreColumnType> = {
id: 'number',
createdAt: 'date',
updatedAt: 'date',
};
export const DATA_TABLE_SYSTEM_COLUMNS = Object.keys(DATA_TABLE_SYSTEM_COLUMN_TYPE_MAP);
export const DATA_TABLE_SYSTEM_TESTING_COLUMN = 'dryRunState';
export type DataStoreRowReturnBase = {
id: number;
createdAt: Date;
updatedAt: Date;
};
export type DataStoreRow = Record<string, DataStoreColumnJsType>;
export type DataStoreRows = DataStoreRow[];
export type DataStoreRowReturn = DataStoreRow & DataStoreRowReturnBase;
export type DataStoreRowsReturn = DataStoreRowReturn[];
export type DataStoreRowReturnWithState = DataStoreRow & {
id: number | null;
createdAt: Date | null;
updatedAt: Date | null;
dryRunState: 'before' | 'after';
};
export type DataStoreRowUpdatePair = {
before: DataStoreRowReturn;
after: DataStoreRowReturn;
};
export type DataTableInsertRowsReturnType = 'all' | 'id' | 'count';
export type DataTableInsertRowsBulkResult = { success: true; insertedRows: number };
export type DataTableInsertRowsResult<
T extends DataTableInsertRowsReturnType = DataTableInsertRowsReturnType,
> = T extends 'all'
? DataStoreRowReturn[]
: T extends 'id'
? Array<Pick<DataStoreRowReturn, 'id'>>
: DataTableInsertRowsBulkResult;
export type DataTableSizeStatus = 'ok' | 'warn' | 'error';
export type DataTableInfo = {
id: string;
name: string;
projectId: string;
projectName: string;
sizeBytes: number;
};
export type DataTableInfoById = Record<string, DataTableInfo>;
export type DataTablesSizeData = {
totalBytes: number;
dataTables: DataTableInfoById;
};
export type DataTablesSizeResult = DataTablesSizeData & {
quotaStatus: DataTableSizeStatus;
};
// APIs for a data store service operating on a specific projectId
export interface IDataStoreProjectAggregateService {
getProjectId(): string;
createDataStore(options: CreateDataStoreOptions): Promise<DataStore>;
getManyAndCount(options: ListDataStoreOptions): Promise<{ count: number; data: DataStore[] }>;
deleteDataStoreAll(): Promise<boolean>;
}
// APIs for a data store service operating on a specific projectId and dataStoreId
export interface IDataStoreProjectService {
updateDataStore(options: UpdateDataStoreOptions): Promise<boolean>;
deleteDataStore(): Promise<boolean>;
getColumns(): Promise<DataStoreColumn[]>;
addColumn(options: AddDataStoreColumnOptions): Promise<DataStoreColumn>;
moveColumn(columnId: string, options: MoveDataStoreColumnOptions): Promise<boolean>;
deleteColumn(columnId: string): Promise<boolean>;
getManyRowsAndCount(
dto: Partial<ListDataStoreRowsOptions>,
): Promise<{ count: number; data: DataStoreRowsReturn }>;
insertRows<T extends DataTableInsertRowsReturnType>(
rows: DataStoreRows,
returnType: T,
): Promise<DataTableInsertRowsResult<T>>;
updateRows(
options: UpdateDataStoreRowOptions,
): Promise<DataStoreRowReturn[] | DataStoreRowReturnWithState[]>;
upsertRow(
options: UpsertDataStoreRowOptions,
): Promise<DataStoreRowReturn[] | DataStoreRowReturnWithState[]>;
deleteRows(options: DeleteDataTableRowsOptions): Promise<DataStoreRowReturn[]>;
}

View File

@ -0,0 +1,190 @@
export type DataTableColumnType = 'string' | 'number' | 'boolean' | 'date';
export type DataTableColumn = {
id: string;
name: string;
type: DataTableColumnType;
index: number;
dataTableId: string;
};
export type DataTable = {
id: string;
name: string;
columns: DataTableColumn[];
createdAt: Date;
updatedAt: Date;
projectId: string;
};
export type CreateDataTableColumnOptions = Pick<DataTableColumn, 'name' | 'type'> &
Partial<Pick<DataTableColumn, 'index'>>;
export type CreateDataTableOptions = Pick<DataTable, 'name'> & {
columns: CreateDataTableColumnOptions[];
};
export type UpdateDataTableOptions = { name: string };
export type ListDataTableOptions = {
filter?: Record<string, string | string[]>;
sortBy?:
| 'name:asc'
| 'name:desc'
| 'createdAt:asc'
| 'createdAt:desc'
| 'updatedAt:asc'
| 'updatedAt:desc'
| 'sizeBytes:asc'
| 'sizeBytes:desc';
take?: number;
skip?: number;
};
export type DataTableFilter = {
type: 'and' | 'or';
filters: Array<{
columnName: string;
condition: 'eq' | 'neq' | 'like' | 'ilike' | 'gt' | 'gte' | 'lt' | 'lte';
value: DataTableColumnJsType;
}>;
};
export type ListDataTableRowsOptions = {
filter?: DataTableFilter;
sortBy?: [string, 'ASC' | 'DESC'];
take?: number;
skip?: number;
};
export type UpdateDataTableRowOptions = {
filter: DataTableFilter;
data: DataTableRow;
dryRun?: boolean;
};
export type UpsertDataTableRowOptions = {
filter: DataTableFilter;
data: DataTableRow;
dryRun?: boolean;
};
export type DeleteDataTableRowsOptions = {
filter: DataTableFilter;
dryRun?: boolean;
};
export type MoveDataTableColumnOptions = {
targetIndex: number;
};
export type AddDataTableColumnOptions = Pick<DataTableColumn, 'name' | 'type'> &
Partial<Pick<DataTableColumn, 'index'>>;
export type DataTableColumnJsType = string | number | boolean | Date | null;
export const DATA_TABLE_SYSTEM_COLUMN_TYPE_MAP: Record<string, DataTableColumnType> = {
id: 'number',
createdAt: 'date',
updatedAt: 'date',
};
export const DATA_TABLE_SYSTEM_COLUMNS = Object.keys(DATA_TABLE_SYSTEM_COLUMN_TYPE_MAP);
export const DATA_TABLE_SYSTEM_TESTING_COLUMN = 'dryRunState';
export type DataTableRowReturnBase = {
id: number;
createdAt: Date;
updatedAt: Date;
};
export type DataTableRow = Record<string, DataTableColumnJsType>;
export type DataTableRows = DataTableRow[];
export type DataTableRowReturn = DataTableRow & DataTableRowReturnBase;
export type DataTableRowsReturn = DataTableRowReturn[];
export type DataTableRowReturnWithState = DataTableRow & {
id: number | null;
createdAt: Date | null;
updatedAt: Date | null;
dryRunState: 'before' | 'after';
};
export type DataTableRowUpdatePair = {
before: DataTableRowReturn;
after: DataTableRowReturn;
};
export type DataTableInsertRowsReturnType = 'all' | 'id' | 'count';
export type DataTableInsertRowsBulkResult = { success: true; insertedRows: number };
export type DataTableInsertRowsResult<
T extends DataTableInsertRowsReturnType = DataTableInsertRowsReturnType,
> = T extends 'all'
? DataTableRowReturn[]
: T extends 'id'
? Array<Pick<DataTableRowReturn, 'id'>>
: DataTableInsertRowsBulkResult;
export type DataTableSizeStatus = 'ok' | 'warn' | 'error';
export type DataTableInfo = {
id: string;
name: string;
projectId: string;
projectName: string;
sizeBytes: number;
};
export type DataTableInfoById = Record<string, DataTableInfo>;
export type DataTablesSizeData = {
totalBytes: number;
dataTables: DataTableInfoById;
};
export type DataTablesSizeResult = DataTablesSizeData & {
quotaStatus: DataTableSizeStatus;
};
// APIs for a data table service operating on a specific projectId
export interface IDataTableProjectAggregateService {
getProjectId(): string;
createDataTable(options: CreateDataTableOptions): Promise<DataTable>;
getManyAndCount(options: ListDataTableOptions): Promise<{ count: number; data: DataTable[] }>;
deleteDataTableAll(): Promise<boolean>;
}
// APIs for a data table service operating on a specific projectId and dataTableId
export interface IDataTableProjectService {
updateDataTable(options: UpdateDataTableOptions): Promise<boolean>;
deleteDataTable(): Promise<boolean>;
getColumns(): Promise<DataTableColumn[]>;
addColumn(options: AddDataTableColumnOptions): Promise<DataTableColumn>;
moveColumn(columnId: string, options: MoveDataTableColumnOptions): Promise<boolean>;
deleteColumn(columnId: string): Promise<boolean>;
getManyRowsAndCount(
dto: Partial<ListDataTableRowsOptions>,
): Promise<{ count: number; data: DataTableRowsReturn }>;
insertRows<T extends DataTableInsertRowsReturnType>(
rows: DataTableRows,
returnType: T,
): Promise<DataTableInsertRowsResult<T>>;
updateRows(
options: UpdateDataTableRowOptions,
): Promise<DataTableRowReturn[] | DataTableRowReturnWithState[]>;
upsertRow(
options: UpsertDataTableRowOptions,
): Promise<DataTableRowReturn[] | DataTableRowReturnWithState[]>;
deleteRows(options: DeleteDataTableRowsOptions): Promise<DataTableRowReturn[]>;
}

View File

@ -7,7 +7,7 @@ export * from './errors';
export * from './constants';
export * from './common';
export * from './cron';
export * from './data-store.types';
export * from './data-table.types';
export * from './deferred-promise';
export * from './global-state';
export * from './interfaces';

View File

@ -14,9 +14,9 @@ import type { URLSearchParams } from 'url';
import type { CODE_EXECUTION_MODES, CODE_LANGUAGES, LOG_LEVELS } from './constants';
import type {
IDataStoreProjectAggregateService,
IDataStoreProjectService,
} from './data-store.types';
IDataTableProjectAggregateService,
IDataTableProjectService,
} from './data-table.types';
import type { IDeferredPromise } from './deferred-promise';
import type { ExecutionCancelledError } from './errors';
import type { ExpressionError } from './errors/expression.error';
@ -925,24 +925,24 @@ type FunctionsBaseWithRequiredKeys<Keys extends keyof FunctionsBase> = Functions
export type ContextType = 'flow' | 'node';
export type DataStoreProxyProvider = {
getDataStoreAggregateProxy(
export type DataTableProxyProvider = {
getDataTableAggregateProxy(
workflow: Workflow,
node: INode,
projectId?: string,
): Promise<IDataStoreProjectAggregateService>;
getDataStoreProxy(
): Promise<IDataTableProjectAggregateService>;
getDataTableProxy(
workflow: Workflow,
node: INode,
dataStoreId: string,
dataTableId: string,
projectId?: string,
): Promise<IDataStoreProjectService>;
): Promise<IDataTableProjectService>;
};
export type DataStoreProxyFunctions = {
// These are optional to account for situations where the data-store module is disabled
getDataStoreAggregateProxy?(): Promise<IDataStoreProjectAggregateService>;
getDataStoreProxy?(dataStoreId: string): Promise<IDataStoreProjectService>;
export type DataTableProxyFunctions = {
// These are optional to account for situations where the data-table module is disabled
getDataTableAggregateProxy?(): Promise<IDataTableProjectAggregateService>;
getDataTableProxy?(dataTableId: string): Promise<IDataTableProjectService>;
};
type BaseExecutionFunctions = FunctionsBaseWithRequiredKeys<'getMode'> & {
@ -1008,7 +1008,7 @@ export type IExecuteFunctions = ExecuteFunctions.GetNodeParameterFn &
DeduplicationHelperFunctions &
FileSystemHelperFunctions &
SSHTunnelFunctions &
DataStoreProxyFunctions & {
DataTableProxyFunctions & {
normalizeItems(items: INodeExecutionData | INodeExecutionData[]): INodeExecutionData[];
constructExecutionMetaData(
inputData: INodeExecutionData[],
@ -1093,7 +1093,7 @@ export interface ILoadOptionsFunctions extends FunctionsBase {
): NodeParameterValueType | object | undefined;
getCurrentNodeParameters(): INodeParameters | undefined;
helpers: RequestHelperFunctions & SSHTunnelFunctions & DataStoreProxyFunctions;
helpers: RequestHelperFunctions & SSHTunnelFunctions & DataTableProxyFunctions;
}
export type FieldValueOption = { name: string; type: FieldType | 'any' };