feat(core): Add workflow review activity log tables and migration (no-changelog) (#35517)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Sandra Zollner <sandra.zollner@n8n.io>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jan Kalkan 2026-08-11 14:25:46 +02:00 committed by GitHub
parent d7dcba5c80
commit 41d54e77f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1561 additions and 44 deletions

View File

@ -126,10 +126,12 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand.
| [public.workflow_publication_trigger_status](public.workflow_publication_trigger_status.md) | 8 | | BASE TABLE |
| [public.workflow_publish_history](public.workflow_publish_history.md) | 6 | | BASE TABLE |
| [public.workflow_published_version](public.workflow_published_version.md) | 4 | | BASE TABLE |
| [public.workflow_review_activity](public.workflow_review_activity.md) | 8 | | BASE TABLE |
| [public.workflow_review_activity_comment](public.workflow_review_activity_comment.md) | 8 | | BASE TABLE |
| [public.workflow_review_request](public.workflow_review_request.md) | 12 | | BASE TABLE |
| [public.workflow_review_request_authors](public.workflow_review_request_authors.md) | 2 | | BASE TABLE |
| [public.workflow_review_request_reviewers](public.workflow_review_request_reviewers.md) | 2 | | BASE TABLE |
| [public.workflow_review_request_workflow](public.workflow_review_request_workflow.md) | 4 | | BASE TABLE |
| [public.workflow_review_request_workflow](public.workflow_review_request_workflow.md) | 5 | | BASE TABLE |
| [public.workflow_statistics](public.workflow_statistics.md) | 7 | | BASE TABLE |
| [public.workflow_statistics_delta](public.workflow_statistics_delta.md) | 6 | | BASE TABLE |
| [public.workflows_tags](public.workflows_tags.md) | 2 | | BASE TABLE |
@ -320,6 +322,11 @@ erDiagram
"public.workflow_publish_history" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;versionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.workflow_published_version" |o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE RESTRICT"
"public.workflow_published_version" }o--|| "public.workflow_history" : "FOREIGN KEY (#quot;publishedVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE RESTRICT"
"public.workflow_review_activity" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_activity" }o--o| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_review_activity" }o--|| "public.workflow_review_request" : "FOREIGN KEY (#quot;workflowReviewRequestId#quot;) REFERENCES workflow_review_request(id) ON DELETE CASCADE"
"public.workflow_review_activity_comment" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_activity_comment" }o--|| "public.workflow_review_activity" : "FOREIGN KEY (#quot;activityId#quot;) REFERENCES workflow_review_activity(id) ON DELETE CASCADE"
"public.workflow_review_request" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_request" }o--o| "public.user" : "FOREIGN KEY (#quot;updatedById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_request" }o--o| "public.user" : "FOREIGN KEY (#quot;closedById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
@ -330,6 +337,7 @@ erDiagram
"public.workflow_review_request_reviewers" }o--|| "public.workflow_review_request" : "FOREIGN KEY (#quot;workflowReviewRequestId#quot;) REFERENCES workflow_review_request(id) ON DELETE CASCADE"
"public.workflow_review_request_workflow" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_review_request_workflow" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;workflowVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.workflow_review_request_workflow" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;baselineVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.workflow_review_request_workflow" }o--|| "public.workflow_review_request" : "FOREIGN KEY (#quot;workflowReviewRequestId#quot;) REFERENCES workflow_review_request(id) ON DELETE CASCADE"
"public.workflows_tags" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflows_tags" }o--|| "public.tag_entity" : "FOREIGN KEY (#quot;tagId#quot;) REFERENCES tag_entity(id) ON DELETE CASCADE"
@ -1506,6 +1514,26 @@ erDiagram
timestamp_3__with_time_zone updatedAt
varchar_36_ workflowId FK
}
"public.workflow_review_activity" {
timestamp_3__with_time_zone createdAt
uuid createdById FK
json data
integer id
varchar_64_ type
integer typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"public.workflow_review_activity_comment" {
integer activityId FK
text body
timestamp_3__with_time_zone createdAt
uuid createdById FK
timestamp_3__with_time_zone deletedAt
json history
integer id
timestamp_3__with_time_zone updatedAt
}
"public.workflow_review_request" {
timestamp_3__with_time_zone approvedAt
uuid closedById FK
@ -1529,6 +1557,7 @@ erDiagram
varchar_36_ workflowReviewRequestId FK
}
"public.workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK

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.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_history](public.agent_history.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.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.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_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) | | |
| id | uuid | gen_random_uuid() | false | [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_history](public.agent_history.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.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.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) | | |
| lastActiveAt | date | | true | | | |
| lastName | varchar(32) | | true | | | |
| mfaEnabled | boolean | false | false | | | |
@ -70,6 +70,8 @@ erDiagram
"public.user_favorites" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
"public.workflow_builder_session" }o--|| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE CASCADE"
"public.workflow_publish_history" }o--o| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_activity" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_activity_comment" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_request" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_request" }o--o| "public.user" : "FOREIGN KEY (#quot;updatedById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_request" }o--o| "public.user" : "FOREIGN KEY (#quot;closedById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
@ -330,6 +332,26 @@ erDiagram
varchar_36_ versionId FK
varchar_36_ workflowId FK
}
"public.workflow_review_activity" {
timestamp_3__with_time_zone createdAt
uuid createdById FK
json data
integer id
varchar_64_ type
integer typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"public.workflow_review_activity_comment" {
integer activityId FK
text body
timestamp_3__with_time_zone createdAt
uuid createdById FK
timestamp_3__with_time_zone deletedAt
json history
integer id
timestamp_3__with_time_zone updatedAt
}
"public.workflow_review_request" {
timestamp_3__with_time_zone approvedAt
uuid closedById FK

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.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.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_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.workflows_tags](public.workflows_tags.md) | | |
| id | varchar(36) | | false | [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.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_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_activity](public.workflow_review_activity.md) [public.workflow_review_request_workflow](public.workflow_review_request_workflow.md) [public.workflows_tags](public.workflows_tags.md) | | |
| isArchived | boolean | false | false | | | |
| meta | json | | true | | | |
| name | varchar(128) | | false | | | |
@ -83,6 +83,7 @@ erDiagram
"public.workflow_publication_trigger_status" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_publish_history" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_published_version" |o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE RESTRICT"
"public.workflow_review_activity" }o--o| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_review_request_workflow" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflows_tags" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_entity" }o--o| "public.folder" : "FOREIGN KEY (#quot;parentFolderId#quot;) REFERENCES folder(id) ON DELETE CASCADE"
@ -301,7 +302,18 @@ erDiagram
timestamp_3__with_time_zone updatedAt
varchar_36_ workflowId FK
}
"public.workflow_review_activity" {
timestamp_3__with_time_zone createdAt
uuid createdById FK
json data
integer id
varchar_64_ type
integer typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"public.workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK

View File

@ -49,6 +49,7 @@ erDiagram
"public.workflow_publish_history" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;versionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.workflow_published_version" }o--|| "public.workflow_history" : "FOREIGN KEY (#quot;publishedVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE RESTRICT"
"public.workflow_review_request_workflow" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;workflowVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.workflow_review_request_workflow" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;baselineVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.workflow_history" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_history" {
@ -111,6 +112,7 @@ erDiagram
varchar_36_ workflowId FK
}
"public.workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK

View File

@ -0,0 +1,125 @@
# public.workflow_review_activity
## Columns
| Name | Type | Default | Nullable | Children | Parents | Comment |
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
| createdById | uuid | | true | | [public.user](public.user.md) | |
| data | json | | true | | | Detail per activity type |
| id | integer | | false | [public.workflow_review_activity_comment](public.workflow_review_activity_comment.md) | | |
| type | varchar(64) | | false | | | Feed entry kind; see WorkflowReviewActivityType in @n8n/api-types |
| typeVersion | integer | 1 | false | | | Schema version of the `data` payload for this `type` |
| workflowId | varchar(36) | | true | | [public.workflow_entity](public.workflow_entity.md) | Scopes the entry to one workflow; NULL for review-level entries like comments |
| workflowReviewRequestId | varchar(36) | | false | | [public.workflow_review_request](public.workflow_review_request.md) | |
## Constraints
| Name | Type | Definition |
| ---- | ---- | ---------- |
| CHK_workflow_review_activity_type | CHECK | CHECK (((type)::text = ANY ((ARRAY['review.opened'::character varying, 'comment.created'::character varying, 'review.changes_requested'::character varying, 'review.version_updated'::character varying, 'review.approved'::character varying, 'workflow.published'::character varying, 'review.closed'::character varying])::text[]))) |
| FK_61048bf6220dd354c955a9d9379 | FOREIGN KEY | FOREIGN KEY ("workflowReviewRequestId") REFERENCES workflow_review_request(id) ON DELETE CASCADE |
| FK_d0162c063ffb699b0f1d1172508 | FOREIGN KEY | FOREIGN KEY ("workflowId") REFERENCES workflow_entity(id) ON DELETE CASCADE |
| FK_fcf78b037a72fc7aa01ab237e08 | FOREIGN KEY | FOREIGN KEY ("createdById") REFERENCES "user"(id) ON DELETE SET NULL |
| PK_f1e66ba0b645dec566057ad21e6 | PRIMARY KEY | PRIMARY KEY (id) |
| workflow_review_activity_createdAt_not_null | n | NOT NULL "createdAt" |
| workflow_review_activity_id_not_null | n | NOT NULL id |
| workflow_review_activity_typeVersion_not_null | n | NOT NULL "typeVersion" |
| workflow_review_activity_type_not_null | n | NOT NULL type |
| workflow_review_activity_workflowReviewRequestId_not_null | n | NOT NULL "workflowReviewRequestId" |
## Indexes
| Name | Definition |
| ---- | ---------- |
| IDX_workflow_review_activity_request | CREATE INDEX "IDX_workflow_review_activity_request" ON public.workflow_review_activity USING btree ("workflowReviewRequestId", id) |
| PK_f1e66ba0b645dec566057ad21e6 | CREATE UNIQUE INDEX "PK_f1e66ba0b645dec566057ad21e6" ON public.workflow_review_activity USING btree (id) |
## Relations
```mermaid
erDiagram
"public.workflow_review_activity" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_activity_comment" }o--|| "public.workflow_review_activity" : "FOREIGN KEY (#quot;activityId#quot;) REFERENCES workflow_review_activity(id) ON DELETE CASCADE"
"public.workflow_review_activity" }o--o| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_review_activity" }o--|| "public.workflow_review_request" : "FOREIGN KEY (#quot;workflowReviewRequestId#quot;) REFERENCES workflow_review_request(id) ON DELETE CASCADE"
"public.workflow_review_activity" {
timestamp_3__with_time_zone createdAt
uuid createdById FK
json data
integer id
varchar_64_ type
integer typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId 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_review_activity_comment" {
integer activityId FK
text body
timestamp_3__with_time_zone createdAt
uuid createdById FK
timestamp_3__with_time_zone deletedAt
json history
integer id
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
}
"public.workflow_review_request" {
timestamp_3__with_time_zone approvedAt
uuid closedById FK
timestamp_3__with_time_zone createdAt
uuid createdById FK
varchar_50_ decision
text description
varchar_36_ id
varchar_36_ projectId FK
varchar_16_ state
varchar_255_ title
timestamp_3__with_time_zone updatedAt
uuid updatedById FK
}
```
---
> Generated by [tbls](https://github.com/k1LoW/tbls)

View File

@ -0,0 +1,83 @@
# public.workflow_review_activity_comment
## Columns
| Name | Type | Default | Nullable | Children | Parents | Comment |
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| activityId | integer | | false | | [public.workflow_review_activity](public.workflow_review_activity.md) | Thread this message belongs to; the activity row is its header |
| body | text | | true | | | Only user-editable text in the feed; nulled on delete |
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
| createdById | uuid | | true | | [public.user](public.user.md) | |
| deletedAt | timestamp(3) with time zone | | true | | | Set when the comment is deleted |
| history | json | | true | | | Comment revision history. |
| id | integer | | false | | | |
| updatedAt | timestamp(3) with time zone | | true | | | Set when the body is edited |
## Constraints
| Name | Type | Definition |
| ---- | ---- | ---------- |
| FK_3cd755e8ce44ee49bdf7e9222ec | FOREIGN KEY | FOREIGN KEY ("activityId") REFERENCES workflow_review_activity(id) ON DELETE CASCADE |
| FK_9cfac4b106553d0930a1962bea0 | FOREIGN KEY | FOREIGN KEY ("createdById") REFERENCES "user"(id) ON DELETE SET NULL |
| PK_5a2a7dea8b7e6afa1001129da27 | PRIMARY KEY | PRIMARY KEY (id) |
| workflow_review_activity_comment_activityId_not_null | n | NOT NULL "activityId" |
| workflow_review_activity_comment_createdAt_not_null | n | NOT NULL "createdAt" |
| workflow_review_activity_comment_id_not_null | n | NOT NULL id |
## Indexes
| Name | Definition |
| ---- | ---------- |
| IDX_workflow_review_activity_comment_activity | CREATE INDEX "IDX_workflow_review_activity_comment_activity" ON public.workflow_review_activity_comment USING btree ("activityId", id) |
| PK_5a2a7dea8b7e6afa1001129da27 | CREATE UNIQUE INDEX "PK_5a2a7dea8b7e6afa1001129da27" ON public.workflow_review_activity_comment USING btree (id) |
## Relations
```mermaid
erDiagram
"public.workflow_review_activity_comment" }o--|| "public.workflow_review_activity" : "FOREIGN KEY (#quot;activityId#quot;) REFERENCES workflow_review_activity(id) ON DELETE CASCADE"
"public.workflow_review_activity_comment" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_activity_comment" {
integer activityId FK
text body
timestamp_3__with_time_zone createdAt
uuid createdById FK
timestamp_3__with_time_zone deletedAt
json history
integer id
timestamp_3__with_time_zone updatedAt
}
"public.workflow_review_activity" {
timestamp_3__with_time_zone createdAt
uuid createdById FK
json data
integer id
varchar_64_ type
integer typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId 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
}
```
---
> Generated by [tbls](https://github.com/k1LoW/tbls)

View File

@ -10,7 +10,7 @@
| createdById | uuid | | true | | [public.user](public.user.md) | |
| decision | varchar(50) | 'pending'::character varying | false | | | Latest review outcome while the request is open |
| description | text | | true | | | |
| id | varchar(36) | | false | [public.workflow_review_request_authors](public.workflow_review_request_authors.md) [public.workflow_review_request_reviewers](public.workflow_review_request_reviewers.md) [public.workflow_review_request_workflow](public.workflow_review_request_workflow.md) | | |
| id | varchar(36) | | false | [public.workflow_review_activity](public.workflow_review_activity.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_review_request_workflow](public.workflow_review_request_workflow.md) | | |
| projectId | varchar(36) | | false | | [public.project](public.project.md) | |
| state | varchar(16) | 'open'::character varying | false | | | Review lifecycle: open reviews accept actions; closed reviews are done |
| title | varchar(255) | | false | | | |
@ -50,6 +50,7 @@ erDiagram
"public.workflow_review_request" }o--o| "public.user" : "FOREIGN KEY (#quot;closedById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_request" }o--o| "public.user" : "FOREIGN KEY (#quot;createdById#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_review_activity" }o--|| "public.workflow_review_request" : "FOREIGN KEY (#quot;workflowReviewRequestId#quot;) REFERENCES workflow_review_request(id) ON DELETE CASCADE"
"public.workflow_review_request_authors" }o--|| "public.workflow_review_request" : "FOREIGN KEY (#quot;workflowReviewRequestId#quot;) REFERENCES workflow_review_request(id) ON DELETE CASCADE"
"public.workflow_review_request_reviewers" }o--|| "public.workflow_review_request" : "FOREIGN KEY (#quot;workflowReviewRequestId#quot;) REFERENCES workflow_review_request(id) ON DELETE CASCADE"
"public.workflow_review_request_workflow" }o--|| "public.workflow_review_request" : "FOREIGN KEY (#quot;workflowReviewRequestId#quot;) REFERENCES workflow_review_request(id) ON DELETE CASCADE"
@ -87,6 +88,16 @@ erDiagram
json settings
timestamp_3__with_time_zone updatedAt
}
"public.workflow_review_activity" {
timestamp_3__with_time_zone createdAt
uuid createdById FK
json data
integer id
varchar_64_ type
integer typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"public.workflow_review_request_authors" {
uuid userId FK
varchar_36_ workflowReviewRequestId FK
@ -96,6 +107,7 @@ erDiagram
varchar_36_ workflowReviewRequestId FK
}
"public.workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK

View File

@ -21,6 +21,7 @@
| Name | Definition |
| ---- | ---------- |
| IDX_workflow_review_request_authors_user | CREATE INDEX "IDX_workflow_review_request_authors_user" ON public.workflow_review_request_authors USING btree ("userId", "workflowReviewRequestId") |
| PK_ae197bb5135d422f6392a879e0c | CREATE UNIQUE INDEX "PK_ae197bb5135d422f6392a879e0c" ON public.workflow_review_request_authors USING btree ("workflowReviewRequestId", "userId") |
## Relations

View File

@ -4,6 +4,7 @@
| Name | Type | Default | Nullable | Children | Parents | Comment |
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| baselineVersionId | varchar(36) | | true | | [public.workflow_history](public.workflow_history.md) | Published workflow_history version captured when the review was approved |
| id | varchar(36) | | false | | | |
| workflowId | varchar(36) | | false | | [public.workflow_entity](public.workflow_entity.md) | |
| workflowReviewRequestId | varchar(36) | | false | | [public.workflow_review_request](public.workflow_review_request.md) | |
@ -16,6 +17,7 @@
| FK_0f6f0f2c6d46b806fee02962ac2 | FOREIGN KEY | FOREIGN KEY ("workflowVersionId") REFERENCES workflow_history("versionId") ON DELETE SET NULL |
| FK_619f5b0544bcec60c3387e82f2f | FOREIGN KEY | FOREIGN KEY ("workflowId") REFERENCES workflow_entity(id) ON DELETE CASCADE |
| FK_e44b652e6dc99ef1364a2d85504 | FOREIGN KEY | FOREIGN KEY ("workflowReviewRequestId") REFERENCES workflow_review_request(id) ON DELETE CASCADE |
| FK_workflow_review_request_workflow_baselineVersionId | FOREIGN KEY | FOREIGN KEY ("baselineVersionId") REFERENCES workflow_history("versionId") ON DELETE SET NULL |
| PK_be3bf4facb054cf2b2b116b3b9c | PRIMARY KEY | PRIMARY KEY (id) |
| workflow_review_request_workfl_workflowReviewRequestId_not_null | n | NOT NULL "workflowReviewRequestId" |
| workflow_review_request_workflow_id_not_null | n | NOT NULL id |
@ -34,16 +36,31 @@
```mermaid
erDiagram
"public.workflow_review_request_workflow" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;baselineVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.workflow_review_request_workflow" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_review_request_workflow" }o--|| "public.workflow_review_request" : "FOREIGN KEY (#quot;workflowReviewRequestId#quot;) REFERENCES workflow_review_request(id) ON DELETE CASCADE"
"public.workflow_review_request_workflow" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;workflowVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
varchar_36_ workflowVersionId FK
}
"public.workflow_history" {
varchar_255_ authors
boolean autosaved
json connections
timestamp_3__with_time_zone createdAt
text description
varchar_128_ name
json nodeGroups
json nodes
timestamp_3__with_time_zone updatedAt
varchar_36_ versionId
varchar_36_ workflowId FK
}
"public.workflow_entity" {
boolean active
varchar_36_ activeVersionId FK
@ -80,19 +97,6 @@ erDiagram
timestamp_3__with_time_zone updatedAt
uuid updatedById FK
}
"public.workflow_history" {
varchar_255_ authors
boolean autosaved
json connections
timestamp_3__with_time_zone createdAt
text description
varchar_128_ name
json nodeGroups
json nodes
timestamp_3__with_time_zone updatedAt
varchar_36_ versionId
varchar_36_ workflowId FK
}
```
---

View File

@ -126,10 +126,12 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand.
| [workflow_publication_trigger_status](workflow_publication_trigger_status.md) | 8 | | table |
| [workflow_publish_history](workflow_publish_history.md) | 6 | | table |
| [workflow_published_version](workflow_published_version.md) | 4 | | table |
| [workflow_review_activity](workflow_review_activity.md) | 8 | | table |
| [workflow_review_activity_comment](workflow_review_activity_comment.md) | 8 | | table |
| [workflow_review_request](workflow_review_request.md) | 12 | | table |
| [workflow_review_request_authors](workflow_review_request_authors.md) | 2 | | table |
| [workflow_review_request_reviewers](workflow_review_request_reviewers.md) | 2 | | table |
| [workflow_review_request_workflow](workflow_review_request_workflow.md) | 4 | | table |
| [workflow_review_request_workflow](workflow_review_request_workflow.md) | 5 | | table |
| [workflow_statistics](workflow_statistics.md) | 7 | | table |
| [workflows_tags](workflows_tags.md) | 2 | | table |
@ -307,6 +309,11 @@ erDiagram
"workflow_published_version" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
"workflow_published_version" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_published_version" }o--|| "workflow_history" : "FOREIGN KEY (publishedVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_activity" }o--o| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_activity" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_activity" }o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_activity_comment" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_activity_comment" }o--|| "workflow_review_activity" : "FOREIGN KEY (activityId) REFERENCES workflow_review_activity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request" }o--o| "user" : "FOREIGN KEY (closedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request" }o--o| "user" : "FOREIGN KEY (updatedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
@ -315,9 +322,10 @@ erDiagram
"workflow_review_request_authors" |o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_reviewers" |o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_reviewers" |o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_workflow" }o--o| "workflow_history" : "FOREIGN KEY (workflowVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request_workflow" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_workflow" }o--o| "workflow_history" : "FOREIGN KEY (baselineVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request_workflow" }o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_workflow" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_workflow" }o--o| "workflow_history" : "FOREIGN KEY (workflowVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflows_tags" |o--|| "tag_entity" : "FOREIGN KEY (tagId) REFERENCES tag_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflows_tags" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@ -1495,6 +1503,26 @@ erDiagram
datetime_3_ updatedAt
varchar_36_ workflowId PK
}
"workflow_review_activity" {
datetime_3_ createdAt
varchar createdById FK
TEXT data
INTEGER id
varchar_64_ type
INTEGER typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"workflow_review_activity_comment" {
INTEGER activityId FK
TEXT body
datetime_3_ createdAt
varchar createdById FK
datetime_3_ deletedAt
TEXT history
INTEGER id
datetime_3_ updatedAt
}
"workflow_review_request" {
datetime_3_ approvedAt
varchar closedById FK
@ -1518,6 +1546,7 @@ erDiagram
varchar_36_ workflowReviewRequestId PK
}
"workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id PK
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK

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 | [agent_eval_dataset](agent_eval_dataset.md) [agent_eval_rating](agent_eval_rating.md) [agent_eval_run](agent_eval_run.md) [agent_history](agent_history.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) [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) [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_request](workflow_review_request.md) [workflow_review_request_authors](workflow_review_request_authors.md) [workflow_review_request_reviewers](workflow_review_request_reviewers.md) | | |
| id | varchar | | true | [agent_eval_dataset](agent_eval_dataset.md) [agent_eval_rating](agent_eval_rating.md) [agent_eval_run](agent_eval_run.md) [agent_history](agent_history.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) [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) [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) | | |
| lastActiveAt | date | | true | | | |
| lastName | varchar(32) | | true | | | |
| mfaEnabled | boolean | FALSE | false | | | |
@ -76,6 +76,8 @@ erDiagram
"user_favorites" }o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_builder_session" }o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_publish_history" }o--o| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_activity" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_activity_comment" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request" }o--o| "user" : "FOREIGN KEY (closedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request" }o--o| "user" : "FOREIGN KEY (updatedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
@ -336,6 +338,26 @@ erDiagram
varchar_36_ versionId FK
varchar_36_ workflowId FK
}
"workflow_review_activity" {
datetime_3_ createdAt
varchar createdById FK
TEXT data
INTEGER id
varchar_64_ type
INTEGER typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"workflow_review_activity_comment" {
INTEGER activityId FK
TEXT body
datetime_3_ createdAt
varchar createdById FK
datetime_3_ deletedAt
TEXT history
INTEGER id
datetime_3_ updatedAt
}
"workflow_review_request" {
datetime_3_ approvedAt
varchar closedById FK

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 | [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) [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_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) [workflows_tags](workflows_tags.md) | | |
| id | varchar(36) | | false | [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) [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_trigger_status](workflow_publication_trigger_status.md) [workflow_publish_history](workflow_publish_history.md) [workflow_published_version](workflow_published_version.md) [workflow_review_activity](workflow_review_activity.md) [workflow_review_request_workflow](workflow_review_request_workflow.md) [workflows_tags](workflows_tags.md) | | |
| isArchived | boolean | FALSE | false | | | |
| meta | TEXT | | true | | | |
| name | varchar(128) | | false | | | |
@ -82,6 +82,7 @@ erDiagram
"workflow_publish_history" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_published_version" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
"workflow_published_version" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_activity" }o--o| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_workflow" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflows_tags" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_entity" }o--o| "folder" : "FOREIGN KEY (parentFolderId) REFERENCES folder (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@ -292,7 +293,18 @@ erDiagram
datetime_3_ updatedAt
varchar_36_ workflowId PK
}
"workflow_review_activity" {
datetime_3_ createdAt
varchar createdById FK
TEXT data
INTEGER id
varchar_64_ type
INTEGER typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id PK
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK

View File

@ -53,6 +53,7 @@ erDiagram
"workflow_publish_history" }o--o| "workflow_history" : "FOREIGN KEY (versionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_published_version" }o--|| "workflow_history" : "FOREIGN KEY (publishedVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
"workflow_published_version" }o--|| "workflow_history" : "FOREIGN KEY (publishedVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_workflow" }o--o| "workflow_history" : "FOREIGN KEY (baselineVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request_workflow" }o--o| "workflow_history" : "FOREIGN KEY (workflowVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_history" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@ -116,6 +117,7 @@ erDiagram
varchar_36_ workflowId PK
}
"workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id PK
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK

