feat(core): Add the migration workflow owner table (no-changelog) (#40223)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
mfsiega 2026-10-07 11:36:58 +00:00 committed by GitHub
parent f26747bfc6
commit bb76c3c99b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 657 additions and 6 deletions

View File

@ -102,6 +102,7 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand.
| [public.mcp_registry_server](public.mcp_registry_server.md) | 7 | | BASE TABLE |
| [public.migration_finding](public.migration_finding.md) | 10 | | BASE TABLE |
| [public.migration_finding_sync](public.migration_finding_sync.md) | 3 | | BASE TABLE |
| [public.migration_workflow_owner](public.migration_workflow_owner.md) | 7 | | BASE TABLE |
| [public.oauth_access_tokens](public.oauth_access_tokens.md) | 3 | | BASE TABLE |
| [public.oauth_authorization_codes](public.oauth_authorization_codes.md) | 13 | | BASE TABLE |
| [public.oauth_clients](public.oauth_clients.md) | 10 | | BASE TABLE |
@ -326,6 +327,9 @@ erDiagram
"public.instance_ai_threads" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
"public.instance_credential_assignment" }o--|| "public.credentials_entity" : "FOREIGN KEY (#quot;credentialId#quot;) REFERENCES credentials_entity(id) ON DELETE RESTRICT"
"public.migration_finding" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.migration_workflow_owner" }o--o| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.migration_workflow_owner" }o--o| "public.user" : "FOREIGN KEY (#quot;assignedById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.migration_workflow_owner" |o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.oauth_access_tokens" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
"public.oauth_access_tokens" }o--|| "public.oauth_clients" : "FOREIGN KEY (#quot;clientId#quot;) REFERENCES oauth_clients(id) ON DELETE CASCADE"
"public.oauth_authorization_codes" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
@ -1363,6 +1367,15 @@ erDiagram
timestamp_3__with_time_zone syncedAt
varchar_16_ targetVersion
}
"public.migration_workflow_owner" {
timestamp_3__with_time_zone assignedAt
uuid assignedById FK
timestamp_3__with_time_zone createdAt
varchar_16_ source
timestamp_3__with_time_zone updatedAt
uuid userId FK
varchar_36_ workflowId FK
}
"public.oauth_access_tokens" {
varchar clientId FK
varchar token

View File

@ -0,0 +1,97 @@
# public.migration_workflow_owner
## Columns
| Name | Type | Default | Nullable | Children | Parents | Comment |
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| assignedAt | timestamp(3) with time zone | | true | | | When a person assigned the owner. NULL for a suggestion. |
| assignedById | uuid | | true | | [public.user](public.user.md) | Who assigned the owner. NULL for a suggestion, or after that user was deleted. |
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
| source | varchar(16) | | false | | | MigrationOwnerSource enum: "suggested" by the heuristic, or "assigned" by a person. |
| updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
| userId | uuid | | true | | [public.user](public.user.md) | The owner. NULL after the user was deleted. |
| workflowId | varchar(36) | | false | | [public.workflow_entity](public.workflow_entity.md) | |
## Constraints
| Name | Type | Definition |
| ---- | ---- | ---------- |
| CHK_migration_workflow_owner_source | CHECK | CHECK (((source)::text = ANY ((ARRAY['suggested'::character varying, 'assigned'::character varying])::text[]))) |
| FK_59cde25bbb0edbb82286e4806d2 | FOREIGN KEY | FOREIGN KEY ("workflowId") REFERENCES workflow_entity(id) ON DELETE CASCADE |
| FK_ca859918be95d42fa6f698791c3 | FOREIGN KEY | FOREIGN KEY ("userId") REFERENCES "user"(id) ON DELETE SET NULL |
| FK_e6e5e4cab49ec23a544ffc9337a | FOREIGN KEY | FOREIGN KEY ("assignedById") REFERENCES "user"(id) ON DELETE SET NULL |
| PK_59cde25bbb0edbb82286e4806d2 | PRIMARY KEY | PRIMARY KEY ("workflowId") |
| migration_workflow_owner_createdAt_not_null | n | NOT NULL "createdAt" |
| migration_workflow_owner_source_not_null | n | NOT NULL source |
| migration_workflow_owner_updatedAt_not_null | n | NOT NULL "updatedAt" |
| migration_workflow_owner_workflowId_not_null | n | NOT NULL "workflowId" |
## Indexes
| Name | Definition |
| ---- | ---------- |
| IDX_ca859918be95d42fa6f698791c | CREATE INDEX "IDX_ca859918be95d42fa6f698791c" ON public.migration_workflow_owner USING btree ("userId") |
| PK_59cde25bbb0edbb82286e4806d2 | CREATE UNIQUE INDEX "PK_59cde25bbb0edbb82286e4806d2" ON public.migration_workflow_owner USING btree ("workflowId") |
## Relations
```mermaid
erDiagram
"public.migration_workflow_owner" }o--o| "public.user" : "FOREIGN KEY (#quot;assignedById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.migration_workflow_owner" }o--o| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.migration_workflow_owner" |o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.migration_workflow_owner" {
timestamp_3__with_time_zone assignedAt
uuid assignedById FK
timestamp_3__with_time_zone createdAt
varchar_16_ source
timestamp_3__with_time_zone updatedAt
uuid userId FK
varchar_36_ workflowId FK
}
"public.user" {
timestamp_3__with_time_zone createdAt
boolean disabled
varchar_255_ email
varchar_32_ firstName
uuid id
date lastActiveAt
varchar_32_ lastName
boolean mfaEnabled
text mfaRecoveryCodes
text mfaSecret
varchar_255_ password
json personalizationAnswers
varchar_128_ roleSlug FK
json settings
timestamp_3__with_time_zone updatedAt
}
"public.workflow_entity" {
boolean active
varchar_36_ activeVersionId FK
json connections
timestamp_3__with_time_zone createdAt
text description
varchar_36_ id
boolean isArchived
json meta
varchar_128_ name
json nodeGroups
json nodes
varchar_36_ parentFolderId FK
json pinData
json settings
varchar sourceWorkflowId
json staticData
integer triggerCount
timestamp_3__with_time_zone updatedAt
integer versionCounter
character_36_ versionId
}
```
---
> Generated by [tbls](https://github.com/k1LoW/tbls)

View File

@ -8,7 +8,7 @@
| disabled | boolean | false | false | | | |
| email | varchar(255) | | true | | | |
| firstName | varchar(32) | | true | | | |
| id | uuid | gen_random_uuid() | false | [public.activity_event](public.activity_event.md) [public.agent_eval_dataset](public.agent_eval_dataset.md) [public.agent_eval_rating](public.agent_eval_rating.md) [public.agent_eval_run](public.agent_eval_run.md) [public.agent_execution_threads](public.agent_execution_threads.md) [public.agent_history](public.agent_history.md) [public.ai_preference](public.ai_preference.md) [public.auth_identity](public.auth_identity.md) [public.chat_hub_agents](public.chat_hub_agents.md) [public.chat_hub_sessions](public.chat_hub_sessions.md) [public.chat_hub_tools](public.chat_hub_tools.md) [public.dynamic_credential_user_entry](public.dynamic_credential_user_entry.md) [public.evaluation_collection](public.evaluation_collection.md) [public.instance_ai_mcp_registry_connections](public.instance_ai_mcp_registry_connections.md) [public.instance_ai_pending_confirmations](public.instance_ai_pending_confirmations.md) [public.instance_ai_thread_grants](public.instance_ai_thread_grants.md) [public.instance_ai_thread_tabs](public.instance_ai_thread_tabs.md) [public.oauth_access_tokens](public.oauth_access_tokens.md) [public.oauth_authorization_codes](public.oauth_authorization_codes.md) [public.oauth_refresh_tokens](public.oauth_refresh_tokens.md) [public.oauth_user_consents](public.oauth_user_consents.md) [public.project](public.project.md) [public.project_relation](public.project_relation.md) [public.trusted_source_identity](public.trusted_source_identity.md) [public.user_api_keys](public.user_api_keys.md) [public.user_favorites](public.user_favorites.md) [public.workflow_builder_session](public.workflow_builder_session.md) [public.workflow_publish_history](public.workflow_publish_history.md) [public.workflow_review_activity](public.workflow_review_activity.md) [public.workflow_review_activity_comment](public.workflow_review_activity_comment.md) [public.workflow_review_request](public.workflow_review_request.md) [public.workflow_review_request_authors](public.workflow_review_request_authors.md) [public.workflow_review_request_reviewers](public.workflow_review_request_reviewers.md) [public.workflow_suggestion](public.workflow_suggestion.md) | | |
| id | uuid | gen_random_uuid() | false | [public.activity_event](public.activity_event.md) [public.agent_eval_dataset](public.agent_eval_dataset.md) [public.agent_eval_rating](public.agent_eval_rating.md) [public.agent_eval_run](public.agent_eval_run.md) [public.agent_execution_threads](public.agent_execution_threads.md) [public.agent_history](public.agent_history.md) [public.ai_preference](public.ai_preference.md) [public.auth_identity](public.auth_identity.md) [public.chat_hub_agents](public.chat_hub_agents.md) [public.chat_hub_sessions](public.chat_hub_sessions.md) [public.chat_hub_tools](public.chat_hub_tools.md) [public.dynamic_credential_user_entry](public.dynamic_credential_user_entry.md) [public.evaluation_collection](public.evaluation_collection.md) [public.instance_ai_mcp_registry_connections](public.instance_ai_mcp_registry_connections.md) [public.instance_ai_pending_confirmations](public.instance_ai_pending_confirmations.md) [public.instance_ai_thread_grants](public.instance_ai_thread_grants.md) [public.instance_ai_thread_tabs](public.instance_ai_thread_tabs.md) [public.migration_workflow_owner](public.migration_workflow_owner.md) [public.oauth_access_tokens](public.oauth_access_tokens.md) [public.oauth_authorization_codes](public.oauth_authorization_codes.md) [public.oauth_refresh_tokens](public.oauth_refresh_tokens.md) [public.oauth_user_consents](public.oauth_user_consents.md) [public.project](public.project.md) [public.project_relation](public.project_relation.md) [public.trusted_source_identity](public.trusted_source_identity.md) [public.user_api_keys](public.user_api_keys.md) [public.user_favorites](public.user_favorites.md) [public.workflow_builder_session](public.workflow_builder_session.md) [public.workflow_publish_history](public.workflow_publish_history.md) [public.workflow_review_activity](public.workflow_review_activity.md) [public.workflow_review_activity_comment](public.workflow_review_activity_comment.md) [public.workflow_review_request](public.workflow_review_request.md) [public.workflow_review_request_authors](public.workflow_review_request_authors.md) [public.workflow_review_request_reviewers](public.workflow_review_request_reviewers.md) [public.workflow_suggestion](public.workflow_suggestion.md) | | |
| lastActiveAt | date | | true | | | |
| lastName | varchar(32) | | true | | | |
| mfaEnabled | boolean | false | false | | | |
@ -65,6 +65,8 @@ erDiagram
"public.instance_ai_pending_confirmations" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
"public.instance_ai_thread_grants" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
"public.instance_ai_thread_tabs" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
"public.migration_workflow_owner" }o--o| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.migration_workflow_owner" }o--o| "public.user" : "FOREIGN KEY (#quot;assignedById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.oauth_access_tokens" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
"public.oauth_authorization_codes" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
"public.oauth_refresh_tokens" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
@ -300,6 +302,15 @@ erDiagram
timestamp_3__with_time_zone updatedAt
uuid userId FK
}
"public.migration_workflow_owner" {
timestamp_3__with_time_zone assignedAt
uuid assignedById FK
timestamp_3__with_time_zone createdAt
varchar_16_ source
timestamp_3__with_time_zone updatedAt
uuid userId FK
varchar_36_ workflowId FK
}
"public.oauth_access_tokens" {
varchar clientId FK
varchar token

View File

@ -9,7 +9,7 @@
| connections | json | | false | | | |
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
| description | text | | true | | | |
| id | varchar(36) | | false | [public.agent_workflow_dependency](public.agent_workflow_dependency.md) [public.ai_builder_temporary_workflow](public.ai_builder_temporary_workflow.md) [public.chat_hub_messages](public.chat_hub_messages.md) [public.chat_hub_sessions](public.chat_hub_sessions.md) [public.evaluation_collection](public.evaluation_collection.md) [public.evaluation_config](public.evaluation_config.md) [public.execution_entity](public.execution_entity.md) [public.insights_metadata](public.insights_metadata.md) [public.migration_finding](public.migration_finding.md) [public.poller_state](public.poller_state.md) [public.processed_data](public.processed_data.md) [public.shared_workflow](public.shared_workflow.md) [public.test_run](public.test_run.md) [public.webhook_entity](public.webhook_entity.md) [public.workflow_builder_session](public.workflow_builder_session.md) [public.workflow_dependency](public.workflow_dependency.md) [public.workflow_history](public.workflow_history.md) [public.workflow_publication_retry_state](public.workflow_publication_retry_state.md) [public.workflow_publication_trigger_status](public.workflow_publication_trigger_status.md) [public.workflow_publish_history](public.workflow_publish_history.md) [public.workflow_published_version](public.workflow_published_version.md) [public.workflow_review_request_workflow](public.workflow_review_request_workflow.md) [public.workflow_suggestion](public.workflow_suggestion.md) [public.workflows_tags](public.workflows_tags.md) | | |
| id | varchar(36) | | false | [public.agent_workflow_dependency](public.agent_workflow_dependency.md) [public.ai_builder_temporary_workflow](public.ai_builder_temporary_workflow.md) [public.chat_hub_messages](public.chat_hub_messages.md) [public.chat_hub_sessions](public.chat_hub_sessions.md) [public.evaluation_collection](public.evaluation_collection.md) [public.evaluation_config](public.evaluation_config.md) [public.execution_entity](public.execution_entity.md) [public.insights_metadata](public.insights_metadata.md) [public.migration_finding](public.migration_finding.md) [public.migration_workflow_owner](public.migration_workflow_owner.md) [public.poller_state](public.poller_state.md) [public.processed_data](public.processed_data.md) [public.shared_workflow](public.shared_workflow.md) [public.test_run](public.test_run.md) [public.webhook_entity](public.webhook_entity.md) [public.workflow_builder_session](public.workflow_builder_session.md) [public.workflow_dependency](public.workflow_dependency.md) [public.workflow_history](public.workflow_history.md) [public.workflow_publication_retry_state](public.workflow_publication_retry_state.md) [public.workflow_publication_trigger_status](public.workflow_publication_trigger_status.md) [public.workflow_publish_history](public.workflow_publish_history.md) [public.workflow_published_version](public.workflow_published_version.md) [public.workflow_review_request_workflow](public.workflow_review_request_workflow.md) [public.workflow_suggestion](public.workflow_suggestion.md) [public.workflows_tags](public.workflows_tags.md) | | |
| isArchived | boolean | false | false | | | |
| meta | json | | true | | | |
| name | varchar(128) | | false | | | |
@ -75,6 +75,7 @@ erDiagram
"public.execution_entity" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) 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.migration_finding" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.migration_workflow_owner" |o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.poller_state" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.processed_data" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.shared_workflow" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
@ -237,6 +238,15 @@ erDiagram
timestamp_3__with_time_zone updatedAt
varchar_36_ workflowId FK
}
"public.migration_workflow_owner" {
timestamp_3__with_time_zone assignedAt
uuid assignedById FK
timestamp_3__with_time_zone createdAt
varchar_16_ source
timestamp_3__with_time_zone updatedAt
uuid userId FK
varchar_36_ workflowId FK
}
"public.poller_state" {
timestamp_3__with_time_zone backoffUntil
integer consecutiveErrors

View File

@ -102,6 +102,7 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand.
| [mcp_registry_server](mcp_registry_server.md) | 7 | | table |
| [migration_finding](migration_finding.md) | 10 | | table |
| [migration_finding_sync](migration_finding_sync.md) | 3 | | table |
| [migration_workflow_owner](migration_workflow_owner.md) | 7 | | table |
| [oauth_access_tokens](oauth_access_tokens.md) | 3 | | table |
| [oauth_authorization_codes](oauth_authorization_codes.md) | 13 | | table |
| [oauth_clients](oauth_clients.md) | 10 | | table |
@ -308,6 +309,9 @@ erDiagram
"instance_ai_threads" }o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"instance_credential_assignment" }o--|| "credentials_entity" : "FOREIGN KEY (credentialId) REFERENCES credentials_entity (id) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
"migration_finding" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"migration_workflow_owner" }o--o| "user" : "FOREIGN KEY (assignedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"migration_workflow_owner" }o--o| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"migration_workflow_owner" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"oauth_access_tokens" }o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"oauth_access_tokens" }o--|| "oauth_clients" : "FOREIGN KEY (clientId) REFERENCES oauth_clients (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"oauth_authorization_codes" }o--|| "oauth_clients" : "FOREIGN KEY (clientId) REFERENCES oauth_clients (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@ -1349,6 +1353,15 @@ erDiagram
datetime_3_ syncedAt
varchar_16_ targetVersion PK
}
"migration_workflow_owner" {
datetime_3_ assignedAt
varchar assignedById FK
datetime_3_ createdAt
varchar_16_ source
datetime_3_ updatedAt
varchar userId FK
varchar_36_ workflowId PK
}
"oauth_access_tokens" {
varchar clientId FK
varchar token PK

View File

@ -0,0 +1,105 @@
# migration_workflow_owner
## Description
<details>
<summary><strong>Table Definition</strong></summary>
```sql
CREATE TABLE "migration_workflow_owner" ("workflowId" varchar(36) PRIMARY KEY NOT NULL, "userId" varchar, "source" varchar(16) NOT NULL, "assignedById" varchar, "assignedAt" datetime(3), "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 "CHK_migration_workflow_owner_source" CHECK ("source" IN ('suggested', 'assigned')), CONSTRAINT "FK_59cde25bbb0edbb82286e4806d2" FOREIGN KEY ("workflowId") REFERENCES "workflow_entity" ("id") ON DELETE CASCADE, CONSTRAINT "FK_ca859918be95d42fa6f698791c3" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE SET NULL, CONSTRAINT "FK_e6e5e4cab49ec23a544ffc9337a" FOREIGN KEY ("assignedById") REFERENCES "user" ("id") ON DELETE SET NULL)
```
</details>
## Columns
| Name | Type | Default | Nullable | Children | Parents | Comment |
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| assignedAt | datetime(3) | | true | | | |
| assignedById | varchar | | true | | [user](user.md) | |
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
| source | varchar(16) | | false | | | |
| updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
| userId | varchar | | true | | [user](user.md) | |
| workflowId | varchar(36) | | false | | [workflow_entity](workflow_entity.md) | |
## Constraints
| Name | Type | Definition |
| ---- | ---- | ---------- |
| - | CHECK | CHECK ("source" IN ('suggested', 'assigned')) |
| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (assignedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE |
| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE |
| - (Foreign key ID: 2) | FOREIGN KEY | FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
| sqlite_autoindex_migration_workflow_owner_1 | PRIMARY KEY | PRIMARY KEY (workflowId) |
| workflowId | PRIMARY KEY | PRIMARY KEY (workflowId) |
## Indexes
| Name | Definition |
| ---- | ---------- |
| IDX_ca859918be95d42fa6f698791c | CREATE INDEX "IDX_ca859918be95d42fa6f698791c" ON "migration_workflow_owner" ("userId") |
| sqlite_autoindex_migration_workflow_owner_1 | PRIMARY KEY (workflowId) |
## Relations
```mermaid
erDiagram
"migration_workflow_owner" }o--o| "user" : "FOREIGN KEY (assignedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"migration_workflow_owner" }o--o| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"migration_workflow_owner" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"migration_workflow_owner" {
datetime_3_ assignedAt
varchar assignedById FK
datetime_3_ createdAt
varchar_16_ source
datetime_3_ updatedAt
varchar userId FK
varchar_36_ workflowId PK
}
"user" {
datetime_3_ createdAt
boolean disabled
varchar_255_ email
varchar_32_ firstName
varchar id PK
date lastActiveAt
varchar_32_ lastName
boolean mfaEnabled
TEXT mfaRecoveryCodes
TEXT mfaSecret
varchar password
TEXT personalizationAnswers
varchar_128_ roleSlug FK
TEXT settings
datetime_3_ updatedAt
}
"workflow_entity" {
boolean active
varchar_36_ activeVersionId FK
TEXT connections
datetime_3_ createdAt
TEXT description
varchar_36_ id PK
boolean isArchived
TEXT meta
varchar_128_ name
TEXT nodeGroups
TEXT nodes
varchar_36_ parentFolderId FK
TEXT pinData
TEXT settings
varchar sourceWorkflowId
TEXT staticData
INTEGER triggerCount
datetime_3_ updatedAt
INTEGER versionCounter
varchar_36_ versionId
}
```
---
> Generated by [tbls](https://github.com/k1LoW/tbls)

View File

@ -19,7 +19,7 @@ CREATE TABLE "user" ("id" varchar PRIMARY KEY, "email" varchar(255), "firstName"
| disabled | boolean | FALSE | false | | | |
| email | varchar(255) | | true | | | |
| firstName | varchar(32) | | true | | | |
| id | varchar | | true | [activity_event](activity_event.md) [agent_eval_dataset](agent_eval_dataset.md) [agent_eval_rating](agent_eval_rating.md) [agent_eval_run](agent_eval_run.md) [agent_execution_threads](agent_execution_threads.md) [agent_history](agent_history.md) [ai_preference](ai_preference.md) [auth_identity](auth_identity.md) [chat_hub_agents](chat_hub_agents.md) [chat_hub_sessions](chat_hub_sessions.md) [chat_hub_tools](chat_hub_tools.md) [dynamic_credential_user_entry](dynamic_credential_user_entry.md) [evaluation_collection](evaluation_collection.md) [instance_ai_mcp_registry_connections](instance_ai_mcp_registry_connections.md) [instance_ai_pending_confirmations](instance_ai_pending_confirmations.md) [instance_ai_thread_grants](instance_ai_thread_grants.md) [instance_ai_thread_tabs](instance_ai_thread_tabs.md) [oauth_access_tokens](oauth_access_tokens.md) [oauth_authorization_codes](oauth_authorization_codes.md) [oauth_refresh_tokens](oauth_refresh_tokens.md) [oauth_user_consents](oauth_user_consents.md) [project](project.md) [project_relation](project_relation.md) [trusted_source_identity](trusted_source_identity.md) [user_api_keys](user_api_keys.md) [user_favorites](user_favorites.md) [workflow_builder_session](workflow_builder_session.md) [workflow_publish_history](workflow_publish_history.md) [workflow_review_activity](workflow_review_activity.md) [workflow_review_activity_comment](workflow_review_activity_comment.md) [workflow_review_request](workflow_review_request.md) [workflow_review_request_authors](workflow_review_request_authors.md) [workflow_review_request_reviewers](workflow_review_request_reviewers.md) [workflow_suggestion](workflow_suggestion.md) | | |
| id | varchar | | true | [activity_event](activity_event.md) [agent_eval_dataset](agent_eval_dataset.md) [agent_eval_rating](agent_eval_rating.md) [agent_eval_run](agent_eval_run.md) [agent_execution_threads](agent_execution_threads.md) [agent_history](agent_history.md) [ai_preference](ai_preference.md) [auth_identity](auth_identity.md) [chat_hub_agents](chat_hub_agents.md) [chat_hub_sessions](chat_hub_sessions.md) [chat_hub_tools](chat_hub_tools.md) [dynamic_credential_user_entry](dynamic_credential_user_entry.md) [evaluation_collection](evaluation_collection.md) [instance_ai_mcp_registry_connections](instance_ai_mcp_registry_connections.md) [instance_ai_pending_confirmations](instance_ai_pending_confirmations.md) [instance_ai_thread_grants](instance_ai_thread_grants.md) [instance_ai_thread_tabs](instance_ai_thread_tabs.md) [migration_workflow_owner](migration_workflow_owner.md) [oauth_access_tokens](oauth_access_tokens.md) [oauth_authorization_codes](oauth_authorization_codes.md) [oauth_refresh_tokens](oauth_refresh_tokens.md) [oauth_user_consents](oauth_user_consents.md) [project](project.md) [project_relation](project_relation.md) [trusted_source_identity](trusted_source_identity.md) [user_api_keys](user_api_keys.md) [user_favorites](user_favorites.md) [workflow_builder_session](workflow_builder_session.md) [workflow_publish_history](workflow_publish_history.md) [workflow_review_activity](workflow_review_activity.md) [workflow_review_activity_comment](workflow_review_activity_comment.md) [workflow_review_request](workflow_review_request.md) [workflow_review_request_authors](workflow_review_request_authors.md) [workflow_review_request_reviewers](workflow_review_request_reviewers.md) [workflow_suggestion](workflow_suggestion.md) | | |
| lastActiveAt | date | | true | | | |
| lastName | varchar(32) | | true | | | |
| mfaEnabled | boolean | FALSE | false | | | |
@ -71,6 +71,8 @@ erDiagram
"instance_ai_pending_confirmations" }o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"instance_ai_thread_grants" |o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"instance_ai_thread_tabs" |o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"migration_workflow_owner" }o--o| "user" : "FOREIGN KEY (assignedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"migration_workflow_owner" }o--o| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"oauth_access_tokens" }o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"oauth_authorization_codes" }o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"oauth_refresh_tokens" }o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@ -306,6 +308,15 @@ erDiagram
datetime_3_ updatedAt
varchar userId PK
}
"migration_workflow_owner" {
datetime_3_ assignedAt
varchar assignedById FK
datetime_3_ createdAt
varchar_16_ source
datetime_3_ updatedAt
varchar userId FK
varchar_36_ workflowId PK
}
"oauth_access_tokens" {
varchar clientId FK
varchar token PK

View File

@ -20,7 +20,7 @@ CREATE TABLE "workflow_entity" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" va
| connections | TEXT | | true | | | |
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
| description | TEXT | | true | | | |
| id | varchar(36) | | false | [agent_workflow_dependency](agent_workflow_dependency.md) [ai_builder_temporary_workflow](ai_builder_temporary_workflow.md) [chat_hub_messages](chat_hub_messages.md) [chat_hub_sessions](chat_hub_sessions.md) [evaluation_collection](evaluation_collection.md) [evaluation_config](evaluation_config.md) [execution_entity](execution_entity.md) [insights_metadata](insights_metadata.md) [migration_finding](migration_finding.md) [poller_state](poller_state.md) [processed_data](processed_data.md) [shared_workflow](shared_workflow.md) [test_run](test_run.md) [workflow_builder_session](workflow_builder_session.md) [workflow_dependency](workflow_dependency.md) [workflow_history](workflow_history.md) [workflow_publication_retry_state](workflow_publication_retry_state.md) [workflow_publication_trigger_status](workflow_publication_trigger_status.md) [workflow_publish_history](workflow_publish_history.md) [workflow_published_version](workflow_published_version.md) [workflow_review_request_workflow](workflow_review_request_workflow.md) [workflow_suggestion](workflow_suggestion.md) [workflows_tags](workflows_tags.md) | | |
| id | varchar(36) | | false | [agent_workflow_dependency](agent_workflow_dependency.md) [ai_builder_temporary_workflow](ai_builder_temporary_workflow.md) [chat_hub_messages](chat_hub_messages.md) [chat_hub_sessions](chat_hub_sessions.md) [evaluation_collection](evaluation_collection.md) [evaluation_config](evaluation_config.md) [execution_entity](execution_entity.md) [insights_metadata](insights_metadata.md) [migration_finding](migration_finding.md) [migration_workflow_owner](migration_workflow_owner.md) [poller_state](poller_state.md) [processed_data](processed_data.md) [shared_workflow](shared_workflow.md) [test_run](test_run.md) [workflow_builder_session](workflow_builder_session.md) [workflow_dependency](workflow_dependency.md) [workflow_history](workflow_history.md) [workflow_publication_retry_state](workflow_publication_retry_state.md) [workflow_publication_trigger_status](workflow_publication_trigger_status.md) [workflow_publish_history](workflow_publish_history.md) [workflow_published_version](workflow_published_version.md) [workflow_review_request_workflow](workflow_review_request_workflow.md) [workflow_suggestion](workflow_suggestion.md) [workflows_tags](workflows_tags.md) | | |
| isArchived | boolean | FALSE | false | | | |
| meta | TEXT | | true | | | |
| name | varchar(128) | | false | | | |
@ -74,6 +74,7 @@ erDiagram
"execution_entity" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE 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"
"migration_finding" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"migration_workflow_owner" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"poller_state" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"processed_data" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"shared_workflow" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@ -236,6 +237,15 @@ erDiagram
datetime_3_ updatedAt
varchar_36_ workflowId FK
}
"migration_workflow_owner" {
datetime_3_ assignedAt
varchar assignedById FK
datetime_3_ createdAt
varchar_16_ source
datetime_3_ updatedAt
varchar userId FK
varchar_36_ workflowId PK
}
"poller_state" {
datetime_3_ backoffUntil
INTEGER consecutiveErrors

View File

@ -0,0 +1,55 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';
const OWNER_TABLE = 'migration_workflow_owner';
// Inlined on purpose: a migration must not import live enums from other packages.
const OWNER_SOURCES = ['suggested', 'assigned'];
/**
* The user responsible for fixing a workflow's migration findings, one row per
* workflow. `source` tells a heuristic suggestion from a choice a person made,
* so a re-run of the heuristic never overwrites an assignment. The row goes
* with its workflow. A deleted user leaves the row with no user, which reads
* as unassigned.
*/
export class CreateMigrationWorkflowOwnerTable1791366497000 implements ReversibleMigration {
async up({ schemaBuilder: { createTable, column } }: MigrationContext) {
await createTable(OWNER_TABLE)
.withColumns(
column('workflowId').varchar(36).primary,
column('userId').uuid.comment('The owner. NULL after the user was deleted.'),
column('source')
.varchar(16)
.notNull.withEnumCheck(OWNER_SOURCES)
.comment(
'MigrationOwnerSource enum: "suggested" by the heuristic, or "assigned" by a person.',
),
column('assignedById').uuid.comment(
'Who assigned the owner. NULL for a suggestion, or after that user was deleted.',
),
column('assignedAt')
.timestampTimezone()
.comment('When a person assigned the owner. NULL for a suggestion.'),
)
.withTimestamps.withIndexOn(['userId'])
.withForeignKey('workflowId', {
tableName: 'workflow_entity',
columnName: 'id',
onDelete: 'CASCADE',
})
.withForeignKey('userId', {
tableName: 'user',
columnName: 'id',
onDelete: 'SET NULL',
})
.withForeignKey('assignedById', {
tableName: 'user',
columnName: 'id',
onDelete: 'SET NULL',
});
}
async down({ schemaBuilder: { dropTable } }: MigrationContext) {
await dropTable(OWNER_TABLE);
}
}

View File

@ -22,7 +22,7 @@ breaking-changes/
...
index.ts # Side-effect imports for all rules
database/
entities/ # migration_finding + migration_finding_sync tables
entities/ # migration_finding, migration_finding_sync, migration_workflow_owner tables
repositories/ # Use-case-named DB access (BaseRepository + OperationContext)
sync/
migration-finding-diff.ts # Pure diff of scan hits against stored findings

View File

@ -0,0 +1,199 @@
import { createWorkflow, testDb, testModules } from '@n8n/backend-test-utils';
import { TransactionRunner, UserRepository } from '@n8n/db';
import type { User } from '@n8n/db';
import { Container } from '@n8n/di';
import { createMember } from '../../../../test/integration/shared/db/users';
import { MigrationWorkflowOwnerRepository } from '../database/repositories/migration-workflow-owner.repository';
// Runs against the real database: the primary key, the insert-or-ignore and the
// foreign keys live in the schema, so a mocked manager would not exercise them.
const ctx = {};
let repository: MigrationWorkflowOwnerRepository;
let alice: User;
let bob: User;
beforeAll(async () => {
await testModules.loadModules(['breaking-changes']);
await testDb.init();
repository = Container.get(MigrationWorkflowOwnerRepository);
});
beforeEach(async () => {
await repository.delete({});
await testDb.truncate(['WorkflowEntity', 'User']);
[alice, bob] = await Promise.all([createMember(), createMember()]);
});
afterAll(async () => {
await testDb.terminate();
});
describe('MigrationWorkflowOwnerRepository', () => {
describe('replaceSuggestions', () => {
test('inserts one suggested row per suggestion, for the batch only', async () => {
const [first, second, other] = await Promise.all([
createWorkflow(),
createWorkflow(),
createWorkflow(),
]);
await repository.replaceSuggestions(
[first.id, second.id],
[
{ workflowId: first.id, userId: alice.id },
{ workflowId: other.id, userId: bob.id },
],
ctx,
);
const rows = await repository.findByWorkflowIds([first.id, second.id, other.id], ctx);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
workflowId: first.id,
userId: alice.id,
source: 'suggested',
assignedById: null,
assignedAt: null,
});
});
test('replaces an earlier suggestion and drops one that has no candidate any more', async () => {
const [first, second] = await Promise.all([createWorkflow(), createWorkflow()]);
await repository.replaceSuggestions(
[first.id, second.id],
[
{ workflowId: first.id, userId: alice.id },
{ workflowId: second.id, userId: alice.id },
],
ctx,
);
await repository.replaceSuggestions(
[first.id, second.id],
[{ workflowId: first.id, userId: bob.id }],
ctx,
);
const rows = await repository.findByWorkflowIds([first.id, second.id], ctx);
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({ workflowId: first.id, userId: bob.id, source: 'suggested' });
});
test('leaves an assigned owner as it is', async () => {
const workflow = await createWorkflow();
const assignedAt = new Date('2026-03-01T12:00:00.000Z');
await repository.insert({
workflowId: workflow.id,
userId: alice.id,
source: 'assigned',
assignedById: bob.id,
assignedAt,
});
await repository.replaceSuggestions(
[workflow.id],
[{ workflowId: workflow.id, userId: bob.id }],
ctx,
);
const [row] = await repository.findByWorkflowIds([workflow.id], ctx);
expect(row).toMatchObject({ userId: alice.id, source: 'assigned', assignedById: bob.id });
expect(row.assignedAt?.getTime()).toBe(assignedAt.getTime());
});
test('rolls back the delete when the insert fails, so earlier suggestions survive', async () => {
const workflow = await createWorkflow();
await repository.replaceSuggestions(
[workflow.id],
[{ workflowId: workflow.id, userId: alice.id }],
ctx,
);
await expect(
repository.replaceSuggestions(
[workflow.id],
[{ workflowId: workflow.id, userId: 'not-a-user' }],
ctx,
),
).rejects.toThrow();
const [row] = await repository.findByWorkflowIds([workflow.id], ctx);
expect(row).toMatchObject({ userId: alice.id, source: 'suggested' });
});
test('does nothing for an empty batch', async () => {
await repository.replaceSuggestions([], [], ctx);
expect(await repository.count()).toBe(0);
});
test('rolls back with the surrounding transaction', async () => {
const workflow = await createWorkflow();
const txRunner = Container.get(TransactionRunner);
await expect(
txRunner.run({}, async (txCtx) => {
await repository.replaceSuggestions(
[workflow.id],
[{ workflowId: workflow.id, userId: alice.id }],
txCtx,
);
throw new Error('abort');
}),
).rejects.toThrow('abort');
expect(await repository.count()).toBe(0);
});
test('rejects a source outside the enum', async () => {
const workflow = await createWorkflow();
await expect(
repository.insert({
workflowId: workflow.id,
userId: alice.id,
source: 'guessed' as never,
assignedById: null,
}),
).rejects.toThrow();
});
});
describe('findByWorkflowIds', () => {
test('returns an empty list for an empty id array', async () => {
expect(await repository.findByWorkflowIds([], ctx)).toEqual([]);
});
});
describe('foreign keys', () => {
test('deletes the row with its workflow', async () => {
const workflow = await createWorkflow();
await repository.replaceSuggestions(
[workflow.id],
[{ workflowId: workflow.id, userId: alice.id }],
ctx,
);
await testDb.truncate(['WorkflowEntity']);
expect(await repository.count()).toBe(0);
});
test('keeps the row without a user when the user is deleted', async () => {
const workflow = await createWorkflow();
await repository.replaceSuggestions(
[workflow.id],
[{ workflowId: workflow.id, userId: alice.id }],
ctx,
);
await Container.get(UserRepository).delete({ id: alice.id });
const [row] = await repository.findByWorkflowIds([workflow.id], ctx);
expect(row).toMatchObject({ workflowId: workflow.id, userId: null, source: 'suggested' });
});
});
});

