-
Notifications
You must be signed in to change notification settings - Fork 41.1k
feat(core): Add Data Store Backend API (no-changelog) #17824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
046b88e
454db2f
ada6537
cc24e98
973c1c9
cf6df74
507bcd0
309e5d8
3e11e93
9318e6e
8b469ff
0e7d5ac
9fcbb9f
0695ed0
01900d5
331fc9b
85a6230
0f8e2b1
cb0455a
b4a10a3
0ff50cf
33f7afe
1e8a78f
461d23c
2b26741
70cb3b6
b1a2b05
fa33743
ade6eba
8434c5a
e8146a6
7967864
8e6811d
e13746d
eb5b4b8
a0a4ac0
8ba6878
1453658
96e7f6e
9d37427
419f517
23d6a84
c0c8eac
5124f2b
3201399
305e321
0079c9c
0ad74e4
815688b
205078f
7f0655e
43c00c7
1663e6f
21b12eb
9490926
b4c4be8
70e0485
f05e13f
c96dcca
fe24022
946aa47
b756b04
aa3fb84
82fe47b
c41af20
543ff08
0aa67ca
7796b75
f31d530
5acd853
20c75e0
7c8a9ed
fe7a8f7
14def7a
954a174
3dbf414
3194867
1841914
3da0eda
4c8b074
98598b9
f0d5ac1
4821a7f
5e8f65d
59fee3a
ac57b61
045b0e1
26a036a
61e72e3
edcf020
b580cba
9bbcfd9
3ed0dc4
9adb2f5
515fee9
2d0d9b8
8527674
91069c1
4551e64
205a612
bc9ff11
8afdfce
44a8c93
933a47d
62a5f37
7270296
e0cae29
a05ecf7
e9e51b4
c647a90
3c5e814
2ef3816
526a676
fe8935d
f4ee35b
81004be
a43e4d2
69842f5
79af2d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { Z } from 'zod-class'; | ||
|
||
import { dataStoreCreateColumnSchema } from '../../schemas/data-store.schema'; | ||
|
||
export class AddDataStoreColumnDto extends Z.class(dataStoreCreateColumnSchema.shape) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { z } from 'zod'; | ||
import { Z } from 'zod-class'; | ||
|
||
export class AddDatastoreRecordsDto extends Z.class({ | ||
records: z.array(z.record(z.string(), z.any())), | ||
}) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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), | ||
}) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Z } from 'zod-class'; | ||
|
||
import { dataStoreColumnNameSchema } from '../../schemas/data-store.schema'; | ||
|
||
export class DeleteDataStoreColumnDto extends Z.class({ | ||
columnName: dataStoreColumnNameSchema, | ||
}) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Z } from 'zod-class'; | ||
|
||
import { dataStoreIdSchema } from '../../schemas/data-store.schema'; | ||
|
||
export class DeleteDataStoreDto extends Z.class({ | ||
id: dataStoreIdSchema, | ||
}) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
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 filterValidator>; | ||
|
||
// --------------------- | ||
// Parameter Validators | ||
// --------------------- | ||
|
||
// Filter parameter validation | ||
const filterValidator = z.object({ | ||
type: chainedFilterSchema.default('and'), | ||
filters: z.array(filterRecord).default([]), | ||
}); | ||
|
||
// 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({ | ||
...paginationSchema, | ||
filter: filterValidator, | ||
sortBy: sortByValidator, | ||
}) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
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(), | ||
// 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note for later: we really need to find a way to factorize all those filter validator (same in list-folder-query.dto and users-list-filter.dto) |
||
.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, | ||
}) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Z } from 'zod-class'; | ||
|
||
import { dataStoreNameSchema } from '../../schemas/data-store.schema'; | ||
|
||
export class RenameDataStoreDto extends Z.class({ | ||
name: dataStoreNameSchema, | ||
}) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { z } from 'zod'; | ||
|
||
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, | ||
columnIndex: z.number().optional(), | ||
}); | ||
export type DataStoreCreateColumnSchema = z.infer<typeof dataStoreCreateColumnSchema>; | ||
|
||
export const dataStoreColumnSchema = dataStoreCreateColumnSchema.extend({ | ||
createdAt: z.string().datetime(), | ||
updatedAt: z.string().datetime(), | ||
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>; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,7 @@ export class ModuleRegistry { | |
private readonly modulesConfig: ModulesConfig, | ||
) {} | ||
|
||
private readonly defaultModules: ModuleName[] = ['insights', 'external-secrets']; | ||
private readonly defaultModules: ModuleName[] = ['insights', 'external-secrets', 'data-store']; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [nit] Shall we make it plural for consistency? |
||
|
||
private readonly activeModules: string[] = []; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import type { MigrationContext, ReversibleMigration } from '../migration-types'; | ||
|
||
const dataStoreTableName = 'data_store_entity'; | ||
const dataStoreColumnTableName = 'data_store_column_entity'; | ||
|
||
export class CreateDataStoreTables1747814180618 implements ReversibleMigration { | ||
async up({ schemaBuilder: { createTable, column } }: MigrationContext) { | ||
await createTable(dataStoreTableName) | ||
.withColumns( | ||
column('id').varchar(36).primary.notNull, | ||
column('name').varchar(128).notNull, | ||
column('projectId').varchar(36).notNull, | ||
column('sizeBytes').int.notNull, | ||
) | ||
.withForeignKey('projectId', { | ||
tableName: 'project', // @Review: this hardcodes a dependency on the project table, is this cool? | ||
columnName: 'id', | ||
onDelete: 'CASCADE', | ||
}) | ||
.withIndexOn(['projectId', 'name'], true).withTimestamps; // @Review: The intention here is to ensure each name can only be used once per project, so [projectId, name] needs to be unique, right?... | ||
dariacodes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
await createTable(dataStoreColumnTableName) | ||
.withColumns( | ||
column('id').varchar(36).primary.notNull, | ||
column('name').varchar(128).notNull, | ||
column('type').varchar(32).notNull, | ||
column('columnIndex').int.notNull, | ||
column('dataStoreId').varchar(36).notNull, | ||
) | ||
.withForeignKey('dataStoreId', { | ||
tableName: dataStoreTableName, | ||
columnName: 'id', | ||
onDelete: 'CASCADE', | ||
}) | ||
.withIndexOn(['dataStoreId', 'name'], true).withTimestamps; | ||
} | ||
|
||
async down({ schemaBuilder: { dropTable } }: MigrationContext) { | ||
await dropTable(dataStoreTableName); | ||
await dropTable(dataStoreColumnTableName); | ||
|
||
// @Review: Should we also drop all created user tables here? | ||
dariacodes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,6 +79,13 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [ | |
'insights:list', | ||
'folder:move', | ||
'oidc:manage', | ||
'dataStore:create', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these are for personal projects also, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be for all projects I think |
||
'dataStore:delete', | ||
'dataStore:read', | ||
'dataStore:update', | ||
'dataStore:list', | ||
'dataStore:readRow', | ||
'dataStore:writeRow', | ||
]; | ||
|
||
export const GLOBAL_ADMIN_SCOPES = GLOBAL_OWNER_SCOPES.concat(); | ||
|
@@ -98,4 +105,7 @@ export const GLOBAL_MEMBER_SCOPES: Scope[] = [ | |
'user:list', | ||
'variable:list', | ||
'variable:read', | ||
'dataStore:read', | ||
'dataStore:list', | ||
'dataStore:readRow', | ||
]; |
Uh oh!
There was an error while loading. Please reload this page.