mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(core): Add promotion schema entities and DTOs (#37939)
This commit is contained in:
parent
a0535781a7
commit
e77b7bb12f
42
docs/generated/postgres-schema/README.md
generated
42
docs/generated/postgres-schema/README.md
generated
|
|
@ -104,6 +104,10 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand.
|
|||
| [public.project_pool_settings](public.project_pool_settings.md) | 4 | | BASE TABLE |
|
||||
| [public.project_relation](public.project_relation.md) | 5 | | BASE TABLE |
|
||||
| [public.project_secrets_provider_access](public.project_secrets_provider_access.md) | 5 | | BASE TABLE |
|
||||
| [public.promotion_config](public.promotion_config.md) | 7 | | BASE TABLE |
|
||||
| [public.promotion_connection](public.promotion_connection.md) | 7 | | BASE TABLE |
|
||||
| [public.promotion_connection_project](public.promotion_connection_project.md) | 4 | | BASE TABLE |
|
||||
| [public.promotion_provider](public.promotion_provider.md) | 8 | | BASE TABLE |
|
||||
| [public.role](public.role.md) | 7 | | BASE TABLE |
|
||||
| [public.role_mapping_rule](public.role_mapping_rule.md) | 7 | | BASE TABLE |
|
||||
| [public.role_mapping_rule_project](public.role_mapping_rule_project.md) | 2 | | BASE TABLE |
|
||||
|
|
@ -307,6 +311,10 @@ erDiagram
|
|||
"public.project_relation" }o--|| "public.role" : "FOREIGN KEY (role) REFERENCES role(slug)"
|
||||
"public.project_secrets_provider_access" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
"public.project_secrets_provider_access" }o--|| "public.secrets_provider_connection" : "FOREIGN KEY (#quot;secretsProviderConnectionId#quot;) REFERENCES secrets_provider_connection(id) ON DELETE CASCADE"
|
||||
"public.promotion_config" }o--|| "public.promotion_connection" : "FOREIGN KEY (#quot;connectionId#quot;) REFERENCES promotion_connection(id) ON DELETE CASCADE"
|
||||
"public.promotion_connection" }o--|| "public.promotion_provider" : "FOREIGN KEY (#quot;providerId#quot;) REFERENCES promotion_provider(id) ON DELETE RESTRICT"
|
||||
"public.promotion_connection_project" |o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
"public.promotion_connection_project" }o--|| "public.promotion_connection" : "FOREIGN KEY (#quot;connectionId#quot;) REFERENCES promotion_connection(id) ON DELETE CASCADE"
|
||||
"public.role_mapping_rule" }o--|| "public.role" : "FOREIGN KEY (role) REFERENCES role(slug) ON UPDATE CASCADE ON DELETE CASCADE"
|
||||
"public.role_mapping_rule_project" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
"public.role_mapping_rule_project" }o--|| "public.role_mapping_rule" : "FOREIGN KEY (#quot;roleMappingRuleId#quot;) REFERENCES role_mapping_rule(id) ON DELETE CASCADE"
|
||||
|
|
@ -1324,6 +1332,40 @@ erDiagram
|
|||
integer secretsProviderConnectionId FK
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_config" {
|
||||
varchar_36_ connectionId FK
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_16_ direction
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
json settings
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_connection" {
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
json target
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_connection_project" {
|
||||
varchar_36_ connectionId FK
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ projectId FK
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_provider" {
|
||||
text auth
|
||||
varchar_32_ authType
|
||||
json config
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
varchar_32_ type
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.role" {
|
||||
timestamp_3__with_time_zone createdAt
|
||||
text description
|
||||
|
|
|
|||
9
docs/generated/postgres-schema/public.project.md
generated
9
docs/generated/postgres-schema/public.project.md
generated
|
|
@ -9,7 +9,7 @@
|
|||
| customTelemetryTags | json | '[]'::json | false | | | |
|
||||
| description | varchar(512) | | true | | | |
|
||||
| icon | json | | true | | | |
|
||||
| id | varchar(36) | | false | [public.activity_event](public.activity_event.md) [public.agent_chat_attachments](public.agent_chat_attachments.md) [public.agent_execution_threads](public.agent_execution_threads.md) [public.agents](public.agents.md) [public.data_table](public.data_table.md) [public.folder](public.folder.md) [public.git_connection_project](public.git_connection_project.md) [public.insights_metadata](public.insights_metadata.md) [public.instance_ai_threads](public.instance_ai_threads.md) [public.project_pool_settings](public.project_pool_settings.md) [public.project_relation](public.project_relation.md) [public.project_secrets_provider_access](public.project_secrets_provider_access.md) [public.role_mapping_rule_project](public.role_mapping_rule_project.md) [public.shared_credentials](public.shared_credentials.md) [public.shared_workflow](public.shared_workflow.md) [public.type_availability_policy_scope](public.type_availability_policy_scope.md) [public.variables](public.variables.md) [public.workflow_review_request](public.workflow_review_request.md) | | |
|
||||
| id | varchar(36) | | false | [public.activity_event](public.activity_event.md) [public.agent_chat_attachments](public.agent_chat_attachments.md) [public.agent_execution_threads](public.agent_execution_threads.md) [public.agents](public.agents.md) [public.data_table](public.data_table.md) [public.folder](public.folder.md) [public.git_connection_project](public.git_connection_project.md) [public.insights_metadata](public.insights_metadata.md) [public.instance_ai_threads](public.instance_ai_threads.md) [public.project_pool_settings](public.project_pool_settings.md) [public.project_relation](public.project_relation.md) [public.project_secrets_provider_access](public.project_secrets_provider_access.md) [public.promotion_connection_project](public.promotion_connection_project.md) [public.role_mapping_rule_project](public.role_mapping_rule_project.md) [public.shared_credentials](public.shared_credentials.md) [public.shared_workflow](public.shared_workflow.md) [public.type_availability_policy_scope](public.type_availability_policy_scope.md) [public.variables](public.variables.md) [public.workflow_review_request](public.workflow_review_request.md) | | |
|
||||
| name | varchar(255) | | false | | | |
|
||||
| type | varchar(36) | | false | | | |
|
||||
| updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
|
||||
|
|
@ -51,6 +51,7 @@ erDiagram
|
|||
"public.project_pool_settings" |o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
"public.project_relation" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
"public.project_secrets_provider_access" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
"public.promotion_connection_project" |o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
"public.role_mapping_rule_project" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
"public.shared_credentials" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
"public.shared_workflow" }o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
|
|
@ -205,6 +206,12 @@ erDiagram
|
|||
integer secretsProviderConnectionId FK
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_connection_project" {
|
||||
varchar_36_ connectionId FK
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ projectId FK
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.role_mapping_rule_project" {
|
||||
varchar_36_ projectId FK
|
||||
varchar_16_ roleMappingRuleId FK
|
||||
|
|
|
|||
66
docs/generated/postgres-schema/public.promotion_config.md
generated
Normal file
66
docs/generated/postgres-schema/public.promotion_config.md
generated
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# public.promotion_config
|
||||
|
||||
## Columns
|
||||
|
||||
| Name | Type | Default | Nullable | Children | Parents | Comment |
|
||||
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
|
||||
| connectionId | varchar(36) | | false | | [public.promotion_connection](public.promotion_connection.md) | |
|
||||
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
|
||||
| direction | varchar(16) | | false | | | PromotionDirection enum: "apply", "promote". Cannot change after creation. |
|
||||
| id | varchar(36) | | false | | | Also the name of the local checkout directory. |
|
||||
| name | varchar(128) | | false | | | |
|
||||
| settings | json | | false | | | Branch settings for this direction, with their own schemaVersion. Validated in code. |
|
||||
| updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
|
||||
|
||||
## Constraints
|
||||
|
||||
| Name | Type | Definition |
|
||||
| ---- | ---- | ---------- |
|
||||
| CHK_promotion_config_direction | CHECK | CHECK (((direction)::text = ANY ((ARRAY['apply'::character varying, 'promote'::character varying])::text[]))) |
|
||||
| FK_promotion_config_connectionId | FOREIGN KEY | FOREIGN KEY ("connectionId") REFERENCES promotion_connection(id) ON DELETE CASCADE |
|
||||
| PK_b4d534111f62d31699e6ceb544f | PRIMARY KEY | PRIMARY KEY (id) |
|
||||
| promotion_config_connectionId_not_null | n | NOT NULL "connectionId" |
|
||||
| promotion_config_createdAt_not_null | n | NOT NULL "createdAt" |
|
||||
| promotion_config_direction_not_null | n | NOT NULL direction |
|
||||
| promotion_config_id_not_null | n | NOT NULL id |
|
||||
| promotion_config_name_not_null | n | NOT NULL name |
|
||||
| promotion_config_settings_not_null | n | NOT NULL settings |
|
||||
| promotion_config_updatedAt_not_null | n | NOT NULL "updatedAt" |
|
||||
|
||||
## Indexes
|
||||
|
||||
| Name | Definition |
|
||||
| ---- | ---------- |
|
||||
| IDX_promotion_config_connectionId_direction | CREATE UNIQUE INDEX "IDX_promotion_config_connectionId_direction" ON public.promotion_config USING btree ("connectionId", direction) |
|
||||
| PK_b4d534111f62d31699e6ceb544f | CREATE UNIQUE INDEX "PK_b4d534111f62d31699e6ceb544f" ON public.promotion_config USING btree (id) |
|
||||
|
||||
## Relations
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
|
||||
"public.promotion_config" }o--|| "public.promotion_connection" : "FOREIGN KEY (#quot;connectionId#quot;) REFERENCES promotion_connection(id) ON DELETE CASCADE"
|
||||
|
||||
"public.promotion_config" {
|
||||
varchar_36_ connectionId FK
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_16_ direction
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
json settings
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_connection" {
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
json target
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> Generated by [tbls](https://github.com/k1LoW/tbls)
|
||||
85
docs/generated/postgres-schema/public.promotion_connection.md
generated
Normal file
85
docs/generated/postgres-schema/public.promotion_connection.md
generated
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# public.promotion_connection
|
||||
|
||||
## Columns
|
||||
|
||||
| Name | Type | Default | Nullable | Children | Parents | Comment |
|
||||
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
|
||||
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
|
||||
| id | varchar(36) | | false | [public.promotion_config](public.promotion_config.md) [public.promotion_connection_project](public.promotion_connection_project.md) | | |
|
||||
| name | varchar(128) | | false | | | |
|
||||
| providerId | varchar(36) | | false | | [public.promotion_provider](public.promotion_provider.md) | The provider whose credentials this connection uses. |
|
||||
| scope | varchar(16) | | false | | | PromotionConnectionScope enum: "instance", "projects". Cannot change after creation. |
|
||||
| target | json | | false | | | Where to push and pull, with its own schemaVersion. Plain Git stores the full remote URL. |
|
||||
| updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
|
||||
|
||||
## Constraints
|
||||
|
||||
| Name | Type | Definition |
|
||||
| ---- | ---- | ---------- |
|
||||
| CHK_promotion_connection_scope | CHECK | CHECK (((scope)::text = ANY ((ARRAY['instance'::character varying, 'projects'::character varying])::text[]))) |
|
||||
| FK_promotion_connection_providerId | FOREIGN KEY | FOREIGN KEY ("providerId") REFERENCES promotion_provider(id) ON DELETE RESTRICT |
|
||||
| PK_65cec79aba16950b45592335204 | PRIMARY KEY | PRIMARY KEY (id) |
|
||||
| promotion_connection_createdAt_not_null | n | NOT NULL "createdAt" |
|
||||
| promotion_connection_id_not_null | n | NOT NULL id |
|
||||
| promotion_connection_name_not_null | n | NOT NULL name |
|
||||
| promotion_connection_providerId_not_null | n | NOT NULL "providerId" |
|
||||
| promotion_connection_scope_not_null | n | NOT NULL scope |
|
||||
| promotion_connection_target_not_null | n | NOT NULL target |
|
||||
| promotion_connection_updatedAt_not_null | n | NOT NULL "updatedAt" |
|
||||
|
||||
## Indexes
|
||||
|
||||
| Name | Definition |
|
||||
| ---- | ---------- |
|
||||
| IDX_9917feaa6ed5705046432b117f | CREATE INDEX "IDX_9917feaa6ed5705046432b117f" ON public.promotion_connection USING btree ("providerId") |
|
||||
| IDX_promotion_connection_scope | CREATE UNIQUE INDEX "IDX_promotion_connection_scope" ON public.promotion_connection USING btree (scope) WHERE ((scope)::text = 'instance'::text) |
|
||||
| PK_65cec79aba16950b45592335204 | CREATE UNIQUE INDEX "PK_65cec79aba16950b45592335204" ON public.promotion_connection USING btree (id) |
|
||||
|
||||
## Relations
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
|
||||
"public.promotion_config" }o--|| "public.promotion_connection" : "FOREIGN KEY (#quot;connectionId#quot;) REFERENCES promotion_connection(id) ON DELETE CASCADE"
|
||||
"public.promotion_connection_project" }o--|| "public.promotion_connection" : "FOREIGN KEY (#quot;connectionId#quot;) REFERENCES promotion_connection(id) ON DELETE CASCADE"
|
||||
"public.promotion_connection" }o--|| "public.promotion_provider" : "FOREIGN KEY (#quot;providerId#quot;) REFERENCES promotion_provider(id) ON DELETE RESTRICT"
|
||||
|
||||
"public.promotion_connection" {
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
json target
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_config" {
|
||||
varchar_36_ connectionId FK
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_16_ direction
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
json settings
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_connection_project" {
|
||||
varchar_36_ connectionId FK
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ projectId FK
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_provider" {
|
||||
text auth
|
||||
varchar_32_ authType
|
||||
json config
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
varchar_32_ type
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> Generated by [tbls](https://github.com/k1LoW/tbls)
|
||||
69
docs/generated/postgres-schema/public.promotion_connection_project.md
generated
Normal file
69
docs/generated/postgres-schema/public.promotion_connection_project.md
generated
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# public.promotion_connection_project
|
||||
|
||||
## Columns
|
||||
|
||||
| Name | Type | Default | Nullable | Children | Parents | Comment |
|
||||
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
|
||||
| connectionId | varchar(36) | | false | | [public.promotion_connection](public.promotion_connection.md) | |
|
||||
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
|
||||
| projectId | varchar(36) | | false | | [public.project](public.project.md) | |
|
||||
| updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
|
||||
|
||||
## Constraints
|
||||
|
||||
| Name | Type | Definition |
|
||||
| ---- | ---- | ---------- |
|
||||
| FK_promotion_connection_project_connectionId | FOREIGN KEY | FOREIGN KEY ("connectionId") REFERENCES promotion_connection(id) ON DELETE CASCADE |
|
||||
| FK_promotion_connection_project_projectId | FOREIGN KEY | FOREIGN KEY ("projectId") REFERENCES project(id) ON DELETE CASCADE |
|
||||
| PK_0c63d9094bf8ca78349ba860147 | PRIMARY KEY | PRIMARY KEY ("projectId") |
|
||||
| promotion_connection_project_connectionId_not_null | n | NOT NULL "connectionId" |
|
||||
| promotion_connection_project_createdAt_not_null | n | NOT NULL "createdAt" |
|
||||
| promotion_connection_project_projectId_not_null | n | NOT NULL "projectId" |
|
||||
| promotion_connection_project_updatedAt_not_null | n | NOT NULL "updatedAt" |
|
||||
|
||||
## Indexes
|
||||
|
||||
| Name | Definition |
|
||||
| ---- | ---------- |
|
||||
| IDX_2c579b1068b66359de1b4d2de5 | CREATE INDEX "IDX_2c579b1068b66359de1b4d2de5" ON public.promotion_connection_project USING btree ("connectionId") |
|
||||
| PK_0c63d9094bf8ca78349ba860147 | CREATE UNIQUE INDEX "PK_0c63d9094bf8ca78349ba860147" ON public.promotion_connection_project USING btree ("projectId") |
|
||||
|
||||
## Relations
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
|
||||
"public.promotion_connection_project" }o--|| "public.promotion_connection" : "FOREIGN KEY (#quot;connectionId#quot;) REFERENCES promotion_connection(id) ON DELETE CASCADE"
|
||||
"public.promotion_connection_project" |o--|| "public.project" : "FOREIGN KEY (#quot;projectId#quot;) REFERENCES project(id) ON DELETE CASCADE"
|
||||
|
||||
"public.promotion_connection_project" {
|
||||
varchar_36_ connectionId FK
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ projectId FK
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_connection" {
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
json target
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.project" {
|
||||
timestamp_3__with_time_zone createdAt
|
||||
uuid creatorId FK
|
||||
json customTelemetryTags
|
||||
varchar_512_ description
|
||||
json icon
|
||||
varchar_36_ id
|
||||
varchar_255_ name
|
||||
varchar_36_ type
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> Generated by [tbls](https://github.com/k1LoW/tbls)
|
||||
68
docs/generated/postgres-schema/public.promotion_provider.md
generated
Normal file
68
docs/generated/postgres-schema/public.promotion_provider.md
generated
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# public.promotion_provider
|
||||
|
||||
## Columns
|
||||
|
||||
| Name | Type | Default | Nullable | Children | Parents | Comment |
|
||||
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
|
||||
| auth | text | | false | | | Encrypted credentials. Never sent to a client. |
|
||||
| authType | varchar(32) | | false | | | PromotionProviderAuthType enum: "ssh-key", "token". "token" is an HTTP(S) username and password. |
|
||||
| config | json | | false | | | Non-secret provider settings, with their own schemaVersion. Validated in code. |
|
||||
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
|
||||
| id | varchar(36) | | false | [public.promotion_connection](public.promotion_connection.md) | | |
|
||||
| name | varchar(128) | | false | | | |
|
||||
| type | varchar(32) | | false | | | PromotionProviderType enum: "git" |
|
||||
| updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
|
||||
|
||||
## Constraints
|
||||
|
||||
| Name | Type | Definition |
|
||||
| ---- | ---- | ---------- |
|
||||
| CHK_promotion_provider_authType | CHECK | CHECK ((("authType")::text = ANY ((ARRAY['ssh-key'::character varying, 'token'::character varying])::text[]))) |
|
||||
| CHK_promotion_provider_type | CHECK | CHECK (((type)::text = 'git'::text)) |
|
||||
| PK_6e968555ff44699f59ad10be765 | PRIMARY KEY | PRIMARY KEY (id) |
|
||||
| promotion_provider_authType_not_null | n | NOT NULL "authType" |
|
||||
| promotion_provider_auth_not_null | n | NOT NULL auth |
|
||||
| promotion_provider_config_not_null | n | NOT NULL config |
|
||||
| promotion_provider_createdAt_not_null | n | NOT NULL "createdAt" |
|
||||
| promotion_provider_id_not_null | n | NOT NULL id |
|
||||
| promotion_provider_name_not_null | n | NOT NULL name |
|
||||
| promotion_provider_type_not_null | n | NOT NULL type |
|
||||
| promotion_provider_updatedAt_not_null | n | NOT NULL "updatedAt" |
|
||||
|
||||
## Indexes
|
||||
|
||||
| Name | Definition |
|
||||
| ---- | ---------- |
|
||||
| PK_6e968555ff44699f59ad10be765 | CREATE UNIQUE INDEX "PK_6e968555ff44699f59ad10be765" ON public.promotion_provider USING btree (id) |
|
||||
|
||||
## Relations
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
|
||||
"public.promotion_connection" }o--|| "public.promotion_provider" : "FOREIGN KEY (#quot;providerId#quot;) REFERENCES promotion_provider(id) ON DELETE RESTRICT"
|
||||
|
||||
"public.promotion_provider" {
|
||||
text auth
|
||||
varchar_32_ authType
|
||||
json config
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
varchar_32_ type
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
"public.promotion_connection" {
|
||||
timestamp_3__with_time_zone createdAt
|
||||
varchar_36_ id
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
json target
|
||||
timestamp_3__with_time_zone updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> Generated by [tbls](https://github.com/k1LoW/tbls)
|
||||
42
docs/generated/sqlite-schema/README.md
generated
42
docs/generated/sqlite-schema/README.md
generated
|
|
@ -104,6 +104,10 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand.
|
|||
| [project_pool_settings](project_pool_settings.md) | 4 | | table |
|
||||
| [project_relation](project_relation.md) | 5 | | table |
|
||||
| [project_secrets_provider_access](project_secrets_provider_access.md) | 5 | | table |
|
||||
| [promotion_config](promotion_config.md) | 7 | | table |
|
||||
| [promotion_connection](promotion_connection.md) | 7 | | table |
|
||||
| [promotion_connection_project](promotion_connection_project.md) | 4 | | table |
|
||||
| [promotion_provider](promotion_provider.md) | 8 | | table |
|
||||
| [role](role.md) | 7 | | table |
|
||||
| [role_mapping_rule](role_mapping_rule.md) | 7 | | table |
|
||||
| [role_mapping_rule_project](role_mapping_rule_project.md) | 2 | | table |
|
||||
|
|
@ -290,6 +294,10 @@ erDiagram
|
|||
"project_relation" |o--|| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"project_secrets_provider_access" |o--|| "secrets_provider_connection" : "FOREIGN KEY (secretsProviderConnectionId) REFERENCES secrets_provider_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"project_secrets_provider_access" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"promotion_config" }o--|| "promotion_connection" : "FOREIGN KEY (connectionId) REFERENCES promotion_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"promotion_connection" }o--|| "promotion_provider" : "FOREIGN KEY (providerId) REFERENCES promotion_provider (id) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
|
||||
"promotion_connection_project" }o--|| "promotion_connection" : "FOREIGN KEY (connectionId) REFERENCES promotion_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"promotion_connection_project" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"role_mapping_rule" }o--|| "role" : "FOREIGN KEY (role) REFERENCES role (slug) ON UPDATE CASCADE ON DELETE CASCADE MATCH NONE"
|
||||
"role_mapping_rule_project" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"role_mapping_rule_project" |o--|| "role_mapping_rule" : "FOREIGN KEY (roleMappingRuleId) REFERENCES role_mapping_rule (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
|
|
@ -1311,6 +1319,40 @@ erDiagram
|
|||
INTEGER secretsProviderConnectionId PK
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_config" {
|
||||
varchar_36_ connectionId FK
|
||||
datetime_3_ createdAt
|
||||
varchar_16_ direction
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
TEXT settings
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_connection" {
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
TEXT target
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_connection_project" {
|
||||
varchar_36_ connectionId FK
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ projectId PK
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_provider" {
|
||||
TEXT auth
|
||||
varchar_32_ authType
|
||||
TEXT config
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
varchar_32_ type
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"role" {
|
||||
datetime_3_ createdAt
|
||||
TEXT description
|
||||
|
|
|
|||
9
docs/generated/sqlite-schema/project.md
generated
9
docs/generated/sqlite-schema/project.md
generated
|
|
@ -20,7 +20,7 @@ CREATE TABLE "project" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" varchar(25
|
|||
| customTelemetryTags | TEXT | '[]' | false | | | |
|
||||
| description | varchar(512) | | true | | | |
|
||||
| icon | TEXT | | true | | | |
|
||||
| id | varchar(36) | | false | [activity_event](activity_event.md) [agent_chat_attachments](agent_chat_attachments.md) [agent_execution_threads](agent_execution_threads.md) [agents](agents.md) [data_table](data_table.md) [folder](folder.md) [git_connection_project](git_connection_project.md) [insights_metadata](insights_metadata.md) [instance_ai_threads](instance_ai_threads.md) [project_pool_settings](project_pool_settings.md) [project_relation](project_relation.md) [project_secrets_provider_access](project_secrets_provider_access.md) [role_mapping_rule_project](role_mapping_rule_project.md) [shared_credentials](shared_credentials.md) [shared_workflow](shared_workflow.md) [type_availability_policy_scope](type_availability_policy_scope.md) [variables](variables.md) [workflow_review_request](workflow_review_request.md) | | |
|
||||
| id | varchar(36) | | false | [activity_event](activity_event.md) [agent_chat_attachments](agent_chat_attachments.md) [agent_execution_threads](agent_execution_threads.md) [agents](agents.md) [data_table](data_table.md) [folder](folder.md) [git_connection_project](git_connection_project.md) [insights_metadata](insights_metadata.md) [instance_ai_threads](instance_ai_threads.md) [project_pool_settings](project_pool_settings.md) [project_relation](project_relation.md) [project_secrets_provider_access](project_secrets_provider_access.md) [promotion_connection_project](promotion_connection_project.md) [role_mapping_rule_project](role_mapping_rule_project.md) [shared_credentials](shared_credentials.md) [shared_workflow](shared_workflow.md) [type_availability_policy_scope](type_availability_policy_scope.md) [variables](variables.md) [workflow_review_request](workflow_review_request.md) | | |
|
||||
| name | varchar(255) | | false | | | |
|
||||
| type | varchar(36) | | false | | | |
|
||||
| updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
|
||||
|
|
@ -57,6 +57,7 @@ erDiagram
|
|||
"project_pool_settings" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"project_relation" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"project_secrets_provider_access" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"promotion_connection_project" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"role_mapping_rule_project" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"shared_credentials" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"shared_workflow" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
|
|
@ -211,6 +212,12 @@ erDiagram
|
|||
INTEGER secretsProviderConnectionId PK
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_connection_project" {
|
||||
varchar_36_ connectionId FK
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ projectId PK
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"role_mapping_rule_project" {
|
||||
varchar_36_ projectId PK
|
||||
varchar_16_ roleMappingRuleId PK
|
||||
|
|
|
|||
71
docs/generated/sqlite-schema/promotion_config.md
generated
Normal file
71
docs/generated/sqlite-schema/promotion_config.md
generated
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# promotion_config
|
||||
|
||||
## Description
|
||||
|
||||
<details>
|
||||
<summary><strong>Table Definition</strong></summary>
|
||||
|
||||
```sql
|
||||
CREATE TABLE "promotion_config" ("id" varchar(36) PRIMARY KEY NOT NULL, "connectionId" varchar(36) NOT NULL, "name" varchar(128) NOT NULL, "direction" varchar(16) NOT NULL, "settings" text NOT NULL, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), CONSTRAINT "CHK_promotion_config_direction" CHECK ("direction" IN ('apply', 'promote')), CONSTRAINT "FK_promotion_config_connectionId" FOREIGN KEY ("connectionId") REFERENCES "promotion_connection" ("id") ON DELETE CASCADE)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Columns
|
||||
|
||||
| Name | Type | Default | Nullable | Children | Parents | Comment |
|
||||
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
|
||||
| connectionId | varchar(36) | | false | | [promotion_connection](promotion_connection.md) | |
|
||||
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
|
||||
| direction | varchar(16) | | false | | | |
|
||||
| id | varchar(36) | | false | | | |
|
||||
| name | varchar(128) | | false | | | |
|
||||
| settings | TEXT | | false | | | |
|
||||
| updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
|
||||
|
||||
## Constraints
|
||||
|
||||
| Name | Type | Definition |
|
||||
| ---- | ---- | ---------- |
|
||||
| - | CHECK | CHECK ("direction" IN ('apply', 'promote')) |
|
||||
| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (connectionId) REFERENCES promotion_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
|
||||
| id | PRIMARY KEY | PRIMARY KEY (id) |
|
||||
| sqlite_autoindex_promotion_config_1 | PRIMARY KEY | PRIMARY KEY (id) |
|
||||
|
||||
## Indexes
|
||||
|
||||
| Name | Definition |
|
||||
| ---- | ---------- |
|
||||
| IDX_promotion_config_connectionId_direction | CREATE UNIQUE INDEX "IDX_promotion_config_connectionId_direction" ON "promotion_config" ("connectionId", "direction") |
|
||||
| sqlite_autoindex_promotion_config_1 | PRIMARY KEY (id) |
|
||||
|
||||
## Relations
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
|
||||
"promotion_config" }o--|| "promotion_connection" : "FOREIGN KEY (connectionId) REFERENCES promotion_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
|
||||
"promotion_config" {
|
||||
varchar_36_ connectionId FK
|
||||
datetime_3_ createdAt
|
||||
varchar_16_ direction
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
TEXT settings
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_connection" {
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
TEXT target
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> Generated by [tbls](https://github.com/k1LoW/tbls)
|
||||
90
docs/generated/sqlite-schema/promotion_connection.md
generated
Normal file
90
docs/generated/sqlite-schema/promotion_connection.md
generated
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# promotion_connection
|
||||
|
||||
## Description
|
||||
|
||||
<details>
|
||||
<summary><strong>Table Definition</strong></summary>
|
||||
|
||||
```sql
|
||||
CREATE TABLE "promotion_connection" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" varchar(128) NOT NULL, "providerId" varchar(36) NOT NULL, "scope" varchar(16) NOT NULL, "target" text NOT NULL, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), CONSTRAINT "CHK_promotion_connection_scope" CHECK ("scope" IN ('instance', 'projects')), CONSTRAINT "FK_promotion_connection_providerId" FOREIGN KEY ("providerId") REFERENCES "promotion_provider" ("id") ON DELETE RESTRICT)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Columns
|
||||
|
||||
| Name | Type | Default | Nullable | Children | Parents | Comment |
|
||||
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
|
||||
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
|
||||
| id | varchar(36) | | false | [promotion_config](promotion_config.md) [promotion_connection_project](promotion_connection_project.md) | | |
|
||||
| name | varchar(128) | | false | | | |
|
||||
| providerId | varchar(36) | | false | | [promotion_provider](promotion_provider.md) | |
|
||||
| scope | varchar(16) | | false | | | |
|
||||
| target | TEXT | | false | | | |
|
||||
| updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
|
||||
|
||||
## Constraints
|
||||
|
||||
| Name | Type | Definition |
|
||||
| ---- | ---- | ---------- |
|
||||
| - | CHECK | CHECK ("scope" IN ('instance', 'projects')) |
|
||||
| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (providerId) REFERENCES promotion_provider (id) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE |
|
||||
| id | PRIMARY KEY | PRIMARY KEY (id) |
|
||||
| sqlite_autoindex_promotion_connection_1 | PRIMARY KEY | PRIMARY KEY (id) |
|
||||
|
||||
## Indexes
|
||||
|
||||
| Name | Definition |
|
||||
| ---- | ---------- |
|
||||
| IDX_9917feaa6ed5705046432b117f | CREATE INDEX "IDX_9917feaa6ed5705046432b117f" ON "promotion_connection" ("providerId") |
|
||||
| IDX_promotion_connection_scope | CREATE UNIQUE INDEX "IDX_promotion_connection_scope" ON "promotion_connection" ("scope") WHERE "scope" = 'instance' |
|
||||
| sqlite_autoindex_promotion_connection_1 | PRIMARY KEY (id) |
|
||||
|
||||
## Relations
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
|
||||
"promotion_config" }o--|| "promotion_connection" : "FOREIGN KEY (connectionId) REFERENCES promotion_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"promotion_connection_project" }o--|| "promotion_connection" : "FOREIGN KEY (connectionId) REFERENCES promotion_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"promotion_connection" }o--|| "promotion_provider" : "FOREIGN KEY (providerId) REFERENCES promotion_provider (id) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
|
||||
|
||||
"promotion_connection" {
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
TEXT target
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_config" {
|
||||
varchar_36_ connectionId FK
|
||||
datetime_3_ createdAt
|
||||
varchar_16_ direction
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
TEXT settings
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_connection_project" {
|
||||
varchar_36_ connectionId FK
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ projectId PK
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_provider" {
|
||||
TEXT auth
|
||||
varchar_32_ authType
|
||||
TEXT config
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
varchar_32_ type
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> Generated by [tbls](https://github.com/k1LoW/tbls)
|
||||
77
docs/generated/sqlite-schema/promotion_connection_project.md
generated
Normal file
77
docs/generated/sqlite-schema/promotion_connection_project.md
generated
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# promotion_connection_project
|
||||
|
||||
## Description
|
||||
|
||||
<details>
|
||||
<summary><strong>Table Definition</strong></summary>
|
||||
|
||||
```sql
|
||||
CREATE TABLE "promotion_connection_project" ("projectId" varchar(36) PRIMARY KEY NOT NULL, "connectionId" varchar(36) NOT NULL, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), CONSTRAINT "FK_promotion_connection_project_projectId" FOREIGN KEY ("projectId") REFERENCES "project" ("id") ON DELETE CASCADE, CONSTRAINT "FK_promotion_connection_project_connectionId" FOREIGN KEY ("connectionId") REFERENCES "promotion_connection" ("id") ON DELETE CASCADE)
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Columns
|
||||
|
||||
| Name | Type | Default | Nullable | Children | Parents | Comment |
|
||||
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
|
||||
| connectionId | varchar(36) | | false | | [promotion_connection](promotion_connection.md) | |
|
||||
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
|
||||
| projectId | varchar(36) | | false | | [project](project.md) | |
|
||||
| updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
|
||||
|
||||
## Constraints
|
||||
|
||||
| Name | Type | Definition |
|
||||
| ---- | ---- | ---------- |
|
||||
| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (connectionId) REFERENCES promotion_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
|
||||
| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
|
||||
| projectId | PRIMARY KEY | PRIMARY KEY (projectId) |
|
||||
| sqlite_autoindex_promotion_connection_project_1 | PRIMARY KEY | PRIMARY KEY (projectId) |
|
||||
|
||||
## Indexes
|
||||
|
||||
| Name | Definition |
|
||||
| ---- | ---------- |
|
||||
| IDX_2c579b1068b66359de1b4d2de5 | CREATE INDEX "IDX_2c579b1068b66359de1b4d2de5" ON "promotion_connection_project" ("connectionId") |
|
||||
| sqlite_autoindex_promotion_connection_project_1 | PRIMARY KEY (projectId) |
|
||||
|
||||
## Relations
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
|
||||
"promotion_connection_project" }o--|| "promotion_connection" : "FOREIGN KEY (connectionId) REFERENCES promotion_connection (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
"promotion_connection_project" |o--|| "project" : "FOREIGN KEY (projectId) REFERENCES project (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
|
||||
|
||||
"promotion_connection_project" {
|
||||
varchar_36_ connectionId FK
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ projectId PK
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_connection" {
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
TEXT target
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"project" {
|
||||
datetime_3_ createdAt
|
||||
varchar creatorId FK
|
||||
TEXT customTelemetryTags
|
||||
varchar_512_ description
|
||||
TEXT icon
|
||||
varchar_36_ id PK
|
||||
varchar_255_ name
|
||||
varchar_36_ type
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> Generated by [tbls](https://github.com/k1LoW/tbls)
|
||||
72
docs/generated/sqlite-schema/promotion_provider.md
generated
Normal file
72
docs/generated/sqlite-schema/promotion_provider.md
generated
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# promotion_provider
|
||||
|
||||
## Description
|
||||
|
||||
<details>
|
||||
<summary><strong>Table Definition</strong></summary>
|
||||
|
||||
```sql
|
||||
CREATE TABLE "promotion_provider" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" varchar(128) NOT NULL, "type" varchar(32) NOT NULL, "authType" varchar(32) NOT NULL, "config" text NOT NULL, "auth" text NOT NULL, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), CONSTRAINT "CHK_promotion_provider_type" CHECK ("type" IN ('git')), CONSTRAINT "CHK_promotion_provider_authType" CHECK ("authType" IN ('ssh-key', 'token')))
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Columns
|
||||
|
||||
| Name | Type | Default | Nullable | Children | Parents | Comment |
|
||||
| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
|
||||
| auth | TEXT | | false | | | |
|
||||
| authType | varchar(32) | | false | | | |
|
||||
| config | TEXT | | false | | | |
|
||||
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
|
||||
| id | varchar(36) | | false | [promotion_connection](promotion_connection.md) | | |
|
||||
| name | varchar(128) | | false | | | |
|
||||
| type | varchar(32) | | false | | | |
|
||||
| updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
|
||||
|
||||
## Constraints
|
||||
|
||||
| Name | Type | Definition |
|
||||
| ---- | ---- | ---------- |
|
||||
| - | CHECK | CHECK ("type" IN ('git')) |
|
||||
| - | CHECK | CHECK ("authType" IN ('ssh-key', 'token')) |
|
||||
| id | PRIMARY KEY | PRIMARY KEY (id) |
|
||||
| sqlite_autoindex_promotion_provider_1 | PRIMARY KEY | PRIMARY KEY (id) |
|
||||
|
||||
## Indexes
|
||||
|
||||
| Name | Definition |
|
||||
| ---- | ---------- |
|
||||
| sqlite_autoindex_promotion_provider_1 | PRIMARY KEY (id) |
|
||||
|
||||
## Relations
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
|
||||
"promotion_connection" }o--|| "promotion_provider" : "FOREIGN KEY (providerId) REFERENCES promotion_provider (id) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
|
||||
|
||||
"promotion_provider" {
|
||||
TEXT auth
|
||||
varchar_32_ authType
|
||||
TEXT config
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
varchar_32_ type
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
"promotion_connection" {
|
||||
datetime_3_ createdAt
|
||||
varchar_36_ id PK
|
||||
varchar_128_ name
|
||||
varchar_36_ providerId FK
|
||||
varchar_16_ scope
|
||||
TEXT target
|
||||
datetime_3_ updatedAt
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> Generated by [tbls](https://github.com/k1LoW/tbls)
|
||||
|
|
@ -461,3 +461,75 @@ export {
|
|||
type PromotableResourceType,
|
||||
type PromoteRequest,
|
||||
} from './promotions/promotable-resource.dto';
|
||||
|
||||
export { promotionDisplayNameSchema } from './promotions/promotion-common.dto';
|
||||
export {
|
||||
CreatePromotionProviderDto,
|
||||
ListPromotionProvidersQueryDto,
|
||||
PromotionProviderCreatedPublicDto,
|
||||
PromotionProviderListPublicDto,
|
||||
PromotionProviderPublicDto,
|
||||
UpdatePromotionProviderDto,
|
||||
promotionGitConfigSchemas,
|
||||
promotionGitSshKeyAuthInputSchema,
|
||||
promotionGitSshKeyAuthUpdateSchema,
|
||||
promotionGitSshKeyConfigSchema,
|
||||
promotionGitTokenAuthInputSchema,
|
||||
promotionGitTokenConfigSchema,
|
||||
promotionProviderAuthInputSchema,
|
||||
promotionProviderAuthTypeSchema,
|
||||
promotionProviderAuthUpdateSchema,
|
||||
promotionProviderConfigSchema,
|
||||
promotionProviderPublicSchema,
|
||||
promotionProviderSummarySchema,
|
||||
promotionProviderTypeSchema,
|
||||
promotionSshKeyTypeSchema,
|
||||
type PromotionProviderAuthInput,
|
||||
type PromotionProviderAuthType,
|
||||
type PromotionProviderAuthUpdate,
|
||||
type PromotionProviderConfig,
|
||||
type PromotionProviderType,
|
||||
type PromotionSshKeyType,
|
||||
} from './promotions/promotion-provider.dto';
|
||||
export {
|
||||
PromotionApplyConfigPublicDto,
|
||||
PromotionPromoteConfigPublicDto,
|
||||
UpsertPromotionApplyConfigDto,
|
||||
UpsertPromotionPromoteConfigDto,
|
||||
promotionApplyConfigPublicSchema,
|
||||
promotionConfigSettingsSchema,
|
||||
promotionConnectionConfigsPublicSchema,
|
||||
promotionDirectionSchema,
|
||||
promotionGitApplySettingsSchema,
|
||||
promotionGitPromoteSettingsSchema,
|
||||
promotionGitSettingsSchemas,
|
||||
promotionPromoteConfigPublicSchema,
|
||||
type PromotionConfigSettings,
|
||||
type PromotionConnectionConfigsPublic,
|
||||
type PromotionDirection,
|
||||
} from './promotions/promotion-config.dto';
|
||||
export {
|
||||
CreatePromotionConnectionDto,
|
||||
ListPromotionConnectionsQueryDto,
|
||||
PromotionConnectionListPublicDto,
|
||||
PromotionConnectionProjectListPublicDto,
|
||||
PromotionConnectionProjectPublicDto,
|
||||
PromotionConnectionPublicDto,
|
||||
UpdatePromotionConnectionDto,
|
||||
promotionConnectionProjectPublicSchema,
|
||||
promotionConnectionPublicSchema,
|
||||
promotionConnectionScopeSchema,
|
||||
promotionConnectionTargetSchema,
|
||||
type PromotionConnectionScope,
|
||||
type PromotionConnectionTarget,
|
||||
} from './promotions/promotion-connection.dto';
|
||||
export {
|
||||
ApplyPackageResultDto,
|
||||
PromotePackageDto,
|
||||
PromotePackageResultDto,
|
||||
applyPackageCountsSchema,
|
||||
applyPackageResultSchema,
|
||||
promotePackageCountsSchema,
|
||||
promotePackageResultSchema,
|
||||
promotionGitResultSchema,
|
||||
} from './promotions/promotion-operations.dto';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
import {
|
||||
PromotionPromoteConfigPublicDto,
|
||||
UpsertPromotionApplyConfigDto,
|
||||
UpsertPromotionPromoteConfigDto,
|
||||
} from '../promotion-config.dto';
|
||||
|
||||
const applySettings = { schemaVersion: 1 as const, branchName: 'dev' };
|
||||
const promoteSettings = {
|
||||
schemaVersion: 1 as const,
|
||||
baseBranchName: 'staging',
|
||||
createBranchOnPromotion: false,
|
||||
};
|
||||
|
||||
describe('UpsertPromotionApplyConfigDto', () => {
|
||||
it('accepts settings with an optional name', () => {
|
||||
expect(UpsertPromotionApplyConfigDto.safeParse({ settings: applySettings }).success).toBe(true);
|
||||
expect(
|
||||
UpsertPromotionApplyConfigDto.safeParse({ name: 'Apply', settings: applySettings }).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// The direction is in the path, so a body that names one is malformed.
|
||||
it('rejects a direction and a connection in the body', () => {
|
||||
expect(
|
||||
UpsertPromotionApplyConfigDto.safeParse({ direction: 'apply', settings: applySettings })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
UpsertPromotionApplyConfigDto.safeParse({ connectionId: 'conn-1', settings: applySettings })
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects promote settings and the branching flag', () => {
|
||||
expect(UpsertPromotionApplyConfigDto.safeParse({ settings: promoteSettings }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
UpsertPromotionApplyConfigDto.safeParse({
|
||||
settings: { ...applySettings, createBranchOnPromotion: true },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('requires a branch name of at most 255 characters', () => {
|
||||
const settings = (branchName: string) => ({ schemaVersion: 1, branchName });
|
||||
|
||||
expect(
|
||||
UpsertPromotionApplyConfigDto.safeParse({ settings: { schemaVersion: 1 } }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
UpsertPromotionApplyConfigDto.safeParse({ settings: settings('a'.repeat(255)) }).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
UpsertPromotionApplyConfigDto.safeParse({ settings: settings('a'.repeat(256)) }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects settings of an unsupported schema version', () => {
|
||||
expect(
|
||||
UpsertPromotionApplyConfigDto.safeParse({
|
||||
settings: { schemaVersion: 2, branchName: 'dev' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpsertPromotionPromoteConfigDto', () => {
|
||||
// A write replaces the whole config, so the flag has no default to fall back on.
|
||||
it('requires createBranchOnPromotion, so a write cannot reset it by omission', () => {
|
||||
expect(
|
||||
UpsertPromotionPromoteConfigDto.safeParse({
|
||||
settings: { schemaVersion: 1, baseBranchName: 'staging' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(UpsertPromotionPromoteConfigDto.safeParse({ settings: promoteSettings }).success).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects the apply branch field', () => {
|
||||
expect(UpsertPromotionPromoteConfigDto.safeParse({ settings: applySettings }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PromotionPromoteConfigPublicDto', () => {
|
||||
const base = {
|
||||
id: 'cfg-1',
|
||||
name: 'Promote',
|
||||
createdAt: '2026-09-07T08:00:00.000Z',
|
||||
updatedAt: '2026-09-07T08:00:00.000Z',
|
||||
};
|
||||
|
||||
it('always reports createBranchOnPromotion', () => {
|
||||
expect(
|
||||
PromotionPromoteConfigPublicDto.safeParse({
|
||||
...base,
|
||||
settings: { schemaVersion: 1, baseBranchName: 'staging' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
PromotionPromoteConfigPublicDto.safeParse({ ...base, settings: promoteSettings }).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import {
|
||||
CreatePromotionConnectionDto,
|
||||
PromotionConnectionPublicDto,
|
||||
UpdatePromotionConnectionDto,
|
||||
} from '../promotion-connection.dto';
|
||||
|
||||
const target = { schemaVersion: 1 as const, remoteUrl: 'git@example.com:team/workflows.git' };
|
||||
const applyConfig = { settings: { schemaVersion: 1 as const, branchName: 'dev' } };
|
||||
const promoteConfig = {
|
||||
settings: {
|
||||
schemaVersion: 1 as const,
|
||||
baseBranchName: 'staging',
|
||||
createBranchOnPromotion: false,
|
||||
},
|
||||
};
|
||||
|
||||
const newConnection = {
|
||||
name: 'Staging',
|
||||
scope: 'instance' as const,
|
||||
providerId: 'prov-1',
|
||||
target,
|
||||
};
|
||||
|
||||
const timestamp = '2026-09-07T08:00:00.000Z';
|
||||
const publicProvider = {
|
||||
id: 'prov-1',
|
||||
name: 'Staging',
|
||||
type: 'git' as const,
|
||||
authType: 'ssh-key' as const,
|
||||
config: {
|
||||
schemaVersion: 1 as const,
|
||||
publicKey: 'ssh-ed25519 AAAAKEY',
|
||||
keyType: 'ed25519' as const,
|
||||
},
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
|
||||
describe('CreatePromotionConnectionDto', () => {
|
||||
it('accepts a connection with no configs, so a public key can be read before cloning', () => {
|
||||
expect(CreatePromotionConnectionDto.safeParse(newConnection).success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a config for each direction, keyed by direction', () => {
|
||||
expect(
|
||||
CreatePromotionConnectionDto.safeParse({
|
||||
...newConnection,
|
||||
configs: { apply: applyConfig, promote: promoteConfig },
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
CreatePromotionConnectionDto.safeParse({ ...newConnection, configs: { apply: applyConfig } })
|
||||
.success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Keying by direction is what makes "one per direction" structural.
|
||||
it('rejects a config array and an unknown direction key', () => {
|
||||
expect(
|
||||
CreatePromotionConnectionDto.safeParse({
|
||||
...newConnection,
|
||||
configs: [applyConfig, promoteConfig],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
CreatePromotionConnectionDto.safeParse({ ...newConnection, configs: { sync: applyConfig } })
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects settings under the wrong direction key', () => {
|
||||
expect(
|
||||
CreatePromotionConnectionDto.safeParse({
|
||||
...newConnection,
|
||||
configs: { apply: promoteConfig },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('requires a provider id and rejects a target of an unsupported version', () => {
|
||||
expect(
|
||||
CreatePromotionConnectionDto.safeParse({ ...newConnection, providerId: '' }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
CreatePromotionConnectionDto.safeParse({
|
||||
...newConnection,
|
||||
target: { schemaVersion: 2, remoteUrl: 'git@example.com:team/workflows.git' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdatePromotionConnectionDto', () => {
|
||||
it('accepts the name, the target, and the provider', () => {
|
||||
expect(
|
||||
UpdatePromotionConnectionDto.safeParse({ name: 'Renamed', target, providerId: 'prov-2' })
|
||||
.success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects configs, a scope change, and an operation payload', () => {
|
||||
expect(
|
||||
UpdatePromotionConnectionDto.safeParse({ name: 'Renamed', configs: { apply: applyConfig } })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(UpdatePromotionConnectionDto.safeParse({ scope: 'projects' }).success).toBe(false);
|
||||
expect(UpdatePromotionConnectionDto.safeParse({ commitMessage: 'Update' }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PromotionConnectionPublicDto', () => {
|
||||
// Detail and list share this shape, so a full provider would put the key in every row.
|
||||
it('embeds the provider without its public key', () => {
|
||||
const result = PromotionConnectionPublicDto.safeParse({
|
||||
id: 'conn-1',
|
||||
name: 'Staging',
|
||||
scope: 'instance',
|
||||
target,
|
||||
provider: publicProvider,
|
||||
configs: {
|
||||
apply: {
|
||||
...applyConfig,
|
||||
id: 'cfg-1',
|
||||
name: 'Apply',
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
},
|
||||
},
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
|
||||
if (!result.success) throw result.error;
|
||||
expect(result.data.provider).not.toHaveProperty('config');
|
||||
expect(JSON.stringify(result.data)).not.toContain('AAAAKEY');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { PromotePackageDto } from '../promotion-operations.dto';
|
||||
|
||||
describe('PromotePackageDto', () => {
|
||||
it('requires a non-blank commit message and accepts an optional force flag', () => {
|
||||
expect(PromotePackageDto.safeParse({ commitMessage: 'Update projects' }).success).toBe(true);
|
||||
expect(PromotePackageDto.safeParse({ commitMessage: 'Update', force: true }).success).toBe(
|
||||
true,
|
||||
);
|
||||
expect(PromotePackageDto.safeParse({}).success).toBe(false);
|
||||
expect(PromotePackageDto.safeParse({ commitMessage: ' ' }).success).toBe(false);
|
||||
});
|
||||
|
||||
// The branch comes from the stored config, never from the request.
|
||||
it('rejects unknown fields, so an operation cannot be steered from the request', () => {
|
||||
expect(
|
||||
PromotePackageDto.safeParse({ commitMessage: 'Update', branchName: 'main' }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import {
|
||||
CreatePromotionProviderDto,
|
||||
PromotionProviderListPublicDto,
|
||||
PromotionProviderPublicDto,
|
||||
UpdatePromotionProviderDto,
|
||||
} from '../promotion-provider.dto';
|
||||
|
||||
const provider = { name: 'Staging', type: 'git' as const };
|
||||
const sshAuth = { authType: 'ssh-key' as const };
|
||||
const tokenAuth = { authType: 'token' as const, username: 'git-user', password: 'secret' };
|
||||
|
||||
const timestamp = '2026-09-07T08:00:00.000Z';
|
||||
const publicProvider = {
|
||||
...provider,
|
||||
id: 'prov-1',
|
||||
authType: 'ssh-key' as const,
|
||||
config: {
|
||||
schemaVersion: 1 as const,
|
||||
publicKey: 'ssh-ed25519 AAAAKEY',
|
||||
keyType: 'ed25519' as const,
|
||||
},
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
|
||||
describe('CreatePromotionProviderDto', () => {
|
||||
it('defaults an SSH key type to ed25519', () => {
|
||||
const result = CreatePromotionProviderDto.safeParse({ ...provider, auth: sshAuth });
|
||||
|
||||
if (!result.success) throw result.error;
|
||||
expect(result.data.auth).toEqual({ authType: 'ssh-key', keyType: 'ed25519' });
|
||||
});
|
||||
|
||||
it('trims the username but stores the password as sent', () => {
|
||||
const result = CreatePromotionProviderDto.safeParse({
|
||||
...provider,
|
||||
auth: { ...tokenAuth, username: ' git-user ', password: ' secret ' },
|
||||
});
|
||||
|
||||
if (!result.success) throw result.error;
|
||||
expect(result.data.auth).toEqual({
|
||||
authType: 'token',
|
||||
username: 'git-user',
|
||||
password: ' secret ',
|
||||
});
|
||||
});
|
||||
|
||||
// The auth type lives inside `auth`, so credentials cannot be paired with
|
||||
// another auth type's fields.
|
||||
it('rejects an auth payload that mixes the two auth types', () => {
|
||||
expect(
|
||||
CreatePromotionProviderDto.safeParse({
|
||||
...provider,
|
||||
auth: { ...tokenAuth, keyType: 'rsa' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
CreatePromotionProviderDto.safeParse({
|
||||
...provider,
|
||||
auth: { authType: 'ssh-key', username: 'git-user', password: 'secret' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an auth payload with no auth type or no credentials', () => {
|
||||
expect(CreatePromotionProviderDto.safeParse({ ...provider, auth: {} }).success).toBe(false);
|
||||
expect(
|
||||
CreatePromotionProviderDto.safeParse({ ...provider, auth: { authType: 'token' } }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
CreatePromotionProviderDto.safeParse({
|
||||
...provider,
|
||||
auth: { authType: 'token', username: 'git-user' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a blank name, an over-long name, and a blank password', () => {
|
||||
expect(
|
||||
CreatePromotionProviderDto.safeParse({ ...provider, name: ' ', auth: tokenAuth }).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
CreatePromotionProviderDto.safeParse({
|
||||
...provider,
|
||||
name: 'a'.repeat(129),
|
||||
auth: tokenAuth,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
CreatePromotionProviderDto.safeParse({
|
||||
...provider,
|
||||
auth: { ...tokenAuth, password: '' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a top-level auth type and a config', () => {
|
||||
expect(
|
||||
CreatePromotionProviderDto.safeParse({ ...provider, authType: 'token', auth: tokenAuth })
|
||||
.success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
CreatePromotionProviderDto.safeParse({
|
||||
...provider,
|
||||
auth: sshAuth,
|
||||
config: { schemaVersion: 1, publicKey: 'ssh-ed25519 AAAAKEY', keyType: 'ed25519' },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UpdatePromotionProviderDto', () => {
|
||||
it('keeps the stored credentials when auth is omitted', () => {
|
||||
expect(UpdatePromotionProviderDto.safeParse({ name: 'Renamed' }).success).toBe(true);
|
||||
});
|
||||
|
||||
it('leaves the key type unset, so a rotation can keep the current algorithm', () => {
|
||||
const result = UpdatePromotionProviderDto.safeParse({ auth: sshAuth });
|
||||
|
||||
if (!result.success) throw result.error;
|
||||
expect(result.data.auth).toEqual({ authType: 'ssh-key' });
|
||||
});
|
||||
|
||||
it('still requires both credentials for a token provider', () => {
|
||||
expect(UpdatePromotionProviderDto.safeParse({ auth: { authType: 'token' } }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(UpdatePromotionProviderDto.safeParse({ auth: tokenAuth }).success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects a change to the provider type or the config', () => {
|
||||
expect(UpdatePromotionProviderDto.safeParse({ type: 'git' }).success).toBe(false);
|
||||
expect(UpdatePromotionProviderDto.safeParse({ config: { schemaVersion: 1 } }).success).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('provider responses', () => {
|
||||
it('leaves out the encrypted credentials', () => {
|
||||
const result = PromotionProviderPublicDto.safeParse({ ...publicProvider, auth: 'encrypted' });
|
||||
|
||||
if (!result.success) throw result.error;
|
||||
expect(result.data).not.toHaveProperty('auth');
|
||||
});
|
||||
|
||||
it('drops the public key from list rows', () => {
|
||||
const result = PromotionProviderListPublicDto.safeParse({
|
||||
data: [publicProvider],
|
||||
nextCursor: null,
|
||||
});
|
||||
|
||||
if (!result.success) throw result.error;
|
||||
expect(result.data.data[0]).not.toHaveProperty('config');
|
||||
expect(JSON.stringify(result.data)).not.toContain('AAAAKEY');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Display name of a provider, connection, or config. All three are `varchar(128)`,
|
||||
* so cap the length here to reject over-long text with a 400 instead of a 500.
|
||||
* Names do not have to be unique.
|
||||
*/
|
||||
export const promotionDisplayNameSchema = z.string().trim().min(1).max(128);
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { promotionDisplayNameSchema } from './promotion-common.dto';
|
||||
import { Z } from '../../zod-class';
|
||||
|
||||
export const promotionDirectionSchema = z.enum(['apply', 'promote']);
|
||||
export type PromotionDirection = z.infer<typeof promotionDirectionSchema>;
|
||||
|
||||
/**
|
||||
* Every config carries a branch for its direction. A direction that is not set up
|
||||
* has no config at all. Git sets no limit on branch names, so cap it here to keep
|
||||
* the payload small. The backend still checks the name with Git.
|
||||
*/
|
||||
const branchNameSchema = z.string().trim().min(1).max(255);
|
||||
|
||||
/** Apply imports the package from `branchName`. */
|
||||
export const promotionGitApplySettingsSchema = z
|
||||
.object({ schemaVersion: z.literal(1), branchName: branchNameSchema })
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Promote starts from `baseBranchName`. With branching off it pushes there
|
||||
* directly. With branching on it creates the promotion branch from there. The
|
||||
* branch it really pushed to comes back in the result and is never saved here.
|
||||
*
|
||||
* `createBranchOnPromotion` is always required. A write replaces the whole config,
|
||||
* so a default here would let an unrelated edit reset it. The off-by-default
|
||||
* choice lives in the UI toggle and the CLI examples instead.
|
||||
*/
|
||||
export const promotionGitPromoteSettingsSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
baseBranchName: branchNameSchema,
|
||||
createBranchOnPromotion: z.boolean(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Settings schema for each direction. Use this map when the direction is known,
|
||||
* such as when checking a stored row. A new provider type adds its own map, with
|
||||
* its own fields and no branch name.
|
||||
*/
|
||||
export const promotionGitSettingsSchemas = {
|
||||
apply: promotionGitApplySettingsSchema,
|
||||
promote: promotionGitPromoteSettingsSchema,
|
||||
} as const;
|
||||
|
||||
export const promotionConfigSettingsSchema = z.union([
|
||||
promotionGitApplySettingsSchema,
|
||||
promotionGitPromoteSettingsSchema,
|
||||
]);
|
||||
export type PromotionConfigSettings = z.infer<typeof promotionConfigSettingsSchema>;
|
||||
|
||||
/*
|
||||
* Request bodies for `PUT /connections/:id/configs/:direction`, which creates the
|
||||
* config or replaces it. The direction is in the path, so the body needs no
|
||||
* discriminator and each direction gets a plain object of its own.
|
||||
*
|
||||
* A write replaces the whole config. An omitted `name` resets it to the direction
|
||||
* label, so a client changing only a branch has to resend the name it wants to
|
||||
* keep. The connection-create payload reuses these same two schemas.
|
||||
*/
|
||||
|
||||
export class UpsertPromotionApplyConfigDto extends Z.class(
|
||||
{
|
||||
name: promotionDisplayNameSchema.optional(),
|
||||
settings: promotionGitApplySettingsSchema,
|
||||
},
|
||||
{ strict: true },
|
||||
) {}
|
||||
|
||||
export class UpsertPromotionPromoteConfigDto extends Z.class(
|
||||
{
|
||||
name: promotionDisplayNameSchema.optional(),
|
||||
settings: promotionGitPromoteSettingsSchema,
|
||||
},
|
||||
{ strict: true },
|
||||
) {}
|
||||
|
||||
const configPublicFields = {
|
||||
/**
|
||||
* Also names the local checkout directory. Stable while the config exists, and
|
||||
* replaced by a new one if the direction is deleted and set up again.
|
||||
*/
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
};
|
||||
|
||||
/*
|
||||
* Responses leave out the connection and the direction: both are the address the
|
||||
* caller already used.
|
||||
*/
|
||||
|
||||
export const promotionApplyConfigPublicSchema = z.object({
|
||||
...configPublicFields,
|
||||
settings: promotionGitApplySettingsSchema,
|
||||
});
|
||||
|
||||
export class PromotionApplyConfigPublicDto extends Z.class(
|
||||
promotionApplyConfigPublicSchema.shape,
|
||||
) {}
|
||||
|
||||
export const promotionPromoteConfigPublicSchema = z.object({
|
||||
...configPublicFields,
|
||||
settings: promotionGitPromoteSettingsSchema,
|
||||
});
|
||||
|
||||
export class PromotionPromoteConfigPublicDto extends Z.class(
|
||||
promotionPromoteConfigPublicSchema.shape,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Configs keyed by direction, so a client reads `configs.promote?.settings`
|
||||
* without narrowing a union. A missing key means that direction is not set up.
|
||||
*/
|
||||
export const promotionConnectionConfigsPublicSchema = z.object({
|
||||
apply: promotionApplyConfigPublicSchema.optional(),
|
||||
promote: promotionPromoteConfigPublicSchema.optional(),
|
||||
});
|
||||
export type PromotionConnectionConfigsPublic = z.infer<
|
||||
typeof promotionConnectionConfigsPublicSchema
|
||||
>;
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { promotionDisplayNameSchema } from './promotion-common.dto';
|
||||
import {
|
||||
UpsertPromotionApplyConfigDto,
|
||||
UpsertPromotionPromoteConfigDto,
|
||||
promotionConnectionConfigsPublicSchema,
|
||||
} from './promotion-config.dto';
|
||||
import { promotionProviderSummarySchema } from './promotion-provider.dto';
|
||||
import { Z } from '../../zod-class';
|
||||
import { publicApiPaginationSchema } from '../pagination/pagination.dto';
|
||||
|
||||
/**
|
||||
* `instance` covers the whole instance. `projects` covers the projects linked to
|
||||
* the connection. Only one instance connection can exist.
|
||||
*/
|
||||
export const promotionConnectionScopeSchema = z.enum(['instance', 'projects']);
|
||||
export type PromotionConnectionScope = z.infer<typeof promotionConnectionScopeSchema>;
|
||||
|
||||
/**
|
||||
* Checks the shape only. The backend checks the URL itself: allowed protocols,
|
||||
* accepted SSH forms, and no credentials in the URL.
|
||||
*/
|
||||
const remoteUrlSchema = z.string().trim().min(1);
|
||||
|
||||
/**
|
||||
* Plain Git points at a repository with its full SSH or HTTP(S) remote URL. A new
|
||||
* provider type adds its own target here and needs no remote URL.
|
||||
*/
|
||||
export const promotionConnectionTargetSchema = z
|
||||
.object({ schemaVersion: z.literal(1), remoteUrl: remoteUrlSchema })
|
||||
.strict();
|
||||
export type PromotionConnectionTarget = z.infer<typeof promotionConnectionTargetSchema>;
|
||||
|
||||
export class CreatePromotionConnectionDto extends Z.class(
|
||||
{
|
||||
name: promotionDisplayNameSchema,
|
||||
scope: promotionConnectionScopeSchema,
|
||||
/** Providers are created on their own route. A connection only picks one. */
|
||||
providerId: z.string().min(1),
|
||||
target: promotionConnectionTargetSchema,
|
||||
/**
|
||||
* Initial configs, keyed by direction, reusing the bodies of the config
|
||||
* routes. Keying makes "one per direction" structural. A connection can start
|
||||
* with no configs, which lets an admin read the provider's public key before
|
||||
* the remote can be cloned.
|
||||
*/
|
||||
configs: z
|
||||
.object({
|
||||
apply: UpsertPromotionApplyConfigDto.schema.optional(),
|
||||
promote: UpsertPromotionPromoteConfigDto.schema.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
},
|
||||
{ strict: true },
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Connection fields only. `scope` cannot change, and configs, project links, and
|
||||
* package operations each have their own route, so a strict shape rejects them
|
||||
* here.
|
||||
*/
|
||||
export class UpdatePromotionConnectionDto extends Z.class(
|
||||
{
|
||||
name: promotionDisplayNameSchema.optional(),
|
||||
target: promotionConnectionTargetSchema.optional(),
|
||||
providerId: z.string().min(1).optional(),
|
||||
},
|
||||
{ strict: true },
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The `providerId` filter shows which connections a provider edit affects. The
|
||||
* instance settings page asks for `scope=instance` rather than taking whichever
|
||||
* connection comes first.
|
||||
*/
|
||||
export class ListPromotionConnectionsQueryDto extends Z.class({
|
||||
limit: publicApiPaginationSchema.limit,
|
||||
cursor: z.string().optional(),
|
||||
scope: promotionConnectionScopeSchema.optional(),
|
||||
providerId: z.string().optional(),
|
||||
}) {}
|
||||
|
||||
/**
|
||||
* There are at most two configs, so they are embedded instead of paginated, keyed
|
||||
* by direction to match the request side.
|
||||
*
|
||||
* The provider is embedded as a summary, which names it but leaves out its public
|
||||
* config. Detail and list share this schema, so the full form would put the SSH
|
||||
* public key in every row of the connection list. Read the key from the provider
|
||||
* detail route.
|
||||
*/
|
||||
export const promotionConnectionPublicSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
scope: promotionConnectionScopeSchema,
|
||||
target: promotionConnectionTargetSchema,
|
||||
provider: promotionProviderSummarySchema,
|
||||
configs: promotionConnectionConfigsPublicSchema,
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export class PromotionConnectionPublicDto extends Z.class(promotionConnectionPublicSchema.shape) {}
|
||||
|
||||
export class PromotionConnectionListPublicDto extends Z.class({
|
||||
data: z.array(promotionConnectionPublicSchema),
|
||||
nextCursor: z.string().nullable(),
|
||||
}) {}
|
||||
|
||||
export const promotionConnectionProjectPublicSchema = z.object({
|
||||
projectId: z.string(),
|
||||
connectionId: z.string(),
|
||||
});
|
||||
|
||||
export class PromotionConnectionProjectPublicDto extends Z.class(
|
||||
promotionConnectionProjectPublicSchema.shape,
|
||||
) {}
|
||||
|
||||
export class PromotionConnectionProjectListPublicDto extends Z.class({
|
||||
projectIds: z.array(z.string()),
|
||||
}) {}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { Z } from '../../zod-class';
|
||||
|
||||
export class PromotePackageDto extends Z.class(
|
||||
{
|
||||
commitMessage: z.string().trim().min(1).max(1000),
|
||||
force: z.boolean().optional(),
|
||||
},
|
||||
{ strict: true },
|
||||
) {}
|
||||
|
||||
/** The Git part of an operation result. A new transport returns its own instead. */
|
||||
export const promotionGitResultSchema = z.object({
|
||||
commitSha: z.string(),
|
||||
/**
|
||||
* For Promote, the branch it pushed to. That is the configured base branch for a
|
||||
* direct push, or the new promotion branch once branching lands. For Apply, the
|
||||
* branch the package came from.
|
||||
*/
|
||||
branchName: z.string(),
|
||||
});
|
||||
|
||||
const count = () => z.number().int().nonnegative();
|
||||
|
||||
/** What ended up in the exported package, after folders and auto-inclusion. */
|
||||
export const promotePackageCountsSchema = z.object({
|
||||
workflows: count(),
|
||||
folders: count(),
|
||||
credentials: count(),
|
||||
dataTables: count(),
|
||||
variables: count(),
|
||||
tags: count(),
|
||||
});
|
||||
|
||||
/** Describes the instance that handled the request, not any other instance. */
|
||||
export const promotePackageResultSchema = z.object({
|
||||
connectionId: z.string(),
|
||||
configId: z.string(),
|
||||
counts: promotePackageCountsSchema,
|
||||
git: promotionGitResultSchema,
|
||||
});
|
||||
|
||||
export class PromotePackageResultDto extends Z.class(promotePackageResultSchema.shape) {}
|
||||
|
||||
/** What the import changed. Partial results are visible here too. */
|
||||
export const applyPackageCountsSchema = z.object({
|
||||
projects: z.object({ created: count(), updated: count(), skipped: count(), deleted: count() }),
|
||||
folders: z.object({ created: count(), skipped: count(), removed: count() }),
|
||||
workflows: z.object({
|
||||
created: count(),
|
||||
updated: count(),
|
||||
skipped: count(),
|
||||
archived: count(),
|
||||
deleted: count(),
|
||||
publishing: z.object({
|
||||
published: count(),
|
||||
unpublished: count(),
|
||||
unchanged: count(),
|
||||
blocked: count(),
|
||||
failed: count(),
|
||||
}),
|
||||
}),
|
||||
credentials: z.object({ matched: count(), stubbed: count() }),
|
||||
dataTables: z.object({ matched: count(), created: count() }),
|
||||
variables: z.object({
|
||||
matched: count(),
|
||||
created: count(),
|
||||
updated: count(),
|
||||
stubbed: count(),
|
||||
missing: count(),
|
||||
}),
|
||||
tags: z.object({
|
||||
matched: count(),
|
||||
created: count(),
|
||||
renamed: count(),
|
||||
reconciled: count(),
|
||||
skipped: count(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const applyPackageResultSchema = z.object({
|
||||
connectionId: z.string(),
|
||||
configId: z.string(),
|
||||
counts: applyPackageCountsSchema,
|
||||
git: promotionGitResultSchema,
|
||||
});
|
||||
|
||||
export class ApplyPackageResultDto extends Z.class(applyPackageResultSchema.shape) {}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { promotionDisplayNameSchema } from './promotion-common.dto';
|
||||
import { Z } from '../../zod-class';
|
||||
import { publicApiPaginationSchema } from '../pagination/pagination.dto';
|
||||
|
||||
export const promotionProviderTypeSchema = z.enum(['git']);
|
||||
export type PromotionProviderType = z.infer<typeof promotionProviderTypeSchema>;
|
||||
|
||||
/** `token` is an HTTP(S) username and password, not a Git host API token. */
|
||||
export const promotionProviderAuthTypeSchema = z.enum(['ssh-key', 'token']);
|
||||
export type PromotionProviderAuthType = z.infer<typeof promotionProviderAuthTypeSchema>;
|
||||
|
||||
/** Key algorithms the backend can generate for an `ssh-key` provider. */
|
||||
export const promotionSshKeyTypeSchema = z.enum(['ed25519', 'rsa']);
|
||||
export type PromotionSshKeyType = z.infer<typeof promotionSshKeyTypeSchema>;
|
||||
|
||||
/**
|
||||
* The backend writes the public key and key type when it generates the key pair.
|
||||
* No request schema accepts them.
|
||||
*/
|
||||
export const promotionGitSshKeyConfigSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(1),
|
||||
publicKey: z.string().min(1),
|
||||
keyType: promotionSshKeyTypeSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
/** HTTP(S) keeps the username with the password, so there is nothing public. */
|
||||
export const promotionGitTokenConfigSchema = z.object({ schemaVersion: z.literal(1) }).strict();
|
||||
|
||||
/**
|
||||
* Config schema for each auth type. Use this map once the auth type is known: the
|
||||
* union below accepts either shape and cannot tell them apart. A new provider type
|
||||
* adds its own map.
|
||||
*/
|
||||
export const promotionGitConfigSchemas = {
|
||||
'ssh-key': promotionGitSshKeyConfigSchema,
|
||||
token: promotionGitTokenConfigSchema,
|
||||
} as const;
|
||||
|
||||
export const promotionProviderConfigSchema = z.union([
|
||||
promotionGitSshKeyConfigSchema,
|
||||
promotionGitTokenConfigSchema,
|
||||
]);
|
||||
export type PromotionProviderConfig = z.infer<typeof promotionProviderConfigSchema>;
|
||||
|
||||
// Trim the username, so a padded value is never saved.
|
||||
const promotionUsernameSchema = z.string().trim().min(1);
|
||||
// Do not trim the password. A token is saved exactly as sent, and a blank one is rejected.
|
||||
const promotionSecretSchema = z.string().min(1);
|
||||
|
||||
/*
|
||||
* The auth payload carries its own `authType`, so it is a discriminated union and
|
||||
* a request cannot pair one auth type with another's fields. Keeping the
|
||||
* discriminator inside `auth` rather than beside it is what lets the request body
|
||||
* stay a plain object: only a field can be a union here, not the body root.
|
||||
*
|
||||
* Responses have no `auth` at all, so they report `authType` at the top level.
|
||||
*/
|
||||
|
||||
/** The backend generates the key pair. The caller only picks the algorithm. */
|
||||
export const promotionGitSshKeyAuthInputSchema = z
|
||||
.object({
|
||||
authType: z.literal('ssh-key'),
|
||||
keyType: promotionSshKeyTypeSchema.default('ed25519'),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* The same payload for an update, but `keyType` has no default. An omitted value
|
||||
* stays omitted, so the service can rotate the key with the algorithm the provider
|
||||
* already uses. A default here would turn an `rsa` provider into an `ed25519` one
|
||||
* on an edit that never mentioned the key.
|
||||
*/
|
||||
export const promotionGitSshKeyAuthUpdateSchema = z
|
||||
.object({
|
||||
authType: z.literal('ssh-key'),
|
||||
keyType: promotionSshKeyTypeSchema.optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/** Username and password are required together, on create and on update. */
|
||||
export const promotionGitTokenAuthInputSchema = z
|
||||
.object({
|
||||
authType: z.literal('token'),
|
||||
username: promotionUsernameSchema,
|
||||
password: promotionSecretSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const promotionProviderAuthInputSchema = z.discriminatedUnion('authType', [
|
||||
promotionGitSshKeyAuthInputSchema,
|
||||
promotionGitTokenAuthInputSchema,
|
||||
]);
|
||||
export type PromotionProviderAuthInput = z.infer<typeof promotionProviderAuthInputSchema>;
|
||||
|
||||
export const promotionProviderAuthUpdateSchema = z.discriminatedUnion('authType', [
|
||||
promotionGitSshKeyAuthUpdateSchema,
|
||||
promotionGitTokenAuthInputSchema,
|
||||
]);
|
||||
export type PromotionProviderAuthUpdate = z.infer<typeof promotionProviderAuthUpdateSchema>;
|
||||
|
||||
/**
|
||||
* The auth type is stated once, inside `auth`, so it always matches the credentials
|
||||
* beside it. The service reads it from there for the `authType` column.
|
||||
*
|
||||
* `git` is the only provider type today, so its two auth methods are the only ones
|
||||
* the union accepts. A second provider type brings its own auth variants, and the
|
||||
* pairing of provider type to auth method becomes a choice to model here.
|
||||
*/
|
||||
export class CreatePromotionProviderDto extends Z.class(
|
||||
{
|
||||
name: promotionDisplayNameSchema,
|
||||
type: promotionProviderTypeSchema,
|
||||
auth: promotionProviderAuthInputSchema,
|
||||
},
|
||||
{ strict: true },
|
||||
) {}
|
||||
|
||||
/**
|
||||
* `type` cannot change and `config` holds generated key material, so a strict shape
|
||||
* rejects both. Leaving out `auth` keeps the stored credentials. Sending it replaces
|
||||
* them.
|
||||
*
|
||||
* `auth.authType` states which credentials are being sent, and the auth type itself
|
||||
* cannot change, so the service compares it with the stored one and rejects a
|
||||
* mismatch. It also reads an omitted `keyType` from the provider's current key type,
|
||||
* so rotating an `rsa` key keeps it `rsa`.
|
||||
*/
|
||||
export class UpdatePromotionProviderDto extends Z.class(
|
||||
{
|
||||
name: promotionDisplayNameSchema.optional(),
|
||||
auth: promotionProviderAuthUpdateSchema.optional(),
|
||||
},
|
||||
{ strict: true },
|
||||
) {}
|
||||
|
||||
/** Leaves out `auth`, which is never returned. */
|
||||
export const promotionProviderPublicSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: promotionProviderTypeSchema,
|
||||
authType: promotionProviderAuthTypeSchema,
|
||||
config: promotionProviderConfigSchema,
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export class PromotionProviderPublicDto extends Z.class(promotionProviderPublicSchema.shape) {}
|
||||
|
||||
/**
|
||||
* A provider without its public config. Used for list rows and inside a
|
||||
* connection, neither of which shows the SSH public key. Only the provider detail
|
||||
* route does.
|
||||
*/
|
||||
export const promotionProviderSummarySchema = promotionProviderPublicSchema.omit({ config: true });
|
||||
|
||||
export class PromotionProviderListPublicDto extends Z.class({
|
||||
data: z.array(promotionProviderSummarySchema),
|
||||
nextCursor: z.string().nullable(),
|
||||
}) {}
|
||||
|
||||
/**
|
||||
* Returns the generated public key on its own, so a client does not have to read
|
||||
* the config variant. `token` providers have none.
|
||||
*/
|
||||
export class PromotionProviderCreatedPublicDto extends Z.class({
|
||||
provider: promotionProviderPublicSchema,
|
||||
publicKey: z.string().nullable(),
|
||||
}) {}
|
||||
|
||||
export class ListPromotionProvidersQueryDto extends Z.class({
|
||||
limit: publicApiPaginationSchema.limit,
|
||||
cursor: z.string().optional(),
|
||||
}) {}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import type { MigrationContext, ReversibleMigration } from '../migration-types';
|
||||
|
||||
const PROVIDER_TABLE = 'promotion_provider';
|
||||
const CONNECTION_TABLE = 'promotion_connection';
|
||||
const CONNECTION_PROJECT_TABLE = 'promotion_connection_project';
|
||||
const CONFIG_TABLE = 'promotion_config';
|
||||
|
||||
/**
|
||||
* Promotions keeps Git credentials on a provider, the remote to talk to on a
|
||||
* connection, and the settings for one direction in a config. Code validates the
|
||||
* JSON payloads, so a new provider type can reuse these tables.
|
||||
*/
|
||||
export class CreatePromotionsTables1788759351166 implements ReversibleMigration {
|
||||
async up(context: MigrationContext) {
|
||||
await this.createProviderTable(context);
|
||||
await this.createConnectionTable(context);
|
||||
await this.createConnectionProjectTable(context);
|
||||
await this.createConfigTable(context);
|
||||
}
|
||||
|
||||
async down({ schemaBuilder: { dropTable } }: MigrationContext) {
|
||||
await dropTable(CONFIG_TABLE);
|
||||
await dropTable(CONNECTION_PROJECT_TABLE);
|
||||
await dropTable(CONNECTION_TABLE);
|
||||
await dropTable(PROVIDER_TABLE);
|
||||
}
|
||||
|
||||
private async createProviderTable({ schemaBuilder: { createTable, column } }: MigrationContext) {
|
||||
await createTable(PROVIDER_TABLE).withColumns(
|
||||
column('id').varchar(36).primary,
|
||||
column('name').varchar(128).notNull,
|
||||
column('type')
|
||||
.varchar(32)
|
||||
.notNull.withEnumCheck(['git'])
|
||||
.comment('PromotionProviderType enum: "git"'),
|
||||
column('authType')
|
||||
.varchar(32)
|
||||
.notNull.withEnumCheck(['ssh-key', 'token'])
|
||||
.comment(
|
||||
'PromotionProviderAuthType enum: "ssh-key", "token". "token" is an HTTP(S) username and password.',
|
||||
),
|
||||
column('config').json.notNull.comment(
|
||||
'Non-secret provider settings, with their own schemaVersion. Validated in code.',
|
||||
),
|
||||
column('auth').text.notNull.comment('Encrypted credentials. Never sent to a client.'),
|
||||
).withTimestamps;
|
||||
}
|
||||
|
||||
private async createConnectionTable({
|
||||
schemaBuilder: { createTable, createIndex, column },
|
||||
tablePrefix,
|
||||
}: MigrationContext) {
|
||||
await createTable(CONNECTION_TABLE)
|
||||
.withColumns(
|
||||
column('id').varchar(36).primary,
|
||||
column('name').varchar(128).notNull,
|
||||
column('providerId')
|
||||
.varchar(36)
|
||||
.notNull.comment('The provider whose credentials this connection uses.'),
|
||||
column('scope')
|
||||
.varchar(16)
|
||||
.notNull.withEnumCheck(['instance', 'projects'])
|
||||
.comment(
|
||||
'PromotionConnectionScope enum: "instance", "projects". Cannot change after creation.',
|
||||
),
|
||||
column('target').json.notNull.comment(
|
||||
'Where to push and pull, with its own schemaVersion. Plain Git stores the full remote URL.',
|
||||
),
|
||||
)
|
||||
// RESTRICT, not CASCADE: one provider can serve many connections, so deleting
|
||||
// it must not take their configuration with it.
|
||||
.withTimestamps.withForeignKey('providerId', {
|
||||
tableName: PROVIDER_TABLE,
|
||||
columnName: 'id',
|
||||
onDelete: 'RESTRICT',
|
||||
name: `FK_${tablePrefix}promotion_connection_providerId`,
|
||||
})
|
||||
// Index the foreign key, for listing a provider's connections and for the
|
||||
// check on delete.
|
||||
.withIndexOn(['providerId']);
|
||||
|
||||
// Only one instance connection is allowed, but any number of project ones.
|
||||
// A count check in code would let two slip through at the same time.
|
||||
await createIndex(CONNECTION_TABLE, ['scope'], true, undefined, '"scope" = \'instance\'');
|
||||
}
|
||||
|
||||
private async createConnectionProjectTable({
|
||||
schemaBuilder: { createTable, column },
|
||||
tablePrefix,
|
||||
}: MigrationContext) {
|
||||
// projectId is the primary key, so a project links to one connection at most.
|
||||
// A connection can hold many links. Both foreign keys cascade, so a link goes
|
||||
// away when either side is deleted.
|
||||
await createTable(CONNECTION_PROJECT_TABLE)
|
||||
.withColumns(
|
||||
column('projectId').varchar(36).primary,
|
||||
column('connectionId').varchar(36).notNull,
|
||||
)
|
||||
.withTimestamps.withForeignKey('projectId', {
|
||||
tableName: 'project',
|
||||
columnName: 'id',
|
||||
onDelete: 'CASCADE',
|
||||
name: `FK_${tablePrefix}promotion_connection_project_projectId`,
|
||||
})
|
||||
.withForeignKey('connectionId', {
|
||||
tableName: CONNECTION_TABLE,
|
||||
columnName: 'id',
|
||||
onDelete: 'CASCADE',
|
||||
name: `FK_${tablePrefix}promotion_connection_project_connectionId`,
|
||||
})
|
||||
// Index the foreign key, for listing a connection's projects and for the
|
||||
// cascade delete.
|
||||
.withIndexOn(['connectionId']);
|
||||
}
|
||||
|
||||
private async createConfigTable({
|
||||
schemaBuilder: { createTable, createIndex, column },
|
||||
tablePrefix,
|
||||
}: MigrationContext) {
|
||||
await createTable(CONFIG_TABLE)
|
||||
.withColumns(
|
||||
column('id').varchar(36).primary.comment('Also the name of the local checkout directory.'),
|
||||
column('connectionId').varchar(36).notNull,
|
||||
column('name').varchar(128).notNull,
|
||||
column('direction')
|
||||
.varchar(16)
|
||||
.notNull.withEnumCheck(['apply', 'promote'])
|
||||
.comment('PromotionDirection enum: "apply", "promote". Cannot change after creation.'),
|
||||
column('settings').json.notNull.comment(
|
||||
'Branch settings for this direction, with their own schemaVersion. Validated in code.',
|
||||
),
|
||||
)
|
||||
.withTimestamps.withForeignKey('connectionId', {
|
||||
tableName: CONNECTION_TABLE,
|
||||
columnName: 'id',
|
||||
onDelete: 'CASCADE',
|
||||
name: `FK_${tablePrefix}promotion_config_connectionId`,
|
||||
});
|
||||
|
||||
// One config per direction. This index also covers connectionId lookups and the
|
||||
// cascade delete, so connectionId needs no index of its own.
|
||||
await createIndex(CONFIG_TABLE, ['connectionId', 'direction'], true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import type { PromotionConfigSettings, PromotionDirection } from '@n8n/api-types';
|
||||
import { JsonColumn, WithTimestampsAndStringId } from '@n8n/db';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, type Relation } from '@n8n/typeorm';
|
||||
|
||||
import { PromotionConnection } from './promotion-connection.entity';
|
||||
|
||||
/**
|
||||
* Settings for one direction of one connection. The generated `id` also names the
|
||||
* local checkout directory.
|
||||
*/
|
||||
@Entity('promotion_config')
|
||||
// One config for each direction. This index also covers lookups by connectionId.
|
||||
@Index(['connectionId', 'direction'], { unique: true })
|
||||
export class PromotionConfig extends WithTimestampsAndStringId {
|
||||
@Column({ type: 'varchar', length: 36 })
|
||||
connectionId: string;
|
||||
|
||||
/** Names are not unique. */
|
||||
@Column({ type: 'varchar', length: 128 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 16 })
|
||||
direction: PromotionDirection;
|
||||
|
||||
/** Branch settings for {@link direction}, with their own `schemaVersion`. */
|
||||
@JsonColumn()
|
||||
settings: PromotionConfigSettings;
|
||||
|
||||
@ManyToOne(() => PromotionConnection, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'connectionId' })
|
||||
connection: Relation<PromotionConnection>;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { WithTimestamps } from '@n8n/db';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
type Relation,
|
||||
} from '@n8n/typeorm';
|
||||
|
||||
import { PromotionConnection } from './promotion-connection.entity';
|
||||
|
||||
/**
|
||||
* Links a project to a promotion connection. The `projectId` primary key keeps a
|
||||
* project on one connection at most. A connection can hold many links, or none.
|
||||
* Which projects and connections are eligible spans tables, so the service checks
|
||||
* that rather than the schema.
|
||||
*
|
||||
* The foreign key to `project` is declared in the migration and not as a
|
||||
* `@ManyToOne`, to keep this module apart from the core `Project` entity. This
|
||||
* follows the workflow-reviews pattern.
|
||||
*/
|
||||
@Entity('promotion_connection_project')
|
||||
export class PromotionConnectionProject extends WithTimestamps {
|
||||
@PrimaryColumn({ type: 'varchar', length: 36 })
|
||||
projectId: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'varchar', length: 36 })
|
||||
connectionId: string;
|
||||
|
||||
@ManyToOne(() => PromotionConnection, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'connectionId' })
|
||||
connection: Relation<PromotionConnection>;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import type { PromotionConnectionScope, PromotionConnectionTarget } from '@n8n/api-types';
|
||||
import { JsonColumn, WithTimestampsAndStringId } from '@n8n/db';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, type Relation } from '@n8n/typeorm';
|
||||
|
||||
import { PromotionProvider } from './promotion-provider.entity';
|
||||
|
||||
/**
|
||||
* One remote, reached with one provider's credentials. The provider, remote URL,
|
||||
* and name are not unique, because a provider can serve several connections.
|
||||
*/
|
||||
@Entity('promotion_connection')
|
||||
// Only one instance connection is allowed, but any number of project ones.
|
||||
@Index(['scope'], { unique: true, where: '"scope" = \'instance\'' })
|
||||
export class PromotionConnection extends WithTimestampsAndStringId {
|
||||
@Column({ type: 'varchar', length: 128 })
|
||||
name: string;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'varchar', length: 36 })
|
||||
providerId: string;
|
||||
|
||||
/**
|
||||
* Cannot change after creation. A change would quietly move the instance
|
||||
* fallback or break existing project links. Create a new connection instead.
|
||||
*/
|
||||
@Column({ type: 'varchar', length: 16 })
|
||||
scope: PromotionConnectionScope;
|
||||
|
||||
/** Where to push and pull, with its own `schemaVersion`. */
|
||||
@JsonColumn()
|
||||
target: PromotionConnectionTarget;
|
||||
|
||||
@ManyToOne(() => PromotionProvider, { onDelete: 'RESTRICT' })
|
||||
@JoinColumn({ name: 'providerId' })
|
||||
provider: Relation<PromotionProvider>;
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import type {
|
||||
PromotionProviderAuthType,
|
||||
PromotionProviderConfig,
|
||||
PromotionProviderType,
|
||||
} from '@n8n/api-types';
|
||||
import { JsonColumn, WithTimestampsAndStringId } from '@n8n/db';
|
||||
import { Column, Entity } from '@n8n/typeorm';
|
||||
|
||||
/**
|
||||
* Credentials for a remote, shared by any number of connections. The database
|
||||
* limits which {@link type} and {@link authType} values are allowed.
|
||||
*/
|
||||
@Entity('promotion_provider')
|
||||
export class PromotionProvider extends WithTimestampsAndStringId {
|
||||
@Column({ type: 'varchar', length: 128 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 32 })
|
||||
type: PromotionProviderType;
|
||||
|
||||
/** `token` is an HTTP(S) username and password, not a Git host API token. */
|
||||
@Column({ type: 'varchar', length: 32 })
|
||||
authType: PromotionProviderAuthType;
|
||||
|
||||
/**
|
||||
* Non-secret settings, with their own `schemaVersion`. This type says what we
|
||||
* write, not what an older row holds, so check a loaded row against the schema
|
||||
* for {@link authType}. The same applies to the other JSON columns in this module.
|
||||
*/
|
||||
@JsonColumn()
|
||||
config: PromotionProviderConfig;
|
||||
|
||||
/** Encrypted credentials. Never sent to a client. */
|
||||
@Column({ type: 'text' })
|
||||
auth: string;
|
||||
}
|
||||
|
|
@ -88,6 +88,11 @@
|
|||
"name": "GitConnectionProject",
|
||||
"path": "packages/cli/src/modules/git-connections.ee/database/entities/git-connection-project.entity.ts",
|
||||
"reason": "Project-scoped git configuration (which connection the project is linked to), not user data. Dropped via the projectId FK cascade when the source project itself is deleted, as in ownership-transfer flows."
|
||||
},
|
||||
{
|
||||
"name": "PromotionConnectionProject",
|
||||
"path": "packages/cli/src/modules/promotions.ee/database/entities/promotion-connection-project.entity.ts",
|
||||
"reason": "Promotion configuration for the project (which connection it is linked to), not user data. Dropped via the projectId FK cascade when the source project is deleted, as in ownership-transfer flows. Replaces GitConnectionProject, which goes away with the old tables."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,307 @@
|
|||
import {
|
||||
createTestMigrationContext,
|
||||
initDbUpToMigration,
|
||||
runSingleMigration,
|
||||
type TestMigrationContext,
|
||||
} from '@n8n/backend-test-utils';
|
||||
import { DbConnection } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { DataSource } from '@n8n/typeorm';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
const MIGRATION_NAME = 'CreatePromotionsTables1788759351166';
|
||||
|
||||
const PROVIDER_TABLE = 'promotion_provider';
|
||||
const CONNECTION_TABLE = 'promotion_connection';
|
||||
const LINK_TABLE = 'promotion_connection_project';
|
||||
const CONFIG_TABLE = 'promotion_config';
|
||||
|
||||
describe('CreatePromotionsTables migration', () => {
|
||||
let dataSource: DataSource;
|
||||
|
||||
beforeAll(async () => {
|
||||
const dbConnection = Container.get(DbConnection);
|
||||
await dbConnection.init();
|
||||
dataSource = Container.get(DataSource);
|
||||
const context = createTestMigrationContext(dataSource);
|
||||
await context.queryRunner.clearDatabase();
|
||||
await context.queryRunner.release();
|
||||
await initDbUpToMigration(MIGRATION_NAME);
|
||||
await runSingleMigration(MIGRATION_NAME);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await Container.get(DbConnection).close();
|
||||
});
|
||||
|
||||
async function insertProvider(
|
||||
context: TestMigrationContext,
|
||||
id: string,
|
||||
overrides: { type?: string; authType?: string } = {},
|
||||
) {
|
||||
const now = new Date();
|
||||
await context.runQuery(
|
||||
`INSERT INTO ${context.escape.tableName(PROVIDER_TABLE)}
|
||||
("id", "name", "type", "authType", "config", "auth", "createdAt", "updatedAt")
|
||||
VALUES (:id, :name, :type, :authType, :config, :auth, :createdAt, :updatedAt)`,
|
||||
{
|
||||
id,
|
||||
name: 'Provider',
|
||||
type: overrides.type ?? 'git',
|
||||
authType: overrides.authType ?? 'ssh-key',
|
||||
config: JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
publicKey: 'ssh-ed25519 KEY',
|
||||
keyType: 'ed25519',
|
||||
}),
|
||||
auth: 'encrypted',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function insertConnection(
|
||||
context: TestMigrationContext,
|
||||
id: string,
|
||||
providerId: string,
|
||||
scope = 'projects',
|
||||
) {
|
||||
const now = new Date();
|
||||
await context.runQuery(
|
||||
`INSERT INTO ${context.escape.tableName(CONNECTION_TABLE)}
|
||||
("id", "name", "providerId", "scope", "target", "createdAt", "updatedAt")
|
||||
VALUES (:id, :name, :providerId, :scope, :target, :createdAt, :updatedAt)`,
|
||||
{
|
||||
id,
|
||||
name: 'Connection',
|
||||
providerId,
|
||||
scope,
|
||||
target: JSON.stringify({ schemaVersion: 1, remoteUrl: 'git@example.com:team/repo.git' }),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function insertProject(context: TestMigrationContext, id: string) {
|
||||
const now = new Date();
|
||||
await context.runQuery(
|
||||
`INSERT INTO ${context.escape.tableName('project')}
|
||||
("id", "name", "type", "customTelemetryTags", "createdAt", "updatedAt")
|
||||
VALUES (:id, :name, :type, :tags, :createdAt, :updatedAt)`,
|
||||
{ id, name: 'Project', type: 'team', tags: '[]', createdAt: now, updatedAt: now },
|
||||
);
|
||||
}
|
||||
|
||||
async function insertLink(
|
||||
context: TestMigrationContext,
|
||||
projectId: string,
|
||||
connectionId: string,
|
||||
) {
|
||||
const now = new Date();
|
||||
await context.runQuery(
|
||||
`INSERT INTO ${context.escape.tableName(LINK_TABLE)}
|
||||
("projectId", "connectionId", "createdAt", "updatedAt")
|
||||
VALUES (:projectId, :connectionId, :createdAt, :updatedAt)`,
|
||||
{ projectId, connectionId, createdAt: now, updatedAt: now },
|
||||
);
|
||||
}
|
||||
|
||||
async function insertConfig(
|
||||
context: TestMigrationContext,
|
||||
id: string,
|
||||
connectionId: string,
|
||||
direction: string,
|
||||
) {
|
||||
const now = new Date();
|
||||
await context.runQuery(
|
||||
`INSERT INTO ${context.escape.tableName(CONFIG_TABLE)}
|
||||
("id", "connectionId", "name", "direction", "settings", "createdAt", "updatedAt")
|
||||
VALUES (:id, :connectionId, :name, :direction, :settings, :createdAt, :updatedAt)`,
|
||||
{
|
||||
id,
|
||||
connectionId,
|
||||
name: direction,
|
||||
direction,
|
||||
settings: JSON.stringify(
|
||||
direction === 'apply'
|
||||
? { schemaVersion: 1, branchName: 'dev' }
|
||||
: { schemaVersion: 1, baseBranchName: 'staging', createBranchOnPromotion: false },
|
||||
),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function countRows(
|
||||
context: TestMigrationContext,
|
||||
table: string,
|
||||
where: string,
|
||||
params: object,
|
||||
) {
|
||||
const rows = await context.runQuery<Array<{ c: number }>>(
|
||||
`SELECT COUNT(*) as c FROM ${context.escape.tableName(table)} WHERE ${where}`,
|
||||
params,
|
||||
);
|
||||
return Number(rows[0].c);
|
||||
}
|
||||
|
||||
describe('Up migration', () => {
|
||||
it('allows one instance connection but any number of project connections', async () => {
|
||||
const context = createTestMigrationContext(dataSource);
|
||||
const providerId = randomUUID();
|
||||
await insertProvider(context, providerId);
|
||||
await insertConnection(context, randomUUID(), providerId, 'instance');
|
||||
|
||||
await expect(
|
||||
insertConnection(context, randomUUID(), providerId, 'instance'),
|
||||
).rejects.toThrow();
|
||||
|
||||
await insertConnection(context, randomUUID(), providerId, 'projects');
|
||||
await insertConnection(context, randomUUID(), providerId, 'projects');
|
||||
expect(
|
||||
await countRows(context, CONNECTION_TABLE, '"scope" = :scope', { scope: 'projects' }),
|
||||
).toBe(2);
|
||||
await context.queryRunner.release();
|
||||
});
|
||||
|
||||
it('rejects values outside the type, auth type, scope, and direction enums', async () => {
|
||||
const context = createTestMigrationContext(dataSource);
|
||||
await expect(insertProvider(context, randomUUID(), { type: 'gitlab' })).rejects.toThrow();
|
||||
await expect(insertProvider(context, randomUUID(), { authType: 'oauth2' })).rejects.toThrow();
|
||||
|
||||
const providerId = randomUUID();
|
||||
await insertProvider(context, providerId);
|
||||
await expect(
|
||||
insertConnection(context, randomUUID(), providerId, 'workflows'),
|
||||
).rejects.toThrow();
|
||||
|
||||
const connectionId = randomUUID();
|
||||
await insertConnection(context, connectionId, providerId);
|
||||
await expect(insertConfig(context, randomUUID(), connectionId, 'sync')).rejects.toThrow();
|
||||
await context.queryRunner.release();
|
||||
});
|
||||
|
||||
it('refuses to delete a provider that a connection still uses', async () => {
|
||||
const context = createTestMigrationContext(dataSource);
|
||||
const providerId = randomUUID();
|
||||
await insertProvider(context, providerId);
|
||||
await insertConnection(context, randomUUID(), providerId);
|
||||
|
||||
await expect(
|
||||
context.runQuery(
|
||||
`DELETE FROM ${context.escape.tableName(PROVIDER_TABLE)} WHERE "id" = :id`,
|
||||
{ id: providerId },
|
||||
),
|
||||
).rejects.toThrow();
|
||||
await context.queryRunner.release();
|
||||
});
|
||||
|
||||
it('allows one config for each direction and rejects a repeated direction', async () => {
|
||||
const context = createTestMigrationContext(dataSource);
|
||||
const providerId = randomUUID();
|
||||
const connectionId = randomUUID();
|
||||
await insertProvider(context, providerId);
|
||||
await insertConnection(context, connectionId, providerId);
|
||||
await insertConfig(context, randomUUID(), connectionId, 'apply');
|
||||
await insertConfig(context, randomUUID(), connectionId, 'promote');
|
||||
|
||||
await expect(insertConfig(context, randomUUID(), connectionId, 'apply')).rejects.toThrow();
|
||||
expect(
|
||||
await countRows(context, CONFIG_TABLE, '"connectionId" = :connectionId', { connectionId }),
|
||||
).toBe(2);
|
||||
await context.queryRunner.release();
|
||||
});
|
||||
|
||||
it('links a project to one connection at most', async () => {
|
||||
const context = createTestMigrationContext(dataSource);
|
||||
const providerId = randomUUID();
|
||||
const connectionId = randomUUID();
|
||||
const otherConnectionId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
await insertProvider(context, providerId);
|
||||
await insertConnection(context, connectionId, providerId);
|
||||
await insertConnection(context, otherConnectionId, providerId);
|
||||
await insertProject(context, projectId);
|
||||
await insertLink(context, projectId, connectionId);
|
||||
|
||||
await expect(insertLink(context, projectId, otherConnectionId)).rejects.toThrow();
|
||||
await context.queryRunner.release();
|
||||
});
|
||||
|
||||
it('deletes the configs and links of a deleted connection, but keeps its provider', async () => {
|
||||
const context = createTestMigrationContext(dataSource);
|
||||
const providerId = randomUUID();
|
||||
const connectionId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
await insertProvider(context, providerId);
|
||||
await insertConnection(context, connectionId, providerId);
|
||||
await insertProject(context, projectId);
|
||||
await insertLink(context, projectId, connectionId);
|
||||
await insertConfig(context, randomUUID(), connectionId, 'apply');
|
||||
|
||||
await context.runQuery(
|
||||
`DELETE FROM ${context.escape.tableName(CONNECTION_TABLE)} WHERE "id" = :id`,
|
||||
{ id: connectionId },
|
||||
);
|
||||
|
||||
expect(
|
||||
await countRows(context, CONFIG_TABLE, '"connectionId" = :connectionId', { connectionId }),
|
||||
).toBe(0);
|
||||
expect(await countRows(context, LINK_TABLE, '"projectId" = :projectId', { projectId })).toBe(
|
||||
0,
|
||||
);
|
||||
expect(await countRows(context, PROVIDER_TABLE, '"id" = :id', { id: providerId })).toBe(1);
|
||||
await context.queryRunner.release();
|
||||
});
|
||||
|
||||
it('deletes only the link of a deleted project, not the connection', async () => {
|
||||
const context = createTestMigrationContext(dataSource);
|
||||
const providerId = randomUUID();
|
||||
const connectionId = randomUUID();
|
||||
const projectId = randomUUID();
|
||||
await insertProvider(context, providerId);
|
||||
await insertConnection(context, connectionId, providerId);
|
||||
await insertProject(context, projectId);
|
||||
await insertLink(context, projectId, connectionId);
|
||||
|
||||
await context.runQuery(
|
||||
`DELETE FROM ${context.escape.tableName('project')} WHERE "id" = :id`,
|
||||
{ id: projectId },
|
||||
);
|
||||
|
||||
expect(await countRows(context, LINK_TABLE, '"projectId" = :projectId', { projectId })).toBe(
|
||||
0,
|
||||
);
|
||||
expect(await countRows(context, CONNECTION_TABLE, '"id" = :id', { id: connectionId })).toBe(
|
||||
1,
|
||||
);
|
||||
await context.queryRunner.release();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Down migration', () => {
|
||||
it('drops the four tables and can be applied again', async () => {
|
||||
const tables = [PROVIDER_TABLE, CONNECTION_TABLE, LINK_TABLE, CONFIG_TABLE];
|
||||
await dataSource.undoLastMigration({ transaction: 'each' });
|
||||
|
||||
const context = createTestMigrationContext(dataSource);
|
||||
for (const table of tables) {
|
||||
expect(await context.queryRunner.hasTable(`${context.tablePrefix}${table}`)).toBe(false);
|
||||
}
|
||||
await context.queryRunner.release();
|
||||
|
||||
await runSingleMigration(MIGRATION_NAME);
|
||||
|
||||
const afterRerun = createTestMigrationContext(dataSource);
|
||||
for (const table of tables) {
|
||||
expect(await afterRerun.queryRunner.hasTable(`${afterRerun.tablePrefix}${table}`)).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
await afterRerun.queryRunner.release();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user