View File

@ -35,7 +35,10 @@ export class BreakingChangesModule implements ModuleInterface {
const { MigrationFindingSync } = await import(
'./database/entities/migration-finding-sync.entity.js'
);
const { MigrationWorkflowOwner } = await import(
'./database/entities/migration-workflow-owner.entity.js'
);
return [MigrationFinding, MigrationFindingSync];
return [MigrationFinding, MigrationFindingSync, MigrationWorkflowOwner];
}
}

View File

@ -0,0 +1,52 @@
import { DateTimeColumn, WithTimestamps } from '@n8n/db';
import type { User, WorkflowEntity } from '@n8n/db';
import {
Column,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryColumn,
type Relation,
} from '@n8n/typeorm';
/** `suggested` by the heuristic, or `assigned` by a person. Only a person changes an assignment. */
export type MigrationOwnerSource = 'suggested' | 'assigned';
/**
* The user responsible for fixing a workflow's migration findings, one row per
* workflow. The row goes with its workflow. A deleted user leaves `userId` null,
* which reads as unassigned.
*/
@Entity({ name: 'migration_workflow_owner' })
@Index(['userId'])
export class MigrationWorkflowOwner extends WithTimestamps {
@PrimaryColumn({ type: 'varchar', length: 36 })
workflowId: string;
@ManyToOne('WorkflowEntity', { onDelete: 'CASCADE' })
@JoinColumn({ name: 'workflowId' })
workflow: Relation<WorkflowEntity>;
@Column({ type: 'uuid', nullable: true })
userId: string | null;
@ManyToOne('User', { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'userId' })
user: Relation<User> | null;
@Column({ type: 'varchar', length: 16 })
source: MigrationOwnerSource;
/** Who assigned the owner. `null` for a suggestion. */
@Column({ type: 'uuid', nullable: true })
assignedById: string | null;
@ManyToOne('User', { onDelete: 'SET NULL', nullable: true })
@JoinColumn({ name: 'assignedById' })
assignedBy: Relation<User> | null;
/** When a person assigned the owner. `null` for a suggestion. */
@DateTimeColumn({ nullable: true })
assignedAt: Date | null;
}

