mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(core): Add Data Store Backend API (no-changelog) (#17824)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { dataStoreCreateColumnSchema } from '../../schemas/data-store.schema';
|
||||
|
||||
export class AddDataStoreColumnDto extends Z.class(dataStoreCreateColumnSchema.shape) {}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { dataStoreColumnNameSchema } from '../../schemas/data-store.schema';
|
||||
|
||||
export class AddDataStoreRowsDto extends Z.class({
|
||||
data: z.array(z.record(dataStoreColumnNameSchema, z.any())),
|
||||
}) {}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import {
|
||||
dataStoreColumnNameSchema,
|
||||
dataStoreColumnTypeSchema,
|
||||
} from '../../schemas/data-store.schema';
|
||||
|
||||
export class CreateDataStoreColumnDto extends Z.class({
|
||||
name: dataStoreColumnNameSchema,
|
||||
type: dataStoreColumnTypeSchema,
|
||||
}) {}
|
||||
@@ -0,0 +1,10 @@
|
||||
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),
|
||||
}) {}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { dataStoreColumnNameSchema } from '../../schemas/data-store.schema';
|
||||
import { paginationSchema } from '../pagination/pagination.dto';
|
||||
|
||||
const FilterConditionSchema = z.union([z.literal('eq'), z.literal('neq')]);
|
||||
export type ListDataStoreContentFilterConditionType = z.infer<typeof FilterConditionSchema>;
|
||||
|
||||
const filterRecord = z.object({
|
||||
columnName: dataStoreColumnNameSchema,
|
||||
condition: FilterConditionSchema.default('eq'),
|
||||
value: z.union([z.string(), z.number(), z.boolean(), z.date()]),
|
||||
});
|
||||
|
||||
const chainedFilterSchema = z.union([z.literal('and'), z.literal('or')]);
|
||||
|
||||
export type ListDataStoreContentFilter = z.infer<typeof filterSchema>;
|
||||
|
||||
// ---------------------
|
||||
// Parameter Validators
|
||||
// ---------------------
|
||||
|
||||
const filterSchema = z.object({
|
||||
type: chainedFilterSchema.default('and'),
|
||||
filters: z.array(filterRecord).default([]),
|
||||
});
|
||||
|
||||
// Filter parameter validation
|
||||
const filterValidator = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val, ctx) => {
|
||||
if (!val) return undefined;
|
||||
try {
|
||||
const parsed: unknown = jsonParse(val);
|
||||
try {
|
||||
return filterSchema.parse(parsed);
|
||||
} catch (e) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid filter fields',
|
||||
path: ['filter'],
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid filter format',
|
||||
path: ['filter'],
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
});
|
||||
|
||||
// SortBy parameter validation
|
||||
const sortByValidator = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val, ctx) => {
|
||||
if (val === undefined) return val;
|
||||
|
||||
if (!val.includes(':')) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid sort format, expected <columnName>:<asc/desc>',
|
||||
path: ['sort'],
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
|
||||
let [column, direction] = val.split(':');
|
||||
|
||||
try {
|
||||
column = dataStoreColumnNameSchema.parse(column);
|
||||
} catch {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid sort columnName',
|
||||
path: ['sort'],
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
|
||||
direction = direction?.toUpperCase();
|
||||
if (direction !== 'ASC' && direction !== 'DESC') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid sort direction',
|
||||
path: ['sort'],
|
||||
});
|
||||
|
||||
return z.NEVER;
|
||||
}
|
||||
return [column, direction] as const;
|
||||
});
|
||||
|
||||
export class ListDataStoreContentQueryDto extends Z.class({
|
||||
take: paginationSchema.take.optional(),
|
||||
skip: paginationSchema.skip.optional(),
|
||||
filter: filterValidator.optional(),
|
||||
sortBy: sortByValidator.optional(),
|
||||
}) {}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { paginationSchema } from '../pagination/pagination.dto';
|
||||
|
||||
const VALID_SORT_OPTIONS = [
|
||||
'name:asc',
|
||||
'name:desc',
|
||||
'createdAt:asc',
|
||||
'createdAt:desc',
|
||||
'updatedAt:asc',
|
||||
'updatedAt:desc',
|
||||
'sizeBytes:asc',
|
||||
'sizeBytes:desc',
|
||||
] as const;
|
||||
|
||||
export type ListDataStoreQuerySortOptions = (typeof VALID_SORT_OPTIONS)[number];
|
||||
|
||||
const FILTER_OPTIONS = {
|
||||
id: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
name: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
projectId: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
// todo: can probably include others here as well?
|
||||
};
|
||||
|
||||
// Filter schema - only allow specific properties
|
||||
const filterSchema = z.object(FILTER_OPTIONS).strict();
|
||||
// ---------------------
|
||||
// Parameter Validators
|
||||
// ---------------------
|
||||
|
||||
// Filter parameter validation
|
||||
const filterValidator = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val, ctx) => {
|
||||
if (!val) return undefined;
|
||||
try {
|
||||
const parsed: unknown = jsonParse(val);
|
||||
try {
|
||||
return filterSchema.parse(parsed);
|
||||
} catch (e) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid filter fields',
|
||||
path: ['filter'],
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
} catch (e) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Invalid filter format',
|
||||
path: ['filter'],
|
||||
});
|
||||
return z.NEVER;
|
||||
}
|
||||
});
|
||||
|
||||
// SortBy parameter validation
|
||||
const sortByValidator = z
|
||||
.enum(VALID_SORT_OPTIONS, { message: `sortBy must be one of: ${VALID_SORT_OPTIONS.join(', ')}` })
|
||||
.optional();
|
||||
|
||||
export class ListDataStoreQueryDto extends Z.class({
|
||||
...paginationSchema,
|
||||
filter: filterValidator,
|
||||
sortBy: sortByValidator,
|
||||
}) {}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class MoveDataStoreColumnDto extends Z.class({
|
||||
targetIndex: z.number(),
|
||||
}) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { dataStoreNameSchema } from '../../schemas/data-store.schema';
|
||||
|
||||
export class UpdateDataStoreDto extends Z.class({
|
||||
name: dataStoreNameSchema,
|
||||
}) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
import { dataStoreColumnNameSchema } from '../../schemas/data-store.schema';
|
||||
|
||||
const dataStoreValueSchema = z.union([z.string(), z.number(), z.boolean(), z.date(), z.null()]);
|
||||
|
||||
const upsertDataStoreRowsShape = {
|
||||
rows: z.array(z.record(dataStoreValueSchema)),
|
||||
matchFields: z.array(dataStoreColumnNameSchema).min(1),
|
||||
};
|
||||
|
||||
export class UpsertDataStoreRowsDto extends Z.class(upsertDataStoreRowsShape) {}
|
||||
@@ -79,3 +79,16 @@ export {
|
||||
} from './user/users-list-filter.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 { UpsertDataStoreRowsDto } from './data-store/upsert-data-store-rows.dto';
|
||||
export { ListDataStoreQueryDto } from './data-store/list-data-store-query.dto';
|
||||
export {
|
||||
ListDataStoreContentQueryDto,
|
||||
ListDataStoreContentFilter,
|
||||
} 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';
|
||||
|
||||
@@ -45,3 +45,15 @@ export {
|
||||
type UsersList,
|
||||
usersListSchema,
|
||||
} from './schemas/user.schema';
|
||||
|
||||
export {
|
||||
DATA_STORE_COLUMN_REGEX,
|
||||
type DataStore,
|
||||
type DataStoreColumn,
|
||||
type DataStoreCreateColumnSchema,
|
||||
type DataStoreColumnJsType,
|
||||
type DataStoreListFilter,
|
||||
type DataStoreRows,
|
||||
type DataStoreListOptions,
|
||||
type DataStoreUserTableName,
|
||||
} from './schemas/data-store.schema';
|
||||
|
||||
56
packages/@n8n/api-types/src/schemas/data-store.schema.ts
Normal file
56
packages/@n8n/api-types/src/schemas/data-store.schema.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { ListDataStoreQueryDto } from '../dto';
|
||||
|
||||
export const dataStoreNameSchema = z.string().trim().min(1).max(128);
|
||||
export const dataStoreIdSchema = z.string().max(36);
|
||||
|
||||
export const DATA_STORE_COLUMN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]*$/;
|
||||
|
||||
export const dataStoreColumnNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(128)
|
||||
.regex(
|
||||
DATA_STORE_COLUMN_REGEX,
|
||||
'Only alphanumeric characters and non-leading dashes are allowed for column names',
|
||||
);
|
||||
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 DataStoreUserTableName = `data_store_user_${string}`;
|
||||
|
||||
export type DataStoreListFilter = {
|
||||
id?: string | string[];
|
||||
projectId?: string | string[];
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type DataStoreListOptions = Partial<ListDataStoreQueryDto> & {
|
||||
filter: { projectId: string };
|
||||
};
|
||||
|
||||
export type DataStoreColumnJsType = string | number | boolean | Date;
|
||||
|
||||
export type DataStoreRows = Array<Record<string, DataStoreColumnJsType | null>>;
|
||||
Reference in New Issue
Block a user