Merge remote-tracking branch 'origin/main' into di/data-tables

This commit is contained in:
Diego Imbert
2025-12-02 14:37:50 +01:00
70 changed files with 3837 additions and 500 deletions
+16
View File
@@ -1,5 +1,21 @@
# Changelog
## [1.588.0](https://github.com/windmill-labs/windmill/compare/v1.587.1...v1.588.0) (2025-12-02)
### Features
* add S3 support to download button and PDF preview components ([#7271](https://github.com/windmill-labs/windmill/issues/7271)) ([a23d4f0](https://github.com/windmill-labs/windmill/commit/a23d4f015aa32e00fd96b8f49bccd37efa1200b2))
* **cli:** jobs migration command ([#7268](https://github.com/windmill-labs/windmill/issues/7268)) ([02e38b4](https://github.com/windmill-labs/windmill/commit/02e38b4463560338817c6ec121d745365a95c2c4))
### Bug Fixes
* **cli:** handle better apps pushing back-compatibility ([dc9d252](https://github.com/windmill-labs/windmill/commit/dc9d25289b7282b0f0bfdf987d8e62e3cd7ea8c6))
* **cli:** handle better public apps in legacy mode ([67e30c7](https://github.com/windmill-labs/windmill/commit/67e30c7741a0391e57e30fbc41c1faa731753dcb))
* configurable timeout for AI requests ([#6497](https://github.com/windmill-labs/windmill/issues/6497)) ([#7267](https://github.com/windmill-labs/windmill/issues/7267)) ([764e1e1](https://github.com/windmill-labs/windmill/commit/764e1e15f5a8c0e6a8ca17981281340b130b5bb3))
* **workspace-dependencies:** implement better caching ([#7273](https://github.com/windmill-labs/windmill/issues/7273)) ([69c550b](https://github.com/windmill-labs/windmill/commit/69c550bca6dffb64bd8150648a6d358a25a018fd))
## [1.587.1](https://github.com/windmill-labs/windmill/compare/v1.587.0...v1.587.1) (2025-12-01)
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job WHERE workspace_id = $1 AND id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "023555d33652d40fa381b1baaae6b319c4bac92cb2d90bb4ffd08e25f4a4d18b"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job_status WHERE id = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "02fdd7b94e6b6c9bb7985dfeb2082655d08946206dcfb25158c10f78619cf7fc"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_stats WHERE workspace_id = $1 AND job_id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "13aaff548ff7d3bd38c40799958b2892b100a6ca1c99396254e24b981a2bb1c0"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_logs WHERE workspace_id = $1 AND job_id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "3bd11d7210bb548df4f18f980ae66cc04d41f6d8917c3c0873304250d39943ff"
}
@@ -0,0 +1,26 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job_queue (\n id,\n workspace_id,\n started_at,\n scheduled_for,\n running,\n canceled_by,\n canceled_reason,\n suspend,\n suspend_until,\n worker,\n extras,\n tag,\n priority\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Timestamptz",
"Timestamptz",
"Bool",
"Varchar",
"Text",
"Int4",
"Timestamptz",
"Varchar",
"Jsonb",
"Varchar",
"Int2"
]
},
"nullable": []
},
"hash": "3e88cef5955beeff980135297c42271eaaddde1f4e3a4ab62628faee2e06e947"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job_queue WHERE workspace_id = $1 AND id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "409b452cfe4d1e36b2152e86ada52ca3f1137dd26c85963863f4a73150fcc0e0"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM resume_job WHERE job = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "504bc8de80cda9c5db7db4e34a01038b29ca689dd87b800cfd95f463592e382c"
}
@@ -0,0 +1,55 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id FROM workspace_dependencies\n WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
]
},
"nullable": [
false
]
},
"hash": "5387fbfd4be55674dcbe9f9b3ce9649d0f5db125776fbdffea993a53a09708de"
}
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -0,0 +1,42 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job_completed (\n id,\n workspace_id,\n started_at,\n completed_at,\n duration_ms,\n result,\n deleted,\n canceled_by,\n canceled_reason,\n flow_status,\n memory_peak,\n status,\n worker,\n workflow_as_code_status,\n result_columns,\n retries,\n extras\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Timestamptz",
"Timestamptz",
"Int8",
"Jsonb",
"Bool",
"Varchar",
"Text",
"Jsonb",
"Int4",
{
"Custom": {
"name": "job_status",
"kind": {
"Enum": [
"success",
"failure",
"canceled",
"skipped"
]
}
}
},
"Varchar",
"Jsonb",
"TextArray",
"UuidArray",
"Jsonb"
]
},
"nullable": []
},
"hash": "73d062a512230f5b27b9a39dc4484b2c6723c5568940d2fdba85ce4800d8c5ca"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n WITH inserted_concurrency_counter AS (\n INSERT INTO concurrency_counter (concurrency_id, job_uuids)\n VALUES ($1, '{}'::jsonb)\n ON CONFLICT DO NOTHING\n )\n INSERT INTO concurrency_key(key, job_id)\n VALUES ($1, $2)\n ON CONFLICT (job_id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Uuid"
]
},
"nullable": []
},
"hash": "82e1ad04f4030fd20e4738b3cebba728eb654566d3f1d9d9711f4340fa1b47fd"
}
@@ -0,0 +1,18 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO job_logs (\n job_id,\n workspace_id,\n logs,\n log_offset,\n log_file_index\n ) VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (job_id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Text",
"Int4",
"TextArray"
]
},
"nullable": []
},
"hash": "8c5a1b4ea98856466a31658cfe953492943c961f98036133437ccc1a621ee89c"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job_runtime (\n id, ping, memory_peak\n ) VALUES (\n $1, $2, $3\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Timestamptz",
"Int4"
]
},
"nullable": []
},
"hash": "95585df7a45e61f0c31a05667b103041f8a893fe9ed2830aecaa4d245b07d739"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM concurrency_key WHERE job_id = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "9d1585d6fccc3c76eb2db98789d6a92a21355b0c153db26ac01e93509aa5a6e4"
}
@@ -0,0 +1,379 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n v2_job.id,\n v2_job.raw_code,\n v2_job.raw_lock,\n v2_job.raw_flow as \"raw_flow: _\",\n v2_job.tag,\n v2_job.created_at,\n v2_job.created_by,\n v2_job.permissioned_as,\n v2_job.permissioned_as_email,\n v2_job.kind as \"kind: _\",\n v2_job.runnable_id,\n v2_job.runnable_path,\n v2_job.parent_job,\n v2_job.root_job,\n v2_job.script_lang as \"script_lang: _\",\n v2_job.script_entrypoint_override,\n v2_job.flow_step,\n v2_job.flow_step_id,\n v2_job.flow_innermost_root_job,\n v2_job.\"trigger\",\n v2_job.trigger_kind as \"trigger_kind: _\",\n v2_job.same_worker,\n v2_job.visible_to_owner,\n v2_job.concurrent_limit,\n v2_job.concurrency_time_window_s,\n v2_job.cache_ttl,\n v2_job.timeout,\n v2_job.priority,\n v2_job.preprocessed,\n v2_job.args as \"args: _\",\n v2_job.labels,\n v2_job.pre_run_error,\n\n v2_job_queue.started_at,\n v2_job_queue.scheduled_for,\n v2_job_queue.running,\n v2_job_queue.canceled_by,\n v2_job_queue.canceled_reason,\n v2_job_queue.suspend,\n v2_job_queue.suspend_until,\n v2_job_queue.worker,\n v2_job_queue.extras as \"extras: _\",\n\n v2_job_runtime.ping,\n v2_job_runtime.memory_peak,\n\n v2_job_status.flow_status as \"flow_status: _\",\n v2_job_status.flow_leaf_jobs as \"flow_leaf_jobs: _\",\n v2_job_status.workflow_as_code_status as \"workflow_as_code_status: _\",\n\n concurrency_key.key as \"concurrency_key?\"\n FROM v2_job_queue\n INNER JOIN v2_job ON v2_job.id = v2_job_queue.id\n LEFT JOIN v2_job_runtime ON v2_job_runtime.id = v2_job_queue.id\n LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id\n LEFT JOIN concurrency_key ON concurrency_key.job_id = v2_job_queue.id\n WHERE v2_job_queue.workspace_id = $1\n AND v2_job_queue.running = false\n AND v2_job.parent_job IS NULL\n AND v2_job.trigger_kind IS DISTINCT FROM 'schedule'\n ORDER BY v2_job.created_at DESC\n LIMIT $2\n OFFSET $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "raw_code",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "raw_lock",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "raw_flow: _",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "permissioned_as_email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "kind: _",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
}
},
{
"ordinal": 10,
"name": "runnable_id",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "parent_job",
"type_info": "Uuid"
},
{
"ordinal": 13,
"name": "root_job",
"type_info": "Uuid"
},
{
"ordinal": 14,
"name": "script_lang: _",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
},
{
"ordinal": 15,
"name": "script_entrypoint_override",
"type_info": "Varchar"
},
{
"ordinal": 16,
"name": "flow_step",
"type_info": "Int4"
},
{
"ordinal": 17,
"name": "flow_step_id",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "flow_innermost_root_job",
"type_info": "Uuid"
},
{
"ordinal": 19,
"name": "trigger",
"type_info": "Varchar"
},
{
"ordinal": 20,
"name": "trigger_kind: _",
"type_info": {
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt"
]
}
}
}
},
{
"ordinal": 21,
"name": "same_worker",
"type_info": "Bool"
},
{
"ordinal": 22,
"name": "visible_to_owner",
"type_info": "Bool"
},
{
"ordinal": 23,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 24,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 25,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 26,
"name": "timeout",
"type_info": "Int4"
},
{
"ordinal": 27,
"name": "priority",
"type_info": "Int2"
},
{
"ordinal": 28,
"name": "preprocessed",
"type_info": "Bool"
},
{
"ordinal": 29,
"name": "args: _",
"type_info": "Jsonb"
},
{
"ordinal": 30,
"name": "labels",
"type_info": "TextArray"
},
{
"ordinal": 31,
"name": "pre_run_error",
"type_info": "Text"
},
{
"ordinal": 32,
"name": "started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 33,
"name": "scheduled_for",
"type_info": "Timestamptz"
},
{
"ordinal": 34,
"name": "running",
"type_info": "Bool"
},
{
"ordinal": 35,
"name": "canceled_by",
"type_info": "Varchar"
},
{
"ordinal": 36,
"name": "canceled_reason",
"type_info": "Text"
},
{
"ordinal": 37,
"name": "suspend",
"type_info": "Int4"
},
{
"ordinal": 38,
"name": "suspend_until",
"type_info": "Timestamptz"
},
{
"ordinal": 39,
"name": "worker",
"type_info": "Varchar"
},
{
"ordinal": 40,
"name": "extras: _",
"type_info": "Jsonb"
},
{
"ordinal": 41,
"name": "ping",
"type_info": "Timestamptz"
},
{
"ordinal": 42,
"name": "memory_peak",
"type_info": "Int4"
},
{
"ordinal": 43,
"name": "flow_status: _",
"type_info": "Jsonb"
},
{
"ordinal": 44,
"name": "flow_leaf_jobs: _",
"type_info": "Jsonb"
},
{
"ordinal": 45,
"name": "workflow_as_code_status: _",
"type_info": "Jsonb"
},
{
"ordinal": 46,
"name": "concurrency_key?",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
true,
true,
true,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
false
]
},
"hash": "a4b6371d33206010b2f3ffd2b09e33244fe8ab9a803248fc23f334034d24aad4"
}
@@ -0,0 +1,125 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job (\n id,\n raw_code,\n raw_lock,\n raw_flow,\n tag,\n workspace_id,\n created_at,\n created_by,\n permissioned_as,\n permissioned_as_email,\n kind,\n runnable_id,\n runnable_path,\n parent_job,\n root_job,\n script_lang,\n script_entrypoint_override,\n flow_step,\n flow_step_id,\n flow_innermost_root_job,\n trigger,\n trigger_kind,\n same_worker,\n visible_to_owner,\n concurrent_limit,\n concurrency_time_window_s,\n cache_ttl,\n timeout,\n priority,\n preprocessed,\n args,\n labels,\n pre_run_error\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10,\n $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23, $24, $25, $26, $27, $28, $29, $30,\n $31, $32, $33\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Jsonb",
"Varchar",
"Varchar",
"Timestamptz",
"Varchar",
"Varchar",
"Varchar",
{
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
},
"Int8",
"Varchar",
"Uuid",
"Uuid",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
},
"Varchar",
"Int4",
"Varchar",
"Uuid",
"Varchar",
{
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt"
]
}
}
},
"Bool",
"Bool",
"Int4",
"Int4",
"Int4",
"Int4",
"Int2",
"Bool",
"Jsonb",
"TextArray",
"Text"
]
},
"nullable": []
},
"hash": "b1a11db5617e8282f5d8256f193be955155b7d0344dfa908451faf2a0ba9269b"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job_status (\n id, flow_status, flow_leaf_jobs, workflow_as_code_status\n ) VALUES (\n $1, $2, $3, $4\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Jsonb",
"Jsonb",
"Jsonb"
]
},
"nullable": []
},
"hash": "c18dc6715b2f895c61d4cbf2d2a671dd89e10000d3313884a663852337a76b2d"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM job_perms WHERE workspace_id = $1 AND job_id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "cca63828cbb043d2c647830cf50b992d8fa53ec801067e5b6c45daf05e61d136"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job_runtime WHERE id = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "d3038367ca90595eb165bcb6d172239b1cd80d969fa36c9cf3dee0c360dec302"
}
@@ -0,0 +1,409 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n v2_job.id,\n v2_job.raw_code,\n v2_job.raw_lock,\n v2_job.raw_flow as \"raw_flow: _\",\n v2_job.tag,\n v2_job.created_at,\n v2_job.created_by,\n v2_job.permissioned_as,\n v2_job.permissioned_as_email,\n v2_job.kind as \"kind: _\",\n v2_job.runnable_id,\n v2_job.runnable_path,\n v2_job.parent_job,\n v2_job.root_job,\n v2_job.script_lang as \"script_lang: _\",\n v2_job.script_entrypoint_override,\n v2_job.flow_step,\n v2_job.flow_step_id,\n v2_job.flow_innermost_root_job,\n v2_job.trigger,\n v2_job.trigger_kind as \"trigger_kind: _\",\n v2_job.same_worker,\n v2_job.visible_to_owner,\n v2_job.concurrent_limit,\n v2_job.concurrency_time_window_s,\n v2_job.cache_ttl,\n v2_job.timeout,\n v2_job.priority,\n v2_job.preprocessed,\n v2_job.args as \"args: _\",\n v2_job.labels,\n v2_job.pre_run_error,\n v2_job_completed.duration_ms,\n v2_job_completed.result as \"result: _\",\n v2_job_completed.deleted,\n v2_job_completed.canceled_by,\n v2_job_completed.canceled_reason,\n v2_job_completed.flow_status as \"flow_status: _\",\n v2_job_completed.started_at,\n v2_job_completed.memory_peak,\n v2_job_completed.status as \"status: _\",\n v2_job_completed.completed_at,\n v2_job_completed.worker,\n v2_job_completed.workflow_as_code_status as \"workflow_as_code_status: _\",\n v2_job_completed.result_columns,\n v2_job_completed.retries,\n v2_job_completed.extras as \"extras: _\",\n job_logs.logs AS logs,\n job_logs.log_offset,\n job_logs.log_file_index\n FROM v2_job_completed\n INNER JOIN v2_job ON v2_job.id = v2_job_completed.id\n LEFT JOIN v2_job_status ON v2_job_completed.id = v2_job_status.id\n LEFT JOIN job_logs ON job_logs.job_id = v2_job.id\n WHERE v2_job_completed.workspace_id = $1\n ORDER BY v2_job.created_at DESC\n LIMIT $2\n OFFSET $3\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "raw_code",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "raw_lock",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "raw_flow: _",
"type_info": "Jsonb"
},
{
"ordinal": 4,
"name": "tag",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "created_by",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "permissioned_as",
"type_info": "Varchar"
},
{
"ordinal": 8,
"name": "permissioned_as_email",
"type_info": "Varchar"
},
{
"ordinal": 9,
"name": "kind: _",
"type_info": {
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
}
},
{
"ordinal": 10,
"name": "runnable_id",
"type_info": "Int8"
},
{
"ordinal": 11,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 12,
"name": "parent_job",
"type_info": "Uuid"
},
{
"ordinal": 13,
"name": "root_job",
"type_info": "Uuid"
},
{
"ordinal": 14,
"name": "script_lang: _",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
},
{
"ordinal": 15,
"name": "script_entrypoint_override",
"type_info": "Varchar"
},
{
"ordinal": 16,
"name": "flow_step",
"type_info": "Int4"
},
{
"ordinal": 17,
"name": "flow_step_id",
"type_info": "Varchar"
},
{
"ordinal": 18,
"name": "flow_innermost_root_job",
"type_info": "Uuid"
},
{
"ordinal": 19,
"name": "trigger",
"type_info": "Varchar"
},
{
"ordinal": 20,
"name": "trigger_kind: _",
"type_info": {
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt"
]
}
}
}
},
{
"ordinal": 21,
"name": "same_worker",
"type_info": "Bool"
},
{
"ordinal": 22,
"name": "visible_to_owner",
"type_info": "Bool"
},
{
"ordinal": 23,
"name": "concurrent_limit",
"type_info": "Int4"
},
{
"ordinal": 24,
"name": "concurrency_time_window_s",
"type_info": "Int4"
},
{
"ordinal": 25,
"name": "cache_ttl",
"type_info": "Int4"
},
{
"ordinal": 26,
"name": "timeout",
"type_info": "Int4"
},
{
"ordinal": 27,
"name": "priority",
"type_info": "Int2"
},
{
"ordinal": 28,
"name": "preprocessed",
"type_info": "Bool"
},
{
"ordinal": 29,
"name": "args: _",
"type_info": "Jsonb"
},
{
"ordinal": 30,
"name": "labels",
"type_info": "TextArray"
},
{
"ordinal": 31,
"name": "pre_run_error",
"type_info": "Text"
},
{
"ordinal": 32,
"name": "duration_ms",
"type_info": "Int8"
},
{
"ordinal": 33,
"name": "result: _",
"type_info": "Jsonb"
},
{
"ordinal": 34,
"name": "deleted",
"type_info": "Bool"
},
{
"ordinal": 35,
"name": "canceled_by",
"type_info": "Varchar"
},
{
"ordinal": 36,
"name": "canceled_reason",
"type_info": "Text"
},
{
"ordinal": 37,
"name": "flow_status: _",
"type_info": "Jsonb"
},
{
"ordinal": 38,
"name": "started_at",
"type_info": "Timestamptz"
},
{
"ordinal": 39,
"name": "memory_peak",
"type_info": "Int4"
},
{
"ordinal": 40,
"name": "status: _",
"type_info": {
"Custom": {
"name": "job_status",
"kind": {
"Enum": [
"success",
"failure",
"canceled",
"skipped"
]
}
}
}
},
{
"ordinal": 41,
"name": "completed_at",
"type_info": "Timestamptz"
},
{
"ordinal": 42,
"name": "worker",
"type_info": "Varchar"
},
{
"ordinal": 43,
"name": "workflow_as_code_status: _",
"type_info": "Jsonb"
},
{
"ordinal": 44,
"name": "result_columns",
"type_info": "TextArray"
},
{
"ordinal": 45,
"name": "retries",
"type_info": "UuidArray"
},
{
"ordinal": 46,
"name": "extras: _",
"type_info": "Jsonb"
},
{
"ordinal": 47,
"name": "logs",
"type_info": "Text"
},
{
"ordinal": 48,
"name": "log_offset",
"type_info": "Int4"
},
{
"ordinal": 49,
"name": "log_file_index",
"type_info": "TextArray"
}
],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": [
false,
true,
true,
true,
false,
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
false,
true,
false,
true,
true,
true,
true,
true,
false,
false,
true,
true,
true,
true,
true,
true,
false,
true
]
},
"hash": "e4d71278fb80126a7a9da73f1889352d4d1e3cb3a8a08f1c9c03055a1cab1235"
}
@@ -1,128 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, content, language AS \"language: ScriptLang\", name, description, archived, workspace_id, created_at\n FROM workspace_dependencies\n WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "content",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "language: ScriptLang",
"type_info": {
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
},
{
"ordinal": 3,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "archived",
"type_info": "Bool"
},
{
"ordinal": 6,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 7,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Text",
"Text",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
}
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false,
false
]
},
"hash": "f0e7d28be69c4b922b34b76abb780766003d79884d33f94a79e61f0259807dbc"
}
@@ -0,0 +1,125 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO v2_job (\n id,\n raw_code,\n raw_lock,\n raw_flow,\n tag,\n workspace_id,\n created_at,\n created_by,\n permissioned_as,\n permissioned_as_email,\n kind,\n runnable_id,\n runnable_path,\n parent_job,\n root_job,\n script_lang,\n script_entrypoint_override,\n flow_step,\n flow_step_id,\n flow_innermost_root_job,\n trigger,\n trigger_kind,\n same_worker,\n visible_to_owner,\n concurrent_limit,\n concurrency_time_window_s,\n cache_ttl,\n timeout,\n priority,\n preprocessed,\n args,\n labels,\n pre_run_error\n ) VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,\n $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33\n )\n ON CONFLICT (id) DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text",
"Jsonb",
"Varchar",
"Varchar",
"Timestamptz",
"Varchar",
"Varchar",
"Varchar",
{
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
},
"Int8",
"Varchar",
"Uuid",
"Uuid",
{
"Custom": {
"name": "script_lang",
"kind": {
"Enum": [
"python3",
"deno",
"go",
"bash",
"postgresql",
"nativets",
"bun",
"mysql",
"bigquery",
"snowflake",
"graphql",
"powershell",
"mssql",
"php",
"bunnative",
"rust",
"ansible",
"csharp",
"oracledb",
"nu",
"java",
"duckdb",
"ruby"
]
}
}
},
"Varchar",
"Int4",
"Varchar",
"Uuid",
"Varchar",
{
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt"
]
}
}
},
"Bool",
"Bool",
"Int4",
"Int4",
"Int4",
"Int4",
"Int2",
"Bool",
"Jsonb",
"TextArray",
"Text"
]
},
"nullable": []
},
"hash": "fa9c2c75b622b23008ef1cdba3cb691ef7d1b0ad9eb2596ccfa7227721a0784f"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM v2_job_completed WHERE workspace_id = $1 AND id = ANY($2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"UuidArray"
]
},
"nullable": []
},
"hash": "fae4c8069ef5389e64a35d3867e9080006f1c36a53e1e9e631d794a876d5e0b3"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM zombie_job_counter WHERE job_id = ANY($1)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "fc191f8598ce030fdee04353ad6ca562984f8b86c2882becef42e838bf79375d"
}
+27 -27
View File
@@ -15162,7 +15162,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"aws-sdk-config",
@@ -15225,7 +15225,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"argon2",
@@ -15346,7 +15346,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -15361,7 +15361,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"chrono",
"lazy_static",
@@ -15375,7 +15375,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"axum",
@@ -15394,7 +15394,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15487,7 +15487,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"regex",
"serde",
@@ -15502,7 +15502,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"bytes",
@@ -15526,7 +15526,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15542,7 +15542,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15551,7 +15551,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15563,7 +15563,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15575,7 +15575,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"gosyn",
@@ -15587,7 +15587,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15599,7 +15599,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"serde_json",
@@ -15611,7 +15611,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"nu-parser",
@@ -15622,7 +15622,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15633,7 +15633,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15645,7 +15645,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15669,7 +15669,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15683,7 +15683,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15700,7 +15700,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15714,7 +15714,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"lazy_static",
@@ -15732,7 +15732,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"serde",
@@ -15743,7 +15743,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"async-recursion",
@@ -15780,7 +15780,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15790,7 +15790,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.587.1"
version = "1.588.0"
dependencies = [
"anyhow",
"async-once-cell",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.587.1"
version = "1.588.0"
authors.workspace = true
edition.workspace = true
@@ -33,7 +33,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.587.1"
version = "1.588.0"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+2 -2
View File
@@ -64,7 +64,7 @@ mod workspace_dependencies {
name: Some("test".to_owned()),
description: None,
}
.create("", "", "", db)
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
.await
.unwrap();
@@ -151,7 +151,7 @@ mod workspace_dependencies {
name: None, // No name = default workspace dependencies
description: None,
}
.create("", "", "", db)
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
.await
.unwrap();
+407 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.587.1
version: 1.588.0
title: Windmill API
contact:
@@ -8919,6 +8919,121 @@ paths:
items:
$ref: "#/components/schemas/CompletedJob"
/w/{workspace}/jobs/completed/export:
get:
summary: export all completed jobs for backup/migration
operationId: exportCompletedJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: All completed jobs exported
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ExportableCompletedJob"
/w/{workspace}/jobs/completed/import:
post:
summary: import completed jobs from backup/migration
operationId: importCompletedJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ExportableCompletedJob"
responses:
"200":
description: Successfully imported completed jobs
content:
text/plain:
schema:
type: string
/w/{workspace}/jobs/queue/export:
get:
summary: export all queued jobs for backup/migration
operationId: exportQueuedJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: All queued jobs exported
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ExportableQueuedJob"
/w/{workspace}/jobs/queue/import:
post:
summary: import queued jobs from backup/migration
operationId: importQueuedJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ExportableQueuedJob"
responses:
"200":
description: Successfully imported queued jobs
content:
text/plain:
schema:
type: string
/w/{workspace}/jobs/delete:
post:
summary: delete jobs by IDs from all related tables
operationId: deleteJobs
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: string
format: uuid
description: Array of job IDs to delete
responses:
"200":
description: Successfully deleted jobs
content:
text/plain:
schema:
type: string
description: Summary of deleted jobs and rows
/w/{workspace}/jobs/list:
get:
summary: list all jobs
@@ -16573,6 +16688,297 @@ components:
- visible_to_owner
- tag
ExportableCompletedJob:
type: object
description: Completed job with full data for export/import operations
properties:
id:
type: string
format: uuid
parent_job:
type: string
format: uuid
created_by:
type: string
created_at:
type: string
format: date-time
started_at:
type: string
format: date-time
completed_at:
type: string
format: date-time
duration_ms:
type: integer
script_path:
type: string
script_hash:
type: string
args:
type: object
description: Full job arguments without size restrictions
result:
type: object
description: Full job result without size restrictions
logs:
type: string
description: Complete job logs from v2_job table
raw_code:
type: string
raw_lock:
type: string
canceled_by:
type: string
canceled_reason:
type: string
job_kind:
type: string
enum:
[
"script",
"preview",
"dependencies",
"flow",
"flowdependencies",
"appdependencies",
"flowpreview",
"script_hub",
"identity",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
]
trigger:
type: string
description: Trigger path for the job (replaces schedule_path)
trigger_kind:
type: string
enum:
[
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
]
permissioned_as:
type: string
permissioned_as_email:
type: string
flow_status:
type: object
description: Flow status from v2_job_status table
workflow_as_code_status:
type: object
raw_flow:
type: object
is_flow_step:
type: boolean
language:
$ref: "#/components/schemas/ScriptLang"
is_skipped:
type: boolean
email:
type: string
visible_to_owner:
type: boolean
mem_peak:
type: integer
tag:
type: string
priority:
type: integer
labels:
type: array
items:
type: string
same_worker:
type: boolean
flow_step_id:
type: string
flow_innermost_root_job:
type: string
format: uuid
concurrent_limit:
type: integer
concurrency_time_window_s:
type: integer
timeout:
type: integer
cache_ttl:
type: integer
self_wait_time_ms:
type: integer
aggregate_wait_time_ms:
type: integer
preprocessed:
type: boolean
worker:
type: string
status:
type: string
description: Actual job status from database
required:
- id
- created_by
- created_at
- job_kind
- permissioned_as
- email
- visible_to_owner
ExportableQueuedJob:
type: object
description: Queued job with full data for export/import operations
properties:
id:
type: string
format: uuid
parent_job:
type: string
format: uuid
created_by:
type: string
created_at:
type: string
format: date-time
started_at:
type: string
format: date-time
scheduled_for:
type: string
format: date-time
script_path:
type: string
script_hash:
type: string
args:
type: object
description: Full job arguments without size restrictions
logs:
type: string
description: Complete job logs from v2_job table
raw_code:
type: string
raw_lock:
type: string
canceled_by:
type: string
canceled_reason:
type: string
job_kind:
type: string
enum:
[
"script",
"preview",
"dependencies",
"flowdependencies",
"appdependencies",
"flow",
"flowpreview",
"script_hub",
"identity",
"deploymentcallback",
"singlestepflow",
"flowscript",
"flownode",
"appscript",
]
trigger:
type: string
description: Trigger path for the job (replaces schedule_path)
trigger_kind:
type: string
enum:
[
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
]
permissioned_as:
type: string
permissioned_as_email:
type: string
flow_status:
type: object
description: Flow status from v2_job_status table
workflow_as_code_status:
type: object
raw_flow:
type: object
is_flow_step:
type: boolean
language:
$ref: "#/components/schemas/ScriptLang"
email:
type: string
visible_to_owner:
type: boolean
mem_peak:
type: integer
tag:
type: string
priority:
type: integer
labels:
type: array
items:
type: string
same_worker:
type: boolean
flow_step_id:
type: string
flow_innermost_root_job:
type: string
format: uuid
concurrent_limit:
type: integer
concurrency_time_window_s:
type: integer
timeout:
type: integer
cache_ttl:
type: integer
self_wait_time_ms:
type: integer
aggregate_wait_time_ms:
type: integer
preprocessed:
type: boolean
suspend:
type: integer
suspend_until:
type: string
format: date-time
required:
- id
- created_by
- created_at
- job_kind
- permissioned_as
- email
- visible_to_owner
ObscuredJob:
type: object
properties:
+67 -2
View File
@@ -14,11 +14,76 @@ use windmill_common::error::{to_anyhow, Error, Result};
use windmill_common::utils::configure_client;
use windmill_common::variables::get_variable_or_self;
// AI timeout configuration constants
const AI_TIMEOUT_MIN_SECS: u64 = 1;
const AI_TIMEOUT_MAX_SECS: u64 = 86400; // 24 hours
const AI_TIMEOUT_DEFAULT_SECS: u64 = 3600; // 1 hour
const HTTP_POOL_MAX_IDLE_PER_HOST: usize = 10;
const HTTP_POOL_IDLE_TIMEOUT_SECS: u64 = 90;
lazy_static::lazy_static! {
/// AI request timeout in seconds.
///
/// This timeout applies to the TOTAL duration of AI HTTP requests,
/// including streaming responses. Default is 3600 seconds (1 hour).
///
/// Can be configured via AI_REQUEST_TIMEOUT_SECONDS environment variable.
/// Valid range: 1-86400 seconds (24 hours).
/// - Minimum (1s): Prevents immediate timeout, allows minimal response time
/// - Maximum (24h): Prevents indefinite hangs while supporting long-running AI operations
/// - Default (1h): Balances responsiveness with support for complex AI tasks
///
/// Note: This is a total request timeout, not an idle timeout.
/// Long-running streaming responses that exceed this duration will be terminated,
/// even if actively receiving data.
///
/// CRITICAL: If using a reverse proxy (NGINX, Traefik, etc.), you MUST configure
/// proxy timeouts to match or exceed this value. Without proper proxy configuration,
/// connections will be terminated prematurely at the proxy layer regardless of this
/// backend timeout setting.
///
/// Example NGINX configuration:
/// location /api/ {
/// proxy_read_timeout 3600s; # Must be >= AI_REQUEST_TIMEOUT_SECONDS
/// proxy_send_timeout 3600s;
/// proxy_connect_timeout 60s;
/// }
static ref AI_TIMEOUT_SECS: u64 = {
match std::env::var("AI_REQUEST_TIMEOUT_SECONDS")
.ok()
.and_then(|s| s.parse::<u64>().ok())
{
Some(timeout) if timeout >= AI_TIMEOUT_MIN_SECS && timeout <= AI_TIMEOUT_MAX_SECS => {
tracing::info!("AI request timeout configured: {}s", timeout);
timeout
},
Some(timeout) => {
tracing::warn!(
"AI_REQUEST_TIMEOUT_SECONDS value {} is out of range ({}-{}), using default {}s",
timeout,
AI_TIMEOUT_MIN_SECS,
AI_TIMEOUT_MAX_SECS,
AI_TIMEOUT_DEFAULT_SECS
);
AI_TIMEOUT_DEFAULT_SECS
},
None => {
tracing::info!(
"AI_REQUEST_TIMEOUT_SECONDS not set, using default {}s",
AI_TIMEOUT_DEFAULT_SECS
);
AI_TIMEOUT_DEFAULT_SECS
},
}
};
static ref HTTP_CLIENT: Client = configure_client(reqwest::ClientBuilder::new()
.timeout(std::time::Duration::from_secs(60 * 5))
.timeout(std::time::Duration::from_secs(*AI_TIMEOUT_SECS))
.pool_max_idle_per_host(HTTP_POOL_MAX_IDLE_PER_HOST)
.pool_idle_timeout(Some(std::time::Duration::from_secs(HTTP_POOL_IDLE_TIMEOUT_SECS)))
.user_agent("windmill/beta"))
.build().unwrap();
.build()
.expect("Failed to build AI HTTP client - check system TLS configuration");
static ref OPENAI_AZURE_BASE_PATH: Option<String> = std::env::var("OPENAI_AZURE_BASE_PATH").ok();
+17
View File
@@ -263,6 +263,11 @@ pub fn workspaced_service() -> Router {
)
.route("/list_filtered_uuids", get(list_filtered_job_uuids))
.route("/queue/list", get(list_queue_jobs))
.route("/queue/export", get(crate::jobs_export::export_queued_jobs))
.route(
"/queue/import",
post(crate::jobs_export::import_queued_jobs),
)
.route("/queue/count", get(count_queue_jobs))
.route("/queue/list_filtered_uuids", get(list_filtered_uuids))
.route("/queue/position/:timestamp", get(get_queue_position))
@@ -274,6 +279,18 @@ pub fn workspaced_service() -> Router {
"/completed/list",
get(list_completed_jobs).layer(cors.clone()),
)
.route(
"/completed/export",
get(crate::jobs_export::export_completed_jobs).layer(cors.clone()),
)
.route(
"/completed/import",
post(crate::jobs_export::import_completed_jobs).layer(cors.clone()),
)
.route(
"/delete",
post(crate::jobs_export::delete_jobs),
)
.route(
"/completed/get/:id",
get(get_completed_job).layer(cors.clone()),
+827
View File
@@ -0,0 +1,827 @@
/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
use axum::{
extract::{Path, Query},
Extension, Json,
};
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use uuid::Uuid;
use windmill_common::{
db::UserDB,
error,
jobs::{JobKind, JobStatus, JobTriggerKind},
scripts::ScriptLang,
utils::{paginate, paginate_without_limits, require_admin, Pagination},
};
use crate::db::ApiAuthed;
#[derive(Serialize, Deserialize)]
pub struct ExportableCompletedJob {
pub id: Uuid,
pub raw_code: Option<String>,
pub raw_lock: Option<String>,
pub raw_flow: Option<sqlx::types::Json<Box<RawValue>>>,
pub tag: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub created_by: String,
pub permissioned_as: String,
pub permissioned_as_email: String,
pub kind: JobKind,
pub runnable_id: Option<i64>,
pub runnable_path: Option<String>,
pub parent_job: Option<Uuid>,
pub root_job: Option<Uuid>,
pub script_lang: Option<ScriptLang>,
pub script_entrypoint_override: Option<String>,
pub flow_step: Option<i32>,
pub flow_step_id: Option<String>,
pub flow_innermost_root_job: Option<Uuid>,
pub trigger: Option<String>,
pub trigger_kind: Option<JobTriggerKind>,
pub same_worker: bool,
pub visible_to_owner: bool,
pub concurrent_limit: Option<i32>,
pub concurrency_time_window_s: Option<i32>,
pub cache_ttl: Option<i32>,
pub timeout: Option<i32>,
pub priority: Option<i16>,
pub preprocessed: Option<bool>,
pub args: Option<sqlx::types::Json<Box<RawValue>>>,
pub labels: Option<Vec<String>>,
pub pre_run_error: Option<String>,
pub duration_ms: i64,
pub result: Option<sqlx::types::Json<Box<RawValue>>>,
pub deleted: bool,
pub canceled_by: Option<String>,
pub canceled_reason: Option<String>,
pub flow_status: Option<sqlx::types::Json<Box<RawValue>>>,
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub memory_peak: Option<i32>,
pub status: JobStatus,
pub completed_at: chrono::DateTime<chrono::Utc>,
pub worker: Option<String>,
pub workflow_as_code_status: Option<sqlx::types::Json<Box<RawValue>>>,
pub result_columns: Option<Vec<String>>,
pub retries: Option<Vec<Uuid>>,
pub extras: Option<sqlx::types::Json<Box<RawValue>>>,
pub logs: Option<String>,
pub log_offset: Option<i32>,
pub log_file_index: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize)]
pub struct ExportableQueuedJob {
// v2_job columns (excluding workspace_id)
pub id: Uuid,
pub raw_code: Option<String>,
pub raw_lock: Option<String>,
pub raw_flow: Option<sqlx::types::Json<Box<RawValue>>>,
pub tag: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub created_by: String,
pub permissioned_as: String,
pub permissioned_as_email: String,
pub kind: JobKind,
pub runnable_id: Option<i64>,
pub runnable_path: Option<String>,
pub parent_job: Option<Uuid>,
pub root_job: Option<Uuid>,
pub script_lang: Option<ScriptLang>,
pub script_entrypoint_override: Option<String>,
pub flow_step: Option<i32>,
pub flow_step_id: Option<String>,
pub flow_innermost_root_job: Option<Uuid>,
pub trigger: Option<String>,
pub trigger_kind: Option<JobTriggerKind>,
pub same_worker: bool,
pub visible_to_owner: bool,
pub concurrent_limit: Option<i32>,
pub concurrency_time_window_s: Option<i32>,
pub cache_ttl: Option<i32>,
pub timeout: Option<i32>,
pub priority: Option<i16>,
pub preprocessed: Option<bool>,
pub args: Option<sqlx::types::Json<Box<RawValue>>>,
pub labels: Option<Vec<String>>,
pub pre_run_error: Option<String>,
// v2_job_queue columns (excluding workspace_id and id/created_at/tag/priority)
pub started_at: Option<chrono::DateTime<chrono::Utc>>,
pub scheduled_for: chrono::DateTime<chrono::Utc>,
pub running: bool,
pub canceled_by: Option<String>,
pub canceled_reason: Option<String>,
pub suspend: Option<i32>,
pub suspend_until: Option<chrono::DateTime<chrono::Utc>>,
pub worker: Option<String>,
pub extras: Option<sqlx::types::Json<Box<RawValue>>>,
// v2_job_runtime columns (excluding id)
pub ping: Option<chrono::DateTime<chrono::Utc>>,
pub memory_peak: Option<i32>,
// v2_job_status columns (excluding id)
pub flow_status: Option<sqlx::types::Json<Box<RawValue>>>,
pub flow_leaf_jobs: Option<sqlx::types::Json<Box<RawValue>>>,
pub workflow_as_code_status: Option<sqlx::types::Json<Box<RawValue>>>,
// concurrency_key table
pub concurrency_key: Option<String>,
}
pub async fn export_completed_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
) -> error::JsonResult<Vec<ExportableCompletedJob>> {
require_admin(authed.is_admin, &authed.username)?;
let (per_page, offset) = paginate(pagination);
let per_page = per_page as i64;
let offset = offset as i64;
let mut tx = user_db.begin(&authed).await?;
let jobs = sqlx::query_as!(
ExportableCompletedJob,
r#"
SELECT
v2_job.id,
v2_job.raw_code,
v2_job.raw_lock,
v2_job.raw_flow as "raw_flow: _",
v2_job.tag,
v2_job.created_at,
v2_job.created_by,
v2_job.permissioned_as,
v2_job.permissioned_as_email,
v2_job.kind as "kind: _",
v2_job.runnable_id,
v2_job.runnable_path,
v2_job.parent_job,
v2_job.root_job,
v2_job.script_lang as "script_lang: _",
v2_job.script_entrypoint_override,
v2_job.flow_step,
v2_job.flow_step_id,
v2_job.flow_innermost_root_job,
v2_job.trigger,
v2_job.trigger_kind as "trigger_kind: _",
v2_job.same_worker,
v2_job.visible_to_owner,
v2_job.concurrent_limit,
v2_job.concurrency_time_window_s,
v2_job.cache_ttl,
v2_job.timeout,
v2_job.priority,
v2_job.preprocessed,
v2_job.args as "args: _",
v2_job.labels,
v2_job.pre_run_error,
v2_job_completed.duration_ms,
v2_job_completed.result as "result: _",
v2_job_completed.deleted,
v2_job_completed.canceled_by,
v2_job_completed.canceled_reason,
v2_job_completed.flow_status as "flow_status: _",
v2_job_completed.started_at,
v2_job_completed.memory_peak,
v2_job_completed.status as "status: _",
v2_job_completed.completed_at,
v2_job_completed.worker,
v2_job_completed.workflow_as_code_status as "workflow_as_code_status: _",
v2_job_completed.result_columns,
v2_job_completed.retries,
v2_job_completed.extras as "extras: _",
job_logs.logs AS logs,
job_logs.log_offset,
job_logs.log_file_index
FROM v2_job_completed
INNER JOIN v2_job ON v2_job.id = v2_job_completed.id
LEFT JOIN v2_job_status ON v2_job_completed.id = v2_job_status.id
LEFT JOIN job_logs ON job_logs.job_id = v2_job.id
WHERE v2_job_completed.workspace_id = $1
ORDER BY v2_job.created_at DESC
LIMIT $2
OFFSET $3
"#,
w_id,
per_page,
offset
)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(jobs))
}
pub async fn export_queued_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
) -> error::JsonResult<Vec<ExportableQueuedJob>> {
require_admin(authed.is_admin, &authed.username)?;
let (per_page, offset) = paginate_without_limits(pagination);
let per_page = per_page as i64;
let offset = offset as i64;
let mut tx = user_db.begin(&authed).await?;
let jobs = sqlx::query_as!(
ExportableQueuedJob,
r#"
SELECT
v2_job.id,
v2_job.raw_code,
v2_job.raw_lock,
v2_job.raw_flow as "raw_flow: _",
v2_job.tag,
v2_job.created_at,
v2_job.created_by,
v2_job.permissioned_as,
v2_job.permissioned_as_email,
v2_job.kind as "kind: _",
v2_job.runnable_id,
v2_job.runnable_path,
v2_job.parent_job,
v2_job.root_job,
v2_job.script_lang as "script_lang: _",
v2_job.script_entrypoint_override,
v2_job.flow_step,
v2_job.flow_step_id,
v2_job.flow_innermost_root_job,
v2_job."trigger",
v2_job.trigger_kind as "trigger_kind: _",
v2_job.same_worker,
v2_job.visible_to_owner,
v2_job.concurrent_limit,
v2_job.concurrency_time_window_s,
v2_job.cache_ttl,
v2_job.timeout,
v2_job.priority,
v2_job.preprocessed,
v2_job.args as "args: _",
v2_job.labels,
v2_job.pre_run_error,
v2_job_queue.started_at,
v2_job_queue.scheduled_for,
v2_job_queue.running,
v2_job_queue.canceled_by,
v2_job_queue.canceled_reason,
v2_job_queue.suspend,
v2_job_queue.suspend_until,
v2_job_queue.worker,
v2_job_queue.extras as "extras: _",
v2_job_runtime.ping,
v2_job_runtime.memory_peak,
v2_job_status.flow_status as "flow_status: _",
v2_job_status.flow_leaf_jobs as "flow_leaf_jobs: _",
v2_job_status.workflow_as_code_status as "workflow_as_code_status: _",
concurrency_key.key as "concurrency_key?"
FROM v2_job_queue
INNER JOIN v2_job ON v2_job.id = v2_job_queue.id
LEFT JOIN v2_job_runtime ON v2_job_runtime.id = v2_job_queue.id
LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id
LEFT JOIN concurrency_key ON concurrency_key.job_id = v2_job_queue.id
WHERE v2_job_queue.workspace_id = $1
AND v2_job_queue.running = false
AND v2_job.parent_job IS NULL
AND v2_job.trigger_kind IS DISTINCT FROM 'schedule'
ORDER BY v2_job.created_at DESC
LIMIT $2
OFFSET $3
"#,
w_id,
per_page,
offset
)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(jobs))
}
pub async fn import_completed_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(jobs): Json<Vec<ExportableCompletedJob>>,
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let mut tx = user_db.begin(&authed).await?;
for job in jobs {
// Insert into v2_job table
sqlx::query!(
r#"
INSERT INTO v2_job (
id,
raw_code,
raw_lock,
raw_flow,
tag,
workspace_id,
created_at,
created_by,
permissioned_as,
permissioned_as_email,
kind,
runnable_id,
runnable_path,
parent_job,
root_job,
script_lang,
script_entrypoint_override,
flow_step,
flow_step_id,
flow_innermost_root_job,
trigger,
trigger_kind,
same_worker,
visible_to_owner,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
timeout,
priority,
preprocessed,
args,
labels,
pre_run_error
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
$21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
job.raw_code,
job.raw_lock,
job.raw_flow as _,
&job.tag,
&w_id,
job.created_at,
&job.created_by,
&job.permissioned_as,
job.permissioned_as_email,
job.kind as _,
job.runnable_id,
job.runnable_path,
job.parent_job,
job.root_job,
job.script_lang as _,
job.script_entrypoint_override,
job.flow_step,
job.flow_step_id,
job.flow_innermost_root_job,
job.trigger,
job.trigger_kind as _,
job.same_worker,
job.visible_to_owner,
job.concurrent_limit,
job.concurrency_time_window_s,
job.cache_ttl,
job.timeout,
job.priority,
job.preprocessed,
job.args as _,
job.labels as _,
job.pre_run_error
)
.execute(&mut *tx)
.await?;
// Insert into v2_job_completed table
sqlx::query!(
r#"
INSERT INTO v2_job_completed (
id,
workspace_id,
started_at,
completed_at,
duration_ms,
result,
deleted,
canceled_by,
canceled_reason,
flow_status,
memory_peak,
status,
worker,
workflow_as_code_status,
result_columns,
retries,
extras
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
&w_id,
job.started_at,
job.completed_at,
job.duration_ms,
job.result as _,
job.deleted,
job.canceled_by,
job.canceled_reason,
job.flow_status as _,
job.memory_peak,
job.status as _,
job.worker,
job.workflow_as_code_status as _,
job.result_columns as _,
job.retries as _,
job.extras as _
)
.execute(&mut *tx)
.await?;
// Insert into job_logs table
if let Some(logs) = &job.logs {
sqlx::query!(
r#"
INSERT INTO job_logs (
job_id,
workspace_id,
logs,
log_offset,
log_file_index
) VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (job_id) DO NOTHING
"#,
job.id,
&w_id,
logs,
job.log_offset,
job.log_file_index as _
)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await?;
Ok(format!("Successfully imported jobs"))
}
pub async fn import_queued_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(jobs): Json<Vec<ExportableQueuedJob>>,
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
let mut tx = user_db.begin(&authed).await?;
for job in jobs {
// Insert into v2_job table
// Insert into v2_job table, fields and order adapted to match ExportableQueuedJob
sqlx::query!(
r#"
INSERT INTO v2_job (
id,
raw_code,
raw_lock,
raw_flow,
tag,
workspace_id,
created_at,
created_by,
permissioned_as,
permissioned_as_email,
kind,
runnable_id,
runnable_path,
parent_job,
root_job,
script_lang,
script_entrypoint_override,
flow_step,
flow_step_id,
flow_innermost_root_job,
trigger,
trigger_kind,
same_worker,
visible_to_owner,
concurrent_limit,
concurrency_time_window_s,
cache_ttl,
timeout,
priority,
preprocessed,
args,
labels,
pre_run_error
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
$21, $22, $23, $24, $25, $26, $27, $28, $29, $30,
$31, $32, $33
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
job.raw_code,
job.raw_lock,
job.raw_flow as _,
&job.tag,
&w_id,
job.created_at,
&job.created_by,
&job.permissioned_as,
&job.permissioned_as_email,
job.kind as _,
job.runnable_id,
job.runnable_path,
job.parent_job,
job.root_job,
job.script_lang as _,
job.script_entrypoint_override,
job.flow_step,
job.flow_step_id,
job.flow_innermost_root_job,
job.trigger,
job.trigger_kind as _,
job.same_worker,
job.visible_to_owner,
job.concurrent_limit,
job.concurrency_time_window_s,
job.cache_ttl,
job.timeout,
job.priority,
job.preprocessed,
job.args as _,
job.labels as _,
job.pre_run_error,
)
.execute(&mut *tx)
.await?;
// Insert into v2_job_queue table
sqlx::query!(
r#"
INSERT INTO v2_job_queue (
id,
workspace_id,
started_at,
scheduled_for,
running,
canceled_by,
canceled_reason,
suspend,
suspend_until,
worker,
extras,
tag,
priority
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
&w_id,
job.started_at,
job.scheduled_for,
job.running,
job.canceled_by,
job.canceled_reason,
job.suspend,
job.suspend_until,
job.worker,
job.extras as _,
&job.tag,
job.priority
)
.execute(&mut *tx)
.await?;
// Insert into v2_job_runtime if any present
if job.ping.is_some() || job.memory_peak.is_some() {
sqlx::query!(
r#"
INSERT INTO v2_job_runtime (
id, ping, memory_peak
) VALUES (
$1, $2, $3
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
job.ping,
job.memory_peak
)
.execute(&mut *tx)
.await?;
}
// Insert into v2_job_status if present
if job.flow_status.is_some()
|| job.flow_leaf_jobs.is_some()
|| job.workflow_as_code_status.is_some()
{
sqlx::query!(
r#"
INSERT INTO v2_job_status (
id, flow_status, flow_leaf_jobs, workflow_as_code_status
) VALUES (
$1, $2, $3, $4
)
ON CONFLICT (id) DO NOTHING
"#,
job.id,
job.flow_status as _,
job.flow_leaf_jobs as _,
job.workflow_as_code_status as _
)
.execute(&mut *tx)
.await?;
}
// Insert concurrency_key if present
if let Some(ref concurrency_key) = job.concurrency_key {
sqlx::query!(
r#"
WITH inserted_concurrency_counter AS (
INSERT INTO concurrency_counter (concurrency_id, job_uuids)
VALUES ($1, '{}'::jsonb)
ON CONFLICT DO NOTHING
)
INSERT INTO concurrency_key(key, job_id)
VALUES ($1, $2)
ON CONFLICT (job_id) DO NOTHING
"#,
concurrency_key,
job.id
)
.execute(&mut *tx)
.await?;
}
}
tx.commit().await?;
Ok(format!("Successfully imported jobs"))
}
pub async fn delete_jobs(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Json(job_ids): Json<Vec<Uuid>>,
) -> error::Result<String> {
require_admin(authed.is_admin, &authed.username)?;
if job_ids.is_empty() {
return Ok("No jobs to delete".to_string());
}
let mut tx = user_db.begin(&authed).await?;
// Delete from all job-related tables
// Order matters due to foreign key constraints
// 1. Delete job logs
let logs_deleted = sqlx::query!(
"DELETE FROM job_logs WHERE workspace_id = $1 AND job_id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 2. Delete job permissions
let perms_deleted = sqlx::query!(
"DELETE FROM job_perms WHERE workspace_id = $1 AND job_id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 3. Delete job stats
let stats_deleted = sqlx::query!(
"DELETE FROM job_stats WHERE workspace_id = $1 AND job_id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 4. Delete resume jobs
let resume_deleted = sqlx::query!("DELETE FROM resume_job WHERE job = ANY($1)", &job_ids)
.execute(&mut *tx)
.await?
.rows_affected();
// 5. Delete from v2_job_runtime
let runtime_deleted = sqlx::query!("DELETE FROM v2_job_runtime WHERE id = ANY($1)", &job_ids)
.execute(&mut *tx)
.await?
.rows_affected();
// 6. Delete from v2_job_status
let status_deleted = sqlx::query!("DELETE FROM v2_job_status WHERE id = ANY($1)", &job_ids)
.execute(&mut *tx)
.await?
.rows_affected();
// 7. Delete from concurrency_key
let concurrency_key_deleted = sqlx::query!(
"DELETE FROM concurrency_key WHERE job_id = ANY($1)",
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 8. Delete from v2_job_queue (for queued jobs)
let queue_deleted = sqlx::query!(
"DELETE FROM v2_job_queue WHERE workspace_id = $1 AND id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 9. Delete from v2_job_completed (for completed jobs)
let completed_deleted = sqlx::query!(
"DELETE FROM v2_job_completed WHERE workspace_id = $1 AND id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 10. Delete zombie job counter
let zombie_deleted = sqlx::query!(
"DELETE FROM zombie_job_counter WHERE job_id = ANY($1)",
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
// 11. Finally, delete from v2_job (main table)
let jobs_deleted = sqlx::query!(
"DELETE FROM v2_job WHERE workspace_id = $1 AND id = ANY($2)",
&w_id,
&job_ids
)
.execute(&mut *tx)
.await?
.rows_affected();
tx.commit().await?;
let total_rows_deleted = logs_deleted
+ perms_deleted
+ stats_deleted
+ resume_deleted
+ runtime_deleted
+ status_deleted
+ concurrency_key_deleted
+ queue_deleted
+ completed_deleted
+ zombie_deleted
+ jobs_deleted;
tracing::info!(
"Successfully deleted {} jobs ({} total rows across all tables) from workspace {}",
job_ids.len(),
total_rows_deleted,
w_id
);
Ok(format!(
"Successfully deleted {} jobs ({} total rows across all tables)",
jobs_deleted, total_rows_deleted
))
}
+1
View File
@@ -127,6 +127,7 @@ pub mod job_helpers_ee;
mod job_helpers_oss;
pub mod job_metrics;
pub mod jobs;
pub mod jobs_export;
#[cfg(all(feature = "oauth2", feature = "private"))]
pub mod oauth2_ee;
#[cfg(feature = "oauth2")]
@@ -13,9 +13,8 @@ use windmill_common::{
workspace_dependencies::WorkspaceDependencies,
DB,
};
use windmill_worker::{
scoped_dependency_map, trigger_dependents_to_recompute_dependencies,
workspace_dependencies::NewWorkspaceDependencies,
use windmill_worker::workspace_dependencies::{
trigger_dependents_to_recompute_dependencies_in_the_background, NewWorkspaceDependencies,
};
use crate::db::ApiAuthed;
@@ -43,10 +42,12 @@ async fn create(
format!(
"{}",
nwd.create(
&authed.email,
&authed.username,
&username_to_permissioned_as(&authed.username),
&db
(
authed.email,
username_to_permissioned_as(&authed.username),
authed.username,
),
db
)
.await?
),
@@ -93,23 +94,20 @@ async fn archive(
let db = &db;
WorkspaceDependencies::archive(params.name.clone(), language, &w_id, db).await?;
trigger_dependents_to_recompute_dependencies(
&w_id,
scoped_dependency_map::ScopedDependencyMap::get_dependents(
WorkspaceDependencies::to_path(&params.name, language)?.as_str(),
&w_id,
db,
)
.await?,
None,
None,
&authed.email,
&authed.username,
&username_to_permissioned_as(&authed.username),
db,
vec![],
trigger_dependents_to_recompute_dependencies_in_the_background(
params.name.is_none(),
w_id,
language,
(
authed.email,
username_to_permissioned_as(&authed.username),
authed.username,
),
WorkspaceDependencies::to_path(&params.name, language)?,
db.clone(),
)
.await
.await;
Ok(())
}
#[axum::debug_handler]
@@ -125,21 +123,18 @@ async fn delete(
let db = &db;
WorkspaceDependencies::delete(params.name.clone(), language, &w_id, db).await?;
trigger_dependents_to_recompute_dependencies(
&w_id,
scoped_dependency_map::ScopedDependencyMap::get_dependents(
WorkspaceDependencies::to_path(&params.name, language)?.as_str(),
&w_id,
db,
)
.await?,
None,
None,
&authed.email,
&authed.username,
&username_to_permissioned_as(&authed.username),
db,
vec![],
trigger_dependents_to_recompute_dependencies_in_the_background(
params.name.is_none(),
w_id,
language,
(
authed.email,
username_to_permissioned_as(&authed.username),
authed.username,
),
WorkspaceDependencies::to_path(&params.name, language)?,
db.clone(),
)
.await
.await;
Ok(())
}
+82 -1
View File
@@ -892,6 +892,86 @@ pub mod job {
}
}
pub mod workspace_dependencies {
use std::{
future::Future,
time::{Duration, Instant},
};
use crate::{error, scripts::ScriptLang, workspace_dependencies::WorkspaceDependencies, DB};
make_static! {
/// Workspace Dependencies by id and workspace cache.
static ref WORKSPACE_DEPENDENCIES: { (i64, String) => WorkspaceDependencies } in "workspace_dependencies" <= 1000;
}
lazy_static::lazy_static! {
/// Cache for checking if default/unnamed workspace dependencies exist for a workspace and language.
/// Cache key: (workspace_id, language)
/// Cache value: (exists: bool, cached_at timestamp)
static ref DEFAULT_WD_EXISTS_CACHE: quick_cache::sync::Cache<(String, ScriptLang), (bool, Instant)> = quick_cache::sync::Cache::new(500);
}
/// Cache timeout for existence checks (10 seconds)
pub const EXISTS_CACHE_TIMEOUT: Duration = Duration::from_secs(10);
pub fn fetch_workspace_dependencies<'c>(
id: i64,
workspace_id: String,
db: &'c DB,
) -> impl Future<Output = error::Result<WorkspaceDependencies>> + 'c {
tracing::debug!(workspace_id = %workspace_id, id, "fetching workspace dependencies");
WORKSPACE_DEPENDENCIES.get_or_insert_async(
(id, workspace_id.clone()),
WorkspaceDependencies::get(id, workspace_id, db),
)
}
pub fn get_cached_is_unnamed_workspace_dependencies_exists<'c>(
language: ScriptLang,
workspace_id: String,
) -> Option<bool> {
let exists_key = (workspace_id.to_string(), language);
if let Some((exists, cached_at)) = DEFAULT_WD_EXISTS_CACHE.get(&exists_key) {
if cached_at.elapsed() < EXISTS_CACHE_TIMEOUT {
tracing::debug!(
workspace_id = %workspace_id,
?language,
exists,
"cache hit for unnamed workspace dependencies existence"
);
return Some(exists);
} else {
tracing::debug!(
workspace_id = %workspace_id,
?language,
"cache expired for unnamed workspace dependencies existence"
);
DEFAULT_WD_EXISTS_CACHE.remove(&exists_key);
}
} else {
tracing::debug!(
workspace_id = %workspace_id,
?language,
"cache miss for unnamed workspace dependencies existence"
);
}
None
}
pub fn set_cached_is_unnamed_workspace_dependencies_exists<'c>(
language: ScriptLang,
workspace_id: String,
exists: bool,
) {
tracing::debug!(
workspace_id = %workspace_id,
?language,
exists,
"setting cache for unnamed workspace dependencies existence"
);
let exists_key = (workspace_id.to_string(), language);
DEFAULT_WD_EXISTS_CACHE.insert(exists_key, (exists, Instant::now()));
}
}
const _: () = {
impl Import for RawFlow {
fn import(src: &impl Storage) -> error::Result<Self> {
@@ -1075,7 +1155,8 @@ const _: () = {
(ScriptHash, |x| format!("{:016x}", x.0)),
((u8, ScriptHash), |x| format!("{:02x}-{:016x}", x.0, x.1.0)),
(FlowNodeId, |x| format!("{:016x}", x.0)),
(AppScriptId, |x| format!("{:016x}", x.0))
(AppScriptId, |x| format!("{:016x}", x.0)),
((i64, String), |x| format!("{}-{}", x.1, x.0))
}
#[cfg(feature = "scoped_cache")]
@@ -114,6 +114,7 @@ pub const ENV_SETTINGS: &[&str] = &[
"DISABLE_S3_STORE",
"PG_SCHEMA",
"PG_LISTENER_REFRESH_PERIOD_SECS",
"AI_REQUEST_TIMEOUT_SECONDS",
];
use crate::error;
+10
View File
@@ -98,6 +98,16 @@ pub enum JobKind {
AIAgent,
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug, PartialEq, Copy, Clone)]
#[sqlx(type_name = "JOB_STATUS", rename_all = "lowercase")]
#[serde(rename_all(serialize = "lowercase", deserialize = "lowercase"))]
pub enum JobStatus {
Success,
Failure,
Canceled,
Skipped,
}
impl JobKind {
pub fn is_flow(&self) -> bool {
matches!(
+61 -14
View File
@@ -231,7 +231,7 @@ impl From<i64> for ScriptHash {
}
}
#[derive(PartialEq, sqlx::Type)]
#[derive(PartialEq, sqlx::Type, Debug)]
#[sqlx(transparent, no_pg_array)]
pub struct ScriptHashes(pub Vec<i64>);
@@ -347,7 +347,7 @@ pub fn id_to_codebase_info(id: &str) -> CodebaseInfo {
let is_esm = id.contains(".esm");
CodebaseInfo { is_tar, is_esm }
}
#[derive(Serialize, sqlx::FromRow)]
#[derive(Serialize, sqlx::FromRow, Debug)]
pub struct Script {
pub workspace_id: String,
pub hash: ScriptHash,
@@ -361,7 +361,8 @@ pub struct Script {
pub archived: bool,
pub schema: Option<Schema>,
pub deleted: bool,
pub is_template: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_template: Option<bool>,
pub extra_perms: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub lock: Option<String>,
@@ -815,6 +816,61 @@ pub fn hash_script(ns: &NewScript) -> i64 {
dh.finish() as i64
}
pub async fn fetch_script_for_update<'a>(
hash: ScriptHash,
w_id: &str,
e: impl sqlx::Executor<'a, Database = sqlx::Postgres>,
) -> crate::error::Result<Option<Script>> {
sqlx::query_as::<_, Script>(
"SELECT
workspace_id,
hash,
path,
parent_hashes,
summary,
description,
content,
created_by,
created_at,
archived,
schema,
deleted,
is_template,
extra_perms,
lock,
lock_error_logs,
language,
kind,
tag,
draft_only,
envs,
concurrency_key,
concurrent_limit,
concurrency_time_window_s,
debounce_key,
debounce_delay_s,
dedicated_worker,
ws_error_handler_muted,
priority,
cache_ttl,
timeout,
delete_after_use,
restart_unless_cancelled,
visible_to_runner_only,
no_main_func,
codebase,
has_preprocessor,
on_behalf_of_email,
assets
FROM script WHERE hash = $1 AND workspace_id = $2 AND archived = false FOR UPDATE",
)
.bind(hash.0)
.bind(w_id)
.fetch_optional(e)
.await
.map_err(crate::error::Error::from)
}
pub struct ClonedScript {
pub old_script: NewScript,
pub new_hash: i64,
@@ -826,16 +882,7 @@ pub async fn clone_script<'c>(
deployment_message: Option<String>,
tx: &mut sqlx::Transaction<'c, sqlx::Postgres>,
) -> crate::error::Result<ClonedScript> {
// TODO:!
let s = sqlx::query_as::<_, Script>(
"SELECT * FROM script WHERE hash = $1 AND workspace_id = $2 AND archived = false FOR UPDATE",
)
.bind(base_hash.0)
.bind(w_id)
.fetch_optional(&mut **tx)
.await?;
let s = if let Some(s) = s {
let s = if let Some(s) = fetch_script_for_update(base_hash, w_id, &mut **tx).await? {
s
} else {
return Err(crate::error::Error::NotFound(format!(
@@ -851,7 +898,7 @@ pub async fn clone_script<'c>(
description: s.description,
content: s.content,
schema: s.schema,
is_template: Some(s.is_template),
is_template: s.is_template,
// TODO: Make it either None everywhere (particularly when raw reqs are calculated)
// Or handle this case and conditionally make Some (only with raw reqs)
lock: None,
@@ -2,9 +2,17 @@ use itertools::Itertools;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlx::PgExecutor;
use std::time::{Duration, Instant};
use crate::{error, scripts::ScriptLang, utils::calculate_hash, worker::Connection};
use crate::{
cache::workspace_dependencies::{
fetch_workspace_dependencies, get_cached_is_unnamed_workspace_dependencies_exists,
set_cached_is_unnamed_workspace_dependencies_exists,
},
error,
scripts::ScriptLang,
utils::calculate_hash,
worker::Connection,
};
use phf::phf_set;
pub static BLACKLIST: phf::Set<&'static str> = phf_set! {
@@ -15,16 +23,8 @@ pub static BLACKLIST: phf::Set<&'static str> = phf_set! {
lazy_static::lazy_static! {
static ref WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES: bool = std::env::var("WMDEBUG_FORCE_V0_WORKSPACE_DEPENDENCIES").is_ok();
/// Simple in-memory cache for workspace dependencies get_latest with 10-second timeout.
/// Cache key: (workspace_id, language, name)
/// Cache value: (Option<WorkspaceDependencies>, cached_at timestamp)
static ref WORKSPACE_DEPENDENCIES_CACHE: quick_cache::sync::Cache<(String, ScriptLang, Option<String>), (Option<WorkspaceDependencies>, Instant)> = quick_cache::sync::Cache::new(1000);
}
/// Cache timeout for workspace dependencies
const CACHE_TIMEOUT: Duration = Duration::from_secs(10);
/// Minimum Windmill version required for workspace dependencies feature
pub const MIN_VERSION_WORKSPACE_DEPENDENCIES: &str = "1.587.0";
@@ -174,6 +174,42 @@ impl WorkspaceDependencies {
.map_err(error::Error::from)
}
async fn get_latest_id<'c>(
name: Option<String>,
language: ScriptLang,
workspace_id: &str,
e: impl PgExecutor<'c>,
) -> error::Result<Option<i64>> {
tracing::debug!(
workspace_id = %workspace_id,
?language,
?name,
"fetching latest workspace dependencies id"
);
let result = sqlx::query_scalar!(
r#"
SELECT id FROM workspace_dependencies
WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3
LIMIT 1
"#,
name,
workspace_id,
language as ScriptLang
)
.fetch_optional(e)
.await
.map_err(error::Error::from)?;
tracing::debug!(
workspace_id = %workspace_id,
?language,
?name,
?result,
"fetched latest workspace dependencies id"
);
Ok(result)
}
/// Gets the latest version of workspace dependencies by name and language.
pub async fn get_latest(
name: Option<String>,
@@ -185,66 +221,84 @@ impl WorkspaceDependencies {
return Ok(None);
}
let cache_key = (workspace_id.to_string(), language, name.clone());
// Check if cached value is still valid
if let Some((cached_value, cached_at)) = WORKSPACE_DEPENDENCIES_CACHE.get(&cache_key) {
if cached_at.elapsed() < CACHE_TIMEOUT {
return Ok(cached_value);
}
// Expired, remove it
WORKSPACE_DEPENDENCIES_CACHE.remove(&cache_key);
if name.is_none()
&& get_cached_is_unnamed_workspace_dependencies_exists(
language,
workspace_id.to_owned(),
)
.map(|exists| exists == false)
.unwrap_or_default()
{
tracing::debug!(
workspace_id = %workspace_id,
?language,
"skipping unnamed workspace dependencies fetch - cached as non-existent"
);
return Ok(None);
}
// Fetch and cache
let fetch = Box::pin(async {
match &conn {
Connection::Sql(db) => sqlx::query_as!(
Self,
r#"
SELECT id, content, language AS "language: ScriptLang", name, description, archived, workspace_id, created_at
FROM workspace_dependencies
WHERE name IS NOT DISTINCT FROM $1 AND workspace_id = $2 AND archived = false AND language = $3
LIMIT 1
"#,
name,
workspace_id,
language as ScriptLang
)
.fetch_optional(db)
.await
.map_err(error::Error::from),
// Fetch from database or HTTP
let wd = match &conn {
Connection::Sql(db) => {
let Some(id) =
Self::get_latest_id(name.clone(), language, workspace_id, db).await?
else {
if name.is_none() {
set_cached_is_unnamed_workspace_dependencies_exists(
language,
workspace_id.to_owned(),
false,
);
}
Connection::Http(http_client) => http_client
.get::<Option<WorkspaceDependencies>>(&format!(
"/api/w/{workspace_id}/agent_workers/workspace_dependencies/get_latest/{}{}",
language.as_str(),
if let Some(ref name_val) = name {
format!("?name={name_val}")
} else {
"".to_owned()
}
))
.await
.map_err(error::Error::from),
tracing::debug!(
workspace_id = %workspace_id,
?language,
?name,
"no latest workspace dependencies found"
);
return Ok(None);
};
tracing::debug!(
workspace_id = %workspace_id,
?language,
?name,
id,
"fetching workspace dependencies by id from cache or db"
);
Some(fetch_workspace_dependencies(id, workspace_id.to_owned(), db).await?)
}
});
let (workspace_dependencies_o, ..) = WORKSPACE_DEPENDENCIES_CACHE
.get_or_insert_async(&cache_key, async {
Ok::<_, error::Error>((fetch.await?, Instant::now()))
})
.await?;
Connection::Http(http_client) => http_client
.get::<Option<WorkspaceDependencies>>(&format!(
"/api/w/{workspace_id}/agent_workers/workspace_dependencies/get_latest/{}{}",
language.as_str(),
if let Some(ref name_val) = name {
format!("?name={name_val}")
} else {
"".to_owned()
}
))
.await
.map_err(error::Error::from)?,
};
Ok(workspace_dependencies_o)
if name.is_none() {
set_cached_is_unnamed_workspace_dependencies_exists(
language,
workspace_id.to_owned(),
wd.is_some(),
);
}
Ok(wd)
}
/// Gets workspace dependencies by their unique ID.
pub async fn get<'c>(
id: i64,
workspace_id: &str,
workspace_id: String,
e: impl PgExecutor<'c>,
) -> error::Result<Option<Self>> {
) -> error::Result<Self> {
sqlx::query_as!(
Self,
r#"
@@ -254,9 +308,9 @@ impl WorkspaceDependencies {
LIMIT 1
"#,
id,
workspace_id
&workspace_id
)
.fetch_optional(e)
.fetch_one(e)
.await
.map_err(error::Error::from)
}
@@ -1,5 +1,8 @@
use serde::{Deserialize, Serialize};
use windmill_common::{error, scripts::ScriptLang, workspace_dependencies::WorkspaceDependencies};
use windmill_common::{
cache::workspace_dependencies::EXISTS_CACHE_TIMEOUT, error, scripts::ScriptLang,
workspace_dependencies::WorkspaceDependencies,
};
use crate::{
scoped_dependency_map::ScopedDependencyMap, trigger_dependents_to_recompute_dependencies,
@@ -25,10 +28,8 @@ impl NewWorkspaceDependencies {
/// and rebuilds the dependency map if this is the first unnamed dependency for the workspace.
pub async fn create<'c>(
self,
email: &str,
created_by: &str,
permissioned_as: &str,
db: &sqlx::Pool<sqlx::Postgres>,
metadata: (String, String, String),
db: sqlx::Pool<sqlx::Postgres>,
) -> error::Result<i64> {
// Check if all workers support workspace dependencies feature
windmill_common::workspace_dependencies::min_version_supports_v0_workspace_dependencies()
@@ -43,7 +44,7 @@ impl NewWorkspaceDependencies {
let setting_name = format!("workspace_dependencies_map_rebuilt:{}", self.workspace_id);
let already_rebuilt =
windmill_common::global_settings::load_value_from_global_settings(
db,
&db,
&setting_name,
)
.await?
@@ -54,11 +55,11 @@ impl NewWorkspaceDependencies {
workspace_id = %self.workspace_id,
"Rebuilding workspace dependencies map for first unnamed workspace dependencies"
);
ScopedDependencyMap::rebuild_map_unchecked(&self.workspace_id, db).await?;
ScopedDependencyMap::rebuild_map_unchecked(&self.workspace_id, &db).await?;
// Mark as rebuilt by creating the setting
windmill_common::global_settings::set_value_in_global_settings(
db,
&db,
&setting_name,
serde_json::json!({}),
)
@@ -111,50 +112,83 @@ impl NewWorkspaceDependencies {
.await?;
tx.commit().await?;
// Make sure trigger dependents will have latest view.
// NOTE: Uncomment for tests
// #[cfg(test)]
// assert_eq!(
// sqlx::query_scalar!(
// "
// SELECT id FROM workspace_dependencies
// WHERE archived = false
// AND name IS NOT DISTINCT FROM $1
// AND workspace_id = $2
// AND language = $3
// ",
// self.name,
// self.workspace_id,
// self.language as ScriptLang,
// )
// .fetch_one(db) // Use db
// .await?,
// new_id
// );
// It's ok to fail, it will return an error and user will get notified that they should redeploy workspace dependencies
trigger_dependents_to_recompute_dependencies(
&self.workspace_id,
crate::scoped_dependency_map::ScopedDependencyMap::get_dependents(
path.as_str(),
&self.workspace_id,
db,
)
.await?,
None,
None,
email,
created_by,
permissioned_as,
trigger_dependents_to_recompute_dependencies_in_the_background(
prev_description.is_none() && self.name.is_none(),
self.workspace_id,
self.language,
metadata,
path,
db,
vec![],
)
.await?;
.await;
Ok(new_id)
}
}
pub async fn trigger_dependents_to_recompute_dependencies_in_the_background(
wait_for_cache_timeout: bool,
workspace_id: String,
language: ScriptLang,
(email, permissioned_as, created_by): (String, String, String),
path: String,
db: sqlx::Pool<sqlx::Postgres>,
) {
tokio::spawn(async move {
if wait_for_cache_timeout {
tracing::debug!(
workspace_id = %workspace_id,
language = ?language,
"waiting for cache timeout after creating first unnamed workspace dependencies"
);
// Wait for cache timeout.
// For context, workers have cache on whether the unnamed workspace dependencies exists or not.
// when we trigger dependents to recompoute dependencies we want to make sure all workers are having cache timed out.
// otherwise it would result into bug, when workers skip fetch of workspace dependencies because they think they don't exist.
tokio::time::sleep(EXISTS_CACHE_TIMEOUT).await;
}
// It's ok to fail, it will return an error and user will get notified that they should redeploy workspace dependencies
if let Err(e) = trigger_dependents_to_recompute_dependencies(
&workspace_id,
match crate::scoped_dependency_map::ScopedDependencyMap::get_dependents(
path.as_str(),
&workspace_id,
&db,
)
.await
{
Ok(importers) => importers,
Err(e) => {
tracing::error!(
workspace_id = %workspace_id,
path = %path,
error = %e,
"CRITICAL: failed to get dependents for workspace dependencies - dependent runnables are not being redeployed. Please contact the Windmill team"
);
return;
}
},
None,
None,
email.as_str(),
created_by.as_str(),
permissioned_as.as_str(),
&db,
vec![],
)
.await
{
tracing::error!(
workspace_id = %workspace_id,
path = %path,
error = %e,
"CRITICAL: failed to trigger dependents to recompute dependencies - dependent runnables are not being redeployed. Please contact the Windmill team"
);
}
});
}
// Type aliases for backward compatibility
pub type RawRequirements = WorkspaceDependencies;
pub type NewRawRequirements = NewWorkspaceDependencies;
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.587.1";
export const VERSION = "v1.588.0";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+23 -10
View File
@@ -16,6 +16,7 @@ import { ListableApp, Policy } from "../../../gen/types.gen.ts";
import { GlobalOptions, isSuperset } from "../../types.ts";
import { readInlinePathSync } from "../../utils/utils.ts";
import devCommand from "./dev.ts";
import { isVersionsGeq15851 } from "../sync/global.ts";
export interface AppFile {
value: any;
@@ -30,22 +31,26 @@ function respecializeFields(fields: Record<string, any>) {
Object.entries(fields).forEach(([k, v]) => {
if (typeof v == "object") {
if (v.value !== undefined) {
fields[k] = { value: v.value, type: "static" }
fields[k] = { value: v.value, type: "static" };
} else if (v.expr !== undefined) {
fields[k] = { expr: v.expr, allowUserResources: v.allowUserResources, type: "javascript" }
fields[k] = {
expr: v.expr,
allowUserResources: v.allowUserResources,
type: "javascript",
};
}
}
})
});
}
export function repopulateFields(runnables: Record<string, any>) {
Object.values(runnables).forEach((v) => {
if (typeof v == "object") {
if (v.fields !== undefined) {
respecializeFields(v.fields)
respecializeFields(v.fields);
}
}
})
});
}
export function replaceInlineScripts(rec: any, localPath: string) {
if (!rec) {
@@ -53,11 +58,14 @@ export function replaceInlineScripts(rec: any, localPath: string) {
}
if (typeof rec == "object") {
return Object.entries(rec).flatMap(([k, v]) => {
if (k == 'runType') {
rec["type"] = 'path'
if (k == "runType") {
if (isVersionsGeq15851()) {
rec["type"] = "path";
}
} else if (k == "inlineScript" && typeof v == "object") {
rec["type"] = 'inline'
if (isVersionsGeq15851()) {
rec["type"] = "inline";
}
const o: Record<string, any> = v as any;
if (o["content"] && o["content"].startsWith("!inline")) {
@@ -114,7 +122,12 @@ export async function pushApp(
const localApp = (await yamlParseFile(path)) as AppFile;
replaceInlineScripts(localApp.value, localPath);
await generatingPolicy(localApp, remotePath, localApp?.["public"] ?? false);
await generatingPolicy(
localApp,
remotePath,
localApp?.["public"] ??
localApp?.["policy"]?.["execution_mode"] == "anonymous"
);
if (app) {
if (isSuperset(localApp, app)) {
log.info(colors.green(`App ${remotePath} is up to date`));
-5
View File
@@ -196,7 +196,6 @@ async function run(
async function generateLocks(
opts: GlobalOptions & {
yes?: boolean;
useRawRequirements?: boolean;
} & SyncOptions,
folder: string | undefined
) {
@@ -322,10 +321,6 @@ const command = new Command()
)
.arguments("[flow:file]")
.option("--yes", "Skip confirmation prompt")
.option(
"-r --use-raw-requirements",
"Use raw requirements (requirements.txt, go.mod, package.json, etc) instead of generating them on the server (can also be set with USE_RAW_REQUIREMENTS=true environment variable)"
)
.option(
"-i --includes <patterns:file[]>",
"Comma separated patterns to specify which file to take into account (among files that are compatible with windmill). Patterns can include * (any string until '/') and ** (any string)"
+282
View File
@@ -0,0 +1,282 @@
// deno-lint-ignore-file no-explicit-any
import { GlobalOptions } from "../../types.ts";
import { requireLogin } from "../../core/auth.ts";
import { resolveWorkspace } from "../../core/context.ts";
import { colors, Command, Confirm, log } from "../../../deps.ts";
import { mergeConfigWithConfigFile } from "../../core/conf.ts";
import * as fs from "node:fs/promises";
import * as wmill from "../../../gen/services.gen.ts";
async function pullJobs(
opts: GlobalOptions & {
completedOutput?: string;
queuedOutput?: string;
skipWorkerCheck?: boolean;
},
workspace?: string
) {
opts = await mergeConfigWithConfigFile(opts);
const ws = await resolveWorkspace({ ...opts, workspace });
await requireLogin(opts);
log.info("Pulling jobs from workspace " + ws.workspaceId);
// Check for active workers and warn user
if (!opts.skipWorkerCheck) {
try {
const workers = await wmill.listWorkers({ pingSince: 60 });
if (workers.length > 0) {
log.info(
colors.yellow(
`\nWarning: Found ${workers.length} active worker(s) on the instance.`
)
);
log.info(
"It's recommended to scale down all workers before exporting jobs to ensure that no new jobs are being created during export."
);
const proceed = await Confirm.prompt({
message: "Do you want to continue with the export anyway?",
default: false,
});
if (!proceed) {
log.info(
"Export cancelled. Please scale down workers and try again."
);
log.info("You can skip this check with --skip-worker-check flag.");
return;
}
}
} catch (e) {
log.debug(`Could not check for active workers: ${e}`);
}
}
// Pull completed jobs
let completedJobs: any[] = [];
let page = 1;
const perPage = 1000;
while (true) {
const batch = await wmill.exportCompletedJobs({
workspace: ws.workspaceId,
page,
perPage,
});
if (batch.length === 0) break;
completedJobs = completedJobs.concat(batch);
if (batch.length < perPage) break;
page++;
}
const completedPath = opts.completedOutput || "completed_jobs.json";
await fs.writeFile(completedPath, JSON.stringify(completedJobs, null, 2));
log.info(
colors.green(
`Successfully pulled ${completedJobs.length} completed jobs to ${completedPath}`
)
);
// Pull queued jobs
let queuedJobs: any[] = [];
page = 1;
while (true) {
const batch = await wmill.exportQueuedJobs({
workspace: ws.workspaceId,
page,
perPage,
});
if (batch.length === 0) break;
queuedJobs = queuedJobs.concat(batch);
if (batch.length < perPage) break;
page++;
}
const queuedPath = opts.queuedOutput || "queued_jobs.json";
await fs.writeFile(queuedPath, JSON.stringify(queuedJobs, null, 2));
log.info(
colors.green(
`Successfully pulled ${queuedJobs.length} queued jobs to ${queuedPath}`
)
);
// Ask to delete all jobs (queued and completed)
const allJobs = [...queuedJobs, ...completedJobs];
if (allJobs.length > 0) {
const confirmed = await Confirm.prompt({
message: `Do you want to delete the ${allJobs.length} pulled jobs (queued + completed) from the workspace? If you don't, you won't be able to import them again on the same instance because of the unique constraint on the job ID.`,
default: false,
});
if (confirmed) {
const jobIds = allJobs.map((job: any) => job.id);
await wmill.deleteJobs({
workspace: ws.workspaceId,
requestBody: jobIds,
});
log.info(
colors.green(`Deleted ${jobIds.length} jobs (queued + completed)`)
);
} else {
log.info("Skipping deletion of jobs");
}
}
}
async function pushJobs(
opts: GlobalOptions & {
completedFile?: string;
queuedFile?: string;
skipWorkerCheck?: boolean;
},
workspace?: string
) {
opts = await mergeConfigWithConfigFile(opts);
const ws = await resolveWorkspace({ ...opts, workspace });
await requireLogin(opts);
log.info(`Pushing jobs to workspace ${ws.workspaceId}`);
// Check for active workers before importing
if (!opts.skipWorkerCheck) {
try {
const workers = await wmill.listWorkers({ pingSince: 60 });
if (workers.length > 0) {
log.info(
colors.yellow(
`\nWarning: Found ${workers.length} active worker(s) on the instance.`
)
);
log.info(
"It's recommended to scale down all workers before importing jobs to ensure:"
);
log.info(
" - No imported jobs are processed immediately during import"
);
log.info(
" - You have time to review or adjust the imported jobs before they start running"
);
log.info("");
const proceed = await Confirm.prompt({
message: "Do you want to continue with the import anyway?",
default: false,
});
if (!proceed) {
log.info(
"Import cancelled. Please scale down workers and try again."
);
log.info("You can skip this check with --skip-worker-check flag.");
return;
}
}
} catch (e) {
log.debug(`Could not check for active workers: ${e}`);
}
}
// Push completed jobs
const completedPath = opts.completedFile || "completed_jobs.json";
try {
const completedContent = await fs.readFile(completedPath, "utf-8");
const completedJobs = JSON.parse(completedContent);
if (!Array.isArray(completedJobs)) {
throw new Error("Completed jobs file must contain an array of jobs");
}
const completedResult = await wmill.importCompletedJobs({
workspace: ws.workspaceId,
requestBody: completedJobs,
});
log.info(colors.green(`Completed jobs: ${completedResult}`));
} catch (e: any) {
if (e.code === "ENOENT") {
log.info(
colors.yellow(
`No completed jobs file found at ${completedPath}, skipping`
)
);
} else {
throw new Error(`Failed to push completed jobs: ${e}`);
}
}
// Push queued jobs
const queuedPath = opts.queuedFile || "queued_jobs.json";
try {
const queuedContent = await fs.readFile(queuedPath, "utf-8");
const queuedJobs = JSON.parse(queuedContent);
if (!Array.isArray(queuedJobs)) {
throw new Error("Queued jobs file must contain an array of jobs");
}
const queuedResult = await wmill.importQueuedJobs({
workspace: ws.workspaceId,
requestBody: queuedJobs,
});
log.info(colors.green(`Queued jobs: ${queuedResult}`));
} catch (e: any) {
if (e.code === "ENOENT") {
log.info(
colors.yellow(`No queued jobs file found at ${queuedPath}, skipping`)
);
} else {
throw new Error(`Failed to push queued jobs: ${e}`);
}
}
}
const pull = new Command()
.description("Pull completed and queued jobs from workspace")
.option(
"-c, --completed-output <file:string>",
"Completed jobs output file (default: completed_jobs.json)"
)
.option(
"-q, --queued-output <file:string>",
"Queued jobs output file (default: queued_jobs.json)"
)
.option(
"--skip-worker-check",
"Skip checking for active workers before export"
)
.arguments("[workspace:string]")
.action(pullJobs as any);
const push = new Command()
.description("Push completed and queued jobs to workspace")
.option(
"-c, --completed-file <file:string>",
"Completed jobs input file (default: completed_jobs.json)"
)
.option(
"-q, --queued-file <file:string>",
"Queued jobs input file (default: queued_jobs.json)"
)
.option(
"--skip-worker-check",
"Skip checking for active workers before import"
)
.arguments("[workspace:string]")
.action(pushJobs as any);
const command = new Command()
.description("Manage jobs (import/export)")
.command("pull", pull)
.command("push", push);
export default command;
+31
View File
@@ -0,0 +1,31 @@
import { colors, log } from "../../../deps.ts";
let GLOBAL_VERSIONS: {
remoteMajor: number | undefined;
remoteMinor: number | undefined;
} = {
remoteMajor: undefined,
remoteMinor: undefined,
};
export function updateGlobalVersions(version: string) {
try {
const [prefix, remoteMinorStr] = version.split(".");
GLOBAL_VERSIONS = {
remoteMajor: parseInt(prefix.split("v")[1]),
remoteMinor: parseInt(remoteMinorStr),
};
} catch (e) {
log.info(colors.gray(`Error reading remote version: ${e}`));
}
}
export function isVersionsGeq15851(): boolean {
return (
GLOBAL_VERSIONS.remoteMajor !== undefined &&
GLOBAL_VERSIONS.remoteMajor >= 1 &&
GLOBAL_VERSIONS.remoteMinor !== undefined &&
GLOBAL_VERSIONS.remoteMinor >= 5851
);
}
+135 -71
View File
@@ -34,7 +34,12 @@ import {
} from "../script/script.ts";
import { handleFile } from "../script/script.ts";
import { deepEqual, isFileResource, isRawAppFile, isWorkspaceDependencies } from "../../utils/utils.ts";
import {
deepEqual,
isFileResource,
isRawAppFile,
isWorkspaceDependencies,
} from "../../utils/utils.ts";
import {
getEffectiveSettings,
mergeConfigWithConfigFile,
@@ -70,6 +75,7 @@ import { extractInlineScripts as extractInlineScriptsForFlows } from "../../../w
import { generateFlowLockInternal } from "../flow/flow_metadata.ts";
import { isExecutionModeAnonymous } from "../app/apps.ts";
import { generateAppLocksInternal } from "../app/app_metadata.ts";
import { updateGlobalVersions } from "./global.ts";
// Merge CLI options with effective settings, preserving CLI flags as overrides
function mergeCliWithEffectiveOptions<
@@ -268,29 +274,28 @@ function extractFields(fields: Record<string, any>) {
Object.entries(fields).forEach(([k, v]) => {
if (typeof v == "object") {
if (v.type == "static") {
fields[k] = { value: v.value }
fields[k] = { value: v.value };
} else if (v.type == "javascript") {
fields[k] = { expr: v.expr, allowUserResources: v.allowUserResources }
fields[k] = { expr: v.expr, allowUserResources: v.allowUserResources };
} else if (v.type == "user") {
fields[k] = undefined
fields[k] = undefined;
}
}
// if (k == 'runType') {
// fields["type"] = undefined
// fields["schema"] = undefined
// }
})
});
}
export function extractFieldsForRawApps(runnables: Record<string, any>) {
Object.values(runnables).forEach((v) => {
if (typeof v == "object") {
if (v.fields !== undefined) {
extractFields(v.fields)
extractFields(v.fields);
}
}
})
});
}
export function extractInlineScriptsForApps(
key: string | undefined,
@@ -303,12 +308,12 @@ export function extractInlineScriptsForApps(
}
if (typeof rec == "object") {
return Object.entries(rec).flatMap(([k, v]) => {
if (k == 'runType') {
rec["type"] = undefined
rec["schema"] = undefined
return []
if (k == "runType") {
rec["type"] = undefined;
rec["schema"] = undefined;
return [];
} else if (k == "inlineScript" && typeof v == "object") {
rec["type"] = undefined
rec["type"] = undefined;
const o: Record<string, any> = v as any;
const name = toId(key ?? "", rec);
const [basePathO, ext] = pathAssigner.assignPath(name, o["language"]);
@@ -351,20 +356,26 @@ function ZipFSElement(
p: string,
f: JSZip.JSZipObject
): Promise<DynFSElement[]> {
const kind: "flow" | "app" | "script" | "resource" | "other" | "raw_app" | "dependencies" =
p.endsWith(".flow.json")
? "flow"
: p.endsWith(".app.json")
? "app"
: p.endsWith(".raw_app.json")
? "raw_app"
: p.endsWith(".script.json")
? "script"
: p.endsWith(".resource.json")
? "resource"
: p.startsWith("dependencies/")
const kind:
| "flow"
| "app"
| "script"
| "resource"
| "other"
| "raw_app"
| "dependencies" = p.endsWith(".flow.json")
? "flow"
: p.endsWith(".app.json")
? "app"
: p.endsWith(".raw_app.json")
? "raw_app"
: p.endsWith(".script.json")
? "script"
: p.endsWith(".resource.json")
? "resource"
: p.startsWith("dependencies/")
? "dependencies"
: "other";
: "other";
const isJson = p.endsWith(".json");
@@ -502,7 +513,7 @@ function ZipFSElement(
undefined,
value,
newPathAssigner(defaultTs),
(key, val_) => key
(key, val_) => key
);
} catch (error) {
log.error(
@@ -759,7 +770,11 @@ export async function* readDirRecursiveWithIgnore(
for await (const e2 of e.c()) {
if (e2.isDirectory) {
const dirName = e2.path.split(SEP).pop();
if (dirName == "node_modules" || dirName == ".claude" || dirName?.startsWith(".")) {
if (
dirName == "node_modules" ||
dirName == ".claude" ||
dirName?.startsWith(".")
) {
continue;
}
}
@@ -792,7 +807,7 @@ export async function elementsToMap(
ignore: (path: string, isDirectory: boolean) => boolean,
json: boolean,
skips: Skips,
specificItems?: SpecificItemsConfig,
specificItems?: SpecificItemsConfig
): Promise<{ [key: string]: string }> {
const map: { [key: string]: string } = {};
const processedBasePaths = new Set<string>();
@@ -802,7 +817,11 @@ export async function elementsToMap(
continue;
}
const path = entry.path;
if (!isFileResource(path) && !isRawAppFile(path) && !isWorkspaceDependencies(path)) {
if (
!isFileResource(path) &&
!isRawAppFile(path) &&
!isWorkspaceDependencies(path)
) {
if (json && path.endsWith(".yaml")) continue;
if (!json && path.endsWith(".json")) continue;
@@ -835,7 +854,11 @@ export async function elementsToMap(
if (isRawAppFile(path)) {
const suffix = path.split(".raw_app" + SEP).pop();
if (suffix?.startsWith("dist/") || suffix == "wmill.d.ts" || suffix == "package-lock.json") {
if (
suffix?.startsWith("dist/") ||
suffix == "wmill.d.ts" ||
suffix == "package-lock.json"
) {
continue;
}
}
@@ -867,7 +890,6 @@ export async function elementsToMap(
continue;
}
// Use getTypeStrFromPath for consistent type detection
try {
const fileType = getTypeStrFromPath(path);
@@ -876,12 +898,15 @@ export async function elementsToMap(
if (skips.skipFlows && fileType === "flow") continue;
if (skips.skipApps && fileType === "app") continue;
if (skips.skipFolders && fileType === "folder") continue;
if (skips.skipWorkspaceDependencies && fileType === "workspace_dependencies") continue;
if (
skips.skipWorkspaceDependencies &&
fileType === "workspace_dependencies"
)
continue;
} catch {
// If getTypeStrFromPath can't determine the type, continue processing the file
}
// Handle branch-specific files - skip files for other branches
if (specificItems && isBranchSpecificFile(path)) {
const currentBranch = getCurrentGitBranch();
@@ -945,7 +970,7 @@ export async function elementsToMap(
// No specific items configuration, use regular path
map[entry.path] = content;
}
}
}
return map;
}
@@ -1090,7 +1115,6 @@ async function compareDynFSElement(
const remoteCodebase: Record<string, string> = {};
for (const [k] of Object.entries(m2)) {
if (m1[k] === undefined) {
if (
!ignoreMetadataDeletion ||
@@ -1239,7 +1263,7 @@ export const isWhitelisted = (p: string) => {
p == "f" ||
p == "g" ||
p == "users" ||
p == "groups" ||
p == "groups" ||
p == "dependencies"
);
};
@@ -1308,7 +1332,10 @@ export async function ignoreF(wmillconf: {
if (wmillconf.includeKey && fileType === "encryption_key") {
return false; // Don't ignore, always include
}
if (!wmillconf.skipWorkspaceDependencies && fileType === "workspace_dependencies") {
if (
!wmillconf.skipWorkspaceDependencies &&
fileType === "workspace_dependencies"
) {
return false; // Don't ignore workspace dependencies (they are always included unless explicitly skipped)
}
} catch {
@@ -1440,22 +1467,22 @@ export async function pull(
// ignore
}
const zipFile = await downloadZip(
workspace,
opts.plainSecrets,
opts.skipVariables,
opts.skipResources,
opts.skipResourceTypes,
opts.skipSecrets,
opts.includeSchedules,
opts.includeTriggers,
opts.includeUsers,
opts.includeGroups,
opts.includeSettings,
opts.includeKey,
opts.skipWorkspaceDependencies,
opts.defaultTs
);
workspace,
opts.plainSecrets,
opts.skipVariables,
opts.skipResources,
opts.skipResourceTypes,
opts.skipSecrets,
opts.includeSchedules,
opts.includeTriggers,
opts.includeUsers,
opts.includeGroups,
opts.includeSettings,
opts.includeKey,
opts.skipWorkspaceDependencies,
opts.defaultTs
);
const remote = ZipFSElement(
zipFile!,
!opts.json,
@@ -1482,7 +1509,6 @@ export async function pull(
`remote (${workspace.name}) -> local: ${changes.length} changes to apply`
);
// Handle JSON output for dry-run
if (opts.dryRun && opts.jsonOutput) {
const result = {
@@ -1671,7 +1697,8 @@ export async function pull(
await readLockfile(); // ensure wmill-lock.yaml exists
const tracker: ChangeTracker = await buildTracker(changes);
const rawWorkspaceDependencies: Record<string, string> = await getRawWorkspaceDependencies();
const rawWorkspaceDependencies: Record<string, string> =
await getRawWorkspaceDependencies();
for (const change of tracker.scripts) {
await generateScriptMetadataInternal(
@@ -1687,7 +1714,14 @@ export async function pull(
}
for (const change of tracker.flows) {
log.info(`Updating lock for flow ${change}`);
await generateFlowLockInternal(change, false, workspace, opts, true, false);
await generateFlowLockInternal(
change,
false,
workspace,
opts,
true,
false
);
}
if (tracker.apps.length > 0) {
log.info(
@@ -1700,7 +1734,14 @@ export async function pull(
}
for (const change of tracker.rawApps) {
log.info(`Updating lock metadata for raw app ${change}`);
await generateAppLocksInternal(change, false, workspace, opts, true, true);
await generateAppLocksInternal(
change,
false,
workspace,
opts,
true,
true
);
}
if (opts.jsonOutput) {
const result = {
@@ -1899,6 +1940,7 @@ export async function push(
} catch {
// ignore
}
const remote = ZipFSElement(
(await downloadZip(
workspace,
@@ -1935,13 +1977,13 @@ export async function push(
specificItems
);
const rawWorkspaceDependencies = await getRawWorkspaceDependencies();
const tracker: ChangeTracker = await buildTracker(changes);
const staleScripts: string[] = [];
const staleFlows: string[] = [];
for (const change of tracker.scripts) {
const stale = await generateScriptMetadataInternal(
change,
@@ -1995,7 +2037,9 @@ export async function push(
}
const version = await fetchVersion(workspace.remote);
if (version) {
updateGlobalVersions(version);
}
log.info(colors.gray("Remote version: " + version));
log.info(
@@ -2097,14 +2141,18 @@ export async function push(
const alreadySynced: string[] = [];
const isRawApp = isRawAppFile(changes[0].path);
if (isRawApp) {
const deleteRawApp = changes.find(change => change.name === "deleted" && change.path.endsWith(".raw_app/raw_app.yaml"))
const deleteRawApp = changes.find(
(change) =>
change.name === "deleted" &&
change.path.endsWith(".raw_app/raw_app.yaml")
);
if (deleteRawApp) {
changes = [deleteRawApp];
} else {
changes.splice(1, changes.length - 1);
}
}
for await (const change of changes) {
let stateTarget = undefined;
if (stateful) {
@@ -2324,13 +2372,16 @@ export async function push(
});
break;
case "raw_app":
if (target.endsWith(".raw_app/raw_app.yaml") || target.endsWith(".raw_app/raw_app.json")) {
if (
target.endsWith(".raw_app/raw_app.yaml") ||
target.endsWith(".raw_app/raw_app.json")
) {
await wmill.deleteApp({
workspace: workspaceId,
path: removeSuffix(target, ".raw_app/raw_app.json"),
});
}
break;
break;
case "schedule":
await wmill.deleteSchedule({
workspace: workspaceId,
@@ -2426,18 +2477,25 @@ export async function push(
});
break;
case "workspace_dependencies":
const relativePath = removePathPrefix(change.path, "dependencies");
const res = workspaceDependenciesPathToLanguageAndFilename(change.path);
const relativePath = removePathPrefix(
change.path,
"dependencies"
);
const res = workspaceDependenciesPathToLanguageAndFilename(
change.path
);
if (!res) {
throw new Error(`Unknown workspace dependencies file format: ${change.path}`);
throw new Error(
`Unknown workspace dependencies file format: ${change.path}`
);
}
const { name, language } = res;
await wmill.deleteWorkspaceDependencies({
workspace: workspaceId,
language,
name
name,
});
break;
@@ -2537,7 +2595,10 @@ const command = new Command()
.option("--skip-flows", "Skip syncing flows")
.option("--skip-apps", "Skip syncing apps")
.option("--skip-folders", "Skip syncing folders")
.option("--skip-workspace-dependencies", "Skip syncing workspace dependencies")
.option(
"--skip-workspace-dependencies",
"Skip syncing workspace dependencies"
)
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
.option("--include-schedules", "Include syncing schedules")
.option("--include-triggers", "Include syncing triggers")
@@ -2586,7 +2647,10 @@ const command = new Command()
.option("--skip-flows", "Skip syncing flows")
.option("--skip-apps", "Skip syncing apps")
.option("--skip-folders", "Skip syncing folders")
.option("--skip-workspace-dependencies", "Skip syncing workspace dependencies")
.option(
"--skip-workspace-dependencies",
"Skip syncing workspace dependencies"
)
// .option("--skip-scripts-metadata", "Skip syncing scripts metadata, focus solely on logic")
.option("--include-schedules", "Include syncing schedules")
.option("--include-triggers", "Include syncing triggers")
+3 -1
View File
@@ -38,6 +38,7 @@ import workers from "./commands/workers/workers.ts";
import queues from "./commands/queues/queues.ts";
import dependencies from "./commands/dependencies/dependencies.ts";
import init from "./commands/init/init.ts";
import jobs from "./commands/jobs/jobs.ts";
export {
flow,
@@ -69,7 +70,7 @@ export {
// }
// });
export const VERSION = "1.587.1";
export const VERSION = "1.588.0";
export const WM_FORK_PREFIX = "wm-fork";
@@ -128,6 +129,7 @@ const command = new Command()
.command("workers", workers)
.command("queues", queues)
.command("dependencies", dependencies)
.command("jobs", jobs)
.command("version --version", "Show version information")
.action(async (opts: any) => {
console.log("CLI version: " + VERSION);
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.587.1",
"version": "1.588.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.587.1",
"version": "1.588.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.587.1",
"version": "1.588.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
+2 -2
View File
@@ -757,8 +757,8 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
<S3FilePicker
bind:this={s3FilePicker}
readOnlyMode={false}
on:selectAndClose={(s3obj) => {
let s = `'${formatS3Object(s3obj.detail)}'`
onSelectAndClose={(s3obj) => {
let s = `'${formatS3Object(s3obj)}'`
if (lang === 'duckdb') {
editor?.insertAtCursor(`SELECT * FROM ${s}`)
} else if (lang === 'python3') {
@@ -2,7 +2,7 @@
import { emptyString, type S3Object } from '$lib/utils'
import { Button, Drawer } from './common'
import DrawerContent from './common/drawer/DrawerContent.svelte'
import { createEventDispatcher, tick, untrack } from 'svelte'
import { tick, untrack } from 'svelte'
import S3FilePickerInner from './S3FilePickerInner.svelte'
import Select from './select/Select.svelte'
import { FileUp } from 'lucide-svelte'
@@ -10,11 +10,6 @@
import { SettingService } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
let dispatch = createEventDispatcher<{
close: { s3: string; storage: string | undefined } | undefined
selectAndClose: { s3: string; storage: string | undefined }
}>()
interface Props {
fromWorkspaceSettings?: boolean
readOnlyMode: boolean
@@ -22,6 +17,8 @@
selectedFileKey?: { s3: string; storage?: string } | undefined
folderOnly?: boolean
regexFilter?: RegExp | undefined
onClose?: () => void
onSelectAndClose?: (selected: { s3: string; storage: string | undefined }) => void
}
let {
@@ -30,7 +27,9 @@
initialFileKey = $bindable(undefined),
selectedFileKey = $bindable(undefined),
folderOnly = false,
regexFilter = undefined
regexFilter = undefined,
onClose,
onSelectAndClose
}: Props = $props()
let drawer: Drawer | undefined = $state()
@@ -76,7 +75,7 @@
<Drawer
bind:this={drawer}
on:close={() => {
dispatch('close')
onClose?.()
s3FilePickerInner?.close?.()
}}
size="1200px"
@@ -93,7 +92,7 @@
<S3FilePickerInner
bind:this={s3FilePickerInner}
on:selectAndClose={(e) => {
dispatch('selectAndClose', e.detail)
onSelectAndClose?.(e.detail)
drawer?.closeDrawer?.()
}}
{fromWorkspaceSettings}
@@ -39,7 +39,7 @@
<S3FilePicker
bind:this={s3FilePicker}
bind:selectedFileKey={value}
on:close={() => {
onClose={() => {
rawValue = JSON.stringify(value, null, 2)
editor?.setCode(rawValue)
}}
@@ -12,6 +12,8 @@
import ComponentErrorHandler from '../helpers/ComponentErrorHandler.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import { userStore } from '$lib/stores'
import { isPartialS3Object, getS3File } from '../../editor/appUtilsS3'
interface Props {
id: string
@@ -37,7 +39,8 @@
initConfig(components['downloadcomponent'].initialData.configuration, configuration)
)
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore, appPath, workspace, isEditor } =
getContext<AppViewerContext>('AppViewerContext')
//used so that we can count number of outputs setup for first refresh
initOutput($worldStore, id, {})
@@ -45,6 +48,10 @@
let beforeIconComponent: any = $state()
let afterIconComponent: any = $state()
let downloadUrl: string | undefined = $state(undefined)
let token = getContext<{ token?: string }>('AuthToken')
async function handleBeforeIcon() {
if (resolvedConfig.beforeIcon) {
beforeIconComponent = await loadIcon(
@@ -69,6 +76,36 @@
}
}
async function loadSource() {
if (isPartialS3Object(resolvedConfig.source)) {
downloadUrl = await getS3File({
source: resolvedConfig.source.s3,
storage: resolvedConfig.source.storage,
presigned: resolvedConfig.source.presigned,
appPath: $appPath,
username: $userStore?.username,
workspace,
token: token?.token,
isEditor,
configuration
})
} else if (resolvedConfig.source && typeof resolvedConfig.source !== 'string') {
throw new Error('Invalid source object' + typeof resolvedConfig.source)
} else if (resolvedConfig.source?.startsWith('s3://')) {
downloadUrl = await getS3File({
source: resolvedConfig.source?.replace('s3://', ''),
appPath: $appPath,
username: $userStore?.username,
workspace,
token: token?.token,
isEditor,
configuration
})
} else {
downloadUrl = transformBareBase64IfNecessary(resolvedConfig.source)
}
}
let css = $state(initCss($app.css?.downloadcomponent, customCss))
$effect(() => {
resolvedConfig.beforeIcon && beforeIconComponent && untrack(() => handleBeforeIcon())
@@ -76,6 +113,9 @@
$effect(() => {
resolvedConfig.afterIcon && afterIconComponent && untrack(() => handleAfterIcon())
})
$effect(() => {
resolvedConfig && loadSource()
})
</script>
<InitializeComponent {id} />
@@ -102,7 +142,9 @@
{#if render}
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<ComponentErrorHandler
hasError={resolvedConfig?.source != undefined && typeof resolvedConfig.source !== 'string'}
hasError={resolvedConfig?.source != undefined &&
typeof resolvedConfig.source !== 'string' &&
!isPartialS3Object(resolvedConfig.source)}
>
<Button
on:pointerdown={(e) => e.stopPropagation()}
@@ -122,7 +164,7 @@
extendedSize={resolvedConfig.size}
color={resolvedConfig.color}
download={resolvedConfig.filename}
href={transformBareBase64IfNecessary(resolvedConfig.source)}
href={downloadUrl}
target="_blank"
ref="external"
nonCaptureEvent
@@ -12,9 +12,9 @@
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import { defaultIfEmptyString } from '$lib/utils'
import { userStore } from '$lib/stores'
import { computeS3ImageViewerPolicy, isPartialS3Object } from '../../editor/appUtilsS3'
import { isPartialS3Object, getS3File } from '../../editor/appUtilsS3'
interface Props {
id: string
@@ -25,14 +25,6 @@
let { id, configuration, customCss = undefined, render }: Props = $props()
function computeForceViewerPolicies() {
if (!isEditor) {
return undefined
}
const policy = computeS3ImageViewerPolicy(configuration)
return policy
}
const resolvedConfig = $state(
initConfig(components['imagecomponent'].initialData.configuration, configuration)
)
@@ -54,43 +46,34 @@
let token = getContext<{ token?: string }>('AuthToken')
async function getS3Image(source: string | undefined, storage?: string, presigned?: string) {
if (!source) return ''
const appPathOrUser = defaultIfEmptyString(
$appPath,
`u/${$userStore?.username ?? 'unknown'}/newapp`
)
const params = new URLSearchParams()
params.append('s3', source)
if (storage) {
params.append('storage', storage)
}
if (token?.token && token.token != '') {
params.append('token', token.token)
}
const forceViewerPolicies = computeForceViewerPolicies()
if (forceViewerPolicies) {
params.append('force_viewer_allowed_s3_keys', JSON.stringify([forceViewerPolicies]))
}
return `/api/w/${workspace}/apps_u/download_s3_file/${appPathOrUser}?${params.toString()}${presigned ? `&${presigned}` : ''}`
}
async function loadImage() {
if (isPartialS3Object(resolvedConfig.source)) {
imageUrl = await getS3Image(
resolvedConfig.source.s3,
resolvedConfig.source.storage,
resolvedConfig.source.presigned
)
imageUrl = await getS3File({
source: resolvedConfig.source.s3,
storage: resolvedConfig.source.storage,
presigned: resolvedConfig.source.presigned,
appPath: $appPath,
username: $userStore?.username,
workspace,
token: token?.token,
isEditor,
configuration
})
} else if (resolvedConfig.source && typeof resolvedConfig.source !== 'string') {
throw new Error('Invalid image object' + typeof resolvedConfig.source)
} else if (
resolvedConfig.sourceKind === 's3 (workspace storage)' ||
resolvedConfig.source?.startsWith('s3://')
) {
imageUrl = await getS3Image(resolvedConfig.source?.replace('s3://', ''))
imageUrl = await getS3File({
source: resolvedConfig.source?.replace('s3://', ''),
appPath: $appPath,
username: $userStore?.username,
workspace,
token: token?.token,
isEditor,
configuration
})
} else if (resolvedConfig.sourceKind === 'png encoded as base64') {
imageUrl = 'data:image/png;base64,' + resolvedConfig.source
} else if (resolvedConfig.sourceKind === 'jpeg encoded as base64') {
@@ -7,6 +7,8 @@
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import { Loader2 } from 'lucide-svelte'
import { userStore } from '$lib/stores'
import { isPartialS3Object, getS3File } from '../../editor/appUtilsS3'
interface Props {
id: string
@@ -17,7 +19,8 @@
let { id, configuration, customCss = undefined, render }: Props = $props()
const { app, worldStore } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore, appPath, workspace, isEditor } =
getContext<AppViewerContext>('AppViewerContext')
const outputs = initOutput($worldStore, id, {
loading: false
@@ -26,6 +29,44 @@
let source: string | ArrayBuffer | undefined = $state(undefined)
let zoom: number | undefined = $state(undefined)
let pdfSource: string | ArrayBuffer | undefined = $state(undefined)
let token = getContext<{ token?: string }>('AuthToken')
async function loadSource() {
if (isPartialS3Object(source)) {
pdfSource = await getS3File({
source: source.s3,
storage: source.storage,
presigned: source.presigned,
appPath: $appPath,
username: $userStore?.username,
workspace,
token: token?.token,
isEditor,
configuration
})
} else if (source && typeof source !== 'string' && !(source instanceof ArrayBuffer)) {
throw new Error('Invalid PDF source object' + typeof source)
} else if (typeof source === 'string' && source?.startsWith('s3://')) {
pdfSource = await getS3File({
source: source?.replace('s3://', ''),
appPath: $appPath,
username: $userStore?.username,
workspace,
token: token?.token,
isEditor,
configuration
})
} else {
pdfSource = source
}
}
$effect(() => {
source && loadSource()
})
let css = $state(initCss($app.css?.pdfcomponent, customCss))
</script>
@@ -50,7 +91,7 @@
<Loader2 class="animate-spin" />
{:then Module}
<Module.default
{source}
source={pdfSource}
{zoom}
class={css?.container?.class}
style={css?.container?.style}
@@ -10,7 +10,7 @@ import type { App } from '../types'
import {
computeS3FileInputPolicy,
computeWorkspaceS3FileInputPolicy,
computeS3ImageViewerPolicy
computeS3FileViewerPolicy
} from './appUtilsS3'
import { collectStaticFields, type TriggerableV2 } from './commonAppUtils'
import type { Policy } from '$lib/gen'
@@ -165,11 +165,16 @@ export async function updatePolicy(app: App, currentPolicy: Policy | undefined):
}
const s3FileKeys = items
.filter((x) => (x.data as AppComponent).type === 'imagecomponent')
.filter(
(x) =>
(x.data as AppComponent).type === 'imagecomponent' ||
(x.data as AppComponent).type === 'pdfcomponent' ||
(x.data as AppComponent).type === 'downloadcomponent'
)
.map((x) => {
const c = x.data as AppComponent
const config = c.configuration
return computeS3ImageViewerPolicy(config)
return computeS3FileViewerPolicy(config)
})
.filter(Boolean) as { s3_path: string; storage?: string | undefined }[]
@@ -1,3 +1,4 @@
import { defaultIfEmptyString } from '$lib/utils'
import type { AppInput, EvalInputV2 } from '../inputType'
import type { App, RichConfigurations } from '../types'
import { collectOneOfFields } from './appUtilsCore'
@@ -86,7 +87,61 @@ export function isPartialS3Object(
return input != undefined && typeof input === 'object' && typeof input['s3'] === 'string'
}
export function computeS3ImageViewerPolicy(config: RichConfigurations) {
function computeForceViewerPolicies({
isEditor,
configuration
}: {
isEditor: boolean
configuration: RichConfigurations
}) {
if (!isEditor) {
return undefined
}
const policy = computeS3FileViewerPolicy(configuration)
return policy
}
export async function getS3File({
source,
storage,
presigned,
appPath,
username,
workspace,
token,
isEditor,
configuration
}: {
source: string | undefined
storage?: string
presigned?: string
appPath: string
username: string | undefined
workspace: string
token: string | undefined
isEditor: boolean
configuration: RichConfigurations
}) {
if (!source) return ''
const appPathOrUser = defaultIfEmptyString(appPath, `u/${username ?? 'unknown'}/newapp`)
const params = new URLSearchParams()
params.append('s3', source)
if (storage) {
params.append('storage', storage)
}
if (token && token != '') {
params.append('token', token)
}
const forceViewerPolicies = computeForceViewerPolicies({ isEditor, configuration })
if (forceViewerPolicies) {
params.append('force_viewer_allowed_s3_keys', JSON.stringify([forceViewerPolicies]))
}
return `/api/w/${workspace}/apps_u/download_s3_file/${appPathOrUser}?${params.toString()}${presigned ? `&${presigned}` : ''}`
}
export function computeS3FileViewerPolicy(config: RichConfigurations) {
if (config.source.type === 'uploadS3' && isPartialS3Object(config.source.value)) {
return {
s3_path: config.source.value.s3,
@@ -1642,6 +1642,9 @@ export const components = {
accept: '*',
convertTo: 'base64'
},
fileUploadS3: {
accept: '*'
},
placeholder: 'Enter URL or upload file (base64)'
},
filename: {
@@ -3459,8 +3462,7 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
convertTo: 'base64'
},
fileUploadS3: {
accept: 'image/*',
convertTo: 'base64'
accept: 'image/*'
}
},
sourceKind: {
@@ -3660,6 +3662,9 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
accept: 'application/pdf',
convertTo: 'base64'
},
fileUploadS3: {
accept: 'application/pdf'
},
placeholder: 'Enter URL or upload file (base64)'
},
zoom: {
@@ -28,6 +28,7 @@
import ConnectionButton from '$lib/components/common/button/ConnectionButton.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import type SimpleEditor from '$lib/components/SimpleEditor.svelte'
interface Props {
id: string
@@ -108,6 +109,7 @@
let s3PickerSelection: { s3: string; storage?: string } | undefined = $state(undefined)
let s3FolderPrefix: string = $state('')
let s3FileUploadRawMode = $state(componentInput?.type == 'uploadS3' && !!componentInput.value?.s3)
let s3JsonEditor: SimpleEditor | undefined = $state()
function updateSelectedS3File() {
if (s3PickerSelection) {
@@ -115,6 +117,7 @@
componentInput.value = {
...s3PickerSelection
}
s3JsonEditor?.setCode(JSON.stringify(s3PickerSelection, null, 2))
}
s3FileUploadRawMode = true
}
@@ -317,6 +320,7 @@
<Module.default
code={JSON.stringify(componentInput.value ?? { s3: '' }, null, 2)}
bind:value={componentInput.value}
bind:editor={s3JsonEditor}
/>
{/await}
{:else}
@@ -351,8 +355,8 @@
<S3FilePicker
bind:this={s3FilePicker}
folderOnly={false}
on:close={(e) => {
s3PickerSelection = e.detail
onSelectAndClose={(selected) => {
s3PickerSelection = selected
updateSelectedS3File()
}}
readOnlyMode={false}
@@ -29,6 +29,7 @@
import S3FilePicker from '$lib/components/S3FilePicker.svelte'
import FileUpload from '$lib/components/common/fileUpload/FileUpload.svelte'
import DucklakePicker from '$lib/components/DucklakePicker.svelte'
import type SimpleEditor from '$lib/components/SimpleEditor.svelte'
interface Props {
componentInput: StaticInput<any> | undefined
@@ -56,8 +57,11 @@
componentInput && appContext?.onchange?.()
})
let s3FileUploadRawMode = $state(false)
let s3FileUploadRawMode = $state(
componentInput?.value && typeof componentInput.value == 'object' && !!componentInput.value?.s3
)
let s3FilePicker: S3FilePicker | undefined = $state(undefined)
let s3JsonEditor: SimpleEditor | undefined = $state()
</script>
{#key subFieldType}
@@ -216,6 +220,7 @@
<Module.default
code={JSON.stringify(componentInput.value ?? { s3: '' }, null, 2)}
bind:value={componentInput.value}
bind:editor={s3JsonEditor}
/>
{/await}
{:else}
@@ -255,13 +260,12 @@
<S3FilePicker
bind:this={s3FilePicker}
readOnlyMode={false}
on:close={(e) => {
if (e.detail) {
if (componentInput) {
componentInput.value = e.detail
}
s3FileUploadRawMode = true
onSelectAndClose={(selected) => {
if (componentInput) {
componentInput.value = selected
s3JsonEditor?.setCode(JSON.stringify(selected, null, 2))
}
s3FileUploadRawMode = true
}}
/>
{:else if format?.startsWith('resource-') && (componentInput.value == undefined || typeof componentInput.value == 'string')}
@@ -51,16 +51,16 @@
{#if $userStore}
<S3FilePicker
bind:this={s3FilePicker}
on:selectAndClose={(ev) => {
onSelectAndClose={(selected) => {
if (multiple) {
if (Array.isArray(value)) {
value.push(ev.detail)
value.push(selected)
} else {
value = [ev.detail]
value = [selected]
}
fileUpload?.addUpload(ev.detail)
fileUpload?.addUpload(selected)
} else {
value = ev.detail
value = selected
fileUpload?.setUpload(value)
}
editor?.setCode(JSON.stringify(value))
@@ -377,7 +377,6 @@
}
}
// Update config for captures
function getCaptureConfig() {
const newCaptureConfig = {
@@ -408,7 +407,7 @@
bind:this={s3FilePicker}
folderOnly={is_static_website}
bind:selectedFileKey={static_asset_config}
on:close={() => {
onClose={() => {
s3Editor?.setCode(JSON.stringify(static_asset_config, null, 2))
s3FileUploadRawMode = true
}}
+2 -2
View File
@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.587.1"
wmill_pg = ">=1.587.1"
wmill = ">=1.588.0"
wmill_pg = ">=1.588.0"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.587.1
version: 1.588.0
title: OpenFlow Spec
contact:
name: Ruben Fiszel
@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.587.1'
ModuleVersion = '1.588.0'
# Supported PSEditions
# CompatiblePSEditions = @()
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.587.1"
version = "1.588.0"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.587.1"
version = "1.588.0"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.587.1",
"version": "1.588.0",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.587.1",
"version": "1.588.0",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {
+1 -1
View File
@@ -1 +1 @@
1.587.1
1.588.0