View File

@ -0,0 +1,72 @@
import { BaseRepository, type OperationContext, TransactionRunner } from '@n8n/db';
import { Service } from '@n8n/di';
import { DataSource, In } from '@n8n/typeorm';
import { MigrationWorkflowOwner } from '../entities/migration-workflow-owner.entity';
/** The owner the heuristic proposes for one workflow. */
export interface OwnerSuggestion {
workflowId: string;
userId: string;
}
@Service()
export class MigrationWorkflowOwnerRepository extends BaseRepository<MigrationWorkflowOwner> {
constructor(dataSource: DataSource, transactionRunner: TransactionRunner) {
super(MigrationWorkflowOwner, dataSource.manager, transactionRunner);
}
async findByWorkflowIds(
workflowIds: string[],
ctx: OperationContext,
): Promise<MigrationWorkflowOwner[]> {
if (workflowIds.length === 0) return [];
return await this.managerFor(ctx).find(MigrationWorkflowOwner, {
where: { workflowId: In(workflowIds) },
});
}
/**
* Writes the heuristic's suggestions for a batch of workflows. A workflow in
* `workflowIds` without a suggestion loses any earlier suggestion. A workflow a
* person assigned keeps its owner: an assignment is never overwritten here.
*/
async replaceSuggestions(
workflowIds: string[],
suggestions: OwnerSuggestion[],
ctx: OperationContext,
): Promise<void> {
if (workflowIds.length === 0) return;
// Both writes succeed or fail together, so a failed insert cannot leave the
// batch without the suggestions it had before.
await this.runInTransaction(ctx, async (manager) => {
// Earlier suggestions for the batch go first, so each workflow ends with at most
// one row. The insert then ignores every workflow that still has a row, which is
// exactly the assigned ones.
await manager.delete(MigrationWorkflowOwner, {
workflowId: In(workflowIds),
source: 'suggested',
});
const batch = new Set(workflowIds);
const rows = suggestions
.filter((suggestion) => batch.has(suggestion.workflowId))
.map((suggestion) => ({
...suggestion,
source: 'suggested' as const,
assignedById: null,
assignedAt: null,
}));
if (rows.length === 0) return;
await manager
.createQueryBuilder()
.insert()
.into(MigrationWorkflowOwner)
.values(rows)
.orIgnore()
.execute();
});
}
}