diff --git a/packages/cli/src/databases/entities/folder.ts b/packages/cli/src/databases/entities/folder.ts new file mode 100644 index 0000000000..6ed45857f5 --- /dev/null +++ b/packages/cli/src/databases/entities/folder.ts @@ -0,0 +1,45 @@ +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, +} from '@n8n/typeorm'; + +import { WithTimestampsAndStringId } from './abstract-entity'; +import { Project } from './project'; +import { TagEntity } from './tag-entity'; +import { type WorkflowEntity } from './workflow-entity'; + +@Entity() +export class Folder extends WithTimestampsAndStringId { + @Column() + name: string; + + @ManyToOne(() => Folder, { nullable: true }) + @JoinColumn({ name: 'parentFolderId' }) + parentFolder: Folder | null; + + @ManyToOne(() => Project) + @JoinColumn({ name: 'projectId' }) + project: Project; + + @OneToMany('WorkflowEntity', 'parentFolder') + workflows: WorkflowEntity[]; + + @ManyToMany(() => TagEntity) + @JoinTable({ + name: 'folder_tag', + joinColumn: { + name: 'folderId', + referencedColumnName: 'id', + }, + inverseJoinColumn: { + name: 'tagId', + referencedColumnName: 'id', + }, + }) + tags: TagEntity[]; +} diff --git a/packages/cli/src/databases/entities/index.ts b/packages/cli/src/databases/entities/index.ts index 1393b6a305..14af478d4c 100644 --- a/packages/cli/src/databases/entities/index.ts +++ b/packages/cli/src/databases/entities/index.ts @@ -10,6 +10,7 @@ import { ExecutionAnnotation } from './execution-annotation.ee'; import { ExecutionData } from './execution-data'; import { ExecutionEntity } from './execution-entity'; import { ExecutionMetadata } from './execution-metadata'; +import { Folder } from './folder'; import { InstalledNodes } from './installed-nodes'; import { InstalledPackages } from './installed-packages'; import { InvalidAuthToken } from './invalid-auth-token'; @@ -66,4 +67,5 @@ export const entities = { TestMetric, TestRun, TestCaseExecution, + Folder, }; diff --git a/packages/cli/src/databases/entities/workflow-entity.ts b/packages/cli/src/databases/entities/workflow-entity.ts index 67d0f0e345..d6f0a615fa 100644 --- a/packages/cli/src/databases/entities/workflow-entity.ts +++ b/packages/cli/src/databases/entities/workflow-entity.ts @@ -1,4 +1,13 @@ -import { Column, Entity, Index, JoinColumn, JoinTable, ManyToMany, OneToMany } from '@n8n/typeorm'; +import { + Column, + Entity, + Index, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, +} from '@n8n/typeorm'; import { Length } from 'class-validator'; import { IConnections, IDataObject, IWorkflowSettings, WorkflowFEMeta } from 'n8n-workflow'; import type { IBinaryKeyData, INode, IPairedItemData } from 'n8n-workflow'; @@ -6,6 +15,7 @@ import type { IBinaryKeyData, INode, IPairedItemData } from 'n8n-workflow'; import type { IWorkflowDb } from '@/interfaces'; import { WithTimestampsAndStringId, dbType, jsonColumnType } from './abstract-entity'; +import { type Folder } from './folder'; import type { SharedWorkflow } from './shared-workflow'; import type { TagEntity } from './tag-entity'; import type { WorkflowStatistics } from './workflow-statistics'; @@ -88,6 +98,13 @@ export class WorkflowEntity extends WithTimestampsAndStringId implements IWorkfl @Column({ default: 0 }) triggerCount: number; + @ManyToOne('Folder', 'workflows', { + nullable: true, + onDelete: 'SET NULL', + }) + @JoinColumn({ name: 'parentFolderId' }) + parentFolder: Folder | null; + display() { return `"${this.name}" (ID: ${this.id})`; } diff --git a/packages/cli/src/databases/migrations/common/1738709609940-CreateFolderTable.ts b/packages/cli/src/databases/migrations/common/1738709609940-CreateFolderTable.ts new file mode 100644 index 0000000000..72955a45f1 --- /dev/null +++ b/packages/cli/src/databases/migrations/common/1738709609940-CreateFolderTable.ts @@ -0,0 +1,60 @@ +import type { MigrationContext, ReversibleMigration } from '@/databases/types'; + +export class CreateFolderTable1738709609940 implements ReversibleMigration { + async up({ runQuery, escape, schemaBuilder: { createTable, column } }: MigrationContext) { + const workflowTable = escape.tableName('workflow_entity'); + const workflowFolderId = escape.columnName('parentFolderId'); + const folderTable = escape.tableName('folder'); + const folderId = escape.columnName('id'); + + await createTable('folder') + .withColumns( + column('id').varchar(36).primary.notNull, + column('name').varchar(128).notNull, + column('parentFolderId').varchar(36).default(null), + column('projectId').varchar(36).notNull, + ) + .withForeignKey('projectId', { + tableName: 'project', + columnName: 'id', + onDelete: 'CASCADE', + }) + .withForeignKey('parentFolderId', { + tableName: 'folder', + columnName: 'id', + onDelete: 'CASCADE', + }) + .withIndexOn(['projectId', 'id'], true).withTimestamps; + + await createTable('folder_tag') + .withColumns( + column('folderId').varchar(36).primary.notNull, + column('tagId').varchar(36).primary.notNull, + ) + .withForeignKey('folderId', { + tableName: 'folder', + columnName: 'id', + onDelete: 'CASCADE', + }) + .withForeignKey('tagId', { + tableName: 'tag_entity', + columnName: 'id', + onDelete: 'CASCADE', + }); + + await runQuery( + `ALTER TABLE ${workflowTable} ADD COLUMN ${workflowFolderId} VARCHAR(36) DEFAULT NULL REFERENCES ${folderTable}(${folderId}) ON DELETE SET NULL`, + ); + } + + async down({ runQuery, escape, schemaBuilder: { dropTable } }: MigrationContext) { + const workflowTable = escape.tableName('workflow_entity'); + const workflowFolderId = escape.columnName('parentFolderId'); + + await runQuery(`ALTER TABLE ${workflowTable} DROP COLUMN ${workflowFolderId}`); + + await dropTable('folder_tag'); + + await dropTable('folder'); + } +} diff --git a/packages/cli/src/databases/migrations/mysqldb/index.ts b/packages/cli/src/databases/migrations/mysqldb/index.ts index 8789f7ac21..c339420a04 100644 --- a/packages/cli/src/databases/migrations/mysqldb/index.ts +++ b/packages/cli/src/databases/migrations/mysqldb/index.ts @@ -79,6 +79,7 @@ import { AddManagedColumnToCredentialsTable1734479635324 } from '../common/17344 import { AddStatsColumnsToTestRun1736172058779 } from '../common/1736172058779-AddStatsColumnsToTestRun'; import { CreateTestCaseExecutionTable1736947513045 } from '../common/1736947513045-CreateTestCaseExecutionTable'; import { AddErrorColumnsToTestRuns1737715421462 } from '../common/1737715421462-AddErrorColumnsToTestRuns'; +import { CreateFolderTable1738709609940 } from '../common/1738709609940-CreateFolderTable'; export const mysqlMigrations: Migration[] = [ InitialMigration1588157391238, @@ -160,4 +161,5 @@ export const mysqlMigrations: Migration[] = [ AddStatsColumnsToTestRun1736172058779, CreateTestCaseExecutionTable1736947513045, AddErrorColumnsToTestRuns1737715421462, + CreateFolderTable1738709609940, ]; diff --git a/packages/cli/src/databases/migrations/postgresdb/index.ts b/packages/cli/src/databases/migrations/postgresdb/index.ts index 40e78bd49c..cb667aabed 100644 --- a/packages/cli/src/databases/migrations/postgresdb/index.ts +++ b/packages/cli/src/databases/migrations/postgresdb/index.ts @@ -79,6 +79,7 @@ import { AddManagedColumnToCredentialsTable1734479635324 } from '../common/17344 import { AddStatsColumnsToTestRun1736172058779 } from '../common/1736172058779-AddStatsColumnsToTestRun'; import { CreateTestCaseExecutionTable1736947513045 } from '../common/1736947513045-CreateTestCaseExecutionTable'; import { AddErrorColumnsToTestRuns1737715421462 } from '../common/1737715421462-AddErrorColumnsToTestRuns'; +import { CreateFolderTable1738709609940 } from '../common/1738709609940-CreateFolderTable'; export const postgresMigrations: Migration[] = [ InitialMigration1587669153312, @@ -160,4 +161,5 @@ export const postgresMigrations: Migration[] = [ AddStatsColumnsToTestRun1736172058779, CreateTestCaseExecutionTable1736947513045, AddErrorColumnsToTestRuns1737715421462, + CreateFolderTable1738709609940, ]; diff --git a/packages/cli/src/databases/migrations/sqlite/1738709609940-CreateFolderTable.ts b/packages/cli/src/databases/migrations/sqlite/1738709609940-CreateFolderTable.ts new file mode 100644 index 0000000000..1ed2179f3d --- /dev/null +++ b/packages/cli/src/databases/migrations/sqlite/1738709609940-CreateFolderTable.ts @@ -0,0 +1,5 @@ +import { CreateFolderTable1738709609940 as BaseMigration } from '../common/1738709609940-CreateFolderTable'; + +export class CreateFolderTable1738709609940 extends BaseMigration { + transaction = false as const; +} diff --git a/packages/cli/src/databases/migrations/sqlite/index.ts b/packages/cli/src/databases/migrations/sqlite/index.ts index 4ee7549f94..5e26e91075 100644 --- a/packages/cli/src/databases/migrations/sqlite/index.ts +++ b/packages/cli/src/databases/migrations/sqlite/index.ts @@ -42,6 +42,7 @@ import { AddMissingPrimaryKeyOnAnnotationTagMapping1728659839644 } from './17286 import { AddProjectIcons1729607673469 } from './1729607673469-AddProjectIcons'; import { AddDescriptionToTestDefinition1731404028106 } from './1731404028106-AddDescriptionToTestDefinition'; import { MigrateTestDefinitionKeyToString1731582748663 } from './1731582748663-MigrateTestDefinitionKeyToString'; +import { CreateFolderTable1738709609940 } from './1738709609940-CreateFolderTable'; import { UniqueWorkflowNames1620821879465 } from '../common/1620821879465-UniqueWorkflowNames'; import { UpdateWorkflowCredentials1630330987096 } from '../common/1630330987096-UpdateWorkflowCredentials'; import { AddNodeIds1658930531669 } from '../common/1658930531669-AddNodeIds'; @@ -154,6 +155,7 @@ const sqliteMigrations: Migration[] = [ AddStatsColumnsToTestRun1736172058779, CreateTestCaseExecutionTable1736947513045, AddErrorColumnsToTestRuns1737715421462, + CreateFolderTable1738709609940, ]; export { sqliteMigrations }; diff --git a/packages/cli/src/databases/repositories/folder.repository.ts b/packages/cli/src/databases/repositories/folder.repository.ts new file mode 100644 index 0000000000..7f1143b83c --- /dev/null +++ b/packages/cli/src/databases/repositories/folder.repository.ts @@ -0,0 +1,11 @@ +import { Service } from '@n8n/di'; +import { DataSource, Repository } from '@n8n/typeorm'; + +import { Folder } from '../entities/folder'; + +@Service() +export class FolderRepository extends Repository { + constructor(dataSource: DataSource) { + super(Folder, dataSource.manager); + } +}