feat: add workspace setting to disable error handler for u/ scripts/flows (#7634)

* feat: add workspace setting to disable error handler for u/ scripts/flows

Add a new workspace setting `error_handler_muted_on_user_path` that allows
disabling the error handler for jobs related to scripts/flows that start
with the "u/" prefix (user scripts/flows).

Changes:
- Add migration to add `error_handler_muted_on_user_path` column to workspace_settings
- Update WorkspaceSettings struct and get_settings query
- Update EditErrorHandler struct and edit_error_handler API endpoint
- Update error handler cache to include the new setting
- Add skip logic in send_error_to_workspace_handler for u/ paths
- Add toggle in workspace settings UI

The implementation uses the existing cached settings to avoid additional
database queries, making it efficient.

Closes #7633

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>

* all

* update ee private

* combine migrations

* sqlx

* fix: migration fixes for auto_invite JSONB column

- Fix TEXT[] to JSONB conversion using to_jsonb()
- Add GIN index on auto_invite for efficient ? operator queries
- Add index cleanup to down migration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* sqlx

* all

* all

* all

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
claude[bot]
2026-01-24 19:26:06 +00:00
committed by GitHub
parent 6a1d457257
commit a9e6ba392d
62 changed files with 1253 additions and 1142 deletions
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET auto_invite = COALESCE(auto_invite, '{}'::jsonb)\n || jsonb_build_object('instance_groups', $2::jsonb, 'instance_groups_roles', $3::jsonb)\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "03caaec9f41be87eac32e163d7a829e58ae993932e9bbad5aebab4770cc44d61"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT success_handler_extra_args FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "success_handler_extra_args",
"type_info": "Json"
}
],
"parameters": {
"Left": []
},
"nullable": [
true
]
},
"hash": "085bbcfcebc9e56984295046c48722f3510548eaf525c499ad0e24cdf8332b22"
}
@@ -30,163 +30,128 @@
},
{
"ordinal": 5,
"name": "auto_invite_domain",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "auto_invite_operator",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "customer_id",
"type_info": "Varchar"
},
{
"ordinal": 8,
"ordinal": 6,
"name": "plan",
"type_info": "Varchar"
},
{
"ordinal": 9,
"ordinal": 7,
"name": "webhook",
"type_info": "Text"
},
{
"ordinal": 10,
"ordinal": 8,
"name": "deploy_to",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "error_handler",
"type_info": "Varchar"
},
{
"ordinal": 12,
"ordinal": 9,
"name": "ai_config",
"type_info": "Jsonb"
},
{
"ordinal": 13,
"name": "error_handler_extra_args",
"type_info": "Json"
},
{
"ordinal": 14,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 15,
"ordinal": 10,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 16,
"ordinal": 11,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"ordinal": 12,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "auto_add",
"type_info": "Bool"
},
{
"ordinal": 19,
"ordinal": 13,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 20,
"ordinal": 14,
"name": "deploy_ui",
"type_info": "Jsonb"
},
{
"ordinal": 21,
"ordinal": 15,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 22,
"ordinal": 16,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 23,
"ordinal": 17,
"name": "operator_settings",
"type_info": "Jsonb"
},
{
"ordinal": 24,
"ordinal": 18,
"name": "teams_command_script",
"type_info": "Text"
},
{
"ordinal": 25,
"ordinal": 19,
"name": "teams_team_id",
"type_info": "Text"
},
{
"ordinal": 26,
"ordinal": 20,
"name": "teams_team_name",
"type_info": "Text"
},
{
"ordinal": 27,
"ordinal": 21,
"name": "git_app_installations",
"type_info": "Jsonb"
},
{
"ordinal": 28,
"ordinal": 22,
"name": "ducklake",
"type_info": "Jsonb"
},
{
"ordinal": 29,
"name": "auto_add_instance_groups",
"type_info": "TextArray"
},
{
"ordinal": 30,
"name": "auto_add_instance_groups_roles",
"type_info": "Jsonb"
},
{
"ordinal": 31,
"ordinal": 23,
"name": "slack_oauth_client_id",
"type_info": "Varchar"
},
{
"ordinal": 32,
"ordinal": 24,
"name": "slack_oauth_client_secret",
"type_info": "Varchar"
},
{
"ordinal": 33,
"ordinal": 25,
"name": "datatable",
"type_info": "Jsonb"
},
{
"ordinal": 34,
"ordinal": 26,
"name": "teams_team_guid",
"type_info": "Text"
},
{
"ordinal": 35,
"name": "success_handler",
"type_info": "Text"
"ordinal": 27,
"name": "auto_invite",
"type_info": "Jsonb"
},
{
"ordinal": 36,
"name": "success_handler_extra_args",
"type_info": "Json"
"ordinal": 28,
"name": "error_handler",
"type_info": "Jsonb"
},
{
"ordinal": 29,
"name": "success_handler",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -209,12 +174,6 @@
true,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
@@ -230,7 +189,6 @@
true,
true,
true,
true,
true
]
},
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET auto_invite = COALESCE(auto_invite, '{}'::jsonb) - 'instance_groups' - 'instance_groups_roles'\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "0984e469bbd5f97cf9a6cac4bf5700bb6b1a99f6abfb3390a54a8a42c6768903"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET auto_invite_domain = NULL, auto_invite_operator = NULL, auto_add = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "0c9ad812013ff476a79ca8d6bb8b7a73d9492e07680732af9af09e223ade1f37"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT success_handler FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "success_handler",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
true
]
},
"hash": "10a40902f5aba38aea23f2b0776b54752362c9cc7deb179f75c7f1795866d13a"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COALESCE(auto_add_instance_groups, '{}') FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "coalesce",
"type_info": "TextArray"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "18e550f4ec23d465632449b88c4b25931f145f771b93828e8e6dfcc1f906443d"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET auto_invite = $1 WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Text"
]
},
"nullable": []
},
"hash": "255ba68caa78d0e814ea817693a319f769907025f9c7d1150d45e8ecb1bff4ab"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT * FROM workspace_settings WHERE workspace_id = $1",
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n slack_oauth_client_id,\n slack_oauth_client_secret,\n customer_id,\n plan,\n webhook,\n deploy_to,\n ai_config,\n large_file_storage,\n datatable,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_invite,\n error_handler,\n success_handler\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
"describe": {
"columns": [
{
@@ -15,92 +15,92 @@
},
{
"ordinal": 2,
"name": "teams_team_id",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "teams_team_name",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "teams_team_guid",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "slack_name",
"type_info": "Varchar"
},
{
"ordinal": 3,
"ordinal": 6,
"name": "slack_command_script",
"type_info": "Varchar"
},
{
"ordinal": 4,
"ordinal": 7,
"name": "teams_command_script",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "slack_email",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "auto_invite_domain",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "auto_invite_operator",
"type_info": "Bool"
},
{
"ordinal": 7,
"name": "customer_id",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "plan",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "webhook",
"type_info": "Text"
"name": "slack_oauth_client_id",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "deploy_to",
"name": "slack_oauth_client_secret",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "error_handler",
"name": "customer_id",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "plan",
"type_info": "Varchar"
},
{
"ordinal": 13,
"name": "webhook",
"type_info": "Text"
},
{
"ordinal": 14,
"name": "deploy_to",
"type_info": "Varchar"
},
{
"ordinal": 15,
"name": "ai_config",
"type_info": "Jsonb"
},
{
"ordinal": 13,
"name": "error_handler_extra_args",
"type_info": "Json"
},
{
"ordinal": 14,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 15,
"ordinal": 16,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 16,
"name": "git_sync",
"ordinal": 17,
"name": "datatable",
"type_info": "Jsonb"
},
{
"ordinal": 17,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "auto_add",
"type_info": "Bool"
"name": "ducklake",
"type_info": "Jsonb"
},
{
"ordinal": 19,
"name": "default_scripts",
"name": "git_sync",
"type_info": "Jsonb"
},
{
@@ -110,83 +110,48 @@
},
{
"ordinal": 21,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 22,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 23,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 22,
"ordinal": 24,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 23,
"ordinal": 25,
"name": "operator_settings",
"type_info": "Jsonb"
},
{
"ordinal": 24,
"name": "teams_command_script",
"type_info": "Text"
},
{
"ordinal": 25,
"name": "teams_team_id",
"type_info": "Text"
},
{
"ordinal": 26,
"name": "teams_team_name",
"type_info": "Text"
},
{
"ordinal": 27,
"name": "git_app_installations",
"type_info": "Jsonb"
},
{
"ordinal": 27,
"name": "auto_invite",
"type_info": "Jsonb"
},
{
"ordinal": 28,
"name": "ducklake",
"name": "error_handler",
"type_info": "Jsonb"
},
{
"ordinal": 29,
"name": "auto_add_instance_groups",
"type_info": "TextArray"
},
{
"ordinal": 30,
"name": "auto_add_instance_groups_roles",
"type_info": "Jsonb"
},
{
"ordinal": 31,
"name": "slack_oauth_client_id",
"type_info": "Varchar"
},
{
"ordinal": 32,
"name": "slack_oauth_client_secret",
"type_info": "Varchar"
},
{
"ordinal": 33,
"name": "datatable",
"type_info": "Jsonb"
},
{
"ordinal": 34,
"name": "teams_team_guid",
"type_info": "Text"
},
{
"ordinal": 35,
"name": "success_handler",
"type_info": "Text"
},
{
"ordinal": 36,
"name": "success_handler_extra_args",
"type_info": "Json"
"type_info": "Jsonb"
}
],
"parameters": {
@@ -199,6 +164,10 @@
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
@@ -209,11 +178,6 @@
true,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
@@ -225,14 +189,8 @@
false,
true,
true,
true,
true,
true,
true,
true,
true,
true
]
},
"hash": "1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597"
"hash": "289919809e16aee33c81951b05a4795de710421bcd3e4c06588e56092677bd05"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = '{\"path\": \"script/f/test/success_handler\", \"extra_args\": {\"key\": \"value\"}}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "28f3dfb70aed943ad3d64da608168c027c645afb73dfc2360245b68ba593cab4"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET auto_add_instance_groups = NULL, auto_add_instance_groups_roles = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "31db242523c534b2abbb5f4985f56dd4493d19611ed988d694d9f4289dc29c48"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "397f5bef6454770910189db812ad549d50af537c43b4b6ded63b6e83070243be"
}
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET auto_add_instance_groups = $2, auto_add_instance_groups_roles = $3 WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"TextArray",
"Jsonb"
]
},
"nullable": []
},
"hash": "404233e74aaafd987879c6c87d1fd80928e6fade09e60a9a2d35121c81885cca"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n workspace_settings\n SET\n success_handler = NULL,\n success_handler_extra_args = NULL\n WHERE\n workspace_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "4977b4027861ca234e95559a8b550bc8849f7ed60a471d46f3be3d39664a48ca"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET\n auto_add_instance_groups = array_remove(auto_add_instance_groups, $1),\n auto_add_instance_groups_roles = auto_add_instance_groups_roles - $1\n WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "4a446b97cf813226d25ca40cefca2999cdcf938f817327e9168b3edb0fce7fb7"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
null
true
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n workspace_settings\n SET\n error_handler = NULL,\n error_handler_extra_args = NULL,\n error_handler_muted_on_cancel = false\n WHERE\n workspace_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "5ad2c883d26f39f3c141806428a329951ef19a1cb3e1b429fcd1abe0e2db45b5"
}
@@ -0,0 +1,35 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id,\n (auto_invite->>'operator')::boolean as \"is_operator: bool\",\n (auto_invite->>'mode') = 'add' as \"is_auto_add: bool\"\n FROM workspace_settings ws\n WHERE (auto_invite->>'domain' = $1 OR auto_invite->>'domain' = '*')\n AND (auto_invite->>'enabled')::boolean = true\n AND NOT EXISTS (SELECT 1 FROM usr WHERE workspace_id = ws.workspace_id AND email = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "is_operator: bool",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "is_auto_add: bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
null,
null
]
},
"hash": "5dafddc81419883beda4cc9acb3679b3d7c97f6c137b189d552182899782e99f"
}
@@ -0,0 +1,94 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n auto_invite,\n webhook,\n deploy_to,\n error_handler,\n ai_config,\n large_file_storage,\n git_sync,\n default_app,\n default_scripts,\n workspace.name as \"name!\",\n mute_critical_alerts,\n color,\n operator_settings\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "auto_invite",
"type_info": "Jsonb"
},
{
"ordinal": 1,
"name": "webhook",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "deploy_to",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "error_handler",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "ai_config",
"type_info": "Jsonb"
},
{
"ordinal": 5,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 7,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "name!",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 11,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "operator_settings",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
true
]
},
"hash": "631ddef13d95a171b2807638485177e9d543b8e59a15f7e35c9b5d06dddd3142"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET success_handler = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "63ad0e3cca16987dba0be7b3efb522c6e652cfa86f10642ade6aaadd6174fb52"
}
@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET auto_invite_domain = $1, auto_invite_operator = $2, auto_add = $4 WHERE workspace_id = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Bool",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "64c5ca0bd4a2c42d6f5415fae1de21295673c45e4caae1bdbb47517d4608dc8d"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = '{\"path\": \"script/f/test/success_handler\"}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "65016ea993fb40d4ab6b0a6f15c2082e8dfbaada01243bae07065c089bc42fa9"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = 'script/f/test/success_handler',\n success_handler_extra_args = '{\"key\": \"value\"}'::json\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "6eaa0af9c5e0a8d64c3202753c9c3cef39361b0987a22b7d2b2346652f81177e"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT auto_invite->'instance_groups' FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "75e740531bf794a8568350348253612cfbdecb9fb9cf768f431d4dbd4cd56cfb"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n workspace_id,\n auto_add_instance_groups_roles\n FROM workspace_settings\n WHERE\n auto_add_instance_groups IS NOT NULL\n AND $1 = ANY(auto_add_instance_groups)\n ",
"query": "\n SELECT\n workspace_id,\n auto_invite->'instance_groups_roles' as instance_groups_roles\n FROM workspace_settings\n WHERE\n auto_invite->'instance_groups' IS NOT NULL\n AND auto_invite->'instance_groups' ? $1\n ",
"describe": {
"columns": [
{
@@ -10,7 +10,7 @@
},
{
"ordinal": 1,
"name": "auto_add_instance_groups_roles",
"name": "instance_groups_roles",
"type_info": "Jsonb"
}
],
@@ -21,8 +21,8 @@
},
"nullable": [
false,
true
null
]
},
"hash": "3ebc91867be0a4830ef4dbbe833aed348f78e3cb68fb1f3c855a491bdcda5017"
"hash": "7e01ef5799168c0fc2779d42ce352827e2fda6711c0a1b104ca6435ddb14b47d"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n eig.igroup as group_name,\n ws.auto_add_instance_groups_roles\n FROM email_to_igroup eig\n INNER JOIN workspace_settings ws ON ws.workspace_id = $1\n WHERE eig.email = $2\n AND eig.igroup = ANY(ws.auto_add_instance_groups)\n ",
"query": "\n SELECT\n eig.igroup as group_name,\n ws.auto_invite->'instance_groups_roles' as instance_groups_roles\n FROM email_to_igroup eig\n INNER JOIN workspace_settings ws ON ws.workspace_id = $1\n WHERE eig.email = $2\n AND ws.auto_invite->'instance_groups' ? eig.igroup\n ",
"describe": {
"columns": [
{
@@ -10,7 +10,7 @@
},
{
"ordinal": 1,
"name": "auto_add_instance_groups_roles",
"name": "instance_groups_roles",
"type_info": "Jsonb"
}
],
@@ -22,8 +22,8 @@
},
"nullable": [
false,
true
null
]
},
"hash": "1ff185d8b8b897a72180cd0002e0f7e9858eef249577ce23969522793c6b5608"
"hash": "7e5913aa98624836a9bd7b5cc5cb10cc79a1249f48de45ea1c15235d4b05576b"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n error_handler->>'path'\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "825ee06c5f88436ff7bb45e389552836ae8496d75d7cf7fee497b14943b1b6f6"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT success_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "87044e1466cd114cbef336620e96dc29f3f930149fedc240d44c614dfc794ff7"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n ig.name as group_name,\n ws.workspace_id,\n w.name as workspace_name,\n ws.auto_add_instance_groups_roles->ig.name as role\n FROM instance_group ig\n INNER JOIN workspace_settings ws ON ws.auto_add_instance_groups IS NOT NULL\n AND ig.name = ANY(ws.auto_add_instance_groups)\n INNER JOIN workspace w ON w.id = ws.workspace_id AND w.deleted = false\n ORDER BY ig.name, ws.workspace_id\n ",
"query": "\n SELECT\n ig.name as group_name,\n ws.workspace_id,\n w.name as workspace_name,\n ws.auto_invite->'instance_groups_roles'->ig.name as role\n FROM instance_group ig\n INNER JOIN workspace_settings ws ON ws.auto_invite->'instance_groups' IS NOT NULL\n AND ws.auto_invite->'instance_groups' ? ig.name\n INNER JOIN workspace w ON w.id = ws.workspace_id AND w.deleted = false\n ORDER BY ig.name, ws.workspace_id\n ",
"describe": {
"columns": [
{
@@ -34,5 +34,5 @@
null
]
},
"hash": "1e6c125c884002a1565b11a2c308ce89f9726d21bbb3cc712ad0b6450cbb44e6"
"hash": "87cb0e9503776dbaea70652612cf43ecedc55b2e6319aefcf9797e7dfad0812c"
}
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n workspace_settings\n SET\n success_handler = $1,\n success_handler_extra_args = $2\n WHERE\n workspace_id = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Json",
"Text"
]
},
"nullable": []
},
"hash": "9044c71dc8aab7f5d17bf27c4702e67f3eee46d7dbccb09cadeaafa0534952d6"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET success_handler = $1 WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Text"
]
},
"nullable": []
},
"hash": "908ab9eda5ed0f3e76ea5a0e8a1714942358e2620f3bdfb080504ec1da332dbd"
}
@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE\n workspace_settings\n SET\n error_handler = $1,\n error_handler_extra_args = $2,\n error_handler_muted_on_cancel = $3\n WHERE\n workspace_id = $4\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Json",
"Bool",
"Text"
]
},
"nullable": []
},
"hash": "97b6e5779661343587c297b275df9e75fd22924ad2dbe124bb361f3e7fd8bfe0"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = 'flow/f/test/success_flow'\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "a2bc410e1066ddc89ca4557ca0e29339e41cbc7a7ef6ad97b3f0eb93de04c359"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, auto_add_instance_groups_roles FROM workspace_settings WHERE $1 = ANY(COALESCE(auto_add_instance_groups, '{}'))",
"query": "\n SELECT workspace_id, auto_invite->'instance_groups_roles' as instance_groups_roles\n FROM workspace_settings\n WHERE auto_invite->'instance_groups' ? $1\n ",
"describe": {
"columns": [
{
@@ -10,7 +10,7 @@
},
{
"ordinal": 1,
"name": "auto_add_instance_groups_roles",
"name": "instance_groups_roles",
"type_info": "Jsonb"
}
],
@@ -21,8 +21,8 @@
},
"nullable": [
false,
true
null
]
},
"hash": "53210d19693ba46d97352b0ee321af59db453243c0cd17928ee942723d9cdc5c"
"hash": "acc9be5aa111985db14025ac0e0845031f65f214bbf1c599c3ee1d1e7ac0b50f"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET error_handler = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "c0635f65d561c1a6b183b8fea45320482375d0bb5e617a9dd236a631a7b9ff05"
}
@@ -1,118 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n -- slack_team_id,\n -- slack_name,\n -- slack_command_script,\n -- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,\n auto_invite_domain IS NOT NULL AS \"auto_invite_enabled!\",\n CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS \"auto_invite_as!\",\n CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS \"auto_invite_mode!\",\n webhook,\n deploy_to,\n error_handler,\n ai_config,\n error_handler_extra_args,\n error_handler_muted_on_cancel,\n large_file_storage,\n git_sync,\n default_app,\n default_scripts,\n workspace.name,\n mute_critical_alerts,\n color,\n operator_settings\n FROM workspace_settings\n LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id\n WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "auto_invite_enabled!",
"type_info": "Bool"
},
{
"ordinal": 1,
"name": "auto_invite_as!",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "auto_invite_mode!",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "webhook",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "deploy_to",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "error_handler",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "ai_config",
"type_info": "Jsonb"
},
{
"ordinal": 7,
"name": "error_handler_extra_args",
"type_info": "Json"
},
{
"ordinal": 8,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 9,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 10,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 11,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 13,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 14,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 15,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 16,
"name": "operator_settings",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null,
null,
null,
true,
true,
true,
true,
true,
false,
true,
true,
true,
true,
false,
true,
true,
true
]
},
"hash": "c12a0b0d423577afbb2772f971bffe2635785b53d31d0ceb3d72226a0c316e8a"
}
@@ -0,0 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = '{\"path\": \"flow/f/test/success_flow\"}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "c2f271bcea1f11b2979152cc743559236fd875192b7b7f4824c63675fdc6a551"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id FROM workspace_settings WHERE auto_add_instance_groups IS NOT NULL AND $1 = ANY(auto_add_instance_groups)",
"query": "SELECT workspace_id FROM workspace_settings WHERE auto_invite->'instance_groups' IS NOT NULL AND auto_invite->'instance_groups' ? $1",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
false
]
},
"hash": "711c72a72298bb656882b35071e910dab721cda070f9c39bba3729bdc1467496"
"hash": "c7c0b7f760f9616ec4a18a8916226b65990f5055c1bc25eedefd303be75f553f"
}
@@ -1,238 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n workspace_id,\n slack_team_id,\n teams_team_id,\n teams_team_name,\n teams_team_guid,\n slack_name,\n slack_command_script,\n teams_command_script,\n slack_email,\n slack_oauth_client_id,\n slack_oauth_client_secret,\n auto_invite_domain,\n auto_invite_operator,\n auto_add,\n customer_id,\n plan,\n webhook,\n deploy_to,\n ai_config,\n error_handler,\n error_handler_extra_args,\n error_handler_muted_on_cancel,\n success_handler,\n success_handler_extra_args,\n large_file_storage,\n datatable,\n ducklake,\n git_sync,\n deploy_ui,\n default_app,\n default_scripts,\n mute_critical_alerts,\n color,\n operator_settings,\n git_app_installations,\n auto_add_instance_groups,\n auto_add_instance_groups_roles\n FROM\n workspace_settings\n WHERE\n workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "slack_team_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "teams_team_id",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "teams_team_name",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "teams_team_guid",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "slack_name",
"type_info": "Varchar"
},
{
"ordinal": 6,
"name": "slack_command_script",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "teams_command_script",
"type_info": "Text"
},
{
"ordinal": 8,
"name": "slack_email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "slack_oauth_client_id",
"type_info": "Varchar"
},
{
"ordinal": 10,
"name": "slack_oauth_client_secret",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "auto_invite_domain",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "auto_invite_operator",
"type_info": "Bool"
},
{
"ordinal": 13,
"name": "auto_add",
"type_info": "Bool"
},
{
"ordinal": 14,
"name": "customer_id",
"type_info": "Varchar"
},
{
"ordinal": 15,
"name": "plan",
"type_info": "Varchar"
},
{
"ordinal": 16,
"name": "webhook",
"type_info": "Text"
},
{
"ordinal": 17,
"name": "deploy_to",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "ai_config",
"type_info": "Jsonb"
},
{
"ordinal": 19,
"name": "error_handler",
"type_info": "Varchar"
},
{
"ordinal": 20,
"name": "error_handler_extra_args",
"type_info": "Json"
},
{
"ordinal": 21,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 22,
"name": "success_handler",
"type_info": "Text"
},
{
"ordinal": 23,
"name": "success_handler_extra_args",
"type_info": "Json"
},
{
"ordinal": 24,
"name": "large_file_storage",
"type_info": "Jsonb"
},
{
"ordinal": 25,
"name": "datatable",
"type_info": "Jsonb"
},
{
"ordinal": 26,
"name": "ducklake",
"type_info": "Jsonb"
},
{
"ordinal": 27,
"name": "git_sync",
"type_info": "Jsonb"
},
{
"ordinal": 28,
"name": "deploy_ui",
"type_info": "Jsonb"
},
{
"ordinal": 29,
"name": "default_app",
"type_info": "Varchar"
},
{
"ordinal": 30,
"name": "default_scripts",
"type_info": "Jsonb"
},
{
"ordinal": 31,
"name": "mute_critical_alerts",
"type_info": "Bool"
},
{
"ordinal": 32,
"name": "color",
"type_info": "Varchar"
},
{
"ordinal": 33,
"name": "operator_settings",
"type_info": "Jsonb"
},
{
"ordinal": 34,
"name": "git_app_installations",
"type_info": "Jsonb"
},
{
"ordinal": 35,
"name": "auto_add_instance_groups",
"type_info": "TextArray"
},
{
"ordinal": 36,
"name": "auto_add_instance_groups_roles",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
true
]
},
"hash": "d0dfd217a4f53aaee7b83d5b5267f74e5ad617830dd835a45e17fd378bcbeada"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET\n auto_invite = jsonb_set(\n jsonb_set(\n COALESCE(auto_invite, '{}'::jsonb),\n '{instance_groups}',\n (SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb) FROM jsonb_array_elements(COALESCE(auto_invite->'instance_groups', '[]'::jsonb)) elem WHERE elem #>> '{}' != $1)\n ),\n '{instance_groups_roles}',\n COALESCE(auto_invite->'instance_groups_roles', '{}'::jsonb) - $1\n )\n WHERE auto_invite->'instance_groups' IS NOT NULL AND auto_invite->'instance_groups' ? $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "dab323eda1fcaff0435e98d77e544ed5d63dd6023b1dab77d5f188b299f03b9d"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n error_handler \n FROM \n workspace_settings \n WHERE \n workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "error_handler",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "e03cfb65fc9b19c7e68e43d14a6e5041db783f1e6d545533a2d95f54e616b9fb"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = 'script/f/test/success_handler'\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "e3f2459d9ac3534531d5724bb2d67be6f33f8ca02ef4768bccd32a33a70a4300"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET error_handler = $1 WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Text"
]
},
"nullable": []
},
"hash": "e844b9ee75a1b5438ffe743185fc78f649db89c78b2b8cd7e778bf18a6b41e67"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "e9808cc152e1ce4717260ebb42f203ca16321a4550073258734a6208b83c00c2"
}
@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "error_handler",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "f8b34e09453d51d3df5be20652938a890ee353fec64ee73c312a3352da7f7515"
}
@@ -1,35 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace_id, auto_invite_operator, auto_add FROM workspace_settings ws WHERE (auto_invite_domain = $1 OR auto_invite_domain = '*') AND NOT EXISTS (SELECT 1 FROM usr WHERE workspace_id = ws.workspace_id AND email = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "auto_invite_operator",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "auto_add",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
true,
true
]
},
"hash": "fc8fa909ef7a15c7ad30dd938be3c13864e855219c7f366c86f0605ec25d1538"
}
+1 -1
View File
@@ -1 +1 @@
1549849fadc4e5634334a384bfe52343eb1e93f0
13825341581c1b9eb7a415f0ae25dbc9ba84059f
@@ -0,0 +1,46 @@
-- Step 0: Drop the GIN index on auto_invite
DROP INDEX IF EXISTS idx_workspace_settings_auto_invite;
-- Step 1: Rename current JSONB columns to temporary names to avoid collision
ALTER TABLE workspace_settings
RENAME COLUMN error_handler TO error_handler_jsonb;
ALTER TABLE workspace_settings
RENAME COLUMN success_handler TO success_handler_jsonb;
-- Step 2: Restore old columns with original names and types
ALTER TABLE workspace_settings
ADD COLUMN IF NOT EXISTS auto_invite_domain VARCHAR(255),
ADD COLUMN IF NOT EXISTS auto_invite_operator BOOLEAN,
ADD COLUMN IF NOT EXISTS auto_add BOOLEAN,
ADD COLUMN IF NOT EXISTS auto_add_instance_groups TEXT[],
ADD COLUMN IF NOT EXISTS auto_add_instance_groups_roles JSONB,
ADD COLUMN IF NOT EXISTS error_handler VARCHAR(255),
ADD COLUMN IF NOT EXISTS error_handler_extra_args JSON,
ADD COLUMN IF NOT EXISTS error_handler_muted_on_cancel BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS error_handler_muted_on_user_path BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS success_handler TEXT,
ADD COLUMN IF NOT EXISTS success_handler_extra_args JSON;
-- Step 3: Migrate data back from JSONB columns to flat columns
UPDATE workspace_settings SET
auto_invite_domain = auto_invite->>'domain',
auto_invite_operator = COALESCE((auto_invite->>'operator')::boolean, false),
auto_add = (auto_invite->>'mode') = 'add',
auto_add_instance_groups = CASE
WHEN auto_invite->'instance_groups' IS NOT NULL AND auto_invite->'instance_groups' != 'null'::jsonb
THEN ARRAY(SELECT jsonb_array_elements_text(auto_invite->'instance_groups'))
ELSE NULL
END,
auto_add_instance_groups_roles = auto_invite->'instance_groups_roles',
error_handler = error_handler_jsonb->>'path',
error_handler_extra_args = (error_handler_jsonb->'extra_args')::json,
error_handler_muted_on_cancel = COALESCE((error_handler_jsonb->>'muted_on_cancel')::boolean, false),
error_handler_muted_on_user_path = COALESCE((error_handler_jsonb->>'muted_on_user_path')::boolean, false),
success_handler = success_handler_jsonb->>'path',
success_handler_extra_args = (success_handler_jsonb->'extra_args')::json;
-- Step 4: Drop the JSONB columns
ALTER TABLE workspace_settings
DROP COLUMN IF EXISTS auto_invite,
DROP COLUMN IF EXISTS error_handler_jsonb,
DROP COLUMN IF EXISTS success_handler_jsonb;
@@ -0,0 +1,59 @@
-- Step 1: Add error_handler_muted_on_user_path column if it doesn't exist (was previously a separate migration)
ALTER TABLE workspace_settings ADD COLUMN IF NOT EXISTS error_handler_muted_on_user_path BOOL NOT NULL DEFAULT false;
-- Step 2: Add new JSONB columns with temporary names (to avoid collision with existing TEXT columns)
ALTER TABLE workspace_settings
ADD COLUMN IF NOT EXISTS auto_invite JSONB,
ADD COLUMN IF NOT EXISTS error_handler_new JSONB,
ADD COLUMN IF NOT EXISTS success_handler_new JSONB;
-- Step 3: Migrate existing data to new columns (use jsonb_strip_nulls to omit null values)
UPDATE workspace_settings SET
auto_invite = jsonb_strip_nulls(jsonb_build_object(
'enabled', auto_invite_domain IS NOT NULL,
'domain', auto_invite_domain,
'operator', COALESCE(auto_invite_operator, false),
'mode', CASE WHEN auto_add THEN 'add' ELSE 'invite' END,
'instance_groups', CASE WHEN auto_add_instance_groups IS NOT NULL THEN to_jsonb(auto_add_instance_groups) ELSE NULL END,
'instance_groups_roles', auto_add_instance_groups_roles
)),
error_handler_new = CASE
WHEN error_handler IS NOT NULL THEN jsonb_strip_nulls(jsonb_build_object(
'path', error_handler,
'extra_args', error_handler_extra_args::jsonb,
'muted_on_cancel', error_handler_muted_on_cancel,
'muted_on_user_path', error_handler_muted_on_user_path
))
ELSE NULL
END,
success_handler_new = CASE
WHEN success_handler IS NOT NULL THEN jsonb_strip_nulls(jsonb_build_object(
'path', success_handler,
'extra_args', success_handler_extra_args::jsonb
))
ELSE NULL
END;
-- Step 4: Drop old columns that are now consolidated
ALTER TABLE workspace_settings
DROP COLUMN IF EXISTS auto_invite_domain,
DROP COLUMN IF EXISTS auto_invite_operator,
DROP COLUMN IF EXISTS auto_add,
DROP COLUMN IF EXISTS auto_add_instance_groups,
DROP COLUMN IF EXISTS auto_add_instance_groups_roles,
DROP COLUMN IF EXISTS error_handler,
DROP COLUMN IF EXISTS error_handler_extra_args,
DROP COLUMN IF EXISTS error_handler_muted_on_cancel,
DROP COLUMN IF EXISTS error_handler_muted_on_user_path,
DROP COLUMN IF EXISTS success_handler,
DROP COLUMN IF EXISTS success_handler_extra_args;
-- Step 5: Rename new columns to their final names
ALTER TABLE workspace_settings
RENAME COLUMN error_handler_new TO error_handler;
ALTER TABLE workspace_settings
RENAME COLUMN success_handler_new TO success_handler;
-- Step 6: Add GIN index on auto_invite for efficient JSONB queries (e.g., ? operator for instance_groups)
-- Using default GIN opclass (not jsonb_path_ops) to support the ? operator
CREATE INDEX IF NOT EXISTS idx_workspace_settings_auto_invite ON workspace_settings USING GIN (auto_invite);
+9 -10
View File
@@ -12,12 +12,11 @@ async fn test_success_handler_cache(db: Pool<Postgres>) -> anyhow::Result<()> {
// First, create a success handler script
let _server = ApiServer::start(db.clone()).await?;
// Set up a success handler in workspace_settings
// Set up a success handler in workspace_settings using new JSONB column
sqlx::query!(
r#"
UPDATE workspace_settings
SET success_handler = 'script/f/test/success_handler',
success_handler_extra_args = '{"key": "value"}'::json
SET success_handler = '{"path": "script/f/test/success_handler", "extra_args": {"key": "value"}}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
@@ -26,7 +25,7 @@ async fn test_success_handler_cache(db: Pool<Postgres>) -> anyhow::Result<()> {
// Verify the success handler was set
let result = sqlx::query_scalar!(
r#"SELECT success_handler FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
r#"SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
@@ -35,7 +34,7 @@ async fn test_success_handler_cache(db: Pool<Postgres>) -> anyhow::Result<()> {
// Verify extra args were set
let extra_args = sqlx::query_scalar!(
r#"SELECT success_handler_extra_args FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
r#"SELECT success_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
@@ -55,7 +54,7 @@ async fn test_success_handler_settings(db: Pool<Postgres>) -> anyhow::Result<()>
// Initially success_handler should be NULL
let initial = sqlx::query_scalar!(
r#"SELECT success_handler FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
r#"SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
@@ -65,7 +64,7 @@ async fn test_success_handler_settings(db: Pool<Postgres>) -> anyhow::Result<()>
sqlx::query!(
r#"
UPDATE workspace_settings
SET success_handler = 'flow/f/test/success_flow'
SET success_handler = '{"path": "flow/f/test/success_flow"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
@@ -73,7 +72,7 @@ async fn test_success_handler_settings(db: Pool<Postgres>) -> anyhow::Result<()>
.await?;
let after_set = sqlx::query_scalar!(
r#"SELECT success_handler FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
r#"SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
@@ -91,7 +90,7 @@ async fn test_success_handler_settings(db: Pool<Postgres>) -> anyhow::Result<()>
.await?;
let after_remove = sqlx::query_scalar!(
r#"SELECT success_handler FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
r#"SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
@@ -152,7 +151,7 @@ export async function main(path: string, email: string, job_id: string, is_flow:
sqlx::query!(
r#"
UPDATE workspace_settings
SET success_handler = 'script/f/test/success_handler'
SET success_handler = '{"path": "script/f/test/success_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
+119 -38
View File
@@ -2337,20 +2337,8 @@ paths:
type: string
teams_team_guid:
type: string
auto_invite_domain:
type: string
auto_invite_operator:
type: boolean
auto_add:
type: boolean
auto_add_instance_groups:
type: array
items:
type: string
auto_add_instance_groups_roles:
type: object
additionalProperties:
type: string
auto_invite:
$ref: "#/components/schemas/AutoInviteConfig"
plan:
type: string
customer_id:
@@ -2362,15 +2350,9 @@ paths:
ai_config:
$ref: "#/components/schemas/AIConfig"
error_handler:
type: string
error_handler_extra_args:
$ref: "#/components/schemas/ScriptArgs"
error_handler_muted_on_cancel:
type: boolean
$ref: "#/components/schemas/ErrorHandlerConfig"
success_handler:
type: string
success_handler_extra_args:
$ref: "#/components/schemas/ScriptArgs"
$ref: "#/components/schemas/SuccessHandlerConfig"
large_file_storage:
$ref: "#/components/schemas/LargeFileStorage"
ducklake:
@@ -2391,8 +2373,6 @@ paths:
type: string
operator_settings:
$ref: "#/components/schemas/OperatorSettings"
required:
- error_handler_muted_on_cancel
/w/{workspace}/workspaces/get_deploy_to:
get:
@@ -3071,14 +3051,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
error_handler:
type: string
error_handler_extra_args:
$ref: "#/components/schemas/ScriptArgs"
error_handler_muted_on_cancel:
type: boolean
$ref: "#/components/schemas/EditErrorHandler"
responses:
"200":
description: status
@@ -3101,12 +3074,7 @@ paths:
content:
application/json:
schema:
type: object
properties:
success_handler:
type: string
success_handler_extra_args:
$ref: "#/components/schemas/ScriptArgs"
$ref: "#/components/schemas/EditSuccessHandler"
responses:
"200":
description: status
@@ -16703,6 +16671,119 @@ components:
# -- INLINE END --
# Do not change line above
AutoInviteConfig:
type: object
description: Configuration for auto-inviting users to the workspace
properties:
enabled:
type: boolean
default: false
domain:
type: string
operator:
type: boolean
default: false
description: If true, auto-invited users are added as operators. If false, they are added as developers.
mode:
type: string
enum: ["invite", "add"]
default: "invite"
instance_groups:
type: array
items:
type: string
instance_groups_roles:
type: object
additionalProperties:
type: string
ErrorHandlerConfig:
type: object
description: Configuration for the workspace error handler
properties:
path:
type: string
description: Path to the error handler script or flow
extra_args:
$ref: "#/components/schemas/ScriptArgs"
muted_on_cancel:
type: boolean
default: false
muted_on_user_path:
type: boolean
default: false
SuccessHandlerConfig:
type: object
description: Configuration for the workspace success handler
properties:
path:
type: string
description: Path to the success handler script or flow
extra_args:
$ref: "#/components/schemas/ScriptArgs"
EditErrorHandler:
description: Request body for editing the workspace error handler. Accepts both new grouped format and legacy flat format for backward compatibility.
oneOf:
- $ref: "#/components/schemas/EditErrorHandlerNew"
- $ref: "#/components/schemas/EditErrorHandlerLegacy"
EditErrorHandlerNew:
type: object
description: New grouped format for editing error handler
properties:
path:
type: string
description: Path to the error handler script or flow
extra_args:
$ref: "#/components/schemas/ScriptArgs"
muted_on_cancel:
type: boolean
default: false
muted_on_user_path:
type: boolean
default: false
EditErrorHandlerLegacy:
type: object
description: Legacy flat format for editing error handler (deprecated, use new format)
properties:
error_handler:
type: string
description: Path to the error handler script or flow
error_handler_extra_args:
$ref: "#/components/schemas/ScriptArgs"
error_handler_muted_on_cancel:
type: boolean
default: false
EditSuccessHandler:
description: Request body for editing the workspace success handler. Accepts both new grouped format and legacy flat format for backward compatibility.
oneOf:
- $ref: "#/components/schemas/EditSuccessHandlerNew"
- $ref: "#/components/schemas/EditSuccessHandlerLegacy"
EditSuccessHandlerNew:
type: object
description: New grouped format for editing success handler
properties:
path:
type: string
description: Path to the success handler script or flow
extra_args:
$ref: "#/components/schemas/ScriptArgs"
EditSuccessHandlerLegacy:
type: object
description: Legacy flat format for editing success handler (deprecated, use new format)
properties:
success_handler:
type: string
description: Path to the success handler script or flow
success_handler_extra_args:
$ref: "#/components/schemas/ScriptArgs"
VaultSettings:
type: object
required:
+4 -4
View File
@@ -289,10 +289,10 @@ async fn toggle_workspace_error_handler(
let error_handler_maybe: Option<String> = sqlx::query_scalar!(
r#"
SELECT
error_handler
FROM
workspace_settings
WHERE
error_handler->>'path'
FROM
workspace_settings
WHERE
workspace_id = $1
"#,
w_id
+12 -5
View File
@@ -682,9 +682,16 @@ async fn add_user_igroup(
#[cfg(all(feature = "private", feature = "enterprise"))]
{
use crate::workspaces_ee::auto_add_user;
let workspaces = sqlx::query!("SELECT workspace_id, auto_add_instance_groups_roles FROM workspace_settings WHERE $1 = ANY(COALESCE(auto_add_instance_groups, '{}'))", &name).fetch_all(&mut *tx).await?;
let workspaces = sqlx::query!(
r#"
SELECT workspace_id, auto_invite->'instance_groups_roles' as instance_groups_roles
FROM workspace_settings
WHERE auto_invite->'instance_groups' ? $1
"#,
&name
).fetch_all(&mut *tx).await?;
for ws in workspaces {
let role = ws.auto_add_instance_groups_roles.and_then(|r| r.get(&name).and_then(|v| v.as_str().map(String::from))).unwrap_or_else(|| "developer".to_string());
let role = ws.instance_groups_roles.and_then(|r| r.get(&name).and_then(|v| v.as_str().map(String::from))).unwrap_or_else(|| "developer".to_string());
let (is_admin, is_operator) = match role.as_str() { "admin" => (true, false), "operator" => (false, true), _ => (false, false) };
auto_add_user(&email, &ws.workspace_id, &is_operator, &mut tx, &authed, Some(serde_json::json!({"source": "instance_group", "group": &name}))).await?;
if is_admin { sqlx::query!("UPDATE usr SET is_admin = true WHERE workspace_id = $1 AND email = $2", &ws.workspace_id, &email).execute(&mut *tx).await?; }
@@ -748,10 +755,10 @@ async fn list_igroups_with_workspaces(Extension(db): Extension<DB>) -> JsonResul
ig.name as group_name,
ws.workspace_id,
w.name as workspace_name,
ws.auto_add_instance_groups_roles->ig.name as role
ws.auto_invite->'instance_groups_roles'->ig.name as role
FROM instance_group ig
INNER JOIN workspace_settings ws ON ws.auto_add_instance_groups IS NOT NULL
AND ig.name = ANY(ws.auto_add_instance_groups)
INNER JOIN workspace_settings ws ON ws.auto_invite->'instance_groups' IS NOT NULL
AND ws.auto_invite->'instance_groups' ? ig.name
INNER JOIN workspace w ON w.id = ws.workspace_id AND w.deleted = false
ORDER BY ig.name, ws.workspace_id
"#
+1 -1
View File
@@ -1467,7 +1467,7 @@ async fn toggle_workspace_error_handler(
let mut tx = user_db.begin(&authed).await?;
let error_handler_maybe: Option<String> = sqlx::query_scalar!(
"SELECT error_handler FROM workspace_settings WHERE workspace_id = $1",
"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = $1",
w_id
)
.fetch_optional(&mut *tx)
+3 -3
View File
@@ -1465,11 +1465,11 @@ async fn convert_user_to_group(
r#"
SELECT
eig.igroup as group_name,
ws.auto_add_instance_groups_roles
ws.auto_invite->'instance_groups_roles' as instance_groups_roles
FROM email_to_igroup eig
INNER JOIN workspace_settings ws ON ws.workspace_id = $1
WHERE eig.email = $2
AND eig.igroup = ANY(ws.auto_add_instance_groups)
AND ws.auto_invite->'instance_groups' ? eig.igroup
"#,
&w_id,
&user_info.email
@@ -1486,7 +1486,7 @@ async fn convert_user_to_group(
// Determine the group with highest precedence (same logic as process_instance_group_auto_adds)
let roles: std::collections::HashMap<String, String> =
if let Some(roles_json) = &eligible_groups[0].auto_add_instance_groups_roles {
if let Some(roles_json) = &eligible_groups[0].instance_groups_roles {
serde_json::from_value(roles_json.clone()).unwrap_or_default()
} else {
std::collections::HashMap::new()
+120 -87
View File
@@ -249,12 +249,6 @@ pub struct WorkspaceSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub slack_oauth_client_secret: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_invite_domain: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_invite_operator: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_add: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub customer_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan: Option<String>,
@@ -265,25 +259,15 @@ pub struct WorkspaceSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_handler: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_handler_extra_args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_handler_muted_on_cancel: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub success_handler: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub success_handler_extra_args: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub large_file_storage: Option<serde_json::Value>, // effectively: DatasetsStorage
pub large_file_storage: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ducklake: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub datatable: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_sync: Option<serde_json::Value>, // effectively: WorkspaceGitSyncSettings
pub git_sync: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deploy_ui: Option<serde_json::Value>, // effectively: WorkspaceDeploymentUISettings
pub deploy_ui: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_app: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -296,13 +280,16 @@ pub struct WorkspaceSettings {
pub operator_settings: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_app_installations: Option<serde_json::Value>,
// Grouped config fields
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_add_instance_groups: Option<Vec<String>>,
pub auto_invite: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_add_instance_groups_roles: Option<serde_json::Value>,
pub error_handler: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub success_handler: Option<serde_json::Value>,
}
// #[derive(sqlx::Type, Serialize, Deserialize, Debug)]
/// #[derive(sqlx::Type, Serialize, Deserialize, Debug)]
// #[sqlx(type_name = "WORKSPACE_KEY_KIND", rename_all = "lowercase")]
// pub enum WorkspaceKeyKind {
// Cloud,
@@ -440,19 +427,84 @@ pub struct NewWorkspaceUser {
pub operator: bool,
}
// New format for error handler (grouped)
#[derive(Deserialize)]
pub struct EditErrorHandler {
pub error_handler: Option<String>,
pub error_handler_extra_args: Option<serde_json::Value>,
pub error_handler_muted_on_cancel: Option<bool>,
#[serde(deny_unknown_fields)]
pub struct EditErrorHandlerNew {
pub path: Option<String>,
pub extra_args: Option<serde_json::Value>,
#[serde(default)]
pub muted_on_cancel: bool,
#[serde(default)]
pub muted_on_user_path: bool,
}
// Legacy format for error handler (flat fields from old CLI)
#[derive(Deserialize)]
pub struct EditSuccessHandler {
pub struct EditErrorHandlerLegacy {
pub error_handler: Option<String>,
pub error_handler_extra_args: Option<serde_json::Value>,
#[serde(default)]
pub error_handler_muted_on_cancel: bool,
}
// Accepts both old and new formats
#[derive(Deserialize)]
#[serde(untagged)]
pub enum EditErrorHandler {
New(EditErrorHandlerNew),
Legacy(EditErrorHandlerLegacy),
}
impl EditErrorHandler {
pub fn into_normalized(self) -> EditErrorHandlerNew {
match self {
EditErrorHandler::New(new) => new,
EditErrorHandler::Legacy(legacy) => EditErrorHandlerNew {
path: legacy.error_handler,
extra_args: legacy.error_handler_extra_args,
muted_on_cancel: legacy.error_handler_muted_on_cancel,
muted_on_user_path: false, // Old format doesn't have this field
},
}
}
}
// New format for success handler (grouped)
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EditSuccessHandlerNew {
pub path: Option<String>,
pub extra_args: Option<serde_json::Value>,
}
// Legacy format for success handler (flat fields from old CLI)
#[derive(Deserialize)]
pub struct EditSuccessHandlerLegacy {
pub success_handler: Option<String>,
pub success_handler_extra_args: Option<serde_json::Value>,
}
// Accepts both old and new formats
#[derive(Deserialize)]
#[serde(untagged)]
pub enum EditSuccessHandler {
New(EditSuccessHandlerNew),
Legacy(EditSuccessHandlerLegacy),
}
impl EditSuccessHandler {
pub fn into_normalized(self) -> EditSuccessHandlerNew {
match self {
EditSuccessHandler::New(new) => new,
EditSuccessHandler::Legacy(legacy) => EditSuccessHandlerNew {
path: legacy.success_handler,
extra_args: legacy.success_handler_extra_args,
},
}
}
}
lazy_static::lazy_static! {
pub static ref EMAIL_REGEXP: Regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
}
@@ -555,19 +607,11 @@ async fn get_settings(
slack_email,
slack_oauth_client_id,
slack_oauth_client_secret,
auto_invite_domain,
auto_invite_operator,
auto_add,
customer_id,
plan,
webhook,
deploy_to,
ai_config,
error_handler,
error_handler_extra_args,
error_handler_muted_on_cancel,
success_handler,
success_handler_extra_args,
large_file_storage,
datatable,
ducklake,
@@ -579,8 +623,9 @@ async fn get_settings(
color,
operator_settings,
git_app_installations,
auto_add_instance_groups,
auto_add_instance_groups_roles
auto_invite,
error_handler,
success_handler
FROM
workspace_settings
WHERE
@@ -745,9 +790,8 @@ async fn get_slack_oauth_config(
) -> JsonResult<GetSlackOAuthConfigResponse> {
require_admin(authed.is_admin, &authed.username)?;
let settings = sqlx::query_as!(
WorkspaceSettings,
"SELECT * FROM workspace_settings WHERE workspace_id = $1",
let settings = sqlx::query!(
"SELECT slack_oauth_client_id, slack_oauth_client_secret FROM workspace_settings WHERE workspace_id = $1",
&w_id
)
.fetch_one(&db)
@@ -2158,6 +2202,9 @@ async fn edit_error_handler(
) -> Result<String> {
require_admin(is_admin, &username)?;
// Normalize to new format (handles both old CLI and new CLI requests)
let ee = ee.into_normalized();
let mut tx = db.begin().await?;
sqlx::query_as!(
@@ -2171,8 +2218,8 @@ async fn edit_error_handler(
.execute(&mut *tx)
.await?;
if let Some(error_handler) = &ee.error_handler {
match ee.error_handler_extra_args.as_ref() {
if let Some(path) = &ee.path {
match ee.extra_args.as_ref() {
Some(extra_args) if extra_args.is_object() => {
let Ok(email_recipients) = serde_json::from_value::<Option<Vec<String>>>(
extra_args["email_recipients"].to_owned(),
@@ -2196,41 +2243,34 @@ async fn edit_error_handler(
None => {}
_ => {
return Err(Error::BadRequest(
"Field `error_handler_extra_args` expected to be JSON object".to_string(),
"Field `extra_args` expected to be JSON object".to_string(),
))
}
}
let mut error_handler = serde_json::json!({
"path": path,
});
if let Some(extra_args) = &ee.extra_args {
error_handler["extra_args"] = extra_args.clone();
}
if ee.muted_on_cancel {
error_handler["muted_on_cancel"] = serde_json::json!(true);
}
if ee.muted_on_user_path {
error_handler["muted_on_user_path"] = serde_json::json!(true);
}
sqlx::query!(
r#"
UPDATE
workspace_settings
SET
error_handler = $1,
error_handler_extra_args = $2,
error_handler_muted_on_cancel = $3
WHERE
workspace_id = $4
"#,
"UPDATE workspace_settings SET error_handler = $1 WHERE workspace_id = $2",
error_handler,
ee.error_handler_extra_args,
ee.error_handler_muted_on_cancel.unwrap_or(false),
&w_id
)
.execute(&mut *tx)
.await?;
} else {
sqlx::query!(
r#"
UPDATE
workspace_settings
SET
error_handler = NULL,
error_handler_extra_args = NULL,
error_handler_muted_on_cancel = false
WHERE
workspace_id = $1
"#,
"UPDATE workspace_settings SET error_handler = NULL WHERE workspace_id = $1",
&w_id
)
.execute(&mut *tx)
@@ -2244,7 +2284,7 @@ async fn edit_error_handler(
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("error_handler", &format!("{:?}", ee.error_handler)[..])].into()),
Some([("error_handler", &format!("{:?}", ee.path)[..])].into()),
)
.await?;
tx.commit().await?;
@@ -2272,6 +2312,9 @@ async fn edit_success_handler(
) -> Result<String> {
require_admin(authed.is_admin, &authed.username)?;
// Normalize to new format (handles both old CLI and new CLI requests)
let es = es.into_normalized();
let mut tx = db.begin().await?;
sqlx::query_as!(
@@ -2285,34 +2328,24 @@ async fn edit_success_handler(
.execute(&mut *tx)
.await?;
if let Some(success_handler) = &es.success_handler {
if let Some(path) = &es.path {
let mut success_handler = serde_json::json!({
"path": path,
});
if let Some(extra_args) = &es.extra_args {
success_handler["extra_args"] = extra_args.clone();
}
sqlx::query!(
r#"
UPDATE
workspace_settings
SET
success_handler = $1,
success_handler_extra_args = $2
WHERE
workspace_id = $3
"#,
"UPDATE workspace_settings SET success_handler = $1 WHERE workspace_id = $2",
success_handler,
es.success_handler_extra_args,
&w_id
)
.execute(&mut *tx)
.await?;
} else {
sqlx::query!(
r#"
UPDATE
workspace_settings
SET
success_handler = NULL,
success_handler_extra_args = NULL
WHERE
workspace_id = $1
"#,
"UPDATE workspace_settings SET success_handler = NULL WHERE workspace_id = $1",
&w_id
)
.execute(&mut *tx)
@@ -2326,7 +2359,7 @@ async fn edit_success_handler(
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("success_handler", &format!("{:?}", es.success_handler)[..])].into()),
Some([("success_handler", &format!("{:?}", es.path)[..])].into()),
)
.await?;
tx.commit().await?;
+136 -23
View File
@@ -177,6 +177,8 @@ pub(crate) struct ArchiveQueryParams {
include_key: Option<bool>,
include_workspace_dependencies: Option<bool>,
default_ts: Option<String>,
/// Settings format version: "v1" (default) returns legacy flat format, "v2" returns grouped format
settings_version: Option<String>,
}
#[inline]
@@ -253,12 +255,41 @@ struct SimplifiedGroup {
admins: Vec<String>,
}
// V2 format: New grouped format
#[derive(Serialize)]
struct SimplifiedSettings {
// slack_team_id: Option<String>,
// slack_name: Option<String>,
// slack_command_script: Option<String>,
// slack_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
auto_invite: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
webhook: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
deploy_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
error_handler: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
success_handler: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
ai_config: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
large_file_storage: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
git_sync: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
default_app: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
default_scripts: Option<Value>,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
mute_critical_alerts: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
operator_settings: Option<serde_json::Value>,
}
// V1 format: Legacy flat format for backward compatibility (matches main branch exactly)
#[derive(Serialize)]
struct SimplifiedSettingsLegacy {
auto_invite_enabled: bool,
auto_invite_as: String,
auto_invite_mode: String,
@@ -290,6 +321,25 @@ struct SimplifiedSettings {
operator_settings: Option<serde_json::Value>,
}
// Internal struct for querying database
#[derive(sqlx::FromRow)]
struct SettingsRow {
auto_invite: Option<Value>,
webhook: Option<String>,
deploy_to: Option<String>,
error_handler: Option<Value>,
success_handler: Option<Value>,
ai_config: Option<serde_json::Value>,
large_file_storage: Option<Value>,
git_sync: Option<Value>,
default_app: Option<String>,
default_scripts: Option<Value>,
name: Option<String>,
mute_critical_alerts: Option<bool>,
color: Option<String>,
operator_settings: Option<serde_json::Value>,
}
pub(crate) async fn tarball_workspace(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -311,6 +361,7 @@ pub(crate) async fn tarball_workspace(
include_key,
include_workspace_dependencies,
default_ts,
settings_version,
}): Query<ArchiveQueryParams>,
) -> Result<([(HeaderName, String); 2], impl IntoResponse)> {
// require_admin(authed.is_admin, &authed.username)?;
@@ -843,41 +894,103 @@ pub(crate) async fn tarball_workspace(
}
if include_settings.unwrap_or(false) {
let settings = sqlx::query_as!(
SimplifiedSettings,
let row = sqlx::query_as::<_, SettingsRow>(
r#"SELECT
-- slack_team_id,
-- slack_name,
-- slack_command_script,
-- CASE WHEN slack_email = 'missing@email.xyz' THEN NULL ELSE slack_email END AS slack_email,
auto_invite_domain IS NOT NULL AS "auto_invite_enabled!",
CASE WHEN auto_invite_operator IS TRUE THEN 'operator' ELSE 'developer' END AS "auto_invite_as!",
CASE WHEN auto_add IS TRUE THEN 'add' ELSE 'invite' END AS "auto_invite_mode!",
auto_invite,
webhook,
deploy_to,
error_handler,
success_handler,
ai_config,
error_handler_extra_args,
error_handler_muted_on_cancel,
large_file_storage,
git_sync,
default_app,
default_scripts,
workspace.name,
workspace.name as name,
mute_critical_alerts,
color,
operator_settings
FROM workspace_settings
LEFT JOIN workspace ON workspace.id = workspace_settings.workspace_id
WHERE workspace_id = $1"#,
&w_id
).fetch_one(&mut *tx).await?;
)
.bind(&w_id)
.fetch_one(&mut *tx)
.await?;
let settings_str = serde_json::to_value(settings)
.map(|v| serde_json::to_string_pretty(&v).ok())
.ok()
.flatten()
.ok_or_else(|| Error::internal_err("Error serializing settings".to_string()))?;
// Use v2 format only if explicitly requested, otherwise use v1 (legacy) for backward compatibility
let settings_str = if settings_version.as_deref() == Some("v2") {
let settings = SimplifiedSettings {
auto_invite: row.auto_invite,
webhook: row.webhook,
deploy_to: row.deploy_to,
error_handler: row.error_handler,
success_handler: row.success_handler,
ai_config: row.ai_config,
large_file_storage: row.large_file_storage,
git_sync: row.git_sync,
default_app: row.default_app,
default_scripts: row.default_scripts,
name: row.name.clone().unwrap_or_default(),
mute_critical_alerts: row.mute_critical_alerts,
color: row.color.clone(),
operator_settings: row.operator_settings.clone(),
};
serde_json::to_value(settings)
.map(|v| serde_json::to_string_pretty(&v).ok())
.ok()
.flatten()
} else {
// V1 (legacy) format: convert JSONB to flat fields (matches main branch exactly)
let (auto_invite_enabled, auto_invite_as, auto_invite_mode) =
if let Some(ref ai) = row.auto_invite {
let enabled = ai.get("enabled").and_then(|v| v.as_bool()).unwrap_or(false);
let operator = ai.get("operator").and_then(|v| v.as_bool()).unwrap_or(false);
let mode = ai.get("mode").and_then(|v| v.as_str()).unwrap_or("invite");
(
enabled,
if operator { "operator".to_string() } else { "developer".to_string() },
mode.to_string(),
)
} else {
(false, "developer".to_string(), "invite".to_string())
};
let (error_handler, error_handler_extra_args, error_handler_muted_on_cancel) =
if let Some(ref eh) = row.error_handler {
let path = eh.get("path").and_then(|v| v.as_str()).map(String::from);
let extra_args = eh.get("extra_args").cloned();
let muted_on_cancel = eh.get("muted_on_cancel").and_then(|v| v.as_bool()).unwrap_or(false);
(path, extra_args, muted_on_cancel)
} else {
(None, None, false)
};
let settings = SimplifiedSettingsLegacy {
auto_invite_enabled,
auto_invite_as,
auto_invite_mode,
webhook: row.webhook,
deploy_to: row.deploy_to,
error_handler,
error_handler_extra_args,
error_handler_muted_on_cancel,
ai_config: row.ai_config,
large_file_storage: row.large_file_storage,
git_sync: row.git_sync,
default_app: row.default_app,
default_scripts: row.default_scripts,
name: row.name.unwrap_or_default(),
mute_critical_alerts: row.mute_critical_alerts,
color: row.color,
operator_settings: row.operator_settings,
};
serde_json::to_value(settings)
.map(|v| serde_json::to_string_pretty(&v).ok())
.ok()
.flatten()
}
.ok_or_else(|| Error::internal_err("Error serializing settings".to_string()))?;
archive
.write_to_archive(&settings_str, "settings.json")
+35 -17
View File
@@ -777,8 +777,8 @@ lazy_static::lazy_static! {
static ref RESTART_UNLESS_CANCELLED_CACHE: Cache<(i64, String), bool> = Cache::new(10000);
// Cache for workspace error handler settings with 60s TTL
// Key: workspace_id, Value: (error_handler, error_handler_extra_args, error_handler_muted_on_cancel, expiry_timestamp)
static ref WORKSPACE_ERROR_HANDLER_CACHE: Cache<String, (Option<String>, Option<Json<Box<RawValue>>>, bool, i64)> = Cache::new(1000);
// Key: workspace_id, Value: (error_handler, error_handler_extra_args, error_handler_muted_on_cancel, error_handler_muted_on_user_path, expiry_timestamp)
static ref WORKSPACE_ERROR_HANDLER_CACHE: Cache<String, (Option<String>, Option<Json<Box<RawValue>>>, bool, bool, i64)> = Cache::new(1000);
// Cache for workspace success handler settings with 60s TTL
// Key: workspace_id, Value: (success_handler, success_handler_extra_args, expiry_timestamp)
@@ -1521,12 +1521,12 @@ pub async fn report_error_to_workspace_handler_or_critical_side_channel(
let w_id = &queued_job.workspace_id;
let row_result = sqlx::query_as::<_, (Option<String>, Option<Json<Box<RawValue>>>)>(
r#"
SELECT
error_handler,
error_handler_extra_args
FROM
workspace_settings
WHERE
SELECT
error_handler->>'path',
(error_handler->'extra_args')::text::json
FROM
workspace_settings
WHERE
workspace_id = $1
"#,
)
@@ -1578,10 +1578,14 @@ pub async fn report_error_to_workspace_handler_or_critical_side_channel(
async fn fetch_error_handler_from_db(
db: &Pool<Postgres>,
w_id: &str,
) -> Result<(Option<String>, Option<Json<Box<RawValue>>>, bool), Error> {
sqlx::query_as::<_, (Option<String>, Option<Json<Box<RawValue>>>, bool)>(
) -> Result<(Option<String>, Option<Json<Box<RawValue>>>, bool, bool), Error> {
sqlx::query_as::<_, (Option<String>, Option<Json<Box<RawValue>>>, Option<bool>, Option<bool>)>(
r#"
SELECT error_handler, error_handler_extra_args, error_handler_muted_on_cancel
SELECT
error_handler->>'path',
(error_handler->'extra_args')::text::json,
(error_handler->>'muted_on_cancel')::boolean,
(error_handler->>'muted_on_user_path')::boolean
FROM workspace_settings
WHERE workspace_id = $1
"#,
@@ -1590,6 +1594,9 @@ async fn fetch_error_handler_from_db(
.fetch_optional(db)
.await
.context("fetching error handler info from workspace_settings")?
.map(|(path, extra_args, muted_on_cancel, muted_on_user_path)| {
(path, extra_args, muted_on_cancel.unwrap_or(false), muted_on_user_path.unwrap_or(false))
})
.ok_or_else(|| Error::internal_err(format!("no workspace settings for id {w_id}")))
}
@@ -1602,16 +1609,16 @@ pub async fn send_error_to_workspace_handler<'a, 'c, T: Serialize + Send + Sync>
let w_id = &queued_job.workspace_id;
let now = chrono::Utc::now().timestamp();
let (error_handler, error_handler_extra_args, error_handler_muted_on_cancel) =
let (error_handler, error_handler_extra_args, error_handler_muted_on_cancel, error_handler_muted_on_user_path) =
if let Some(cached) = WORKSPACE_ERROR_HANDLER_CACHE.get(w_id) {
if cached.3 > now {
(cached.0.clone(), cached.1.clone(), cached.2)
if cached.4 > now {
(cached.0.clone(), cached.1.clone(), cached.2, cached.3)
} else {
let row = fetch_error_handler_from_db(db, w_id).await?;
let expiry = now + WORKSPACE_HANDLER_CACHE_TTL_SECONDS;
WORKSPACE_ERROR_HANDLER_CACHE.insert(
w_id.clone(),
(row.0.clone(), row.1.clone(), row.2, expiry),
(row.0.clone(), row.1.clone(), row.2, row.3, expiry),
);
row
}
@@ -1620,7 +1627,7 @@ pub async fn send_error_to_workspace_handler<'a, 'c, T: Serialize + Send + Sync>
let expiry = now + WORKSPACE_HANDLER_CACHE_TTL_SECONDS;
WORKSPACE_ERROR_HANDLER_CACHE.insert(
w_id.clone(),
(row.0.clone(), row.1.clone(), row.2, expiry),
(row.0.clone(), row.1.clone(), row.2, row.3, expiry),
);
row
};
@@ -1629,6 +1636,15 @@ pub async fn send_error_to_workspace_handler<'a, 'c, T: Serialize + Send + Sync>
return Ok(());
}
// Skip error handler for scripts/flows starting with u/
if error_handler_muted_on_user_path {
if let Some(ref path) = queued_job.runnable_path {
if path.starts_with("u/") {
return Ok(());
}
}
}
if let Some(error_handler) = error_handler {
let ws_error_handler_muted: Option<bool> = match queued_job.kind {
JobKind::Script => {
@@ -1685,7 +1701,9 @@ async fn fetch_success_handler_from_db(
) -> Result<(Option<String>, Option<Json<Box<RawValue>>>), Error> {
sqlx::query_as::<_, (Option<String>, Option<Json<Box<RawValue>>>)>(
r#"
SELECT success_handler, success_handler_extra_args
SELECT
success_handler->>'path',
(success_handler->'extra_args')::text::json
FROM workspace_settings
WHERE workspace_id = $1
"#,
+1 -1
View File
@@ -40,7 +40,7 @@ export async function downloadZip(
}&skip_secrets=${skipSecrets ?? false}&include_schedules=${includeSchedules ?? false
}&include_triggers=${includeTriggers ?? false}&include_users=${includeUsers ?? false
}&include_groups=${includeGroups ?? false}&include_settings=${includeSettings ?? false
}&include_key=${includeKey ?? false}&include_workspace_dependencies=${includeWorkspaceDependenciesValue}&default_ts=${defaultTs ?? "bun"}&skip_resource_types=${skipResourceTypes ?? false}`;
}&include_key=${includeKey ?? false}&include_workspace_dependencies=${includeWorkspaceDependenciesValue}&default_ts=${defaultTs ?? "bun"}&skip_resource_types=${skipResourceTypes ?? false}&settings_version=v2`;
const zipResponse = await fetch(url, {
headers: requestHeaders,
+156 -58
View File
@@ -8,19 +8,37 @@ import { deepEqual } from "../utils/utils.ts";
import { removeWorkerPrefix } from "../commands/worker-groups/worker-groups.ts";
import { decrypt, encrypt } from "../utils/local_encryption.ts";
// New grouped config interfaces
export interface AutoInviteConfig {
enabled: boolean;
domain?: string;
operator: boolean;
mode: "invite" | "add";
instance_groups?: string[];
instance_groups_roles?: Record<string, string>;
}
export interface ErrorHandlerConfig {
path?: string;
extra_args?: any;
muted_on_cancel?: boolean;
muted_on_user_path?: boolean;
}
export interface SuccessHandlerConfig {
path?: string;
extra_args?: any;
}
export interface SimplifiedSettings {
// slack_team_id?: string;
// slack_name?: string;
// slack_command_script?: string;
// slack_email?: string;
auto_invite_enabled: boolean;
auto_invite_as: string;
auto_invite_mode: string;
// Grouped format (current)
auto_invite?: AutoInviteConfig;
error_handler?: ErrorHandlerConfig;
success_handler?: SuccessHandlerConfig;
// Other fields
webhook?: string;
deploy_to?: string;
error_handler?: string;
error_handler_extra_args?: any;
error_handler_muted_on_cancel?: boolean;
ai_config?: AIConfig;
large_file_storage?: any;
git_sync?: any;
@@ -32,6 +50,84 @@ export interface SimplifiedSettings {
operator_settings?: any;
}
// Legacy settings interface for reading old settings.yaml files
interface LegacySimplifiedSettings {
auto_invite_enabled?: boolean;
auto_invite_as?: string;
auto_invite_mode?: string;
error_handler?: string;
error_handler_extra_args?: any;
error_handler_muted_on_cancel?: boolean;
success_handler?: string;
success_handler_extra_args?: any;
// Other fields same as SimplifiedSettings
webhook?: string;
deploy_to?: string;
ai_config?: AIConfig;
large_file_storage?: any;
git_sync?: any;
default_app?: string;
default_scripts?: any;
name?: string;
mute_critical_alerts?: boolean;
color?: string;
operator_settings?: any;
}
// Helper to convert legacy flat settings to new grouped format
export function migrateToGroupedFormat(settings: any): SimplifiedSettings {
const result: SimplifiedSettings = { name: settings.name ?? "" };
// Copy non-legacy fields
if (settings.webhook !== undefined) result.webhook = settings.webhook;
if (settings.deploy_to !== undefined) result.deploy_to = settings.deploy_to;
if (settings.ai_config !== undefined) result.ai_config = settings.ai_config;
if (settings.large_file_storage !== undefined) result.large_file_storage = settings.large_file_storage;
if (settings.git_sync !== undefined) result.git_sync = settings.git_sync;
if (settings.default_app !== undefined) result.default_app = settings.default_app;
if (settings.default_scripts !== undefined) result.default_scripts = settings.default_scripts;
if (settings.mute_critical_alerts !== undefined) result.mute_critical_alerts = settings.mute_critical_alerts;
if (settings.color !== undefined) result.color = settings.color;
if (settings.operator_settings !== undefined) result.operator_settings = settings.operator_settings;
// Handle auto_invite: check if already grouped or needs migration
if (settings.auto_invite && typeof settings.auto_invite === "object") {
result.auto_invite = settings.auto_invite;
} else if (settings.auto_invite_enabled !== undefined) {
// Legacy format
result.auto_invite = {
enabled: settings.auto_invite_enabled,
operator: settings.auto_invite_as === "operator",
mode: (settings.auto_invite_mode as "invite" | "add") ?? "invite",
};
}
// Handle error_handler: check if already grouped or needs migration
if (settings.error_handler && typeof settings.error_handler === "object") {
result.error_handler = settings.error_handler;
} else if (typeof settings.error_handler === "string") {
// Legacy format (error_handler was a string path)
result.error_handler = {
path: settings.error_handler,
extra_args: settings.error_handler_extra_args,
muted_on_cancel: settings.error_handler_muted_on_cancel ?? false,
};
}
// Handle success_handler: check if already grouped or needs migration
if (settings.success_handler && typeof settings.success_handler === "object") {
result.success_handler = settings.success_handler;
} else if (typeof settings.success_handler === "string") {
// Legacy format (success_handler was a string path)
result.success_handler = {
path: settings.success_handler,
extra_args: settings.success_handler_extra_args,
};
}
return result;
}
const INSTANCE_SETTINGS_PATH = "instance_settings.yaml";
let instanceSettingsPath = INSTANCE_SETTINGS_PATH;
async function checkInstanceSettingsPath(opts: InstanceSyncOptions) {
@@ -52,8 +148,11 @@ export async function pushWorkspaceSettings(
workspace: string,
_path: string,
settings: SimplifiedSettings | undefined,
localSettings: SimplifiedSettings
localSettings: SimplifiedSettings | any
) {
// Migrate local settings from legacy format if needed
localSettings = migrateToGroupedFormat(localSettings);
try {
const remoteSettings = await wmill.getSettings({
workspace,
@@ -63,22 +162,13 @@ export async function pushWorkspaceSettings(
workspace,
});
// Build settings from remote (now using grouped format)
settings = {
// slack_team_id: remoteSettings.slack_team_id,
// slack_name: remoteSettings.slack_name,
// slack_command_script: remoteSettings.slack_command_script,
// slack_email: remoteSettings.slack_email,
auto_invite_enabled: remoteSettings.auto_invite_domain !== null,
auto_invite_as: remoteSettings.auto_invite_operator
? "operator"
: "developer",
auto_invite_mode: remoteSettings.auto_add ? "add" : "invite",
auto_invite: remoteSettings.auto_invite as AutoInviteConfig | undefined,
error_handler: remoteSettings.error_handler as ErrorHandlerConfig | undefined,
success_handler: remoteSettings.success_handler as SuccessHandlerConfig | undefined,
webhook: remoteSettings.webhook,
deploy_to: remoteSettings.deploy_to,
error_handler: remoteSettings.error_handler,
error_handler_extra_args: remoteSettings.error_handler_extra_args,
error_handler_muted_on_cancel:
remoteSettings.error_handler_muted_on_cancel ?? false,
ai_config: remoteSettings.ai_config,
large_file_storage: remoteSettings.large_file_storage,
git_sync: remoteSettings.git_sync,
@@ -98,8 +188,9 @@ export async function pushWorkspaceSettings(
return;
}
log.debug(`Workspace settings are not up-to-date, updating...`);
if (localSettings.webhook !== settings.webhook) {
log.debug(`Updateing webhook...`);
log.debug(`Updating webhook...`);
await wmill.editWebhook({
workspace,
requestBody: {
@@ -107,32 +198,25 @@ export async function pushWorkspaceSettings(
},
});
}
if (
localSettings.auto_invite_as !== settings.auto_invite_as ||
localSettings.auto_invite_enabled !== settings.auto_invite_enabled ||
localSettings.auto_invite_mode !== settings.auto_invite_mode
) {
// Handle auto_invite using grouped format
if (!deepEqual(localSettings.auto_invite, settings.auto_invite)) {
log.debug(`Updating auto invite...`);
if (!["operator", "developer"].includes(settings.auto_invite_as)) {
const localAutoInvite = localSettings.auto_invite;
if (localAutoInvite?.mode && !["add", "invite"].includes(localAutoInvite.mode)) {
throw new Error(
`Invalid value for auto_invite_as. Valid values are "operator" and "developer"`
);
}
if (!["add", "invite"].includes(settings.auto_invite_mode)) {
throw new Error(
`Invalid value for auto_invite_mode. Valid values are "invite" and "add"`
`Invalid value for auto_invite.mode. Valid values are "invite" and "add"`
);
}
try {
await wmill.editAutoInvite({
workspace,
requestBody: localSettings.auto_invite_enabled
requestBody: localAutoInvite?.enabled
? {
operator: localSettings.auto_invite_as === "operator",
operator: localAutoInvite.operator ?? false,
invite_all: true,
auto_add: localSettings.auto_invite_mode === "add",
auto_add: localAutoInvite.mode === "add",
}
: {},
});
@@ -143,16 +227,17 @@ export async function pushWorkspaceSettings(
);
await wmill.editAutoInvite({
workspace,
requestBody: localSettings.auto_invite_enabled
requestBody: localAutoInvite?.enabled
? {
operator: localSettings.auto_invite_as === "operator",
operator: localAutoInvite.operator ?? false,
invite_all: false,
auto_add: localSettings.auto_invite_mode === "add",
auto_add: localAutoInvite.mode === "add",
}
: {},
});
}
}
if (!deepEqual(localSettings.ai_config, settings.ai_config)) {
log.debug(`Updating copilot settings...`);
await wmill.editCopilotConfig({
@@ -160,26 +245,35 @@ export async function pushWorkspaceSettings(
requestBody: localSettings.ai_config ?? {},
});
}
if (
localSettings.error_handler != settings.error_handler ||
!deepEqual(
localSettings.error_handler_extra_args,
settings.error_handler_extra_args
) ||
(localSettings.error_handler_muted_on_cancel ?? false) !=
(settings.error_handler_muted_on_cancel ?? false)
) {
// Handle error_handler using grouped format
if (!deepEqual(localSettings.error_handler, settings.error_handler)) {
log.debug(`Updating error handler...`);
const localErrorHandler = localSettings.error_handler;
await wmill.editErrorHandler({
workspace,
requestBody: {
error_handler: localSettings.error_handler,
error_handler_extra_args: localSettings.error_handler_extra_args,
error_handler_muted_on_cancel:
localSettings.error_handler_muted_on_cancel ?? false,
path: localErrorHandler?.path,
extra_args: localErrorHandler?.extra_args,
muted_on_cancel: localErrorHandler?.muted_on_cancel ?? false,
muted_on_user_path: localErrorHandler?.muted_on_user_path ?? false,
},
});
}
// Handle success_handler using grouped format
if (!deepEqual(localSettings.success_handler, settings.success_handler)) {
log.debug(`Updating success handler...`);
const localSuccessHandler = localSettings.success_handler;
await wmill.editSuccessHandler({
workspace,
requestBody: {
path: localSuccessHandler?.path,
extra_args: localSuccessHandler?.extra_args,
},
});
}
if (localSettings.deploy_to != settings.deploy_to) {
log.debug(`Updating deploy to...`);
await wmill.editDeployTo({
@@ -189,6 +283,7 @@ export async function pushWorkspaceSettings(
},
});
}
if (
!deepEqual(localSettings.large_file_storage, settings.large_file_storage)
) {
@@ -200,6 +295,7 @@ export async function pushWorkspaceSettings(
},
});
}
if (!deepEqual(localSettings.git_sync, settings.git_sync)) {
log.debug(`Updating git sync...`);
await wmill.editWorkspaceGitSyncConfig({
@@ -209,6 +305,7 @@ export async function pushWorkspaceSettings(
},
});
}
if (!deepEqual(localSettings.default_scripts, settings.default_scripts)) {
log.debug(`Updating default scripts...`);
await wmill.editDefaultScripts({
@@ -216,6 +313,7 @@ export async function pushWorkspaceSettings(
requestBody: localSettings.default_scripts,
});
}
if (localSettings.default_app != settings.default_app) {
log.debug(`Updating default app...`);
await wmill.editWorkspaceDefaultApp({
@@ -256,7 +354,7 @@ export async function pushWorkspaceSettings(
});
}
if (localSettings.operator_settings != settings.operator_settings) {
if (!deepEqual(localSettings.operator_settings, settings.operator_settings)) {
log.debug(`Updating operator settings...`);
await wmill.updateOperatorSettings({
workspace,
@@ -92,11 +92,19 @@
async function loadSettings(): Promise<void> {
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
auto_invite_domain = settings.auto_invite_domain
operatorOnly = settings.auto_invite_operator
autoAdd = settings.auto_add
autoAddInstanceGroups = settings.auto_add_instance_groups || []
autoAddInstanceGroupsRoles = settings.auto_add_instance_groups_roles || {}
const autoInvite = settings.auto_invite as {
enabled?: boolean
domain?: string
operator?: boolean
mode?: string
instance_groups?: string[]
instance_groups_roles?: Record<string, string>
} | undefined
auto_invite_domain = autoInvite?.enabled ? (autoInvite?.domain ?? '*') : undefined
operatorOnly = autoInvite?.operator ?? false
autoAdd = autoInvite?.mode === 'add'
autoAddInstanceGroups = autoInvite?.instance_groups || []
autoAddInstanceGroupsRoles = autoInvite?.instance_groups_roles || {}
}
let getUsagePromise: CancelablePromise<UserUsage[]> | undefined = undefined
@@ -98,6 +98,7 @@
let errorHandlerItemKind: 'flow' | 'script' = $state('script')
let errorHandlerExtraArgs: Record<string, any> = $state({})
let errorHandlerMutedOnCancel: boolean | undefined = $state(undefined)
let errorHandlerMutedOnUserPath: boolean | undefined = $state(undefined)
let successHandlerScriptPath: string | undefined = $state(undefined)
let criticalAlertUIMuted: boolean | undefined = $state(undefined)
let initialCriticalAlertUIMuted: boolean | undefined = $state(undefined)
@@ -335,11 +336,14 @@
initialCodeCompletionModel = codeCompletionModel
initialCustomPrompts = clone(customPrompts)
initialMaxTokensPerModel = clone(maxTokensPerModel)
errorHandlerItemKind = settings.error_handler
? (settings.error_handler.split('/')[0] as 'flow' | 'script')
const errorHandler = settings.error_handler as { path?: string; extra_args?: any; muted_on_cancel?: boolean; muted_on_user_path?: boolean } | undefined
const errorHandlerPath = errorHandler?.path ?? ''
errorHandlerItemKind = errorHandlerPath
? (errorHandlerPath.split('/')[0] as 'flow' | 'script')
: 'script'
errorHandlerScriptPath = (settings.error_handler ?? '').split('/').slice(1).join('/')
errorHandlerMutedOnCancel = settings.error_handler_muted_on_cancel
errorHandlerScriptPath = errorHandlerPath.split('/').slice(1).join('/')
errorHandlerMutedOnCancel = errorHandler?.muted_on_cancel
errorHandlerMutedOnUserPath = errorHandler?.muted_on_user_path
criticalAlertUIMuted = settings.mute_critical_alerts
initialCriticalAlertUIMuted = settings.mute_critical_alerts
if (emptyString($enterpriseLicense)) {
@@ -347,8 +351,9 @@
} else {
errorHandlerSelected = getHandlerType(errorHandlerScriptPath)
}
errorHandlerExtraArgs = settings.error_handler_extra_args ?? {}
successHandlerScriptPath = (settings.success_handler ?? '').split('/').slice(1).join('/')
errorHandlerExtraArgs = errorHandler?.extra_args ?? {}
const successHandler = settings.success_handler as { path?: string; extra_args?: any } | undefined
successHandlerScriptPath = (successHandler?.path ?? '').split('/').slice(1).join('/')
workspaceDefaultAppPath = settings.default_app
s3ResourceSettings = convertBackendSettingsToFrontendSettings(
@@ -498,9 +503,10 @@
await WorkspaceService.editErrorHandler({
workspace: $workspaceStore!,
requestBody: {
error_handler: `${errorHandlerItemKind}/${errorHandlerScriptPath}`,
error_handler_extra_args: errorHandlerExtraArgs,
error_handler_muted_on_cancel: errorHandlerMutedOnCancel
path: `${errorHandlerItemKind}/${errorHandlerScriptPath}`,
extra_args: errorHandlerExtraArgs,
muted_on_cancel: errorHandlerMutedOnCancel,
muted_on_user_path: errorHandlerMutedOnUserPath
}
})
sendUserToast(`workspace error handler set to ${errorHandlerScriptPath}`)
@@ -508,9 +514,10 @@
await WorkspaceService.editErrorHandler({
workspace: $workspaceStore!,
requestBody: {
error_handler: undefined,
error_handler_extra_args: undefined,
error_handler_muted_on_cancel: undefined
path: undefined,
extra_args: undefined,
muted_on_cancel: undefined,
muted_on_user_path: undefined
}
})
sendUserToast(`workspace error handler removed`)
@@ -522,7 +529,7 @@
await WorkspaceService.editSuccessHandler({
workspace: $workspaceStore!,
requestBody: {
success_handler: `script/${successHandlerScriptPath}`
path: `script/${successHandlerScriptPath}`
}
})
sendUserToast(`workspace success handler set to ${successHandlerScriptPath}`)
@@ -530,7 +537,7 @@
await WorkspaceService.editSuccessHandler({
workspace: $workspaceStore!,
requestBody: {
success_handler: undefined
path: undefined
}
})
sendUserToast(`workspace success handler removed`)
@@ -1156,6 +1163,14 @@
bind:checked={errorHandlerMutedOnCancel}
options={{ right: 'Do not run error handler for canceled jobs' }}
/>
<Toggle
disabled={!$enterpriseLicense ||
((errorHandlerSelected === 'slack' || errorHandlerSelected === 'teams') &&
!emptyString(errorHandlerScriptPath) &&
emptyString(errorHandlerExtraArgs['channel']))}
bind:checked={errorHandlerMutedOnUserPath}
options={{ right: 'Do not run error handler for u/ scripts and flows' }}
/>
<Button
disabled={!$enterpriseLicense ||
((errorHandlerSelected === 'slack' || errorHandlerSelected === 'teams') &&