From d42484b7f62957ba236a3a4eb88af0486341e724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ir=C3=A9n=C3=A9e?= Date: Wed, 19 Aug 2026 07:21:32 +0000 Subject: [PATCH] feat(core): Add project Git connection table (no-changelog) (#36549) Co-authored-by: Claude Opus 4.8 (1M context) --- docs/generated/postgres-schema/README.md | 9 ++ .../postgres-schema/public.git_connection.md | 9 +- .../public.git_connection_project.md | 75 +++++++++ .../postgres-schema/public.project.md | 9 +- docs/generated/sqlite-schema/README.md | 9 ++ .../generated/sqlite-schema/git_connection.md | 9 +- .../sqlite-schema/git_connection_project.md | 83 ++++++++++ docs/generated/sqlite-schema/project.md | 9 +- ...9039726-CreateGitConnectionProjectTable.ts | 35 +++++ .../entities/git-connection-project.entity.ts | 34 ++++ .../ownership-transfer.manifest.json | 5 + ...reate-git-connection-project-table.test.ts | 145 ++++++++++++++++++ 12 files changed, 427 insertions(+), 4 deletions(-) create mode 100644 docs/generated/postgres-schema/public.git_connection_project.md create mode 100644 docs/generated/sqlite-schema/git_connection_project.md create mode 100644 packages/@n8n/db/src/migrations/common/1787089039726-CreateGitConnectionProjectTable.ts create mode 100644 packages/cli/src/modules/git-connections.ee/database/entities/git-connection-project.entity.ts create mode 100644 packages/cli/test/migration/1787089039726-create-git-connection-project-table.test.ts diff --git a/docs/generated/postgres-schema/README.md b/docs/generated/postgres-schema/README.md index cbb14852774..bdeb92ec079 100644 --- a/docs/generated/postgres-schema/README.md +++ b/docs/generated/postgres-schema/README.md @@ -64,6 +64,7 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand. | [public.folder](public.folder.md) | 6 | | BASE TABLE | | [public.folder_tag](public.folder_tag.md) | 2 | | BASE TABLE | | [public.git_connection](public.git_connection.md) | 13 | | BASE TABLE | +| [public.git_connection_project](public.git_connection_project.md) | 4 | | BASE TABLE | | [public.insights_by_period](public.insights_by_period.md) | 6 | | BASE TABLE | | [public.insights_metadata](public.insights_metadata.md) | 5 | | BASE TABLE | | [public.insights_raw](public.insights_raw.md) | 5 | | BASE TABLE | @@ -248,6 +249,8 @@ erDiagram "public.folder" }o--o| "public.folder" : "FOREIGN KEY (#quot;parentFolderId#quot;) REFERENCES folder(id) ON DELETE CASCADE" "public.folder_tag" }o--|| "public.tag_entity" : "FOREIGN KEY (#quot;tagId#quot;) REFERENCES tag_entity(id) ON DELETE CASCADE" "public.folder_tag" }o--|| "public.folder" : "FOREIGN KEY (#quot;folderId#quot;) REFERENCES folder(id) ON DELETE CASCADE" +"public.git_connection_project" |o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" +"public.git_connection_project" }o--|| "public.git_connection" : "FOREIGN KEY (#quot;gitConnectionId#quot;) REFERENCES git_connection(id) ON DELETE CASCADE" "public.insights_by_period" }o--|| "public.insights_metadata" : "FOREIGN KEY (#quot;metaId#quot;) REFERENCES insights_metadata(#quot;metaId#quot;) ON DELETE CASCADE" "public.insights_metadata" }o--o| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE SET NULL" "public.insights_metadata" }o--o| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE SET NULL" @@ -925,6 +928,12 @@ erDiagram text repositoryUrl timestamp_3__with_time_zone updatedAt } +"public.git_connection_project" { + timestamp_3__with_time_zone createdAt + varchar_36_ gitConnectionId FK + varchar_36_ projectId FK + timestamp_3__with_time_zone updatedAt +} "public.insights_by_period" { integer id integer metaId FK diff --git a/docs/generated/postgres-schema/public.git_connection.md b/docs/generated/postgres-schema/public.git_connection.md index 1e65a373bcd..7665993346a 100644 --- a/docs/generated/postgres-schema/public.git_connection.md +++ b/docs/generated/postgres-schema/public.git_connection.md @@ -11,7 +11,7 @@ | encryptedPassword | text | | true | | | Encrypted HTTPS password/token; set when connectionType is 'https', null for 'ssh'. | | encryptedPrivateKey | text | | true | | | Encrypted SSH private key; set when connectionType is 'ssh', null for 'https'. | | encryptedUsername | text | | true | | | Encrypted HTTPS username; set when connectionType is 'https', null for 'ssh'. | -| id | varchar(36) | | false | | | | +| id | varchar(36) | | false | [public.git_connection_project](public.git_connection_project.md) | | | | keyGeneratorType | varchar(16) | | true | | | GitKeyGeneratorType enum: "ed25519", "rsa" | | name | varchar(128) | | false | | | | | publicKey | text | | true | | | SSH public key; set when connectionType is 'ssh', null for 'https'. | @@ -45,6 +45,7 @@ ```mermaid erDiagram +"public.git_connection_project" }o--|| "public.git_connection" : "FOREIGN KEY (#quot;gitConnectionId#quot;) REFERENCES git_connection(id) ON DELETE CASCADE" "public.git_connection" { varchar_64_ baseCommit @@ -61,6 +62,12 @@ erDiagram text repositoryUrl timestamp_3__with_time_zone updatedAt } +"public.git_connection_project" { + timestamp_3__with_time_zone createdAt + varchar_36_ gitConnectionId FK + varchar_36_ projectId FK + timestamp_3__with_time_zone updatedAt +} ``` --- diff --git a/docs/generated/postgres-schema/public.git_connection_project.md b/docs/generated/postgres-schema/public.git_connection_project.md new file mode 100644 index 00000000000..e573e0a3645 --- /dev/null +++ b/docs/generated/postgres-schema/public.git_connection_project.md @@ -0,0 +1,75 @@ +# public.git_connection_project + +## Columns + +| Name | Type | Default | Nullable | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | -------- | ------- | ------- | +| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | | +| gitConnectionId | varchar(36) | | false | | [public.git_connection](public.git_connection.md) | | +| projectId | varchar(36) | | false | | [public.project](public.project.md) | | +| updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| FK_12a7fe192a1ee1d933e712e6b1c | FOREIGN KEY | FOREIGN KEY ("gitConnectionId") REFERENCES git_connection(id) ON DELETE CASCADE | +| FK_5affe1c6075bd7ac20297b7947c | FOREIGN KEY | FOREIGN KEY ("projectId") REFERENCES project(id) ON DELETE CASCADE | +| PK_5affe1c6075bd7ac20297b7947c | PRIMARY KEY | PRIMARY KEY ("projectId") | +| git_connection_project_createdAt_not_null | n | NOT NULL "createdAt" | +| git_connection_project_gitConnectionId_not_null | n | NOT NULL "gitConnectionId" | +| git_connection_project_projectId_not_null | n | NOT NULL "projectId" | +| git_connection_project_updatedAt_not_null | n | NOT NULL "updatedAt" | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| IDX_12a7fe192a1ee1d933e712e6b1 | CREATE INDEX "IDX_12a7fe192a1ee1d933e712e6b1" ON public.git_connection_project USING btree ("gitConnectionId") | +| PK_5affe1c6075bd7ac20297b7947c | CREATE UNIQUE INDEX "PK_5affe1c6075bd7ac20297b7947c" ON public.git_connection_project USING btree ("projectId") | + +## Relations + +```mermaid +erDiagram + +"public.git_connection_project" }o--|| "public.git_connection" : "FOREIGN KEY (#quot;gitConnectionId#quot;) REFERENCES git_connection(id) ON DELETE CASCADE" +"public.git_connection_project" |o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" + +"public.git_connection_project" { + timestamp_3__with_time_zone createdAt + varchar_36_ gitConnectionId FK + varchar_36_ projectId FK + timestamp_3__with_time_zone updatedAt +} +"public.git_connection" { + varchar_64_ baseCommit + varchar_255_ branchName + varchar_16_ connectionType + timestamp_3__with_time_zone createdAt + text encryptedPassword + text encryptedPrivateKey + text encryptedUsername + varchar_36_ id + varchar_16_ keyGeneratorType + varchar_128_ name + text publicKey + text repositoryUrl + timestamp_3__with_time_zone updatedAt +} +"public.project" { + timestamp_3__with_time_zone createdAt + uuid creatorId FK + json customTelemetryTags + varchar_512_ description + json icon + varchar_36_ id + varchar_255_ name + varchar_36_ type + timestamp_3__with_time_zone updatedAt +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/generated/postgres-schema/public.project.md b/docs/generated/postgres-schema/public.project.md index 72ad3ec05e1..01f8e3d5f54 100644 --- a/docs/generated/postgres-schema/public.project.md +++ b/docs/generated/postgres-schema/public.project.md @@ -9,7 +9,7 @@ | customTelemetryTags | json | '[]'::json | false | | | | | description | varchar(512) | | true | | | | | icon | json | | true | | | | -| id | varchar(36) | | false | [public.agent_chat_attachments](public.agent_chat_attachments.md) [public.agent_execution_threads](public.agent_execution_threads.md) [public.agents](public.agents.md) [public.data_table](public.data_table.md) [public.folder](public.folder.md) [public.insights_metadata](public.insights_metadata.md) [public.instance_ai_threads](public.instance_ai_threads.md) [public.project_relation](public.project_relation.md) [public.project_secrets_provider_access](public.project_secrets_provider_access.md) [public.role_mapping_rule_project](public.role_mapping_rule_project.md) [public.shared_credentials](public.shared_credentials.md) [public.shared_workflow](public.shared_workflow.md) [public.variables](public.variables.md) [public.workflow_review_request](public.workflow_review_request.md) | | | +| id | varchar(36) | | false | [public.agent_chat_attachments](public.agent_chat_attachments.md) [public.agent_execution_threads](public.agent_execution_threads.md) [public.agents](public.agents.md) [public.data_table](public.data_table.md) [public.folder](public.folder.md) [public.git_connection_project](public.git_connection_project.md) [public.insights_metadata](public.insights_metadata.md) [public.instance_ai_threads](public.instance_ai_threads.md) [public.project_relation](public.project_relation.md) [public.project_secrets_provider_access](public.project_secrets_provider_access.md) [public.role_mapping_rule_project](public.role_mapping_rule_project.md) [public.shared_credentials](public.shared_credentials.md) [public.shared_workflow](public.shared_workflow.md) [public.variables](public.variables.md) [public.workflow_review_request](public.workflow_review_request.md) | | | | name | varchar(255) | | false | | | | | type | varchar(36) | | false | | | | | updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | | @@ -44,6 +44,7 @@ erDiagram "public.agents" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" "public.data_table" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" "public.folder" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" +"public.git_connection_project" |o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" "public.insights_metadata" }o--o| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE SET NULL" "public.instance_ai_threads" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" "public.project_relation" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE" @@ -146,6 +147,12 @@ erDiagram varchar_36_ projectId FK timestamp_3__with_time_zone updatedAt } +"public.git_connection_project" { + timestamp_3__with_time_zone createdAt + varchar_36_ gitConnectionId FK + varchar_36_ projectId FK + timestamp_3__with_time_zone updatedAt +} "public.insights_metadata" { integer metaId varchar_36_ projectId FK diff --git a/docs/generated/sqlite-schema/README.md b/docs/generated/sqlite-schema/README.md index ce7b56753ca..b03b8c2d88d 100644 --- a/docs/generated/sqlite-schema/README.md +++ b/docs/generated/sqlite-schema/README.md @@ -64,6 +64,7 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand. | [folder](folder.md) | 6 | | table | | [folder_tag](folder_tag.md) | 2 | | table | | [git_connection](git_connection.md) | 13 | | table | +| [git_connection_project](git_connection_project.md) | 4 | | table | | [insights_by_period](insights_by_period.md) | 6 | | table | | [insights_metadata](insights_metadata.md) | 5 | | table | | [insights_raw](insights_raw.md) | 5 | | table | @@ -231,6 +232,8 @@ erDiagram "folder" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "folder_tag" |o--|| "tag_entity" : "FOREIGN KEY (tagId) REFERENCES tag_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "folder_tag" |o--|| "folder" : "FOREIGN KEY (folderId) REFERENCES folder (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" +"git_connection_project" }o--|| "git_connection" : "FOREIGN KEY (gitConnectionId) REFERENCES git_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" +"git_connection_project" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "insights_by_period" }o--|| "insights_metadata" : "FOREIGN KEY (metaId) REFERENCES insights_metadata (metaId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "insights_metadata" }o--o| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" "insights_metadata" }o--o| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" @@ -912,6 +915,12 @@ erDiagram TEXT repositoryUrl datetime_3_ updatedAt } +"git_connection_project" { + datetime_3_ createdAt + varchar_36_ gitConnectionId FK + varchar_36_ projectId PK + datetime_3_ updatedAt +} "insights_by_period" { INTEGER id INTEGER metaId FK diff --git a/docs/generated/sqlite-schema/git_connection.md b/docs/generated/sqlite-schema/git_connection.md index 0aa52753f91..281a454b4f7 100644 --- a/docs/generated/sqlite-schema/git_connection.md +++ b/docs/generated/sqlite-schema/git_connection.md @@ -22,7 +22,7 @@ CREATE TABLE "git_connection" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" var | encryptedPassword | TEXT | | true | | | | | encryptedPrivateKey | TEXT | | true | | | | | encryptedUsername | TEXT | | true | | | | -| id | varchar(36) | | false | | | | +| id | varchar(36) | | false | [git_connection_project](git_connection_project.md) | | | | keyGeneratorType | varchar(16) | | true | | | | | name | varchar(128) | | false | | | | | publicKey | TEXT | | true | | | | @@ -51,6 +51,7 @@ CREATE TABLE "git_connection" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" var ```mermaid erDiagram +"git_connection_project" }o--|| "git_connection" : "FOREIGN KEY (gitConnectionId) REFERENCES git_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "git_connection" { varchar_64_ baseCommit @@ -67,6 +68,12 @@ erDiagram TEXT repositoryUrl datetime_3_ updatedAt } +"git_connection_project" { + datetime_3_ createdAt + varchar_36_ gitConnectionId FK + varchar_36_ projectId PK + datetime_3_ updatedAt +} ``` --- diff --git a/docs/generated/sqlite-schema/git_connection_project.md b/docs/generated/sqlite-schema/git_connection_project.md new file mode 100644 index 00000000000..7511b892348 --- /dev/null +++ b/docs/generated/sqlite-schema/git_connection_project.md @@ -0,0 +1,83 @@ +# git_connection_project + +## Description + +
+Table Definition + +```sql +CREATE TABLE "git_connection_project" ("projectId" varchar(36) PRIMARY KEY NOT NULL, "gitConnectionId" varchar(36) NOT NULL, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), CONSTRAINT "FK_5affe1c6075bd7ac20297b7947c" FOREIGN KEY ("projectId") REFERENCES "project" ("id") ON DELETE CASCADE, CONSTRAINT "FK_12a7fe192a1ee1d933e712e6b1c" FOREIGN KEY ("gitConnectionId") REFERENCES "git_connection" ("id") ON DELETE CASCADE) +``` + +
+ +## Columns + +| Name | Type | Default | Nullable | Children | Parents | Comment | +| ---- | ---- | ------- | -------- | -------- | ------- | ------- | +| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | | +| gitConnectionId | varchar(36) | | false | | [git_connection](git_connection.md) | | +| projectId | varchar(36) | | false | | [project](project.md) | | +| updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | | + +## Constraints + +| Name | Type | Definition | +| ---- | ---- | ---------- | +| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (gitConnectionId) REFERENCES git_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE | +| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE | +| projectId | PRIMARY KEY | PRIMARY KEY (projectId) | +| sqlite_autoindex_git_connection_project_1 | PRIMARY KEY | PRIMARY KEY (projectId) | + +## Indexes + +| Name | Definition | +| ---- | ---------- | +| IDX_12a7fe192a1ee1d933e712e6b1 | CREATE INDEX "IDX_12a7fe192a1ee1d933e712e6b1" ON "git_connection_project" ("gitConnectionId") | +| sqlite_autoindex_git_connection_project_1 | PRIMARY KEY (projectId) | + +## Relations + +```mermaid +erDiagram + +"git_connection_project" }o--|| "git_connection" : "FOREIGN KEY (gitConnectionId) REFERENCES git_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" +"git_connection_project" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" + +"git_connection_project" { + datetime_3_ createdAt + varchar_36_ gitConnectionId FK + varchar_36_ projectId PK + datetime_3_ updatedAt +} +"git_connection" { + varchar_64_ baseCommit + varchar_255_ branchName + varchar_16_ connectionType + datetime_3_ createdAt + TEXT encryptedPassword + TEXT encryptedPrivateKey + TEXT encryptedUsername + varchar_36_ id PK + varchar_16_ keyGeneratorType + varchar_128_ name + TEXT publicKey + TEXT repositoryUrl + datetime_3_ updatedAt +} +"project" { + datetime_3_ createdAt + varchar creatorId FK + TEXT customTelemetryTags + varchar_512_ description + TEXT icon + varchar_36_ id PK + varchar_255_ name + varchar_36_ type + datetime_3_ updatedAt +} +``` + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/docs/generated/sqlite-schema/project.md b/docs/generated/sqlite-schema/project.md index bebeac94aa6..c88b3ac120f 100644 --- a/docs/generated/sqlite-schema/project.md +++ b/docs/generated/sqlite-schema/project.md @@ -20,7 +20,7 @@ CREATE TABLE "project" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" varchar(25 | customTelemetryTags | TEXT | '[]' | false | | | | | description | varchar(512) | | true | | | | | icon | TEXT | | true | | | | -| id | varchar(36) | | false | [agent_chat_attachments](agent_chat_attachments.md) [agent_execution_threads](agent_execution_threads.md) [agents](agents.md) [data_table](data_table.md) [folder](folder.md) [insights_metadata](insights_metadata.md) [instance_ai_threads](instance_ai_threads.md) [project_relation](project_relation.md) [project_secrets_provider_access](project_secrets_provider_access.md) [role_mapping_rule_project](role_mapping_rule_project.md) [shared_credentials](shared_credentials.md) [shared_workflow](shared_workflow.md) [variables](variables.md) [workflow_review_request](workflow_review_request.md) | | | +| id | varchar(36) | | false | [agent_chat_attachments](agent_chat_attachments.md) [agent_execution_threads](agent_execution_threads.md) [agents](agents.md) [data_table](data_table.md) [folder](folder.md) [git_connection_project](git_connection_project.md) [insights_metadata](insights_metadata.md) [instance_ai_threads](instance_ai_threads.md) [project_relation](project_relation.md) [project_secrets_provider_access](project_secrets_provider_access.md) [role_mapping_rule_project](role_mapping_rule_project.md) [shared_credentials](shared_credentials.md) [shared_workflow](shared_workflow.md) [variables](variables.md) [workflow_review_request](workflow_review_request.md) | | | | name | varchar(255) | | false | | | | | type | varchar(36) | | false | | | | | updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | | @@ -50,6 +50,7 @@ erDiagram "agents" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "data_table" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "folder" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" +"git_connection_project" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "insights_metadata" }o--o| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE" "instance_ai_threads" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" "project_relation" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE" @@ -152,6 +153,12 @@ erDiagram varchar_36_ projectId FK datetime_3_ updatedAt } +"git_connection_project" { + datetime_3_ createdAt + varchar_36_ gitConnectionId FK + varchar_36_ projectId PK + datetime_3_ updatedAt +} "insights_metadata" { INTEGER metaId varchar_36_ projectId FK diff --git a/packages/@n8n/db/src/migrations/common/1787089039726-CreateGitConnectionProjectTable.ts b/packages/@n8n/db/src/migrations/common/1787089039726-CreateGitConnectionProjectTable.ts new file mode 100644 index 00000000000..89c6dc626f3 --- /dev/null +++ b/packages/@n8n/db/src/migrations/common/1787089039726-CreateGitConnectionProjectTable.ts @@ -0,0 +1,35 @@ +import type { MigrationContext, ReversibleMigration } from '../migration-types'; + +const TABLE = 'git_connection_project'; +const PROJECT_TABLE = 'project'; +const GIT_CONNECTION_TABLE = 'git_connection'; + +export class CreateGitConnectionProjectTable1787089039726 implements ReversibleMigration { + async up({ schemaBuilder: { createTable, column } }: MigrationContext) { + // projectId is the primary key: a project can be linked to at most one + // connection, while a connection can hold many project links. Both FKs + // cascade so a link disappears when either side is deleted. + await createTable(TABLE) + .withColumns( + column('projectId').varchar(36).primary, + column('gitConnectionId').varchar(36).notNull, + ) + .withForeignKey('projectId', { + tableName: PROJECT_TABLE, + columnName: 'id', + onDelete: 'CASCADE', + }) + .withForeignKey('gitConnectionId', { + tableName: GIT_CONNECTION_TABLE, + columnName: 'id', + onDelete: 'CASCADE', + }) + // A connection holds many projects; index the FK for connection-scoped + // reads ("list projects for this connection") and the cascade delete. + .withIndexOn(['gitConnectionId']).withTimestamps; + } + + async down({ schemaBuilder: { dropTable } }: MigrationContext) { + await dropTable(TABLE); + } +} diff --git a/packages/cli/src/modules/git-connections.ee/database/entities/git-connection-project.entity.ts b/packages/cli/src/modules/git-connections.ee/database/entities/git-connection-project.entity.ts new file mode 100644 index 00000000000..9fa2021d930 --- /dev/null +++ b/packages/cli/src/modules/git-connections.ee/database/entities/git-connection-project.entity.ts @@ -0,0 +1,34 @@ +import { WithTimestamps } from '@n8n/db'; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryColumn, + type Relation, +} from '@n8n/typeorm'; + +import { GitConnection } from './git-connection.entity'; + +/** + * Links a project to a git connection. The `projectId` primary key enforces the + * one-connection-per-project rule; a connection can hold many project links. + * + * The FK to `project` (with cascade delete) is declared in the migration rather + * than as a `@ManyToOne`, to keep the module entity decoupled from the core + * `Project` entity — mirroring the workflow-reviews pattern. + */ +@Entity('git_connection_project') +export class GitConnectionProject extends WithTimestamps { + @PrimaryColumn({ type: 'varchar', length: 36 }) + projectId: string; + + @Index() + @Column({ type: 'varchar', length: 36 }) + gitConnectionId: string; + + @ManyToOne(() => GitConnection, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'gitConnectionId' }) + gitConnection: Relation; +} diff --git a/packages/cli/src/services/ownership-transfer/ownership-transfer.manifest.json b/packages/cli/src/services/ownership-transfer/ownership-transfer.manifest.json index 16421168ab1..33738ccf1bb 100644 --- a/packages/cli/src/services/ownership-transfer/ownership-transfer.manifest.json +++ b/packages/cli/src/services/ownership-transfer/ownership-transfer.manifest.json @@ -68,6 +68,11 @@ "name": "WorkflowReviewRequest", "path": "packages/@n8n/db/src/entities/workflow-review-request.ee.ts", "reason": "Not transferred with the project. Open reviews are closed when their workflow is archived, moved to another project, or deleted (workflow lifecycle hooks); rows are removed via the projectId FK cascade when the source project itself is deleted, as in ownership-transfer flows. WorkflowReviewActivity and WorkflowReviewActivityComment ride this entity's cascade and have no projectId of their own, so they never appear in this manifest." + }, + { + "name": "GitConnectionProject", + "path": "packages/cli/src/modules/git-connections.ee/database/entities/git-connection-project.entity.ts", + "reason": "Project-scoped git configuration (which connection the project is linked to), not user data. Dropped via the projectId FK cascade when the source project itself is deleted, as in ownership-transfer flows." } ] } diff --git a/packages/cli/test/migration/1787089039726-create-git-connection-project-table.test.ts b/packages/cli/test/migration/1787089039726-create-git-connection-project-table.test.ts new file mode 100644 index 00000000000..5ff97d97194 --- /dev/null +++ b/packages/cli/test/migration/1787089039726-create-git-connection-project-table.test.ts @@ -0,0 +1,145 @@ +import { + createTestMigrationContext, + initDbUpToMigration, + runSingleMigration, + type TestMigrationContext, +} from '@n8n/backend-test-utils'; +import { DbConnection } from '@n8n/db'; +import { Container } from '@n8n/di'; +import { DataSource } from '@n8n/typeorm'; +import { randomUUID } from 'node:crypto'; + +const MIGRATION_NAME = 'CreateGitConnectionProjectTable1787089039726'; + +describe('CreateGitConnectionProjectTable migration', () => { + let dataSource: DataSource; + + beforeAll(async () => { + const dbConnection = Container.get(DbConnection); + await dbConnection.init(); + dataSource = Container.get(DataSource); + const context = createTestMigrationContext(dataSource); + await context.queryRunner.clearDatabase(); + await context.queryRunner.release(); + await initDbUpToMigration(MIGRATION_NAME); + await runSingleMigration(MIGRATION_NAME); + }); + + afterAll(async () => { + await Container.get(DbConnection).close(); + }); + + async function insertProject(context: TestMigrationContext, id: string) { + const table = context.escape.tableName('project'); + const now = new Date(); + await context.runQuery( + `INSERT INTO ${table} ("id", "name", "type", "customTelemetryTags", "createdAt", "updatedAt") + VALUES (:id, :name, :type, :tags, :createdAt, :updatedAt)`, + { id, name: 'Project', type: 'team', tags: '[]', createdAt: now, updatedAt: now }, + ); + } + + async function insertConnection(context: TestMigrationContext, id: string) { + const table = context.escape.tableName('git_connection'); + const now = new Date(); + await context.runQuery( + `INSERT INTO ${table} ("id", "name", "repositoryUrl", "connectionType", "createdAt", "updatedAt") + VALUES (:id, :name, :repositoryUrl, :connectionType, :createdAt, :updatedAt)`, + { + id, + name: 'Connection', + repositoryUrl: 'https://example.com/org/repo.git', + connectionType: 'https', + createdAt: now, + updatedAt: now, + }, + ); + } + + async function insertLink( + context: TestMigrationContext, + projectId: string, + connectionId: string, + ) { + const table = context.escape.tableName('git_connection_project'); + const now = new Date(); + await context.runQuery( + `INSERT INTO ${table} ("projectId", "gitConnectionId", "createdAt", "updatedAt") + VALUES (:projectId, :gitConnectionId, :createdAt, :updatedAt)`, + { projectId, gitConnectionId: connectionId, createdAt: now, updatedAt: now }, + ); + } + + async function countLinks(context: TestMigrationContext, projectId: string) { + const table = context.escape.tableName('git_connection_project'); + const rows = await context.runQuery>( + `SELECT COUNT(*) as c FROM ${table} WHERE "projectId" = :projectId`, + { projectId }, + ); + return Number(rows[0].c); + } + + describe('Up migration', () => { + it('enforces one connection per project via the primary key', async () => { + const context = createTestMigrationContext(dataSource); + const projectId = randomUUID(); + const connectionId = randomUUID(); + await insertProject(context, projectId); + await insertConnection(context, connectionId); + await insertLink(context, projectId, connectionId); + + await expect(insertLink(context, projectId, connectionId)).rejects.toThrow(); + await context.queryRunner.release(); + }); + + it('cascades the link away when the git connection is deleted', async () => { + const context = createTestMigrationContext(dataSource); + const projectId = randomUUID(); + const connectionId = randomUUID(); + await insertProject(context, projectId); + await insertConnection(context, connectionId); + await insertLink(context, projectId, connectionId); + + await context.runQuery( + `DELETE FROM ${context.escape.tableName('git_connection')} WHERE "id" = :id`, + { id: connectionId }, + ); + + expect(await countLinks(context, projectId)).toBe(0); + await context.queryRunner.release(); + }); + + it('cascades the link away when the project is deleted', async () => { + const context = createTestMigrationContext(dataSource); + const projectId = randomUUID(); + const connectionId = randomUUID(); + await insertProject(context, projectId); + await insertConnection(context, connectionId); + await insertLink(context, projectId, connectionId); + + await context.runQuery( + `DELETE FROM ${context.escape.tableName('project')} WHERE "id" = :id`, + { id: projectId }, + ); + + expect(await countLinks(context, projectId)).toBe(0); + await context.queryRunner.release(); + }); + }); + + describe('Down migration', () => { + it('drops the table and can be re-applied', async () => { + await dataSource.undoLastMigration({ transaction: 'each' }); + + const context = createTestMigrationContext(dataSource); + const table = `${context.tablePrefix}git_connection_project`; + expect(await context.queryRunner.hasTable(table)).toBe(false); + await context.queryRunner.release(); + + await runSingleMigration(MIGRATION_NAME); + const context2 = createTestMigrationContext(dataSource); + expect(await context2.queryRunner.hasTable(table)).toBe(true); + await context2.queryRunner.release(); + }); + }); +});