View File

@ -0,0 +1,130 @@
# workflow_review_activity
## Description
<details>
<summary><strong>Table Definition</strong></summary>
```sql
CREATE TABLE "workflow_review_activity" ("id" integer PRIMARY KEY NOT NULL, "workflowReviewRequestId" varchar(36) NOT NULL, "type" varchar(64) NOT NULL, "typeVersion" integer NOT NULL DEFAULT (1), "data" text, "createdById" varchar, "workflowId" varchar(36), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), CONSTRAINT "CHK_workflow_review_activity_type" CHECK ("type" IN ('review.opened', 'comment.created', 'review.changes_requested', 'review.version_updated', 'review.approved', 'workflow.published', 'review.closed')), CONSTRAINT "FK_61048bf6220dd354c955a9d9379" FOREIGN KEY ("workflowReviewRequestId") REFERENCES "workflow_review_request" ("id") ON DELETE CASCADE, CONSTRAINT "FK_fcf78b037a72fc7aa01ab237e08" FOREIGN KEY ("createdById") REFERENCES "user" ("id") ON DELETE SET NULL, CONSTRAINT "FK_d0162c063ffb699b0f1d1172508" FOREIGN KEY ("workflowId") REFERENCES "workflow_entity" ("id") ON DELETE CASCADE)
```
</details>
## Columns
| Name | Type | Default | Nullable | Children | Parents | Comment |
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
| createdById | varchar | | true | | [user](user.md) | |
| data | TEXT | | true | | | |
| id | INTEGER | | false | [workflow_review_activity_comment](workflow_review_activity_comment.md) | | |
| type | varchar(64) | | false | | | |
| typeVersion | INTEGER | 1 | false | | | |
| workflowId | varchar(36) | | true | | [workflow_entity](workflow_entity.md) | |
| workflowReviewRequestId | varchar(36) | | false | | [workflow_review_request](workflow_review_request.md) | |
## Constraints
| Name | Type | Definition |
| ---- | ---- | ---------- |
| - | CHECK | CHECK ("type" IN ('review.opened', 'comment.created', 'review.changes_requested', 'review.version_updated', 'review.approved', 'workflow.published', 'review.closed')) |
| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE |
| - (Foreign key ID: 2) | FOREIGN KEY | FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
| id | PRIMARY KEY | PRIMARY KEY (id) |
## Indexes
| Name | Definition |
| ---- | ---------- |
| IDX_workflow_review_activity_request | CREATE INDEX "IDX_workflow_review_activity_request" ON "workflow_review_activity" ("workflowReviewRequestId", "id") |
## Relations
```mermaid
erDiagram
"workflow_review_activity" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_activity_comment" }o--|| "workflow_review_activity" : "FOREIGN KEY (activityId) REFERENCES workflow_review_activity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_activity" }o--o| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_activity" }o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_activity" {
datetime_3_ createdAt
varchar createdById FK
TEXT data
INTEGER id
varchar_64_ type
INTEGER typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"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_review_activity_comment" {
INTEGER activityId FK
TEXT body
datetime_3_ createdAt
varchar createdById FK
datetime_3_ deletedAt
TEXT history
INTEGER id
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
}
"workflow_review_request" {
datetime_3_ approvedAt
varchar closedById FK
datetime_3_ createdAt
varchar createdById FK
varchar_50_ decision
TEXT description
varchar_36_ id PK
varchar_36_ projectId FK
varchar_16_ state
varchar_255_ title
datetime_3_ updatedAt
varchar updatedById FK
}
```
---
> Generated by [tbls](https://github.com/k1LoW/tbls)

View File

@ -0,0 +1,90 @@
# workflow_review_activity_comment
## Description
<details>
<summary><strong>Table Definition</strong></summary>
```sql
CREATE TABLE "workflow_review_activity_comment" ("id" integer PRIMARY KEY NOT NULL, "activityId" integer NOT NULL, "createdById" varchar, "body" text, "history" text, "updatedAt" datetime(3), "deletedAt" datetime(3), "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), CONSTRAINT "FK_3cd755e8ce44ee49bdf7e9222ec" FOREIGN KEY ("activityId") REFERENCES "workflow_review_activity" ("id") ON DELETE CASCADE, CONSTRAINT "FK_9cfac4b106553d0930a1962bea0" FOREIGN KEY ("createdById") REFERENCES "user" ("id") ON DELETE SET NULL)
```
</details>
## Columns
| Name | Type | Default | Nullable | Children | Parents | Comment |
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| activityId | INTEGER | | false | | [workflow_review_activity](workflow_review_activity.md) | |
| body | TEXT | | true | | | |
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
| createdById | varchar | | true | | [user](user.md) | |
| deletedAt | datetime(3) | | true | | | |
| history | TEXT | | true | | | |
| id | INTEGER | | false | | | |
| updatedAt | datetime(3) | | true | | | |
## Constraints
| Name | Type | Definition |
| ---- | ---- | ---------- |
| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE |
| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (activityId) REFERENCES workflow_review_activity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
| id | PRIMARY KEY | PRIMARY KEY (id) |
## Indexes
| Name | Definition |
| ---- | ---------- |
| IDX_workflow_review_activity_comment_activity | CREATE INDEX "IDX_workflow_review_activity_comment_activity" ON "workflow_review_activity_comment" ("activityId", "id") |
## Relations
```mermaid
erDiagram
"workflow_review_activity_comment" }o--|| "workflow_review_activity" : "FOREIGN KEY (activityId) REFERENCES workflow_review_activity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_activity_comment" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_activity_comment" {
INTEGER activityId FK
TEXT body
datetime_3_ createdAt
varchar createdById FK
datetime_3_ deletedAt
TEXT history
INTEGER id
datetime_3_ updatedAt
}
"workflow_review_activity" {
datetime_3_ createdAt
varchar createdById FK
TEXT data
INTEGER id
varchar_64_ type
INTEGER typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"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
}
```
---
> Generated by [tbls](https://github.com/k1LoW/tbls)

View File

@ -21,7 +21,7 @@ CREATE TABLE "workflow_review_request" ("id" varchar(36) PRIMARY KEY NOT NULL, "
| createdById | varchar | | true | | [user](user.md) | |
| decision | varchar(50) | 'pending' | false | | | |
| description | TEXT | | true | | | |
| id | varchar(36) | | false | [workflow_review_request_authors](workflow_review_request_authors.md) [workflow_review_request_reviewers](workflow_review_request_reviewers.md) [workflow_review_request_workflow](workflow_review_request_workflow.md) | | |
| id | varchar(36) | | false | [workflow_review_activity](workflow_review_activity.md) [workflow_review_request_authors](workflow_review_request_authors.md) [workflow_review_request_reviewers](workflow_review_request_reviewers.md) [workflow_review_request_workflow](workflow_review_request_workflow.md) | | |
| projectId | varchar(36) | | false | | [project](project.md) | |
| state | varchar(16) | 'open' | false | | | |
| title | varchar(255) | | false | | | |
@ -55,6 +55,7 @@ erDiagram
"workflow_review_request" }o--o| "user" : "FOREIGN KEY (closedById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request" }o--o| "user" : "FOREIGN KEY (createdById) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_activity" }o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_authors" |o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_reviewers" |o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_workflow" }o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@ -92,6 +93,16 @@ erDiagram
TEXT settings
datetime_3_ updatedAt
}
"workflow_review_activity" {
datetime_3_ createdAt
varchar createdById FK
TEXT data
INTEGER id
varchar_64_ type
INTEGER typeVersion
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
}
"workflow_review_request_authors" {
varchar userId PK
varchar_36_ workflowReviewRequestId PK
@ -101,6 +112,7 @@ erDiagram
varchar_36_ workflowReviewRequestId PK
}
"workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id PK
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK

View File

@ -32,6 +32,7 @@ CREATE TABLE "workflow_review_request_authors" ("workflowReviewRequestId" varcha
| Name | Definition |
| ---- | ---------- |
| IDX_workflow_review_request_authors_user | CREATE INDEX "IDX_workflow_review_request_authors_user" ON "workflow_review_request_authors" ("userId", "workflowReviewRequestId") |
| sqlite_autoindex_workflow_review_request_authors_1 | PRIMARY KEY (workflowReviewRequestId, userId) |
## Relations

View File

@ -6,7 +6,7 @@
<summary><strong>Table Definition</strong></summary>
```sql
CREATE TABLE "workflow_review_request_workflow" ("id" varchar(36) PRIMARY KEY NOT NULL, "workflowReviewRequestId" varchar(36) NOT NULL, "workflowId" varchar(36) NOT NULL, "workflowVersionId" varchar(36), CONSTRAINT "FK_e44b652e6dc99ef1364a2d85504" FOREIGN KEY ("workflowReviewRequestId") REFERENCES "workflow_review_request" ("id") ON DELETE CASCADE, CONSTRAINT "FK_619f5b0544bcec60c3387e82f2f" FOREIGN KEY ("workflowId") REFERENCES "workflow_entity" ("id") ON DELETE CASCADE, CONSTRAINT "FK_0f6f0f2c6d46b806fee02962ac2" FOREIGN KEY ("workflowVersionId") REFERENCES "workflow_history" ("versionId") ON DELETE SET NULL)
CREATE TABLE "workflow_review_request_workflow" ("id" varchar(36) PRIMARY KEY NOT NULL, "workflowReviewRequestId" varchar(36) NOT NULL, "workflowId" varchar(36) NOT NULL, "workflowVersionId" varchar(36), "baselineVersionId" varchar(36), CONSTRAINT "FK_0f6f0f2c6d46b806fee02962ac2" FOREIGN KEY ("workflowVersionId") REFERENCES "workflow_history" ("versionId") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_619f5b0544bcec60c3387e82f2f" FOREIGN KEY ("workflowId") REFERENCES "workflow_entity" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_e44b652e6dc99ef1364a2d85504" FOREIGN KEY ("workflowReviewRequestId") REFERENCES "workflow_review_request" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_workflow_review_request_workflow_baselineVersionId" FOREIGN KEY ("baselineVersionId") REFERENCES "workflow_history" ("versionId") ON DELETE SET NULL)
```
</details>
@ -15,6 +15,7 @@ CREATE TABLE "workflow_review_request_workflow" ("id" varchar(36) PRIMARY KEY NO
| Name | Type | Default | Nullable | Children | Parents | Comment |
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
| baselineVersionId | varchar(36) | | true | | [workflow_history](workflow_history.md) | |
| id | varchar(36) | | false | | | |
| workflowId | varchar(36) | | false | | [workflow_entity](workflow_entity.md) | |
| workflowReviewRequestId | varchar(36) | | false | | [workflow_review_request](workflow_review_request.md) | |
@ -24,9 +25,10 @@ CREATE TABLE "workflow_review_request_workflow" ("id" varchar(36) PRIMARY KEY NO
| Name | Type | Definition |
| ---- | ---- | ---------- |
| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (workflowVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE |
| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
| - (Foreign key ID: 2) | FOREIGN KEY | FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (baselineVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE |
| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
| - (Foreign key ID: 2) | FOREIGN KEY | FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
| - (Foreign key ID: 3) | FOREIGN KEY | FOREIGN KEY (workflowVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE |
| id | PRIMARY KEY | PRIMARY KEY (id) |
| sqlite_autoindex_workflow_review_request_workflow_1 | PRIMARY KEY | PRIMARY KEY (id) |
@ -34,8 +36,8 @@ CREATE TABLE "workflow_review_request_workflow" ("id" varchar(36) PRIMARY KEY NO
| Name | Definition |
| ---- | ---------- |
| IDX_workflow_review_request_workflow_workflow_request | CREATE INDEX "IDX_workflow_review_request_workflow_workflow_request"<br /> ON "workflow_review_request_workflow"("workflowId", "workflowReviewRequestId") |
| UQ_workflow_review_request_workflow_request_workflow | CREATE UNIQUE INDEX "UQ_workflow_review_request_workflow_request_workflow"<br /> ON "workflow_review_request_workflow"("workflowReviewRequestId", "workflowId") |
| IDX_workflow_review_request_workflow_workflow_request | CREATE INDEX "IDX_workflow_review_request_workflow_workflow_request" ON "workflow_review_request_workflow" ("workflowId", "workflowReviewRequestId") |
| UQ_workflow_review_request_workflow_request_workflow | CREATE UNIQUE INDEX "UQ_workflow_review_request_workflow_request_workflow" ON "workflow_review_request_workflow" ("workflowReviewRequestId", "workflowId") |
| sqlite_autoindex_workflow_review_request_workflow_1 | PRIMARY KEY (id) |
## Relations
@ -43,16 +45,31 @@ CREATE TABLE "workflow_review_request_workflow" ("id" varchar(36) PRIMARY KEY NO
```mermaid
erDiagram
"workflow_review_request_workflow" }o--o| "workflow_history" : "FOREIGN KEY (baselineVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request_workflow" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_workflow" }o--|| "workflow_review_request" : "FOREIGN KEY (workflowReviewRequestId) REFERENCES workflow_review_request (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_review_request_workflow" }o--o| "workflow_history" : "FOREIGN KEY (workflowVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_review_request_workflow" {
varchar_36_ baselineVersionId FK
varchar_36_ id PK
varchar_36_ workflowId FK
varchar_36_ workflowReviewRequestId FK
varchar_36_ workflowVersionId FK
}
"workflow_history" {
varchar_255_ authors
boolean autosaved
TEXT connections
datetime_3_ createdAt
TEXT description
varchar_128_ name
TEXT nodeGroups
TEXT nodes
datetime_3_ updatedAt
varchar_36_ versionId PK
varchar_36_ workflowId FK
}
"workflow_entity" {
boolean active
varchar_36_ activeVersionId FK
@ -89,19 +106,6 @@ erDiagram
datetime_3_ updatedAt
varchar updatedById FK
}
"workflow_history" {
varchar_255_ authors
boolean autosaved
TEXT connections
datetime_3_ createdAt
TEXT description
varchar_128_ name
TEXT nodeGroups
TEXT nodes
datetime_3_ updatedAt
varchar_36_ versionId PK
varchar_36_ workflowId FK
}
```
---

View File

@ -13,6 +13,7 @@ export * from './instance-registry-types';
export * from './redaction-enforcement';
export * from './redaction-enforcement-floor';
export * from './workflow-reviews-policy';
export type * from './workflow-review-activity';
export type * from './workflow-review-eligible-reviewer';
export * from './workflow-review-request-summary';
export * from './workflow-publish-blocked-details';

View File

@ -0,0 +1,18 @@
/**
* Feed entry kinds, named `<model>.<event>`: a dot separates model from event, snake_case within a
* multi-word event. Mirrored by a CHECK constraint on `workflow_review_activity.type`, so a new
* kind needs a migration.
*
* Here rather than on the entity because it is a wire type: the frontend switches on these
* values to pick a renderer.
*/
export type WorkflowReviewActivityType =
| 'review.opened'
| 'comment.created'
| 'review.changes_requested'
| 'review.version_updated'
| 'review.approved'
| 'workflow.published'
/** Closed without an approval: workflow archived or deleted, or the review abandoned. An
* approval writes `review.approved` instead, never both. */
| 'review.closed';

View File

@ -72,6 +72,8 @@ import {
} from './workflow-publication-trigger-status';
import { WorkflowPublishHistory } from './workflow-publish-history';
import { WorkflowPublishedVersion } from './workflow-published-version';
import { WorkflowReviewActivityComment } from './workflow-review-activity-comment.ee';
import { WorkflowReviewActivity } from './workflow-review-activity.ee';
import { WorkflowReviewRequestAuthor } from './workflow-review-request-author.ee';
import { WorkflowReviewRequestReviewer } from './workflow-review-request-reviewer.ee';
import { WorkflowReviewRequestWorkflow } from './workflow-review-request-workflow.ee';
@ -156,6 +158,8 @@ export {
WorkflowReviewRequestWorkflow,
WorkflowReviewRequestReviewer,
WorkflowReviewRequestAuthor,
WorkflowReviewActivity,
WorkflowReviewActivityComment,
ExecutionData,
ExecutionMetadata,
AnnotationTagEntity,
@ -213,6 +217,8 @@ export const entities = {
WorkflowReviewRequestWorkflow,
WorkflowReviewRequestReviewer,
WorkflowReviewRequestAuthor,
WorkflowReviewActivity,
WorkflowReviewActivityComment,
ExecutionData,
ExecutionMetadata,
AnnotationTagEntity,

View File

@ -0,0 +1,47 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from '@n8n/typeorm';
import type { IDataObject } from 'n8n-workflow';
import { DateTimeColumn, JsonColumn, WithCreatedAt } from './abstract-entity';
/**
* One message in a comment thread. A `type = 'comment.created'` activity entry is the thread header,
* owning these rows; replies are messages here, not further activity entries. A header with no messages
* cannot be rendered, having no `body` of its own, so create a thread and its first message in one
* transaction.
*/
@Entity({ name: 'workflow_review_activity_comment' })
@Index('IDX_workflow_review_activity_comment_activity', ['activityId', 'id'])
export class WorkflowReviewActivityComment extends WithCreatedAt {
/**
* Orders the messages within a thread. Same SQLite id-reuse caveat as
* `WorkflowReviewActivity.id`, scoped by `activityId`.
*/
@PrimaryGeneratedColumn()
id: number;
/**
* Thread this message belongs to. This table carries no `workflowReviewRequestId`, and `id` is
* globally sequential and enumerable across projects, so a by-id mutation must authorise by
* joining comment -> activity -> request -> project.
*/
@Column({ type: 'int' })
activityId: number;
/** Author of this message. The header's `createdById` only covers whoever opened the thread. */
@Column({ type: 'uuid', nullable: true })
createdById: string | null;
/** Only user-editable text in the feed; nulled on delete. */
@Column({ type: 'text', nullable: true })
body: string | null;
@JsonColumn({ nullable: true })
history: IDataObject[] | null;
/** Intentionally not `@UpdateDateColumn`: stays null until the body is actually edited. */
@DateTimeColumn({ nullable: true })
updatedAt: Date | null;
@DateTimeColumn({ nullable: true })
deletedAt: Date | null;
}

View File

@ -0,0 +1,51 @@
import type { WorkflowReviewActivityType } from '@n8n/api-types';
import { Column, Entity, Index, PrimaryGeneratedColumn } from '@n8n/typeorm';
import type { IDataObject } from 'n8n-workflow';
import { JsonColumn, WithCreatedAt } from './abstract-entity';
/**
* Altering this table on SQLite recreates it, and `workflow_review_activity_comment` references it
* with ON DELETE CASCADE, so the rebuild takes the comment rows with it: any later migration
* touching these columns needs a `sqlite/` subclass with `withFKsDisabled`.
*/
@Entity({ name: 'workflow_review_activity' })
@Index('IDX_workflow_review_activity_request', ['workflowReviewRequestId', 'id'])
export class WorkflowReviewActivity extends WithCreatedAt {
/**
* Autoincrement int, not the usual nanoid: the feed orders by id and pages on it as a cursor.
* On SQLite this is a plain rowid alias, so ids are reused once the top rows are deleted, which
* deleting a review request does. Never trust a cursor across a deletion; scope reads by
* `workflowReviewRequestId`.
*/
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 36 })
workflowReviewRequestId: string;
@Column({ type: 'varchar', length: 64 })
type: WorkflowReviewActivityType;
@Column({ type: 'int', default: 1 })
typeVersion: number;
/**
* Immutable per-type detail. Ids only: a user id stored here would survive user deletion,
* unlike `createdById`, so a type needing another actor gets its own `SET NULL` column.
*/
@JsonColumn({ nullable: true })
data: IDataObject | null;
/** Who produced this entry. For a comment thread, whoever opened it. */
@Column({ type: 'uuid', nullable: true })
createdById: string | null;
/**
* Scopes an entry to one workflow; `null` for review-level entries such as comments. A column
* rather than a key in `data` so the feed can query and filter on it directly, for instance to
* leave out entries about workflows the reader may not open. Nothing writes it yet.
*/
@Column({ type: 'varchar', length: 36, nullable: true })
workflowId: string | null;
}

View File

@ -1,6 +1,9 @@
import { Entity, PrimaryColumn } from '@n8n/typeorm';
import { Entity, Index, PrimaryColumn } from '@n8n/typeorm';
@Entity({ name: 'workflow_review_request_authors' })
// The primary key leads on `workflowReviewRequestId`, so "which reviews did I author" cannot use
// it. Mirrors the index the reviewers table carries for the same lookup.
@Index('IDX_workflow_review_request_authors_user', ['userId', 'workflowReviewRequestId'])
export class WorkflowReviewRequestAuthor {
@PrimaryColumn({ type: 'varchar', length: 36 })
workflowReviewRequestId: string;

View File

@ -20,4 +20,7 @@ export class WorkflowReviewRequestWorkflow extends WithStringId {
@Column({ type: 'varchar', length: 36, nullable: true })
workflowVersionId: string | null;
@Column({ type: 'varchar', length: 36, nullable: true })
baselineVersionId: string | null;
}

View File

@ -0,0 +1,201 @@
import type { MigrationContext, ReversibleMigration } from '../migration-types';
const ACTIVITY_TABLE = 'workflow_review_activity';
const COMMENT_TABLE = 'workflow_review_activity_comment';
const REQUEST_TABLE = 'workflow_review_request';
const REQUEST_WORKFLOW_TABLE = 'workflow_review_request_workflow';
const REQUEST_AUTHORS_TABLE = 'workflow_review_request_authors';
const WORKFLOW_TABLE = 'workflow_entity';
const WORKFLOW_HISTORY_TABLE = 'workflow_history';
const USER_TABLE = 'user';
const BASELINE_VERSION_COLUMN = 'baselineVersionId';
// Named explicitly because SQLite drops a foreign key by matching the constraint name only:
// an unnamed one never matches the loaded (auto-named) constraint and silently stays behind.
const BASELINE_VERSION_FK = 'FK_workflow_review_request_workflow_baselineVersionId';
const ACTIVITY_TYPES = [
'review.opened',
'comment.created',
'review.changes_requested',
'review.version_updated',
'review.approved',
'workflow.published',
'review.closed',
];
export class CreateWorkflowReviewActivityTablesAndBaseline1785843640527
implements ReversibleMigration
{
async up(context: MigrationContext) {
await this.createActivityTable(context);
await this.createCommentTable(context);
await this.addBaselineVersionColumn(context);
await this.addAuthorsUserIndex(context);
}
async down(context: MigrationContext) {
await this.dropAuthorsUserIndex(context);
await this.dropBaselineVersionColumn(context);
// Comment table first: its FK blocks dropping the activity table on Postgres.
await context.schemaBuilder.dropTable(COMMENT_TABLE);
await context.schemaBuilder.dropTable(ACTIVITY_TABLE);
}
private async createActivityTable({
schemaBuilder: { createTable, createIndex, column },
tablePrefix,
}: MigrationContext) {
await createTable(ACTIVITY_TABLE)
.withColumns(
column('id').int.primary.autoGenerate2,
column('workflowReviewRequestId').varchar(36).notNull,
// Widening this list later needs a sqlite/ subclass with `withFKsDisabled = true as
// const`: on SQLite dropEnumCheck/addEnumCheck recreate the table, and the comment
// table's ON DELETE CASCADE FK would take every comment row with it.
column('type')
.varchar(64)
.notNull.withEnumCheck(ACTIVITY_TYPES)
.comment('Feed entry kind; see WorkflowReviewActivityType in @n8n/api-types'),
column('typeVersion')
.int.notNull.default(1)
.comment('Schema version of the `data` payload for this `type`'),
column('data').json.comment('Detail per activity type'),
column('createdById').uuid,
// A column rather than a key in `data` so the feed can query and filter on it
// directly, for instance by what the reader may open.
column('workflowId')
.varchar(36)
.comment('Scopes the entry to one workflow; NULL for review-level entries like comments'),
)
.withCreatedAt.withForeignKey('workflowReviewRequestId', {
tableName: REQUEST_TABLE,
columnName: 'id',
onDelete: 'CASCADE',
})
.withForeignKey('createdById', {
tableName: USER_TABLE,
columnName: 'id',
onDelete: 'SET NULL',
})
// CASCADE, never SET NULL: NULL means "review-level", so nulling this on workflow
// deletion would widen a scoped entry to everyone who can read the review.
.withForeignKey('workflowId', {
tableName: WORKFLOW_TABLE,
columnName: 'id',
onDelete: 'CASCADE',
});
// Feed read: all entries for one request ORDER BY id, keyset paginated on id.
// Leading column also serves the request FK cascade.
await createIndex(
ACTIVITY_TABLE,
['workflowReviewRequestId', 'id'],
false,
`IDX_${tablePrefix}workflow_review_activity_request`,
);
}
private async createCommentTable({
schemaBuilder: { createTable, createIndex, column },
tablePrefix,
}: MigrationContext) {
await createTable(COMMENT_TABLE)
.withColumns(
column('id').int.primary.autoGenerate2,
column('activityId').int.notNull.comment(
'Thread this message belongs to; the activity row is its header',
),
column('createdById').uuid,
column('body').text.comment('Only user-editable text in the feed; nulled on delete'),
column('history').json.comment('Comment revision history.'),
column('updatedAt').timestampTimezone().comment('Set when the body is edited'),
column('deletedAt').timestampTimezone().comment('Set when the comment is deleted'),
)
.withCreatedAt.withForeignKey('activityId', {
tableName: ACTIVITY_TABLE,
columnName: 'id',
onDelete: 'CASCADE',
})
.withForeignKey('createdById', {
tableName: USER_TABLE,
columnName: 'id',
onDelete: 'SET NULL',
});
// Thread read: all messages for one activity row ORDER BY id.
// Leading column also serves the activity FK cascade.
await createIndex(
COMMENT_TABLE,
['activityId', 'id'],
false,
`IDX_${tablePrefix}workflow_review_activity_comment_activity`,
);
}
/**
* Rides along with the activity tables so the review feature needs a single schema change:
* the feed records that a version was approved, this column pins which one.
*/
private async addBaselineVersionColumn({
schemaBuilder: { addColumns, addForeignKey, column },
}: MigrationContext) {
await addColumns(
REQUEST_WORKFLOW_TABLE,
[
column(BASELINE_VERSION_COLUMN)
.varchar(36)
.comment('Published workflow_history version captured when the review was approved'),
],
{ recreatesOnSqlite: true },
);
await addForeignKey(
REQUEST_WORKFLOW_TABLE,
BASELINE_VERSION_COLUMN,
[WORKFLOW_HISTORY_TABLE, 'versionId'],
BASELINE_VERSION_FK,
'SET NULL',
);
}
/**
* The authors table's primary key leads on `workflowReviewRequestId`, so "which reviews did I
* author" cannot use it. Mirrors the index the reviewers table already carries. Added here
* rather than in its own migration because that table's own migration has already shipped.
*/
private async addAuthorsUserIndex({
schemaBuilder: { createIndex },
tablePrefix,
}: MigrationContext) {
await createIndex(
REQUEST_AUTHORS_TABLE,
['userId', 'workflowReviewRequestId'],
false,
`IDX_${tablePrefix}workflow_review_request_authors_user`,
);
}
private async dropAuthorsUserIndex({
schemaBuilder: { dropIndex },
tablePrefix,
}: MigrationContext) {
await dropIndex(REQUEST_AUTHORS_TABLE, ['userId', 'workflowReviewRequestId'], {
customIndexName: `IDX_${tablePrefix}workflow_review_request_authors_user`,
});
}
private async dropBaselineVersionColumn({
schemaBuilder: { dropColumns, dropForeignKey },
}: MigrationContext) {
await dropForeignKey(
REQUEST_WORKFLOW_TABLE,
BASELINE_VERSION_COLUMN,
[WORKFLOW_HISTORY_TABLE, 'versionId'],
BASELINE_VERSION_FK,
);
await dropColumns(REQUEST_WORKFLOW_TABLE, [BASELINE_VERSION_COLUMN], {
recreatesOnSqlite: true,
});
}
}

View File

@ -67,7 +67,7 @@
{
"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."
"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."
}
]
}

View File

@ -0,0 +1,595 @@
import {
createTestMigrationContext,
initDbUpToMigration,
runSingleMigration,
undoLastSingleMigration,
type TestMigrationContext,
} from '@n8n/backend-test-utils';
import { DbConnection, WorkflowReviewActivity, WorkflowReviewActivityComment } from '@n8n/db';
import { Container } from '@n8n/di';
import { DataSource } from '@n8n/typeorm';
import { generateNanoId } from '@n8n/utils/generate-nano-id';
import { randomUUID } from 'node:crypto';
const MIGRATION_NAME = 'CreateWorkflowReviewActivityTablesAndBaseline1785843640527';
const ACTIVITY_TABLE = 'workflow_review_activity';
const COMMENT_TABLE = 'workflow_review_activity_comment';
const REQUEST_WORKFLOW_TABLE = 'workflow_review_request_workflow';
const BASELINE_VERSION_COLUMN = 'baselineVersionId';
const BASELINE_VERSION_FK = 'FK_workflow_review_request_workflow_baselineVersionId';
const AUTHORS_TABLE = 'workflow_review_request_authors';
const AUTHORS_USER_INDEX_SUFFIX = 'workflow_review_request_authors_user';
async function tableExists(context: TestMigrationContext, table: string): Promise<boolean> {
const name = `${context.tablePrefix}${table}`;
if (context.isSqlite) {
const rows = await context.runQuery<Array<{ name: string }>>(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = :name",
{ name },
);
return rows.length > 0;
}
const rows = await context.runQuery<Array<{ tablename: string }>>(
'SELECT tablename FROM pg_tables WHERE tablename = :name',
{ name },
);
return rows.length > 0;
}
async function columnMeta(
context: TestMigrationContext,
table: string,
columnName: string,
): Promise<{ nullable: boolean } | undefined> {
if (context.isSqlite) {
const rows = await context.runQuery<Array<{ name: string; notnull: number }>>(
`PRAGMA table_info(${context.escape.tableName(table)})`,
);
const row = rows.find(({ name }) => name === columnName);
return row && { nullable: Number(row.notnull) === 0 };
}
const rows = await context.runQuery<Array<{ is_nullable: string }>>(
`SELECT is_nullable FROM information_schema.columns
WHERE table_name = :tableName AND column_name = :columnName`,
{ tableName: `${context.tablePrefix}${table}`, columnName },
);
return rows[0] && { nullable: rows[0].is_nullable === 'YES' };
}
async function indexExists(
context: TestMigrationContext,
table: string,
indexNameSuffix: string,
): Promise<boolean> {
// The DSL composes a custom index name as `IDX_<prefix><name>`, prefix in the middle, so a
// plain `<prefix><name>` misses it on the Postgres leg, which runs with DB_TABLE_PREFIX.
const name = `IDX_${context.tablePrefix}${indexNameSuffix}`;
if (context.isSqlite) {
const rows = await context.runQuery<Array<{ name: string }>>(
"SELECT name FROM sqlite_master WHERE type = 'index' AND name = :name",
{ name },
);
return rows.length > 0;
}
const rows = await context.runQuery<Array<{ indexname: string }>>(
'SELECT indexname FROM pg_indexes WHERE tablename = :tableName AND indexname = :name',
{ tableName: `${context.tablePrefix}${table}`, name },
);
return rows.length > 0;
}
async function foreignKeyNames(
context: TestMigrationContext,
table: string,
columnName: string,
): Promise<string[]> {
if (context.isSqlite) {
const [{ sql }] = await context.runQuery<Array<{ sql: string }>>(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = :name",
{ name: `${context.tablePrefix}${table}` },
);
const pattern = new RegExp(`CONSTRAINT "([^"]+)" FOREIGN KEY \\("${columnName}"\\)`, 'g');
return [...sql.matchAll(pattern)].map(([, name]) => name);
}
const rows = await context.runQuery<Array<{ constraintName: string }>>(
`SELECT tc.constraint_name AS ${context.escape.columnName('constraintName')}
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
WHERE tc.table_name = :tableName
AND tc.constraint_type = 'FOREIGN KEY'
AND kcu.column_name = :columnName`,
{ tableName: `${context.tablePrefix}${table}`, columnName },
);
return rows.map((row) => row.constraintName);
}
describe('CreateWorkflowReviewActivityTablesAndBaseline Migration', () => {
let dataSource: DataSource;
/** Seeded before the migration runs, so the column add has existing data to preserve. */
let preExistingLinkRowId: string;
beforeAll(async () => {
const dbConnection = Container.get(DbConnection);
await dbConnection.init();
dataSource = Container.get(DataSource);
const context = createTestMigrationContext(dataSource);
try {
await context.queryRunner.clearDatabase();
} finally {
await context.queryRunner.release();
}
await initDbUpToMigration(MIGRATION_NAME);
const seedContext = createTestMigrationContext(dataSource);
try {
preExistingLinkRowId = await seedLinkedWorkflowRow(seedContext);
} finally {
await seedContext.queryRunner.release();
}
});
afterAll(async () => {
await Container.get(DbConnection).close();
});
async function seedUser(context: TestMigrationContext): Promise<string> {
const userId = randomUUID();
const now = new Date();
await context.runQuery(
`INSERT INTO ${context.escape.tableName('user')} ("id", "email", "firstName", "lastName", "password", "createdAt", "updatedAt")
VALUES (:id, :email, :firstName, :lastName, :password, :createdAt, :updatedAt)`,
{
id: userId,
email: `${userId}@example.com`,
firstName: 'Activity',
lastName: 'Tester',
password: 'hashed',
createdAt: now,
updatedAt: now,
},
);
return userId;
}
async function seedRequest(context: TestMigrationContext): Promise<string> {
const projectId = generateNanoId();
const requestId = generateNanoId();
const now = new Date();
await context.runQuery(
`INSERT INTO ${context.escape.tableName('project')} ("id", "name", "type", "createdAt", "updatedAt")
VALUES (:id, :name, :type, :createdAt, :updatedAt)`,
{
id: projectId,
name: `Activity test project ${projectId}`,
type: 'team',
createdAt: now,
updatedAt: now,
},
);
await context.runQuery(
`INSERT INTO ${context.escape.tableName('workflow_review_request')}
("id", "projectId", "state", "decision", "title", "createdAt", "updatedAt")
VALUES (:id, :projectId, :state, :decision, :title, :createdAt, :updatedAt)`,
{
id: requestId,
projectId,
state: 'open',
decision: 'pending',
title: 'Activity test review',
createdAt: now,
updatedAt: now,
},
);
return requestId;
}
/**
* Inserts an activity row without an `id` and reads the assigned one back as the table max.
* Safe because this suite runs single-fork with no file parallelism. `INSERT ... RETURNING`
* is not portable here: the SQLite pooled driver replaces the result of any INSERT with
* `lastID`, where Postgres returns rows.
*/
async function insertActivity(
context: TestMigrationContext,
fields: {
workflowReviewRequestId: string;
type?: string;
createdById?: string;
workflowId?: string;
},
): Promise<number> {
const activityTable = context.escape.tableName(ACTIVITY_TABLE);
await context.runQuery(
`INSERT INTO ${activityTable} ("workflowReviewRequestId", "type", "createdById", "workflowId", "createdAt")
VALUES (:workflowReviewRequestId, :type, :createdById, :workflowId, :createdAt)`,
{
workflowReviewRequestId: fields.workflowReviewRequestId,
type: fields.type ?? 'review.opened',
createdById: fields.createdById ?? null,
workflowId: fields.workflowId ?? null,
createdAt: new Date(),
},
);
return await maxActivityId(context);
}
async function maxActivityId(context: TestMigrationContext): Promise<number> {
const [{ maxId }] = await context.runQuery<Array<{ maxId: number | null }>>(
`SELECT MAX("id") AS "maxId" FROM ${context.escape.tableName(ACTIVITY_TABLE)}`,
);
return maxId ?? 0;
}
/**
* A `workflow_review_request_workflow` row in its pre-migration shape. Adding a column to that
* table recreates it on SQLite, so this is the data the recreate has to carry across.
*/
async function seedLinkedWorkflowRow(context: TestMigrationContext): Promise<string> {
const rowId = generateNanoId();
const requestId = await seedRequest(context);
const workflowId = await seedWorkflow(context);
await context.runQuery(
`INSERT INTO ${context.escape.tableName(REQUEST_WORKFLOW_TABLE)}
("id", "workflowReviewRequestId", "workflowId", "workflowVersionId")
VALUES (:id, :requestId, :workflowId, :versionId)`,
{ id: rowId, requestId, workflowId, versionId: null },
);
return rowId;
}
async function seedWorkflow(context: TestMigrationContext): Promise<string> {
const workflowId = generateNanoId();
const now = new Date();
await context.runQuery(
`INSERT INTO ${context.escape.tableName('workflow_entity')}
("id", "name", "active", "nodes", "connections", "versionId", "createdAt", "updatedAt")
VALUES (:id, :name, :active, :nodes, :connections, :versionId, :createdAt, :updatedAt)`,
{
id: workflowId,
name: `Activity test workflow ${workflowId}`,
active: false,
nodes: '[]',
connections: '{}',
versionId: randomUUID(),
createdAt: now,
updatedAt: now,
},
);
return workflowId;
}
async function insertComment(
context: TestMigrationContext,
activityId: number | null,
createdById?: string,
): Promise<void> {
await context.runQuery(
`INSERT INTO ${context.escape.tableName(COMMENT_TABLE)} ("activityId", "createdById", "body", "createdAt")
VALUES (:activityId, :createdById, :body, :createdAt)`,
{
activityId,
createdById: createdById ?? null,
body: 'Comment body',
createdAt: new Date(),
},
);
}
describe('up', () => {
beforeAll(async () => {
await runSingleMigration(MIGRATION_NAME);
});
it('round-trips an activity and its comment thread through the entities', async () => {
const context = createTestMigrationContext(dataSource);
let requestId: string;
let userId: string;
try {
requestId = await seedRequest(context);
userId = await seedUser(context);
} finally {
await context.queryRunner.release();
}
const activityRepository = dataSource.getRepository(WorkflowReviewActivity);
const commentRepository = dataSource.getRepository(WorkflowReviewActivityComment);
const data = { workflowId: 'wf-1', versionId: 'v-1' };
const history = [{ body: 'First draft', editedAt: '2026-01-02T03:04:05.678Z' }];
const editedAt = new Date('2026-01-02T03:04:05.678Z');
const savedSubmission = await activityRepository.save(
activityRepository.create({
workflowReviewRequestId: requestId,
type: 'review.opened',
data,
}),
);
const savedThread = await activityRepository.save(
activityRepository.create({ workflowReviewRequestId: requestId, type: 'comment.created' }),
);
const savedMessage = await commentRepository.save(
commentRepository.create({
activityId: savedThread.id,
body: 'Looks good',
history,
updatedAt: editedAt,
}),
);
const savedReply = await commentRepository.save(
commentRepository.create({
activityId: savedThread.id,
body: 'Agreed',
createdById: userId,
}),
);
// Assert on freshly loaded rows only: what `save()` returns is partly the in-memory
// object, so unset columns read back as `undefined` and `data` is the very object passed in.
const submission = await activityRepository.findOneByOrFail({ id: savedSubmission.id });
const thread = await activityRepository.findOneByOrFail({ id: savedThread.id });
const message = await commentRepository.findOneByOrFail({ id: savedMessage.id });
const reply = await commentRepository.findOneByOrFail({ id: savedReply.id });
expect(typeof submission.id).toBe('number');
expect(thread.id).toBeGreaterThan(submission.id);
expect(submission.type).toBe('review.opened');
expect(submission.typeVersion).toBe(1);
expect(submission.data).toEqual(data);
expect(submission.createdById).toBeNull();
expect(submission.createdAt).toBeInstanceOf(Date);
expect(thread.data).toBeNull();
// One activity row owns many message rows, ordered by their own id.
expect(message.activityId).toBe(thread.id);
expect(reply.activityId).toBe(thread.id);
expect(reply.id).toBeGreaterThan(message.id);
expect(message.body).toBe('Looks good');
expect(message.history).toEqual(history);
expect(message.updatedAt).toEqual(editedAt);
expect(message.deletedAt).toBeNull();
expect(message.createdAt).toBeInstanceOf(Date);
expect(message.createdById).toBeNull();
expect(reply.createdById).toBe(userId);
});
it('rejects a comment with no activityId and an activity with an unknown type', async () => {
const context = createTestMigrationContext(dataSource);
try {
// SQLite says "NOT NULL constraint failed", Postgres "violates not-null constraint".
await expect(insertComment(context, null)).rejects.toThrow(/not[- ]null constraint/i);
// SQLite says "CHECK constraint failed", Postgres "violates check constraint".
const requestId = await seedRequest(context);
await expect(
insertActivity(context, { workflowReviewRequestId: requestId, type: 'nonsense' }),
).rejects.toThrow(/check constraint/i);
} finally {
await context.queryRunner.release();
}
});
it('removes activity and comment rows when the review request is deleted', async () => {
const context = createTestMigrationContext(dataSource);
try {
const requestId = await seedRequest(context);
const activityId = await insertActivity(context, {
workflowReviewRequestId: requestId,
type: 'comment.created',
});
await insertComment(context, activityId);
await context.runQuery(
`DELETE FROM ${context.escape.tableName('workflow_review_request')} WHERE "id" = :id`,
{ id: requestId },
);
// Both hops of request -> activity -> comment.
const activityRows = await context.runQuery<unknown[]>(
`SELECT "id" FROM ${context.escape.tableName(ACTIVITY_TABLE)} WHERE "id" = :id`,
{ id: activityId },
);
const commentRows = await context.runQuery<unknown[]>(
`SELECT "id" FROM ${context.escape.tableName(COMMENT_TABLE)} WHERE "activityId" = :id`,
{ id: activityId },
);
expect(activityRows).toHaveLength(0);
expect(commentRows).toHaveLength(0);
} finally {
await context.queryRunner.release();
}
});
it('removes every message in a thread when its activity row is deleted', async () => {
const context = createTestMigrationContext(dataSource);
try {
const requestId = await seedRequest(context);
const activityId = await insertActivity(context, {
workflowReviewRequestId: requestId,
type: 'comment.created',
});
await insertComment(context, activityId);
await insertComment(context, activityId);
const commentsQuery = `SELECT "id" FROM ${context.escape.tableName(COMMENT_TABLE)} WHERE "activityId" = :id`;
expect(await context.runQuery<unknown[]>(commentsQuery, { id: activityId })).toHaveLength(
2,
);
await context.runQuery(
`DELETE FROM ${context.escape.tableName(ACTIVITY_TABLE)} WHERE "id" = :id`,
{ id: activityId },
);
expect(await context.runQuery<unknown[]>(commentsQuery, { id: activityId })).toHaveLength(
0,
);
} finally {
await context.queryRunner.release();
}
});
it('keeps activity and comment rows and nulls createdById when the author is deleted', async () => {
const context = createTestMigrationContext(dataSource);
try {
const requestId = await seedRequest(context);
const userId = await seedUser(context);
const activityId = await insertActivity(context, {
workflowReviewRequestId: requestId,
type: 'comment.created',
createdById: userId,
});
await insertComment(context, activityId, userId);
await context.runQuery(`DELETE FROM ${context.escape.tableName('user')} WHERE "id" = :id`, {
id: userId,
});
const activityRows = await context.runQuery<Array<{ createdById: string | null }>>(
`SELECT "createdById" FROM ${context.escape.tableName(ACTIVITY_TABLE)} WHERE "id" = :id`,
{ id: activityId },
);
const commentRows = await context.runQuery<Array<{ createdById: string | null }>>(
`SELECT "createdById" FROM ${context.escape.tableName(COMMENT_TABLE)} WHERE "activityId" = :id`,
{ id: activityId },
);
expect(activityRows).toEqual([{ createdById: null }]);
expect(commentRows).toEqual([{ createdById: null }]);
} finally {
await context.queryRunner.release();
}
});
it('keeps rows that already existed in the table it adds the column to', async () => {
const context = createTestMigrationContext(dataSource);
try {
const rows = await context.runQuery<Array<{ baselineVersionId: string | null }>>(
`SELECT "baselineVersionId" FROM ${context.escape.tableName(REQUEST_WORKFLOW_TABLE)} WHERE "id" = :id`,
{ id: preExistingLinkRowId },
);
expect(rows).toEqual([{ baselineVersionId: null }]);
} finally {
await context.queryRunner.release();
}
});
it('adds baselineVersionId as nullable so reviews closed before this migration stay valid', async () => {
const context = createTestMigrationContext(dataSource);
try {
expect(await columnMeta(context, REQUEST_WORKFLOW_TABLE, BASELINE_VERSION_COLUMN)).toEqual({
nullable: true,
});
} finally {
await context.queryRunner.release();
}
});
// The whole reason `workflowId` is CASCADE and not SET NULL: NULL marks a review-level
// entry, so nulling on delete would widen a workflow-scoped entry to everyone who can
// read the review.
it('drops entries about a deleted workflow and keeps the review-level ones', async () => {
const context = createTestMigrationContext(dataSource);
try {
const requestId = await seedRequest(context);
const workflowId = await seedWorkflow(context);
const scopedId = await insertActivity(context, {
workflowReviewRequestId: requestId,
type: 'workflow.published',
workflowId,
});
const reviewLevelId = await insertActivity(context, {
workflowReviewRequestId: requestId,
type: 'comment.created',
});
await insertComment(context, reviewLevelId);
await context.runQuery(
`DELETE FROM ${context.escape.tableName('workflow_entity')} WHERE "id" = :id`,
{ id: workflowId },
);
const remaining = await context.runQuery<Array<{ id: number }>>(
`SELECT "id" FROM ${context.escape.tableName(ACTIVITY_TABLE)} WHERE "workflowReviewRequestId" = :requestId`,
{ requestId },
);
expect(remaining.map(({ id }) => Number(id))).toEqual([reviewLevelId]);
expect(remaining.map(({ id }) => Number(id))).not.toContain(scopedId);
// The comment on the surviving entry rides its parent, so it stays too.
const commentRows = await context.runQuery<unknown[]>(
`SELECT "id" FROM ${context.escape.tableName(COMMENT_TABLE)} WHERE "activityId" = :id`,
{ id: reviewLevelId },
);
expect(commentRows).toHaveLength(1);
} finally {
await context.queryRunner.release();
}
});
// The authors table ships in an earlier migration, so this index is created here and has
// to be dropped explicitly in `down()`; nothing else removes it.
it('indexes the authors table by user', async () => {
const context = createTestMigrationContext(dataSource);
try {
expect(await indexExists(context, AUTHORS_TABLE, AUTHORS_USER_INDEX_SUFFIX)).toBe(true);
} finally {
await context.queryRunner.release();
}
});
// The name is what makes `down()` deterministic: SQLite drops a foreign key by matching the
// constraint name, and an auto-generated one never matches what the DSL passes in.
it('names the baseline foreign key explicitly', async () => {
const context = createTestMigrationContext(dataSource);
try {
expect(
await foreignKeyNames(context, REQUEST_WORKFLOW_TABLE, BASELINE_VERSION_COLUMN),
).toEqual([BASELINE_VERSION_FK]);
} finally {
await context.queryRunner.release();
}
});
});
describe('down', () => {
// The drop order only matters on Postgres: the SQLite query runner disables foreign keys
// for every migration run, so a wrong order would still pass on that leg.
it('drops both tables and the baseline column', async () => {
// Assert the precondition inside this test, not in the `up` block: under a filtered run
// (`vitest -t`) the `up` hook is skipped, so without this the test would revert whatever
// migration preceded ours and pass by finding tables that never existed.
const before = createTestMigrationContext(dataSource);
try {
expect(await tableExists(before, ACTIVITY_TABLE)).toBe(true);
expect(await tableExists(before, COMMENT_TABLE)).toBe(true);
expect(
await columnMeta(before, REQUEST_WORKFLOW_TABLE, BASELINE_VERSION_COLUMN),
).toBeDefined();
expect(await indexExists(before, AUTHORS_TABLE, AUTHORS_USER_INDEX_SUFFIX)).toBe(true);
} finally {
// Release before reverting: holding a pooled lease across the revert exhausts the
// Postgres pool and the migration times out trying to connect.
await before.queryRunner.release();
}
await undoLastSingleMigration();
const after = createTestMigrationContext(dataSource);
try {
expect(await tableExists(after, ACTIVITY_TABLE)).toBe(false);
expect(await tableExists(after, COMMENT_TABLE)).toBe(false);
expect(
await columnMeta(after, REQUEST_WORKFLOW_TABLE, BASELINE_VERSION_COLUMN),
).toBeUndefined();
// The authors table survives the revert, so a missed dropIndex would leave this behind.
expect(await indexExists(after, AUTHORS_TABLE, AUTHORS_USER_INDEX_SUFFIX)).toBe(false);
} finally {
await after.queryRunner.release();
}
});
});
});