Merge branch 'main' into dieri/sharding-v0

This commit is contained in:
dieriba
2025-10-05 13:51:51 +02:00
201 changed files with 5654 additions and 2710 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
toolchain: 1.90.0
- name: Read EE repo commit hash
run: |
echo "ee_repo_ref=$(cat ./backend/ee-repo-ref.txt)" >> "$GITHUB_ENV"
echo "ee_repo_ref=$(cat ./ee-repo-ref.txt)" >> "$GITHUB_ENV"
- uses: actions/checkout@v4
with:
+30
View File
@@ -1,5 +1,35 @@
# Changelog
## [1.555.1](https://github.com/windmill-labs/windmill/compare/v1.555.0...v1.555.1) (2025-10-04)
### Bug Fixes
* app button tooltip also when disabled + audit log filters ([#6751](https://github.com/windmill-labs/windmill/issues/6751)) ([605c552](https://github.com/windmill-labs/windmill/commit/605c5526f83d3b985f04570039bb4671cc5912f4))
* init git repo preview save ([#6753](https://github.com/windmill-labs/windmill/issues/6753)) ([d9c01e0](https://github.com/windmill-labs/windmill/commit/d9c01e0c0392f13e52ad92f87c4216e64e89a3bc))
## [1.555.0](https://github.com/windmill-labs/windmill/compare/v1.554.1...v1.555.0) (2025-10-03)
### Features
* end user email env var ([#6750](https://github.com/windmill-labs/windmill/issues/6750)) ([3907c9f](https://github.com/windmill-labs/windmill/commit/3907c9f9512ebd73daf0a2f3ee2e8db6fb9f4df6))
* **flow:** add option to turn flow into chat ([#6658](https://github.com/windmill-labs/windmill/issues/6658)) ([047420e](https://github.com/windmill-labs/windmill/commit/047420e5ad7b6178291bc7ed75d029794760d18b))
### Bug Fixes
* **backend:** prevent s3 file upload infinite loop ([#6742](https://github.com/windmill-labs/windmill/issues/6742)) ([6d436d7](https://github.com/windmill-labs/windmill/commit/6d436d745994f954a37f33ca2cc2e9f0801b16b9))
* show that user is disabled in workspacelist ([#6748](https://github.com/windmill-labs/windmill/issues/6748)) ([c658f32](https://github.com/windmill-labs/windmill/commit/c658f321d68e2d72622d9d167b20cac67364651c))
* top level assigment doesn't propagate to setContext ([#6745](https://github.com/windmill-labs/windmill/issues/6745)) ([06b152b](https://github.com/windmill-labs/windmill/commit/06b152b295cd4892d7309651d382a05cdcf7d378))
## [1.554.1](https://github.com/windmill-labs/windmill/compare/v1.554.0...v1.554.1) (2025-10-02)
### Bug Fixes
* **backend:** concurrency limits preprocessor ([#6727](https://github.com/windmill-labs/windmill/issues/6727)) ([cdb7524](https://github.com/windmill-labs/windmill/commit/cdb75241188ee0a6d7bc62ca6dd639606eef426a))
## [1.554.0](https://github.com/windmill-labs/windmill/compare/v1.553.0...v1.554.0) (2025-10-01)
@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT data FROM app_bundles WHERE app_version_id = $1 AND file_type = $2 AND w_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "data",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Int8",
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "01050e7057f3d1971ad9e47ac83bf6a3c3c9f41689c3607f0b264437ae6b3324"
}
@@ -46,11 +46,11 @@
]
},
"nullable": [
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
true,
true
]
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow_conversation_message\n SET content = $1\n WHERE job_id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Uuid"
]
},
"nullable": []
},
"hash": "20888e946a5597989488e2a54e354e4ce2872f2bc3af7b26fcaafe8703ba70bf"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
"query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
"describe": {
"columns": [
{
@@ -241,6 +241,11 @@
"ordinal": 31,
"name": "visible_to_owner",
"type_info": "Bool"
},
{
"ordinal": 32,
"name": "permissioned_as_end_user_email",
"type_info": "Text"
}
],
"parameters": {
@@ -280,8 +285,9 @@
true,
true,
true,
false
false,
null
]
},
"hash": "f3f96e066716e61042519a645d487b578bc63792cdb0f7ddaeb82e9771287c22"
"hash": "3162ec92bb32af47a71cc41172cc740b5dea1304ce4dfdb4d3d0efa4266f38c5"
}
@@ -0,0 +1,64 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, conversation_id, message_type as \"message_type: MessageType\", content, job_id, created_at\n FROM (\n SELECT id, conversation_id, message_type, content, job_id, created_at\n FROM flow_conversation_message\n WHERE conversation_id = $1\n ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n LIMIT $2 OFFSET $3\n ) AS messages\n ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "conversation_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "message_type: MessageType",
"type_info": {
"Custom": {
"name": "message_type",
"kind": {
"Enum": [
"user",
"assistant"
]
}
}
}
},
{
"ordinal": 3,
"name": "content",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "job_id",
"type_info": "Uuid"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid",
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
false,
false,
true,
false
]
},
"hash": "5f57ec3330a6576f4017fd469848a1c214896dc5abbf2b898ac9a68810fd89dd"
}
@@ -0,0 +1,27 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id)\n VALUES ($1, $2, $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
{
"Custom": {
"name": "message_type",
"kind": {
"Enum": [
"user",
"assistant"
]
}
}
},
"Text",
"Uuid"
]
},
"nullable": []
},
"hash": "69606859fe08d24f0306b866f9f50ad766d56a0d5aa8f0784f79aa5e211b00e4"
}
@@ -0,0 +1,62 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO flow_conversation (id, workspace_id, flow_path, created_by, title)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id, workspace_id, flow_path, title, created_at, updated_at, created_by",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "flow_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "created_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Varchar",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "6bd23a98838e3eec309e6b696edc776bd56fc9dae1238b3272557d1562400dbe"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (flow_status->>'memory_id')::uuid as memory_id \n FROM v2_job_status \n WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "memory_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "7628e7690aaf6da3dba3cf78dc9e78fe32c3a001c08920e02fb850695afdb7ec"
}
@@ -1,51 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO script (summary, description, dedicated_worker, content, workspace_id, path, hash, language, tag, created_by, lock) VALUES ('', '', true, $1, $2, $3, $4, $5, $6, $7, '') ON CONFLICT (workspace_id, hash) DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Varchar",
"Varchar",
"Int8",
{
"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",
"Varchar"
]
},
"nullable": []
},
"hash": "804fc11e35f4afc0db194b6fe2594f91df7e588d4d2431bc85f4d8734920c8bf"
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1 AND workspace_id = $2)",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "81e997576319ae1d6d9a91f76465f6fa53892ff223f3b9d9ad3f4a1d2e720cc8"
}
@@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_queue (id, workspace_id, scheduled_for, tag) SELECT unnest($1::uuid[]), $2, now(), $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "85705fc3d7f8ba5f1b12d5fb222c38fc64deb1226aab9dc3bc4465324fce37d1"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id) \n values ($1, $32, $33, $34, $35, $36, $37, $2) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (id, workspace_id, raw_code, raw_lock, raw_flow, tag, parent_job,\n created_by, permissioned_as, runnable_id, runnable_path, args, kind, trigger,\n script_lang, same_worker, pre_run_error, permissioned_as_email, visible_to_owner,\n flow_innermost_root_job, root_job, concurrent_limit, concurrency_time_window_s, timeout, flow_step_id,\n cache_ttl, priority, trigger_kind, script_entrypoint_override, preprocessed)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) \n values ($1, $32, $33, $34, $35, $36, $37, $2, $41) \n ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31)",
"describe": {
"columns": [],
"parameters": {
@@ -123,10 +123,11 @@
}
}
},
"Bool"
"Bool",
"Varchar"
]
},
"nullable": []
},
"hash": "193d292c5ed44bf5266ad52c83704c3a36aa284fab3b7e638dbca12ac846b82b"
"hash": "8d67ed8e1271a27b072a8e8ae9973e372949f5ec7d71b80d1ba3eaddf851adce"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_status (id, flow_status) SELECT unnest($1::uuid[]), $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray",
"Jsonb"
]
},
"nullable": []
},
"hash": "a0b3e10e077d30c1da135dff9feca3761d400391f1f46a8294da3e6c9af63887"
}
@@ -1,89 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id, raw_flow) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 FROM generate_series(1, 1)) RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Int8",
"Varchar",
{
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
},
{
"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",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Jsonb"
]
},
"nullable": [
false
]
},
"hash": "ab04cda71f8e2be9acbecabe1ee5ef756b8e5c1955fbe111df9ee171dc262338"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO app_bundles (app_version_id, w_id, file_type, data) VALUES ($1, $2, $3, $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Varchar",
"Varchar",
"Bytea"
]
},
"nullable": []
},
"hash": "abaae3dde751a41b2dbb7856ece1c840d0ea8d59346ed9e88f6f609edb543d7e"
}
@@ -18,8 +18,8 @@
"Left": []
},
"nullable": [
true,
false
false,
true
]
},
"hash": "b3dbdfb50ee8118bdaed3164b210cb549a34b96554ae1872355b90304f5dcb76"
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job_runtime (id) SELECT unnest($1::uuid[])",
"describe": {
"columns": [],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": []
},
"hash": "b4a9abcb38997587b28655b0f4a212a5bd4039b57fab20b163617e33a4c9dd46"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE flow_conversation SET updated_at = NOW() WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "bdb1ece5c233f242cf341c089a2f2b785dfa5cc14d9be224c0707e10247ed8b7"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false",
"query": "SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,\n CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,\n usr.disabled\n FROM workspace\n JOIN usr ON usr.workspace_id = workspace.id\n JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id\n WHERE usr.email = $1 AND workspace.deleted = false",
"describe": {
"columns": [
{
@@ -32,6 +32,11 @@
"ordinal": 5,
"name": "operator_settings",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "disabled",
"type_info": "Bool"
}
],
"parameters": {
@@ -45,8 +50,9 @@
false,
true,
true,
null
null,
false
]
},
"hash": "d0037961e8e787c4277afc3eb79f3b72e3323f878387de8b6fa31493f1215a77"
"hash": "c095a9658c542efc9f0255a1b536d2fd8a25fe4cd57c223db7d744493f8470c6"
}
@@ -0,0 +1,59 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by\n FROM flow_conversation\n WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "flow_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "title",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "updated_at",
"type_info": "Timestamptz"
},
{
"ordinal": 6,
"name": "created_by",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
false,
false,
false,
true,
false,
false,
false
]
},
"hash": "c383cc023714b361d10c10e8fef1fc148ab1da942951ee9ffdddaecee76a6be9"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2",
"query": "SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version\n FROM flow\n INNER JOIN flow_version\n ON flow_version.id = $3\n WHERE flow.path = $1 and flow.workspace_id = $2",
"describe": {
"columns": [
{
@@ -25,16 +25,21 @@
},
{
"ordinal": 4,
"name": "chat_input_enabled",
"type_info": "Bool"
},
{
"ordinal": 5,
"name": "on_behalf_of_email",
"type_info": "Text"
},
{
"ordinal": 5,
"ordinal": 6,
"name": "edited_by",
"type_info": "Varchar"
},
{
"ordinal": 6,
"ordinal": 7,
"name": "version",
"type_info": "Int8"
}
@@ -51,10 +56,11 @@
true,
null,
null,
null,
true,
false,
false
]
},
"hash": "9b60fa8a1003015bc5a7cdbee9a4486b313d45347dfd9d4793d60e2760763ca3"
"hash": "e1409c67b93881cf68f6ac9c8bae0856cf426c7e7860c8b5b799972baa8e6945"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM flow_conversation_message WHERE job_id = $1 AND message_type = 'assistant')",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "e2f4eeb896c22e3215370ed30ed5cc890503e4197b0fdb3a906504eba166aa97"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM flow_conversation WHERE id = $1 AND workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "e6c4454e552dc82db1af2d6b887ebb4b78eb58600cc21e3ffb12dd9e5a0a6f08"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE v2_job_status \n SET flow_status = jsonb_set(\n flow_status,\n '{memory_id}',\n to_jsonb($2::uuid)\n )\n WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "f5568a691ec5931634cf986f806f5eae7bb8ed0f5c6e54ca3f49a991c53ed50d"
}
@@ -1,11 +1,11 @@
{
"db_name": "PostgreSQL",
"query": "SELECT hash FROM script WHERE path = $1 AND workspace_id = $2",
"query": "SELECT app.versions[array_upper(app.versions, 1)] FROM app\n WHERE app.path = $1 AND app.workspace_id = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"name": "versions",
"type_info": "Int8"
}
],
@@ -16,8 +16,8 @@
]
},
"nullable": [
false
null
]
},
"hash": "16e4b1bead9fc77fd98658b8cb8cc6d6bf1df758b30e99bd661da866062ef14f"
"hash": "fad966db585b91c9ce143c9aa26a826aec1ddb193a7f4988c5f12b1a2d8ce071"
}
@@ -1,89 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO v2_job (id, runnable_id, runnable_path, kind, script_lang, tag, created_by, permissioned_as, permissioned_as_email, workspace_id) (SELECT gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9 FROM generate_series(1, $10)) RETURNING id",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Int8",
"Varchar",
{
"Custom": {
"name": "job_kind",
"kind": {
"Enum": [
"script",
"preview",
"flow",
"dependencies",
"flowpreview",
"script_hub",
"identity",
"flowdependencies",
"http",
"graphql",
"postgresql",
"noop",
"appdependencies",
"deploymentcallback",
"singlescriptflow",
"flowscript",
"flownode",
"appscript",
"aiagent"
]
}
}
},
{
"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",
"Varchar",
"Varchar",
"Varchar",
"Varchar",
"Int4"
]
},
"nullable": [
false
]
},
"hash": "ff0403790674cdb07022af71c2377afbd8b3a660b3be27514b517c077c63c238"
}
+115 -110
View File
@@ -169,9 +169,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.20"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -779,9 +779,9 @@ dependencies = [
[[package]]
name = "aws-credential-types"
version = "1.2.6"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d025db5d9f52cbc413b167136afb3d8aeea708c0d8884783cf6253be5e22f6f2"
checksum = "799a1290207254984cb7c05245111bc77958b92a3c9bb449598044b36341cce6"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -1763,18 +1763,18 @@ dependencies = [
[[package]]
name = "bytemuck"
version = "1.23.2"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.10.1"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29"
checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
@@ -1935,9 +1935,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.39"
version = "1.2.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f"
checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -2674,7 +2674,7 @@ dependencies = [
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core 0.9.11",
"parking_lot_core 0.9.12",
]
[[package]]
@@ -2688,7 +2688,7 @@ dependencies = [
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core 0.9.11",
"parking_lot_core 0.9.12",
]
[[package]]
@@ -2745,7 +2745,7 @@ dependencies = [
"itertools 0.14.0",
"log",
"object_store",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"parquet",
"rand 0.8.5",
"regex",
@@ -2780,7 +2780,7 @@ dependencies = [
"itertools 0.14.0",
"log",
"object_store",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"tokio",
]
@@ -2953,7 +2953,7 @@ dependencies = [
"itertools 0.14.0",
"log",
"object_store",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"parquet",
"rand 0.8.5",
"tokio",
@@ -2978,7 +2978,7 @@ dependencies = [
"futures",
"log",
"object_store",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"rand 0.8.5",
"tempfile",
"url",
@@ -3114,7 +3114,7 @@ dependencies = [
"datafusion-common",
"datafusion-expr",
"datafusion-physical-plan",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"paste",
]
@@ -3255,7 +3255,7 @@ dependencies = [
"indexmap 2.11.1",
"itertools 0.14.0",
"log",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"pin-project-lite",
"tokio",
]
@@ -3280,7 +3280,7 @@ dependencies = [
"itertools 0.14.0",
"log",
"object_store",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"tokio",
]
@@ -3409,7 +3409,7 @@ dependencies = [
"indexmap 2.11.1",
"log",
"once_cell",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"serde",
"serde_json",
"sha2 0.10.9",
@@ -3491,7 +3491,7 @@ dependencies = [
"indexmap 2.11.1",
"libc",
"memoffset",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"percent-encoding",
"pin-project",
"serde",
@@ -3736,7 +3736,7 @@ dependencies = [
"log",
"once_cell",
"os_pipe",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"pin-project",
"rand 0.8.5",
"tokio",
@@ -4116,7 +4116,7 @@ dependencies = [
"log",
"node_resolver",
"once_cell",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"sys_traits",
"thiserror 2.0.17",
"url",
@@ -4273,7 +4273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6742a724e8becb372a74c650a1aefb8924a5b8107f7d75b3848763ea24b27a87"
dependencies = [
"futures-util",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"tokio",
]
@@ -4821,13 +4821,20 @@ dependencies = [
[[package]]
name = "dyn-stack"
version = "0.13.0"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490bd48eb68fffcfed519b4edbfd82c69cbe741d175b84f0e0cbe8c57cbe0bdd"
checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8"
dependencies = [
"bytemuck",
"dyn-stack-macros",
]
[[package]]
name = "dyn-stack-macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05dbec7076f432bb132db738df90d87a4f5789e99f59e7b1219a6b8ef61eaa68"
[[package]]
name = "dynasm"
version = "1.2.3"
@@ -5264,9 +5271,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3"
[[package]]
name = "fixedbitset"
@@ -5499,7 +5506,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
]
[[package]]
@@ -5613,7 +5620,7 @@ version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451"
dependencies = [
"dyn-stack 0.13.0",
"dyn-stack 0.13.2",
"gemm-c32 0.18.2",
"gemm-c64 0.18.2",
"gemm-common 0.18.2",
@@ -5648,7 +5655,7 @@ version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847"
dependencies = [
"dyn-stack 0.13.0",
"dyn-stack 0.13.2",
"gemm-common 0.18.2",
"num-complex",
"num-traits",
@@ -5678,7 +5685,7 @@ version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf"
dependencies = [
"dyn-stack 0.13.0",
"dyn-stack 0.13.2",
"gemm-common 0.18.2",
"num-complex",
"num-traits",
@@ -5714,7 +5721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3"
dependencies = [
"bytemuck",
"dyn-stack 0.13.0",
"dyn-stack 0.13.2",
"half",
"libm",
"num-complex",
@@ -5752,7 +5759,7 @@ version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109"
dependencies = [
"dyn-stack 0.13.0",
"dyn-stack 0.13.2",
"gemm-common 0.18.2",
"gemm-f32 0.18.2",
"half",
@@ -5785,7 +5792,7 @@ version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864"
dependencies = [
"dyn-stack 0.13.0",
"dyn-stack 0.13.2",
"gemm-common 0.18.2",
"num-complex",
"num-traits",
@@ -5815,7 +5822,7 @@ version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd"
dependencies = [
"dyn-stack 0.13.0",
"dyn-stack 0.13.2",
"gemm-common 0.18.2",
"num-complex",
"num-traits",
@@ -6390,7 +6397,7 @@ dependencies = [
"ipconfig",
"moka",
"once_cell",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"rand 0.9.0",
"resolv-conf",
"serde",
@@ -7461,7 +7468,7 @@ dependencies = [
"json-patch",
"k8s-openapi",
"kube-client",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"pin-project",
"serde",
"serde_json",
@@ -7631,11 +7638,11 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libproc"
version = "0.14.10"
version = "0.14.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78a09b56be5adbcad5aa1197371688dc6bb249a26da3bca2011ee2fb987ebfb"
checksum = "a54ad7278b8bc5301d5ffd2a94251c004feb971feba96c971ea4063645990757"
dependencies = [
"bindgen 0.70.1",
"bindgen 0.72.1",
"errno",
"libc",
]
@@ -7648,7 +7655,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags 2.9.4",
"libc",
"redox_syscall 0.5.17",
"redox_syscall 0.5.18",
]
[[package]]
@@ -7736,11 +7743,10 @@ checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
[[package]]
name = "lock_api"
version = "0.4.13"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"autocfg",
"scopeguard",
]
@@ -8192,7 +8198,7 @@ dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
"equivalent",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"portable-atomic",
"rustc_version 0.4.1",
"smallvec",
@@ -8912,7 +8918,7 @@ dependencies = [
"hyper 1.7.0",
"itertools 0.14.0",
"md-5 0.10.6",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"percent-encoding",
"quick-xml 0.37.5",
"rand 0.9.0",
@@ -9344,12 +9350,12 @@ dependencies = [
[[package]]
name = "parking_lot"
version = "0.12.4"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core 0.9.11",
"parking_lot_core 0.9.12",
]
[[package]]
@@ -9368,15 +9374,15 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.11"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.17",
"redox_syscall 0.5.18",
"smallvec",
"windows-targets 0.52.6",
"windows-link 0.2.0",
]
[[package]]
@@ -9502,20 +9508,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.2"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8"
checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4"
dependencies = [
"memchr",
"thiserror 2.0.17",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.2"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663"
checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de"
dependencies = [
"pest",
"pest_generator",
@@ -9523,9 +9528,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.8.2"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f"
checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843"
dependencies = [
"pest",
"pest_meta",
@@ -9536,9 +9541,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.8.2"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420"
checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a"
dependencies = [
"pest",
"sha2 0.10.9",
@@ -10042,7 +10047,7 @@ dependencies = [
"fnv",
"lazy_static",
"memchr",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"thiserror 2.0.17",
]
@@ -10100,9 +10105,9 @@ dependencies = [
[[package]]
name = "psm"
version = "0.1.26"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c"
dependencies = [
"cc",
]
@@ -10210,7 +10215,7 @@ dependencies = [
"ahash 0.8.12",
"equivalent",
"hashbrown 0.15.5",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
]
[[package]]
@@ -10518,9 +10523,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.17"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.9.4",
]
@@ -11117,7 +11122,7 @@ dependencies = [
"once_cell",
"ring 0.17.14",
"rustls-pki-types",
"rustls-webpki 0.103.6",
"rustls-webpki 0.103.7",
"subtle",
"zeroize",
]
@@ -11222,9 +11227,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.6"
version = "0.103.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb"
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
dependencies = [
"aws-lc-rs",
"ring 0.17.14",
@@ -12444,9 +12449,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stacker"
version = "0.1.21"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b"
checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59"
dependencies = [
"cc",
"cfg-if",
@@ -13592,7 +13597,7 @@ dependencies = [
"io-uring",
"libc",
"mio 1.0.4",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"pin-project-lite",
"signal-hook-registry",
"slab",
@@ -13657,7 +13662,7 @@ dependencies = [
"futures-channel",
"futures-util",
"log",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"percent-encoding",
"phf 0.11.3",
"pin-project-lite",
@@ -13683,7 +13688,7 @@ dependencies = [
"futures-channel",
"futures-util",
"log",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"percent-encoding",
"phf 0.11.3",
"pin-project-lite",
@@ -13974,7 +13979,7 @@ dependencies = [
"cookie 0.18.1",
"futures-util",
"http 1.3.1",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"pin-project-lite",
"tower-layer",
"tower-service",
@@ -14278,9 +14283,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "typetag"
@@ -14978,7 +14983,7 @@ dependencies = [
"log",
"naga",
"once_cell",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"profiling",
"raw-window-handle",
"ron",
@@ -15020,7 +15025,7 @@ dependencies = [
"ndk-sys",
"objc",
"once_cell",
"parking_lot 0.12.4",
"parking_lot 0.12.5",
"profiling",
"range-alloc",
"raw-window-handle",
@@ -15119,7 +15124,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"aws-sdk-config",
@@ -15180,7 +15185,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"argon2",
@@ -15300,7 +15305,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -15315,7 +15320,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"chrono",
"serde",
@@ -15328,7 +15333,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"axum",
@@ -15347,7 +15352,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15432,7 +15437,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"regex",
"serde",
@@ -15447,7 +15452,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"bytes",
@@ -15471,7 +15476,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15483,7 +15488,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15492,7 +15497,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15504,7 +15509,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15516,7 +15521,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"gosyn",
@@ -15528,7 +15533,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15540,7 +15545,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15552,7 +15557,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"nu-parser",
@@ -15563,7 +15568,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15574,7 +15579,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15586,7 +15591,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15609,7 +15614,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15623,7 +15628,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15640,7 +15645,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15654,7 +15659,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15672,7 +15677,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"getrandom 0.2.16",
@@ -15697,7 +15702,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15707,7 +15712,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15740,7 +15745,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
@@ -15750,7 +15755,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.554.0"
version = "1.555.1"
dependencies = [
"anyhow",
"async-once-cell",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.554.0"
version = "1.555.1"
authors.workspace = true
edition.workspace = true
@@ -34,7 +34,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.554.0"
version = "1.555.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+1 -1
View File
@@ -1 +1 @@
fc153796d006d08777b5b778d4f370a60495c3af
33bdef405c678616b12084cf779a68a62d1f477e
@@ -0,0 +1,12 @@
-- Add down migration script here
-- Drop indexes
DROP INDEX IF EXISTS idx_conversation_message_conversation_time;
DROP INDEX IF EXISTS idx_flow_conversation_workspace_path;
-- Drop tables (order matters due to foreign keys)
DROP TABLE IF EXISTS flow_conversation_message;
DROP TABLE IF EXISTS flow_conversation;
-- Drop enum
DROP TYPE IF EXISTS MESSAGE_TYPE;
@@ -0,0 +1,57 @@
-- Add up migration script here
-- Create message_type enum
CREATE TYPE MESSAGE_TYPE AS ENUM ('user', 'assistant');
-- Create flow_conversation table
CREATE TABLE flow_conversation (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id),
flow_path VARCHAR(255) NOT NULL,
title VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_by VARCHAR(50) NOT NULL
);
-- Create flow_conversation_message table
CREATE TABLE flow_conversation_message (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES flow_conversation(id) ON DELETE CASCADE,
message_type MESSAGE_TYPE NOT NULL,
content TEXT NOT NULL,
job_id UUID REFERENCES v2_job(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Basic indexes for performance
CREATE INDEX idx_flow_conversation_workspace_path ON flow_conversation(workspace_id, flow_path, updated_at DESC);
CREATE INDEX idx_conversation_message_conversation_time ON flow_conversation_message(conversation_id, created_at DESC);
-- Grant permissions
GRANT ALL ON flow_conversation TO windmill_admin;
GRANT ALL ON flow_conversation TO windmill_user;
GRANT ALL ON flow_conversation_message TO windmill_admin;
GRANT ALL ON flow_conversation_message TO windmill_user;
-- RLS policies
ALTER TABLE flow_conversation ENABLE ROW LEVEL SECURITY;
ALTER TABLE flow_conversation_message ENABLE ROW LEVEL SECURITY;
-- Admin policies - admins can access all conversations
CREATE POLICY admin_policy ON flow_conversation FOR ALL TO windmill_admin USING (true);
CREATE POLICY admin_policy ON flow_conversation_message FOR ALL TO windmill_admin USING (true);
-- User policies - users can only access their own conversations
CREATE POLICY see_own ON flow_conversation FOR ALL TO windmill_user
USING (flow_conversation.created_by = current_setting('session.user'));
-- Users can see messages of conversations they own
CREATE POLICY see_own ON flow_conversation_message FOR ALL TO windmill_user
USING (
EXISTS (
SELECT 1 FROM flow_conversation
WHERE flow_conversation.id = flow_conversation_message.conversation_id
AND flow_conversation.created_by = current_setting('session.user')
)
);
@@ -0,0 +1,2 @@
-- Add down migration script here
DROP TABLE app_bundles;
@@ -0,0 +1,8 @@
-- Add up migration script here
CREATE TABLE app_bundles (
app_version_id BIGINT NOT NULL,
w_id VARCHAR(255) NOT NULL,
file_type VARCHAR(10) NOT NULL,
data BYTEA NOT NULL,
PRIMARY KEY (app_version_id, file_type)
);
@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE job_perms DROP COLUMN end_user_email;
@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE job_perms ADD COLUMN end_user_email VARCHAR(255);
+37 -3
View File
@@ -2,6 +2,7 @@
use anyhow::anyhow;
use lazy_static::lazy_static;
#[cfg(not(target_arch = "wasm32"))]
use regex::Regex;
#[cfg(target_arch = "wasm32")]
@@ -491,13 +492,15 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
let mut args = vec![];
let mut hm: HashMap<i32, String> = HashMap::new();
for cap in RE_CODE_PGSQL.captures_iter(code) {
let typ = cap
.get(2)
.map(|cap| transform_types_with_spaces(&cap, &code))
.unwrap_or("text");
hm.insert(
cap.get(1)
.and_then(|x| x.as_str().parse::<i32>().ok())
.ok_or_else(|| anyhow!("Impossible to parse arg digit"))?,
cap.get(2)
.map(|x| x.as_str().to_string())
.unwrap_or_else(|| "text".to_string()),
typ.to_string(),
);
}
for (i, v) in hm.iter() {
@@ -543,6 +546,37 @@ fn parse_pg_file(code: &str) -> anyhow::Result<Option<Vec<Arg>>> {
Ok(Some(args))
}
// The regex doesn't parse types with space such as "character varying"
// So we look for them manually and replace them with their shorter counterpart
fn transform_types_with_spaces<'a>(cap: &regex::Match<'a>, code: &str) -> &'a str {
lazy_static! {
static ref TYPES: [(&'static str, &'static str); 6] = [
("character varying", "varchar"),
("double precision", "double"),
("time with time zone", "timetz"),
("time without time zone", "time"),
("timestamp with time zone", "timestamptz"),
("timestamp without time zone", "timestamp"),
];
}
let typ = &code[cap.start()..];
for (long_type, alias) in TYPES.iter() {
let mut typ = typ;
let mut found_mismatch = false;
for token in long_type.split(' ') {
if typ.len() < token.len() || !typ[..token.len()].eq_ignore_ascii_case(token) {
found_mismatch = true;
break;
}
typ = typ[token.len()..].trim_start();
}
if !found_mismatch {
return alias;
}
}
cap.as_str()
}
pub fn parse_sql_statement_named_params(code: &str, prefix: char) -> HashSet<String> {
let mut arg_names = HashSet::new();
run_on_sql_statement_matches(
+1
View File
@@ -156,6 +156,7 @@ impl RunJob {
None,
None,
false,
None,
)
.await
.expect("push has to succeed");
+2
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
set -e
# Default directory
EE_DIR="../windmill-ee-private"
+392 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.554.0
version: 1.555.1
title: Windmill API
contact:
@@ -5727,6 +5727,12 @@ paths:
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
requestBody:
description: script args
@@ -5743,6 +5749,230 @@ paths:
application/json:
schema: {}
/w/{workspace}/jobs/run_and_stream/f/{path}:
post:
summary: run flow by path and stream updates via SSE
operationId: runAndStreamFlowByPath
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
requestBody:
description: flow args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
get:
summary: run flow by path with GET and stream updates via SSE
operationId: runAndStreamFlowByPathGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
/w/{workspace}/jobs/run_and_stream/p/{path}:
post:
summary: run script by path and stream updates via SSE
operationId: runAndStreamScriptByPath
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
requestBody:
description: script args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
get:
summary: run script by path with GET and stream updates via SSE
operationId: runAndStreamScriptByPathGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/ScriptPath"
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
/w/{workspace}/jobs/run_and_stream/h/{hash}:
post:
summary: run script by hash and stream updates via SSE
operationId: runAndStreamScriptByHash
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: hash
in: path
required: true
schema:
type: string
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
requestBody:
description: script args
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ScriptArgs"
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
get:
summary: run script by hash with GET and stream updates via SSE
operationId: runAndStreamScriptByHashGet
tags:
- job
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: hash
in: path
required: true
schema:
type: string
- $ref: "#/components/parameters/ParentJob"
- $ref: "#/components/parameters/WorkerTag"
- $ref: "#/components/parameters/CacheTtl"
- $ref: "#/components/parameters/NewJobId"
- $ref: "#/components/parameters/IncludeHeader"
- $ref: "#/components/parameters/QueueLimit"
- $ref: "#/components/parameters/Payload"
- $ref: "#/components/parameters/SkipPreprocessor"
- name: poll_delay_ms
description: delay between polling for job updates in milliseconds
in: query
schema:
type: integer
format: int64
responses:
"200":
description: server-sent events stream of job updates
content:
text/event-stream:
schema:
type: string
/w/{workspace}/jobs/result_by_id/{flow_job_id}/{node_id}:
get:
summary: get job result by id
@@ -6250,6 +6480,81 @@ paths:
schema:
type: string
/w/{workspace}/flow_conversations/list:
get:
summary: list flow conversations
operationId: listFlowConversations
tags:
- flow_conversation
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
- name: flow_path
description: filter conversations by flow path
in: query
schema:
type: string
responses:
"200":
description: flow conversations list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FlowConversation"
/w/{workspace}/flow_conversations/delete/{conversation_id}:
delete:
summary: delete flow conversation
operationId: deleteFlowConversation
tags:
- flow_conversation
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: conversation_id
description: conversation id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: flow conversation deleted
content:
text/plain:
schema:
type: string
/w/{workspace}/flow_conversations/{conversation_id}/messages:
get:
summary: list conversation messages
operationId: listConversationMessages
tags:
- flow_conversation
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- name: conversation_id
description: conversation id
in: path
required: true
schema:
type: string
format: uuid
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PerPage"
responses:
"200":
description: conversation messages
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/FlowConversationMessage"
/w/{workspace}/raw_apps/list:
get:
summary: list all raw apps
@@ -6698,6 +7003,23 @@ paths:
schema:
type: string
/w/{workspace}/apps/secret_of_latest_version/{path}:
get:
summary: get public secret of latest version of an app bundle
operationId: getPublicSecretOfLatestVersionOfApp
tags:
- app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: app secret
content:
text/plain:
schema:
type: string
/w/{workspace}/apps/get/v/{id}:
get:
summary: get app by version
@@ -7122,6 +7444,12 @@ paths:
in: query
schema:
type: boolean
- name: memory_id
description: memory ID for chat-enabled flows
in: query
schema:
type: string
format: uuid
requestBody:
description: flow args
required: true
@@ -14454,6 +14782,66 @@ components:
# -- INLINE END --
# Do not change line above
FlowConversation:
type: object
required:
[id, workspace_id, flow_path, created_at, updated_at, created_by]
properties:
id:
type: string
format: uuid
description: Unique identifier for the conversation
workspace_id:
type: string
description: The workspace ID where the conversation belongs
flow_path:
type: string
description: Path of the flow this conversation is for
title:
type: string
description: Optional title for the conversation
nullable: true
created_at:
type: string
format: date-time
description: When the conversation was created
updated_at:
type: string
format: date-time
description: When the conversation was last updated
created_by:
type: string
description: Username who created the conversation
FlowConversationMessage:
type: object
required: [id, conversation_id, message_type, content, created_at]
properties:
id:
type: string
format: uuid
description: Unique identifier for the message
conversation_id:
type: string
format: uuid
description: The conversation this message belongs to
message_type:
type: string
enum: [user, assistant, system]
description: Type of the message
content:
type: string
description: The message content
job_id:
type: string
format: uuid
nullable: true
description: Associated job ID if this message came from a flow run
created_at:
type: string
format: date-time
description: When the message was created
EndpointTool:
type: object
required: [name, description, instructions, path, method]
@@ -17654,11 +18042,14 @@ components:
created_by:
type: string
nullable: true
disabled:
type: boolean
required:
- id
- name
- username
- color
- disabled
required:
- email
- workspaces
@@ -17,7 +17,7 @@ use crate::db::DB;
use axum::Router;
#[cfg(not(feature = "private"))]
pub fn global_service() -> Router {
pub fn global_service(_job_completed_tx: windmill_worker::JobCompletedSender) -> Router {
Router::new()
}
+185 -31
View File
@@ -88,6 +88,10 @@ pub fn workspaced_service() -> Router {
.route("/get/lite/*path", get(get_app_lite))
.route("/get/draft/*path", get(get_app_w_draft))
.route("/secret_of/*path", get(get_secret_id))
.route(
"/secret_of_latest_version/*path",
get(get_latest_version_secret_id),
)
.route("/get/v/*id", get(get_app_by_id))
.route("/get_data/v/*id", get(get_raw_app_data))
.route("/exists/*path", get(exists_app))
@@ -389,19 +393,83 @@ async fn list_apps(
Ok(Json(rows))
}
async fn get_raw_app_data(Path((w_id, version_id)): Path<(String, String)>) -> Result<Response> {
let file_path = format!("/tmp/wmill/{}/{}", w_id, version_id);
let file = tokio::fs::File::open(file_path).await?;
let stream = tokio_util::io::ReaderStream::new(file);
let res = Response::builder().header(
http::header::CONTENT_TYPE,
if version_id.ends_with(".css") {
"text/css"
} else {
"text/javascript"
},
);
Ok(res.body(Body::from_stream(stream)).unwrap())
async fn get_raw_app_data(
Path((w_id, secret_with_ext)): Path<(String, String)>,
Extension(db): Extension<DB>,
) -> Result<Response> {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let object_store = windmill_common::s3_helpers::get_object_store().await;
// tracing::info!("secret_with_ext: {}", secret_with_ext);
let mut splitted = secret_with_ext.split('.');
let secret_id = splitted.next().unwrap_or("");
if secret_id.is_empty() {
return Err(Error::BadRequest("Invalid secret".to_string()));
}
let id = get_id_from_secret(
&db,
&w_id,
secret_id.to_string(),
Some(BUNDLE_SECRET_PREFIX),
)
.await?;
let file_type = splitted.next().unwrap_or("");
let file_type = if file_type == "css" {
"css"
} else if file_type == "js" {
"js"
} else {
return Err(Error::BadRequest(
"Invalid file type, only .css and .js are supported".to_string(),
));
};
// tracing::info!("file_type: {}", file_type);
#[allow(unused_assignments)]
let mut body: Option<Body> = None;
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = object_store {
let path = format!("/app_bundles/{}/{}.{}", w_id, id, file_type);
let stream = os
.get(&object_store::path::Path::from(path))
.await?
.bytes()
.await?;
tracing::info!("stream: {}", stream.len());
body = Some(Body::from(stream));
}
if body.is_none() {
let get_raw_app_file = sqlx::query_scalar!(
"SELECT data FROM app_bundles WHERE app_version_id = $1 AND file_type = $2 AND w_id = $3",
id,
file_type,
&w_id,
)
.fetch_optional(&db)
.await?;
if let Some(file) = get_raw_app_file {
body = Some(Body::from(file));
}
}
if let Some(body) = body {
// let stream = tokio_util::io::ReaderStream::new(file);
let res = Response::builder().header(
http::header::CONTENT_TYPE,
if file_type == "css" {
"text/css"
} else {
"text/javascript"
},
);
Ok(res.body(body).unwrap())
} else {
return Err(Error::NotFound("File not found".to_string()));
}
}
// async fn get_app_version(
@@ -692,14 +760,7 @@ async fn get_public_app_by_secret(
Extension(db): Extension<DB>,
Path((w_id, secret)): Path<(String, String)>,
) -> JsonResult<AppWithLastVersion> {
let mc = build_crypt(&db, &w_id).await?;
let decrypted = mc
.decrypt_bytes_to_bytes(&(hex::decode(secret)?))
.map_err(|e| Error::internal_err(e.to_string()))?;
let bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?;
let id: i64 = bytes.parse().map_err(to_anyhow)?;
let id = get_id_from_secret(&db, &w_id, secret, None).await?;
let app_o = sqlx::query_as::<_, AppWithLastVersion>(
"SELECT app.id, app.path, app.summary, app.versions, app.policy, app.custom_path,
@@ -747,6 +808,27 @@ async fn get_public_app_by_secret(
Ok(Json(app))
}
async fn get_id_from_secret(
db: &DB,
w_id: &str,
secret: String,
prefix: Option<&str>,
) -> Result<i64> {
let mc = build_crypt(db, w_id).await?;
let decrypted = mc
.decrypt_bytes_to_bytes(&(hex::decode(secret)?))
.map_err(|e| Error::internal_err(e.to_string()))?;
let mut bytes = str::from_utf8(&decrypted).map_err(to_anyhow)?;
if let Some(prefix) = prefix {
if !bytes.starts_with(prefix) {
return Err(Error::BadRequest("Invalid secret".to_string()));
}
bytes = bytes.strip_prefix(prefix).unwrap_or("");
}
let id: i64 = bytes.parse().map_err(to_anyhow)?;
Ok(id)
}
async fn get_public_resource(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
@@ -803,16 +885,85 @@ async fn get_secret_id(
Ok(hx)
}
const BUNDLE_SECRET_PREFIX: &str = "bundle_";
async fn get_latest_version_secret_id(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> Result<String> {
let path = path.to_path();
check_scopes(&authed, || format!("apps:read:{}", path))?;
let mut tx = user_db.begin(&authed).await?;
let id_o = sqlx::query_scalar!(
"SELECT app.versions[array_upper(app.versions, 1)] FROM app
WHERE app.path = $1 AND app.workspace_id = $2",
path,
&w_id
)
.fetch_optional(&mut *tx)
.await?
.flatten();
tx.commit().await?;
let id = not_found_if_none(id_o, "App", path.to_string())?;
let mc = build_crypt(&db, &w_id).await?;
let hx = hex::encode(mc.encrypt_str_to_bytes(format!("{}{}", BUNDLE_SECRET_PREFIX, id)));
Ok(hx)
}
async fn store_raw_app_file<'a>(
w_id: &str,
id: &i64,
file_type: &str,
data: bytes::Bytes,
tx: &mut sqlx::Transaction<'a, sqlx::Postgres>,
) -> Result<()> {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
{
let object_store = windmill_common::s3_helpers::get_object_store().await;
let path: String = format!("/app_bundles/{}/{}.{}", w_id, id, file_type);
if let Some(os) = object_store {
if let Err(e) = os
.put(&object_store::path::Path::from(path.clone()), data.into())
.await
{
tracing::error!("Failed to put snapshot to s3 at {path}: {:?}", e);
return Err(windmill_common::error::Error::ExecutionErr(format!(
"Failed to put {path} to s3"
)));
}
tracing::info!("Successfully put snapshot to s3 at {path}");
return Ok(());
}
}
sqlx::query!(
"INSERT INTO app_bundles (app_version_id, w_id, file_type, data) VALUES ($1, $2, $3, $4)",
id,
w_id,
file_type,
data.to_vec()
)
.execute(&mut **tx)
.await?;
Ok(())
}
macro_rules! process_app_multipart {
($authed:expr, $user_db:expr, $db:expr, $w_id:expr, $path:expr, $multipart:expr, $internal_fn:expr) => {
async {
let mut saved_app = None;
let mut uploaded_js = false;
//todo: use s3 instead
let file_path = format!("/tmp/wmill/{}", $w_id);
std::fs::create_dir_all(&file_path).unwrap();
let mut multipart = $multipart;
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field.name().unwrap().to_string();
@@ -831,9 +982,8 @@ macro_rules! process_app_multipart {
.await?;
saved_app = Some((npath, nid, ntx));
} else if name == "js" {
if let Some((_npath, id, _tx)) = saved_app.as_ref() {
let file_path = format!("{}/{}.js", file_path, id);
std::fs::write(file_path, data).unwrap();
if let Some((_npath, id, tx)) = saved_app.as_mut() {
store_raw_app_file($w_id, &id, "js", data, tx).await?;
uploaded_js = true;
} else {
return Err(Error::BadRequest(
@@ -841,9 +991,8 @@ macro_rules! process_app_multipart {
));
}
} else if name == "css" {
if let Some((_npath, id, _tx)) = saved_app.as_ref() {
let file_path = format!("{}/{}.css", file_path, id);
std::fs::write(file_path, data).unwrap();
if let Some((_npath, id, tx)) = saved_app.as_mut() {
store_raw_app_file($w_id, &id, "css", data, tx).await?;
} else {
return Err(Error::BadRequest(
"App payload need to be created first".to_string(),
@@ -1090,6 +1239,7 @@ async fn create_app_internal<'a>(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
@@ -1469,6 +1619,7 @@ async fn update_app_internal<'a>(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tracing::info!("Pushed app dependency job {}", dependency_job_uuid);
@@ -1755,6 +1906,8 @@ async fn execute_component(
(email.as_str(), permissioned_as)
};
let end_user_email = opt_authed.as_ref().map(|a| a.email.clone());
let (uuid, tx) = push(
&db,
tx,
@@ -1784,6 +1937,7 @@ async fn execute_component(
None,
None,
false,
end_user_email,
)
.await?;
tx.commit().await?;
@@ -0,0 +1,291 @@
use axum::{
extract::{Path, Query},
routing::{delete, get},
Extension, Json, Router,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sql_builder::prelude::*;
use sqlx::{FromRow, Postgres};
use uuid::Uuid;
use crate::db::ApiAuthed;
use windmill_common::{
db::UserDB,
error::{JsonResult, Result},
utils::{not_found_if_none, paginate, Pagination},
};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, sqlx::Type)]
#[sqlx(type_name = "MESSAGE_TYPE", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum MessageType {
User,
Assistant,
}
pub fn workspaced_service() -> Router {
Router::new()
.route("/list", get(list_conversations))
.route("/delete/:conversation_id", delete(delete_conversation))
.route("/:conversation_id/messages", get(list_messages))
}
#[derive(Serialize, FromRow, Debug)]
pub struct FlowConversation {
pub id: Uuid,
pub workspace_id: String,
pub flow_path: String,
pub title: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: String,
}
#[derive(Serialize, FromRow, Debug)]
pub struct FlowConversationMessage {
pub id: Uuid,
pub conversation_id: Uuid,
pub message_type: MessageType,
pub content: String,
pub job_id: Option<Uuid>,
pub created_at: DateTime<Utc>,
}
#[derive(Deserialize)]
pub struct ListConversationsQuery {
pub flow_path: Option<String>,
}
async fn list_conversations(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path(w_id): Path<String>,
Query(pagination): Query<Pagination>,
Query(query): Query<ListConversationsQuery>,
) -> JsonResult<Vec<FlowConversation>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.clone().begin(&authed).await?;
let mut sqlb = SqlBuilder::select_from("flow_conversation");
sqlb.fields(&[
"id",
"workspace_id",
"flow_path",
"title",
"created_at",
"updated_at",
"created_by",
])
.and_where_eq("workspace_id", "?".bind(&w_id));
if let Some(flow_path) = &query.flow_path {
sqlb.and_where_eq("flow_path", "?".bind(flow_path));
}
sqlb.order_by("updated_at", true)
.limit(per_page as i64)
.offset(offset as i64);
let sql = sqlb.sql().map_err(|e| {
windmill_common::error::Error::InternalErr(format!("Failed to build SQL: {}", e))
})?;
let conversations = sqlx::query_as::<Postgres, FlowConversation>(&sql)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(conversations))
}
pub async fn get_or_create_conversation_with_id(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
w_id: &str,
flow_path: &str,
username: &str,
title: &str,
conversation_id: Uuid,
) -> Result<FlowConversation> {
// Check if conversation already exists
let existing_conversation = sqlx::query_as!(
FlowConversation,
"SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by
FROM flow_conversation
WHERE id = $1 AND workspace_id = $2",
conversation_id,
w_id
)
.fetch_optional(&mut **tx)
.await?;
if let Some(existing) = existing_conversation {
return Ok(existing);
}
// Create new conversation with provided ID
let conversation = sqlx::query_as!(
FlowConversation,
"INSERT INTO flow_conversation (id, workspace_id, flow_path, created_by, title)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, workspace_id, flow_path, title, created_at, updated_at, created_by",
conversation_id,
w_id,
flow_path,
username,
title
)
.fetch_one(&mut **tx)
.await?;
Ok(conversation)
}
async fn delete_conversation(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, conversation_id)): Path<(String, Uuid)>,
) -> Result<String> {
let mut tx = user_db.clone().begin(&authed).await?;
// Verify the conversation exists and belongs to the user
let conversation = sqlx::query_as!(
FlowConversation,
"SELECT id, workspace_id, flow_path, title, created_at, updated_at, created_by
FROM flow_conversation
WHERE id = $1 AND workspace_id = $2",
conversation_id,
&w_id
)
.fetch_optional(&mut *tx)
.await?;
not_found_if_none(conversation, "Conversation", conversation_id.to_string())?;
// Delete the conversation (messages will be cascade deleted)
sqlx::query!(
"DELETE FROM flow_conversation WHERE id = $1 AND workspace_id = $2",
conversation_id,
&w_id
)
.execute(&mut *tx)
.await?;
tx.commit().await?;
// Delete associated memory in background (non-blocking cleanup)
let w_id_clone = w_id.clone();
tokio::spawn(async move {
if let Err(e) =
windmill_worker::memory_oss::delete_conversation_memory(&w_id_clone, conversation_id)
.await
{
tracing::error!(
"Failed to delete memory for conversation {} in workspace {}: {:?}",
conversation_id,
w_id_clone,
e
);
}
});
Ok(format!("Conversation {} deleted", conversation_id))
}
async fn list_messages(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Path((w_id, conversation_id)): Path<(String, Uuid)>,
Query(pagination): Query<Pagination>,
) -> JsonResult<Vec<FlowConversationMessage>> {
let (per_page, offset) = paginate(pagination);
let mut tx = user_db.clone().begin(&authed).await?;
// Verify the conversation exists and belongs to the user
let conversation_exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1 AND workspace_id = $2)",
conversation_id,
&w_id
)
.fetch_one(&mut *tx)
.await?
.unwrap_or(false);
if !conversation_exists {
return Err(windmill_common::error::Error::NotFound(format!(
"Conversation not found or access denied: {}",
conversation_id
)));
}
// Fetch messages for this conversation, oldest first, but reverse the order of the messages for easy rendering on the frontend
let messages = sqlx::query_as!(
FlowConversationMessage,
r#"SELECT id, conversation_id, message_type as "message_type: MessageType", content, job_id, created_at
FROM (
SELECT id, conversation_id, message_type, content, job_id, created_at
FROM flow_conversation_message
WHERE conversation_id = $1
ORDER BY created_at DESC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END
LIMIT $2 OFFSET $3
) AS messages
ORDER BY created_at ASC, CASE WHEN message_type = 'user' THEN 0 ELSE 1 END
"#,
conversation_id,
per_page as i64,
offset as i64
)
.fetch_all(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(messages))
}
// Helper function to create a message using an existing transaction
pub async fn create_message(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
conversation_id: Uuid,
message_type: MessageType,
content: &str,
job_id: Option<Uuid>,
workspace_id: &str,
) -> windmill_common::error::Result<()> {
// Verify the conversation exists and belongs to the user
let conversation_exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM flow_conversation WHERE id = $1 AND workspace_id = $2)",
conversation_id,
workspace_id
)
.fetch_one(&mut **tx)
.await?
.unwrap_or(false);
if !conversation_exists {
return Err(windmill_common::error::Error::NotFound(format!(
"Conversation not found or access denied: {}",
conversation_id
)));
}
// Insert the message
sqlx::query!(
"INSERT INTO flow_conversation_message (conversation_id, message_type, content, job_id)
VALUES ($1, $2, $3, $4)",
conversation_id,
message_type as MessageType,
content,
job_id
)
.execute(&mut **tx)
.await?;
// Update conversation updated_at timestamp
sqlx::query!(
"UPDATE flow_conversation SET updated_at = NOW() WHERE id = $1",
conversation_id
)
.execute(&mut **tx)
.await?;
Ok(())
}
+3
View File
@@ -545,6 +545,7 @@ async fn create_flow(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
@@ -1012,6 +1013,7 @@ async fn update_flow(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
sqlx::query!(
@@ -1593,6 +1595,7 @@ mod tests {
priority: None,
early_return: None,
concurrency_key: None,
chat_input_enabled: None,
};
let expect = serde_json::json!({
"modules": [
+163 -13
View File
@@ -19,7 +19,6 @@ use quick_cache::sync::Cache;
use serde_json::value::RawValue;
use serde_json::Value;
use sqlx::Pool;
use windmill_common::s3_helpers::{upload_artifact_to_store, BundleFormat};
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::ops::{Deref, DerefMut};
@@ -37,6 +36,7 @@ use windmill_common::jobs::{
check_tag_available_for_workspace_internal, format_completed_job_result, format_result,
DynamicInput, ENTRYPOINT_OVERRIDE,
};
use windmill_common::s3_helpers::{upload_artifact_to_store, BundleFormat};
use windmill_common::utils::{RunnableKind, WarnAfterExt};
use windmill_common::worker::{Connection, CLOUD_HOSTED, TMP_DIR};
use windmill_common::DYNAMIC_INPUT_CACHE;
@@ -104,6 +104,8 @@ use windmill_queue::{
PushArgsOwned, PushIsolationLevel,
};
use crate::flow_conversations::{self, MessageType};
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST])
@@ -1757,6 +1759,7 @@ pub struct RunJobQuery {
pub cache_ttl: Option<i32>,
pub skip_preprocessor: Option<bool>,
pub poll_delay_ms: Option<u64>,
pub memory_id: Option<Uuid>,
}
impl RunJobQuery {
@@ -3612,7 +3615,7 @@ struct Preview {
tag: Option<String>,
dedicated_worker: Option<bool>,
lock: Option<String>,
format: Option<String>
format: Option<String>,
}
#[derive(Deserialize)]
@@ -3997,6 +4000,87 @@ async fn batch_rerun_handle_job(
))
}
/// Set the memory_id in flow_status for agent memory persistence
async fn set_flow_memory_id(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
job_id: Uuid,
memory_id: Uuid,
) -> error::Result<()> {
sqlx::query!(
"UPDATE v2_job_status
SET flow_status = jsonb_set(
flow_status,
'{memory_id}',
to_jsonb($2::uuid)
)
WHERE id = $1",
job_id,
memory_id
)
.execute(&mut **tx)
.await?;
Ok(())
}
async fn handle_chat_conversation_messages(
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
authed: &ApiAuthed,
w_id: &str,
flow_path: &str,
run_query: &RunJobQuery,
args: &PushArgsOwned,
uuid: Uuid,
) -> error::Result<()> {
let memory_id = run_query.memory_id.ok_or_else(|| {
windmill_common::error::Error::BadRequest(
"memory_id is required for chat-enabled flows".to_string(),
)
})?;
let user_msg_raw = args.args.get("user_message").ok_or_else(|| {
windmill_common::error::Error::BadRequest(
"user_message argument is required for chat-enabled flows".to_string(),
)
})?;
let user_msg = serde_json::from_str::<String>(user_msg_raw.get())?;
// Create conversation with provided ID (or get existing one)
flow_conversations::get_or_create_conversation_with_id(
tx,
w_id,
flow_path,
&authed.username,
&user_msg,
memory_id,
)
.await?;
// Create user message
flow_conversations::create_message(
tx,
memory_id,
MessageType::User,
&user_msg,
None, // No job_id for user message
w_id,
)
.await?;
// Create placeholder assistant message in the same transaction as the job
flow_conversations::create_message(
tx,
memory_id,
MessageType::Assistant,
"", // Empty content, will be updated when job completes
Some(uuid), // Associate with the job
w_id,
)
.await?;
Ok(())
}
pub async fn run_flow_by_path(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -4043,6 +4127,7 @@ pub async fn run_flow_by_path_inner(
tag,
dedicated_worker,
has_preprocessor,
chat_input_enabled,
on_behalf_of_email,
edited_by,
early_return,
@@ -4068,11 +4153,11 @@ pub async fn run_flow_by_path_inner(
&authed.email,
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db, authed.clone().into()),
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()),
)
};
let (uuid, tx) = push(
let (uuid, mut tx) = push(
&db,
tx,
&w_id,
@@ -4083,7 +4168,7 @@ pub async fn run_flow_by_path_inner(
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
},
PushArgs { args: &args.args, extra: args.extra },
PushArgs { args: &args.args, extra: args.extra.clone() },
authed.display_username(),
email,
permissioned_as,
@@ -4104,8 +4189,29 @@ pub async fn run_flow_by_path_inner(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
// Set memory_id if provided (for agent memory)
if let Some(memory_id) = run_query.memory_id {
set_flow_memory_id(&mut tx, uuid, memory_id).await?;
}
// Handle conversation messages for chat-enabled flows
if chat_input_enabled.unwrap_or(false) {
handle_chat_conversation_messages(
&mut tx,
&authed,
&w_id,
&flow_path.to_string(),
&run_query,
&args,
uuid,
)
.await?;
}
tx.commit().await?;
Ok((uuid, early_return))
}
@@ -4200,6 +4306,7 @@ pub async fn restart_flow(
completed_job.priority,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -4302,6 +4409,7 @@ pub async fn run_script_by_path_inner(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
@@ -4454,6 +4562,7 @@ pub async fn run_workflow_as_code(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
@@ -4997,6 +5106,7 @@ pub async fn run_wait_result_job_by_path_get(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5149,6 +5259,7 @@ pub async fn run_wait_result_script_by_path_internal(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5265,6 +5376,7 @@ pub async fn run_wait_result_script_by_hash(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5517,6 +5629,7 @@ pub async fn run_wait_result_flow_by_path_internal(
dedicated_worker,
early_return,
has_preprocessor,
chat_input_enabled,
on_behalf_of_email,
edited_by,
version,
@@ -5539,11 +5652,11 @@ pub async fn run_wait_result_flow_by_path_internal(
&authed.email,
username_to_permissioned_as(&authed.username),
Some(authed.clone().into()),
PushIsolationLevel::Isolated(user_db, authed.clone().into()),
PushIsolationLevel::Isolated(user_db.clone(), authed.clone().into()),
)
};
let (uuid, tx) = push(
let (uuid, mut tx) = push(
&db,
tx,
&w_id,
@@ -5554,7 +5667,7 @@ pub async fn run_wait_result_flow_by_path_internal(
apply_preprocessor: !run_query.skip_preprocessor.unwrap_or(false)
&& has_preprocessor.unwrap_or(false),
},
PushArgs { args: &args.args, extra: args.extra },
PushArgs { args: &args.args, extra: args.extra.clone() },
authed.display_username(),
email,
permissioned_as,
@@ -5575,8 +5688,29 @@ pub async fn run_wait_result_flow_by_path_internal(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
// Set conversation_id if provided (for agent memory)
if let Some(memory_id) = run_query.memory_id {
set_flow_memory_id(&mut tx, uuid, memory_id).await?;
}
// Handle conversation messages for chat-enabled flows
if chat_input_enabled.unwrap_or(false) {
handle_chat_conversation_messages(
&mut tx,
&authed,
&w_id,
&flow_path.to_string(),
&run_query,
&args,
uuid,
)
.await?;
}
tx.commit().await?;
run_wait_result(&db, uuid, w_id, early_return, &authed.username).await
@@ -5646,6 +5780,7 @@ async fn run_preview_script(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5677,7 +5812,6 @@ async fn run_wait_result_preview_script(
return result;
}
async fn run_bundle_preview_script(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -5686,7 +5820,6 @@ async fn run_bundle_preview_script(
Query(run_query): Query<RunJobQuery>,
mut multipart: axum::extract::Multipart,
) -> error::Result<(StatusCode, String)> {
if authed.is_operator {
return Err(error::Error::NotAuthorized(
"Operators cannot run preview jobs for security reasons".to_string(),
@@ -5705,7 +5838,10 @@ async fn run_bundle_preview_script(
let data = data.map_err(to_anyhow)?;
if name == "preview" {
let preview: Preview = serde_json::from_slice(&data).map_err(to_anyhow)?;
format = preview.format.and_then(|s| BundleFormat::from_string(&s)).unwrap_or(BundleFormat::Cjs);
format = preview
.format
.and_then(|s| BundleFormat::from_string(&s))
.unwrap_or(BundleFormat::Cjs);
let scheduled_for = run_query.get_scheduled_for(&db).await?;
let tag = run_query.tag.clone().or(preview.tag.clone());
@@ -5726,7 +5862,10 @@ async fn run_bundle_preview_script(
ltx,
&w_id,
JobPayload::Code(RawCode {
hash: Some(windmill_common::scripts::codebase_to_hash(is_tar, format == BundleFormat::Esm)),
hash: Some(windmill_common::scripts::codebase_to_hash(
is_tar,
format == BundleFormat::Esm,
)),
content: preview.content.unwrap_or_default(),
path: preview.path,
language: preview.language.unwrap_or(ScriptLang::Deno),
@@ -5758,6 +5897,7 @@ async fn run_bundle_preview_script(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
job_id = Some(uuid);
@@ -5785,7 +5925,12 @@ async fn run_bundle_preview_script(
uploaded = true;
let path = windmill_common::s3_helpers::bundle(&w_id, &id);
upload_artifact_to_store(&path, data, &windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR).await?;
upload_artifact_to_store(
&path,
data,
&windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR,
)
.await?;
}
// println!("Length of `{}` is {} bytes", name, data.len());
}
@@ -5890,6 +6035,7 @@ async fn run_dependencies_job(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -5957,6 +6103,7 @@ async fn run_flow_dependencies_job(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -6444,6 +6591,7 @@ async fn run_preview_flow_job(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -6617,6 +6765,7 @@ async fn run_dynamic_select(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
tx.commit().await?;
@@ -6744,6 +6893,7 @@ pub async fn run_job_by_hash_inner(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
+16 -4
View File
@@ -87,6 +87,7 @@ pub mod ee;
pub mod ee_oss;
pub mod embeddings;
mod favorite;
mod flow_conversations;
pub mod flows;
mod folders;
mod granular_acls;
@@ -411,7 +412,7 @@ pub async fn run_server(
};
#[cfg(feature = "agent_worker_server")]
let (agent_workers_router, agent_workers_bg_processor, agent_workers_killpill_tx) =
let (agent_workers_router, agent_workers_bg_processor, agent_workers_job_completed_tx) =
if server_mode {
agent_workers_oss::workspaced_service(db.clone(), _base_internal_url.clone())
} else {
@@ -443,6 +444,10 @@ pub async fn run_server(
.nest("/drafts", drafts::workspaced_service())
.nest("/favorites", favorite::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest(
"/flow_conversations",
flow_conversations::workspaced_service(),
)
.nest("/folders", folders::workspaced_service())
.nest("/groups", groups::workspaced_service())
.nest("/inputs", inputs::workspaced_service())
@@ -535,7 +540,14 @@ pub async fn run_server(
.nest("/agent_workers", {
#[cfg(feature = "agent_worker_server")]
{
agent_workers_oss::global_service().layer(Extension(agent_cache.clone()))
if let Some(agent_workers_job_completed_tx) =
agent_workers_job_completed_tx.clone()
{
agent_workers_oss::global_service(agent_workers_job_completed_tx)
.layer(Extension(agent_cache.clone()))
} else {
Router::new()
}
}
#[cfg(not(feature = "agent_worker_server"))]
{
@@ -705,8 +717,8 @@ pub async fn run_server(
let server = server.with_graceful_shutdown(async move {
killpill_rx.recv().await.ok();
#[cfg(feature = "agent_worker_server")]
if let Some(agent_workers_killpill_tx) = agent_workers_killpill_tx {
if let Err(e) = agent_workers_killpill_tx.kill().await {
if let Some(agent_workers_job_completed_tx) = agent_workers_job_completed_tx {
if let Err(e) = agent_workers_job_completed_tx.kill().await {
tracing::error!("Error killing agent workers: {e:#}");
}
}
+3 -1
View File
@@ -515,7 +515,8 @@ pub async fn transform_json_value<'c>(
match v {
Value::String(y) if y.starts_with("$var:") => {
let path = y.strip_prefix("$var:").unwrap();
let userdb_authed = UserDbWithOptAuthed { authed: authed, user_db: user_db.clone(), db: db.clone() };
let userdb_authed =
UserDbWithOptAuthed { authed: authed, user_db: user_db.clone(), db: db.clone() };
let v = crate::variables::get_value_internal(
&userdb_authed,
@@ -616,6 +617,7 @@ pub async fn transform_json_value<'c>(
job.root_job.map(|x| x.to_string()),
Some(job.scheduled_for.clone()),
None,
None,
)
.await;
+13 -3
View File
@@ -41,7 +41,12 @@ use windmill_audit::ActionKind;
use windmill_worker::process_relative_imports;
use windmill_common::{
assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind, AssetWithAltAccessType}, error::to_anyhow, s3_helpers::upload_artifact_to_store, scripts::hash_script, utils::WarnAfterExt, worker::CLOUD_HOSTED
assets::{clear_asset_usage, insert_asset_usage, AssetUsageKind, AssetWithAltAccessType},
error::to_anyhow,
s3_helpers::upload_artifact_to_store,
scripts::hash_script,
utils::WarnAfterExt,
worker::CLOUD_HOSTED,
};
use windmill_common::{
@@ -418,7 +423,12 @@ async fn create_snapshot_script(
uploaded = true;
let path = windmill_common::s3_helpers::bundle(&w_id, &hash);
upload_artifact_to_store(&path, data, &windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR).await?;
upload_artifact_to_store(
&path,
data,
&windmill_common::worker::ROOT_STANDALONE_BUNDLE_DIR,
)
.await?;
}
// println!("Length of `{}` is {} bytes", name, data.len());
}
@@ -438,7 +448,6 @@ async fn create_snapshot_script(
return Ok((StatusCode::CREATED, format!("{}", script_hash.unwrap())));
}
async fn list_paths_from_workspace_runnable(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -980,6 +989,7 @@ async fn create_script_internal<'c>(
None,
Some(&authed.clone().into()),
false,
None,
)
.await?;
Ok((hash, new_tx, None))
+14 -13
View File
@@ -42,7 +42,6 @@ use windmill_common::{
},
parse_postgres_url,
server::Smtp,
utils::build_arg_str,
};
pub fn global_service() -> Router {
@@ -617,19 +616,21 @@ async fn create_ducklake_database(
// We have to connect to the newly created database as admin to grant permissions
let pg_creds = parse_postgres_url(&get_database_url().await?)?;
let Some(wm_pg_pwd) = pg_creds.password else {
return Err(error::Error::BadRequest("Password not found".to_string()));
let ssl_mode = match pg_creds.ssl_mode.as_deref() {
Some("allow") => "prefer".to_string(),
Some("verify-ca") | Some("verify-full") => "require".to_string(),
Some(s) => s.to_string(),
None => "prefer".to_string(),
};
let conn_str: String = build_arg_str(
&[
("host", Some(&pg_creds.host)),
("port", pg_creds.port.map(|p| p.to_string()).as_deref()),
("password", Some(&wm_pg_pwd)),
("user", pg_creds.username.as_deref()),
("dbname", Some(&dbname)),
],
" ",
"=",
let conn_str = format!(
"postgres://{user}:{password}@{host}:{port}/{dbname}?sslmode={sslmode}",
user = urlencoding::encode(&pg_creds.username.unwrap_or_else(|| "postgres".to_string())),
password = urlencoding::encode(&pg_creds.password.as_deref().unwrap_or("")),
host = urlencoding::encode(&pg_creds.host),
port = pg_creds.port.unwrap_or(5432),
dbname = dbname,
sslmode = ssl_mode
);
let (client, connection) = tokio::time::timeout(
std::time::Duration::from_secs(20),
@@ -684,7 +684,8 @@ pub async fn trigger_runnable_and_wait_for_raw_result_with_error_ctx(
error_handler_path,
error_handler_args,
trigger_path,
).await?;
)
.await?;
if !success {
Err(windmill_common::error::Error::internal_err(format!(
@@ -857,6 +858,7 @@ async fn trigger_script_with_retry_and_error_handler(
None,
push_authed.as_ref(),
false,
None,
)
.await?;
tx.commit().await?;
+8 -10
View File
@@ -83,6 +83,7 @@ async fn list_contextual_variables(
Some("017e0ad5-f499-73b6-5488-92a61c5196dd".to_string()),
Some(chrono::offset::Utc::now()),
Some(ScriptHash(1234567890)),
None,
)
.await
.to_vec(),
@@ -843,11 +844,11 @@ pub async fn get_value_internal<'a, 'e, A: sqlx::Acquire<'e, Database = Postgres
} else if !value.is_empty() {
let mc = build_crypt(&db, &w_id).await?;
decrypt(&mc, value).map_err(|e| {
Error::internal_err(format!(
"Error decrypting variable {}: {}",
variable.path, e
))
})?
Error::internal_err(format!(
"Error decrypting variable {}: {}",
variable.path, e
))
})?
} else {
"".to_string()
}
@@ -884,11 +885,8 @@ pub async fn get_variable_or_self(path: String, db: &DB, w_id: &str) -> Result<S
if record.is_secret {
let mc = build_crypt(db, w_id).await?;
value = decrypt(&mc, value).map_err(|e| {
Error::internal_err(format!(
"Error decrypting variable {}: {}",
path, e
))
})?;
Error::internal_err(format!("Error decrypting variable {}: {}", path, e))
})?;
}
Ok(value)
+3 -1
View File
@@ -366,6 +366,7 @@ struct UserWorkspace {
pub color: Option<String>,
pub operator_settings: Option<Option<serde_json::Value>>,
pub parent_workspace_id: Option<String>,
pub disabled: bool,
}
#[derive(Deserialize)]
@@ -2109,7 +2110,8 @@ async fn user_workspaces(
let workspaces = sqlx::query_as!(
UserWorkspace,
"SELECT workspace.id, workspace.name, usr.username, workspace_settings.color, workspace.parent_workspace_id,
CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings
CASE WHEN usr.operator THEN workspace_settings.operator_settings ELSE NULL END as operator_settings,
usr.disabled
FROM workspace
JOIN usr ON usr.workspace_id = workspace.id
JOIN workspace_settings ON workspace_settings.workspace_id = workspace.id
@@ -45,6 +45,10 @@ pub struct FlowStatus {
pub restarted_from: Option<RestartedFrom>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stream_job: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chat_input_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_id: Option<Uuid>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
@@ -508,6 +512,8 @@ impl FlowStatus {
restarted_from: None,
user_states: HashMap::new(),
stream_job: None,
chat_input_enabled: f.chat_input_enabled,
memory_id: None,
}
}
+2
View File
@@ -137,6 +137,8 @@ pub struct FlowValue {
pub priority: Option<i16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub concurrency_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chat_input_enabled: Option<bool>,
}
impl FlowValue {
+2 -1
View File
@@ -682,6 +682,7 @@ pub struct FlowVersionInfo {
pub tag: Option<String>,
pub early_return: Option<String>,
pub has_preprocessor: Option<bool>,
pub chat_input_enabled: Option<bool>,
pub on_behalf_of_email: Option<String>,
pub edited_by: String,
pub dedicated_worker: Option<bool>,
@@ -803,7 +804,7 @@ pub fn get_latest_flow_version_info_for_path_from_version<
let mut conn = db.acquire().await?;
let info = sqlx::query_as!(
FlowVersionInfo,
"SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, on_behalf_of_email, edited_by, flow_version.id AS version
"SELECT tag, dedicated_worker, flow_version.value->>'early_return' as early_return, flow_version.value->>'preprocessor_module' IS NOT NULL as has_preprocessor, (flow_version.value->>'chat_input_enabled')::boolean as chat_input_enabled, on_behalf_of_email, edited_by, flow_version.id AS version
FROM flow
INNER JOIN flow_version
ON flow_version.id = $3
+45 -65
View File
@@ -14,6 +14,7 @@ use object_store::gcp::GoogleCloudStorageBuilder;
use object_store::ObjectStore;
#[cfg(feature = "parquet")]
use object_store::{aws::AmazonS3Builder, ClientOptions};
use quick_cache::sync::Cache;
#[cfg(feature = "parquet")]
use reqwest::header::HeaderMap;
use serde::de::Visitor;
@@ -414,18 +415,6 @@ pub struct AzureBlobResource {
pub federated_token_file: Option<String>,
}
impl AzureBlobResource {
pub fn get_endpoint_url(&self) -> error::Result<String> {
Ok(render_endpoint(
self.endpoint.clone().unwrap_or_else(|| "".to_string()),
self.use_ssl.unwrap_or(false),
None,
None,
"".to_string(),
))
}
}
fn as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::de::Deserializer<'de>,
@@ -526,7 +515,6 @@ pub async fn build_object_store_client(
}
}
#[derive(PartialEq)]
pub enum BundleFormat {
Esm,
@@ -534,7 +522,7 @@ pub enum BundleFormat {
}
impl BundleFormat {
pub fn from_string(s: &str) -> Option<Self> {
pub fn from_string(s: &str) -> Option<Self> {
match s {
"esm" => Some(Self::Esm),
"cjs" => Some(Self::Cjs),
@@ -543,49 +531,51 @@ impl BundleFormat {
}
}
pub async fn upload_artifact_to_store(path: &str, data: bytes::Bytes, standalone_dir: &str) -> error::Result<()> {
pub async fn upload_artifact_to_store(
path: &str,
data: bytes::Bytes,
standalone_dir: &str,
) -> error::Result<()> {
#[cfg(all(feature = "enterprise", feature = "parquet"))]
let object_store = crate::s3_helpers::get_object_store().await;
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
let object_store: Option<()> = None;
Ok(if &crate::utils::MODE_AND_ADDONS.mode
== &crate::utils::Mode::Standalone
&& object_store.is_none()
{
let path = format!("{}/{}", standalone_dir, path);
tracing::info!("Writing file to path {path}");
let split_path = path.split("/").collect::<Vec<&str>>();
std::fs::create_dir_all(
split_path[..split_path.len() - 1].join("/"),
)?;
crate::worker::write_file_bytes(
&path,
&data,
)?;
} else {
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
Ok(
if &crate::utils::MODE_AND_ADDONS.mode == &crate::utils::Mode::Standalone
&& object_store.is_none()
{
return Err(error::Error::ExecutionErr("codebase is an EE feature".to_string()));
}
let path = format!("{}/{}", standalone_dir, path);
tracing::info!("Writing file to path {path}");
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = object_store {
let split_path = path.split("/").collect::<Vec<&str>>();
std::fs::create_dir_all(split_path[..split_path.len() - 1].join("/"))?;
if let Err(e) = os
.put(&object_store::path::Path::from(path), data.into())
.await
{
tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e);
return Err(error::Error::ExecutionErr(format!("Failed to put {path} to s3")));
}
crate::worker::write_file_bytes(&path, &data)?;
} else {
return Err(error::Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string()));
}
})
}
#[cfg(not(all(feature = "enterprise", feature = "parquet")))]
{
return Err(error::Error::ExecutionErr(
"codebase is an EE feature".to_string(),
));
}
#[cfg(all(feature = "enterprise", feature = "parquet"))]
if let Some(os) = object_store {
if let Err(e) = os
.put(&object_store::path::Path::from(path), data.into())
.await
{
tracing::info!("Failed to put snapshot to s3 at {path}: {:?}", e);
return Err(error::Error::ExecutionErr(format!(
"Failed to put {path} to s3"
)));
}
} else {
return Err(error::Error::BadConfig("Object store is required for snapshot script and is not configured for servers".to_string()));
}
},
)
}
#[cfg(feature = "parquet")]
pub async fn attempt_fetch_bytes(
@@ -1246,21 +1236,11 @@ pub fn duckdb_connection_settings_internal(
return Ok(response);
}
impl ObjectStoreResource {
pub fn get_endpoint_url(&self) -> error::Result<String> {
match self {
ObjectStoreResource::S3(s3_resource) => Ok(render_endpoint(
s3_resource.endpoint.clone(),
s3_resource.use_ssl,
s3_resource.port,
s3_resource.path_style,
s3_resource.bucket.clone(),
)),
ObjectStoreResource::Gcs(gcs_resource) => Ok(format!(
"https://storage.googleapis.com/{}",
gcs_resource.bucket
)),
ObjectStoreResource::Azure(az_resource) => az_resource.get_endpoint_url(),
}
}
// DuckDB does not parse anything in case of S3 errors and just returns a generic error message.
// To display better error messages, we cache the errors in a Map<Token, ErrorMessage>
//
// We leverage the fact that workers have an internal server to insert the error message
// from the S3 Proxy, and read it directly in memory from the worker.
lazy_static::lazy_static! {
pub static ref S3_PROXY_LAST_ERRORS_CACHE: Cache<String, String> = Cache::new(4);
}
+1
View File
@@ -246,6 +246,7 @@ pub async fn now_from_db<'c, E: sqlx::PgExecutor<'c>>(
) -> Result<chrono::DateTime<chrono::Utc>> {
Ok(sqlx::query_scalar!("SELECT now()")
.fetch_one(db)
.warn_after_seconds_with_sql(1, "now_from_db".to_string())
.await?
.unwrap())
}
+12 -5
View File
@@ -165,11 +165,11 @@ pub async fn get_secret_value_as_admin(
if !value.is_empty() {
let mc = build_crypt(db, w_id).await?;
decrypt(&mc, value).map_err(|e| {
crate::error::Error::internal_err(format!(
"Error decrypting variable {}: {}",
variable.path, e
))
})?
crate::error::Error::internal_err(format!(
"Error decrypting variable {}: {}",
variable.path, e
))
})?
} else {
"".to_string()
}
@@ -219,6 +219,7 @@ pub async fn get_reserved_variables(
root_job_id: Option<String>,
scheduled_for: Option<chrono::DateTime<Utc>>,
runnable_id: Option<ScriptHash>,
end_user_email: Option<String>,
) -> Vec<ContextualVariable> {
let state_path = {
let trigger = if schedule_path.is_some() {
@@ -389,6 +390,12 @@ pub async fn get_reserved_variables(
description: "Hash of the script. Useful as cache key for cache that should be runnable specific.".to_string(),
is_custom: false,
},
ContextualVariable {
name: "WM_END_USER_EMAIL".to_string(),
value: end_user_email.unwrap_or_else(|| "".to_string()),
description: "Email of the end user that executed the current script. Only available when triggered from an app.".to_string(),
is_custom: false,
},
].into_iter().chain(custom_envs.into_iter().map(|(name, value)| ContextualVariable {
name,
value,
+1 -1
View File
@@ -395,7 +395,7 @@ fn format_pull_query(peek: String) -> String {
j.timeout, j.flow_step_id, j.cache_ttl, j.priority, j.raw_code, j.raw_lock, j.raw_flow,
j.script_entrypoint_override, j.preprocessed, pj.runnable_path as parent_runnable_path,
COALESCE(p.email, j.permissioned_as_email) as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin,
p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders
p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders, p.end_user_email as permissioned_as_end_user_email
FROM q, j
LEFT JOIN v2_job_status f USING (id)
LEFT JOIN job_perms p ON p.job_id = j.id
+162 -12
View File
@@ -466,6 +466,7 @@ pub async fn push_init_job<'c>(
None,
None,
false,
None,
)
.await?;
inner_tx.commit().await?;
@@ -520,6 +521,7 @@ pub async fn push_periodic_bash_job<'c>(
None,
None,
false,
None,
)
.await?;
inner_tx.commit().await?;
@@ -766,6 +768,11 @@ lazy_static::lazy_static! {
pub static ref MAX_RESULT_SIZE_MB: usize = std::env::var("MAX_RESULT_SIZE_MB").unwrap_or("500".to_string()).parse().unwrap_or(500);
}
#[derive(Deserialize)]
struct OutputWrapper {
output: String,
}
pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
db: &DB,
job_queue_db: &DB,
@@ -845,6 +852,59 @@ pub async fn add_completed_job<T: Serialize + Send + Sync + ValidableJson>(
restart_job_if_perpetual(db, queued_job, &canceled_by).await?;
// Update conversation message if it's a flow and it's done (both success and error cases)
if !skipped && flow_is_done {
let chat_input_enabled = queued_job.parse_chat_input_enabled();
if chat_input_enabled.unwrap_or(false) {
let content = if let Ok(wrapper) = serde_json::from_value::<OutputWrapper>(
serde_json::to_value(result.0).unwrap_or(serde_json::Value::Null),
) {
// Successfully deserialized to OutputWrapper, use the output field
wrapper.output
} else {
// No string output field, use the whole result
serde_json::to_value(result.0)
.ok()
.and_then(|v| {
if let serde_json::Value::String(s) = v {
Some(s)
} else {
serde_json::to_string_pretty(&v).ok()
}
})
.unwrap_or_else(|| {
if success {
"Job completed successfully".to_string()
} else {
"Job failed".to_string()
}
})
};
// check if flow_conversation_message exists
let flow_conversation_message_exists = sqlx::query_scalar!(
"SELECT EXISTS(SELECT 1 FROM flow_conversation_message WHERE job_id = $1 AND message_type = 'assistant')",
queued_job.id
)
.fetch_one(db)
.await?;
if flow_conversation_message_exists.unwrap_or(false) {
// Update the assistant message using direct DB access
let _ = sqlx::query!(
"UPDATE flow_conversation_message
SET content = $1
WHERE job_id = $2
",
content,
queued_job.id,
)
.execute(db)
.await;
}
}
}
// tracing::error!("4 {:?}", start.elapsed());
Ok((queued_job.id, duration))
@@ -1363,6 +1423,7 @@ async fn restart_job_if_perpetual_inner(
queued_job.priority,
None,
false,
None,
)
.await?;
tx.commit().await?;
@@ -1850,6 +1911,7 @@ pub async fn push_error_handler<'a, 'c, T: Serialize + Send + Sync>(
priority,
None,
false,
None,
)
.await?;
tx.commit().await?;
@@ -1905,6 +1967,12 @@ pub struct MiniPulledJob {
pub trigger: Option<String>,
pub trigger_kind: Option<JobTriggerKind>,
pub visible_to_owner: bool,
pub permissioned_as_end_user_email: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct FlowStatusChatInputEnabled {
chat_input_enabled: Option<bool>,
}
impl MiniPulledJob {
@@ -1931,6 +1999,13 @@ impl MiniPulledJob {
.and_then(|v| serde_json::from_str::<FlowStatus>((**v).get()).ok())
}
pub fn parse_chat_input_enabled(&self) -> Option<bool> {
self.flow_status
.as_ref()
.and_then(|v| serde_json::from_str::<FlowStatusChatInputEnabled>((**v).get()).ok())
.and_then(|f| f.chat_input_enabled)
}
pub fn from(job: &QueuedJob) -> MiniPulledJob {
MiniPulledJob {
workspace_id: job.workspace_id.clone(),
@@ -1969,6 +2044,7 @@ impl MiniPulledJob {
None
},
visible_to_owner: job.visible_to_owner.clone(),
permissioned_as_end_user_email: None,
}
}
pub fn is_flow(&self) -> bool {
@@ -2169,7 +2245,8 @@ pub async fn get_mini_pulled_job<'c>(
script_entrypoint_override,
trigger,
trigger_kind as \"trigger_kind: JobTriggerKind\",
visible_to_owner
visible_to_owner,
NULL as permissioned_as_end_user_email
FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
job_id,
)
@@ -2182,6 +2259,37 @@ pub async fn get_mini_pulled_job<'c>(
pub struct PulledJobResult {
pub job: Option<PulledJob>,
pub suspended: bool,
pub missing_concurrency_key: bool,
}
pub enum PulledJobResultToJobErr {
MissingConcurrencyKey(JobCompleted),
}
impl PulledJobResult {
pub fn to_pulled_job(self) -> Result<Option<PulledJob>, PulledJobResultToJobErr> {
match self {
PulledJobResult { job: Some(job), missing_concurrency_key: true, .. } => Err(
PulledJobResultToJobErr::MissingConcurrencyKey(JobCompleted {
preprocessed_args: None,
job: Arc::new(job.job),
success: false,
result: Arc::new(windmill_common::worker::to_raw_value(&json!({
"name": "InternalErr",
"message": "The job has a concurrency limit but concurrency key couldn't be found. This is an unexpected behavior that should never happen. Please report this to support."}
))),
result_columns: None,
mem_peak: 0,
cached_res_path: None,
token: "".to_string(),
canceled_by: None,
duration: None,
has_stream: Some(false),
}),
),
PulledJobResult { job, .. } => Ok(job),
}
}
}
pub async fn pull(
@@ -2201,7 +2309,11 @@ pub async fn pull(
}
if pull_loop_count > 1000 {
tracing::error!("Pull job loop count exceeded 1000, breaking");
return Ok(PulledJobResult { job: None, suspended: false });
return Ok(PulledJobResult {
job: None,
suspended: false,
missing_concurrency_key: false,
});
}
if let Some((query_suspended, query_no_suspend)) = query_o {
let njob = {
@@ -2213,16 +2325,43 @@ pub async fn pull(
.fetch_optional(job_queue_db)
.await?
};
if let Some(job) = job {
PulledJobResult { job: Some(job), suspended: true }
let (job, suspended) = if let Some(job) = job {
(Some(job), true)
} else {
let job = sqlx::query_as::<_, PulledJob>(query_no_suspend)
.bind(worker_name)
.fetch_optional(job_queue_db)
.await?;
PulledJobResult { job, suspended: false }
}
};
(job, false)
};
#[cfg(all(feature = "enterprise", feature = "private"))]
let pulled_job_result = match job {
Some(job) if job.concurrent_limit.is_some() => {
let job = crate::jobs_ee::apply_concurrency_limit(
db,
pull_loop_count,
suspended,
job,
)
.await?;
job.unwrap_or(PulledJobResult {
job: None,
suspended,
missing_concurrency_key: false,
})
}
_ => PulledJobResult { job, suspended, missing_concurrency_key: false },
};
#[cfg(not(all(feature = "enterprise", feature = "private")))]
let pulled_job_result =
PulledJobResult { job, suspended, missing_concurrency_key: false };
Ok::<_, Error>(pulled_job_result)
}?;
if let Some(job) = njob.job.as_ref() {
if job.is_flow() || job.is_dependency() {
let per_workspace = per_workspace_tag(&job.workspace_id).await;
@@ -2258,7 +2397,7 @@ pub async fn pull(
.await?;
let Some(job) = job else {
return Ok(PulledJobResult { job: None, suspended });
return Ok(PulledJobResult { job: None, suspended, missing_concurrency_key: false });
};
let has_concurent_limit = job.concurrent_limit.is_some();
@@ -2281,7 +2420,11 @@ pub async fn pull(
if METRICS_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
QUEUE_PULL_COUNT.inc();
}
return Ok(PulledJobResult { job: Some(pulled_job), suspended });
return Ok(PulledJobResult {
job: Some(pulled_job),
suspended,
missing_concurrency_key: false,
});
}
#[cfg(all(feature = "enterprise", feature = "private"))]
@@ -3062,6 +3205,7 @@ pub async fn push<'c, 'd>(
_priority_override: Option<i16>,
authed: Option<&Authed>,
running: bool, // whether the job is already running: only set this to true if you don't want the job to be picked up by a worker from the queue. It will also set started_at to now.
end_user_email: Option<String>,
) -> Result<(Uuid, Transaction<'c, Postgres>), Error> {
#[cfg(feature = "cloud")]
if *CLOUD_HOSTED {
@@ -3558,6 +3702,8 @@ pub async fn push<'c, 'd>(
user_states,
preprocessor_module: None,
stream_job: None,
chat_input_enabled: None,
memory_id: None,
}
}
_ => {
@@ -3688,6 +3834,7 @@ pub async fn push<'c, 'd>(
early_return: None,
skip_expr: None,
preprocessor_module: None,
chat_input_enabled: None,
};
// this is a new flow being pushed, flow_status is set to flow_value:
let flow_status: FlowStatus = FlowStatus::new(&flow_value);
@@ -3813,6 +3960,8 @@ pub async fn push<'c, 'd>(
user_states,
preprocessor_module: None,
stream_job: None,
chat_input_enabled: None,
memory_id: None,
};
let value = flow_data.value();
let priority = value.priority;
@@ -4164,8 +4313,8 @@ pub async fn push<'c, 'd>(
INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)
),
inserted_job_perms AS (
INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id)
values ($1, $32, $33, $34, $35, $36, $37, $2)
INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email)
values ($1, $32, $33, $34, $35, $36, $37, $2, $41)
ON CONFLICT (job_id) DO UPDATE SET email = $32, username = $33, is_admin = $34, is_operator = $35, folders = $36, groups = $37, workspace_id = $2
)
INSERT INTO v2_job_queue
@@ -4215,6 +4364,7 @@ pub async fn push<'c, 'd>(
root_job,
trigger_kind as Option<JobTriggerKind>,
running,
end_user_email,
)
.execute(if !shard_mode {&mut *tx} else {
println!("Push");
@@ -4681,7 +4831,7 @@ pub async fn get_same_worker_job(
v2_job.raw_flow,
pj.runnable_path as parent_runnable_path,
p.email as permissioned_as_email, p.username as permissioned_as_username, p.is_admin as permissioned_as_is_admin,
p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders
p.is_operator as permissioned_as_is_operator, p.groups as permissioned_as_groups, p.folders as permissioned_as_folders, p.end_user_email as permissioned_as_end_user_email
FROM v2_job_queue
INNER JOIN v2_job ON v2_job.id = v2_job_queue.id
LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id
+16
View File
@@ -22,6 +22,7 @@ use windmill_common::get_latest_flow_version_info_for_path_from_version;
use windmill_common::jobs::check_tag_available_for_workspace_internal;
use windmill_common::jobs::JobPayload;
use windmill_common::schedule::schedule_to_user;
use windmill_common::utils::WarnAfterExt;
use windmill_common::FlowVersionInfo;
use windmill_common::DB;
use windmill_common::{
@@ -76,6 +77,7 @@ pub async fn push_scheduled_job<'c>(
&schedule.path
)
.execute(&mut *tx)
.warn_after_seconds_with_sql(1, "update_schedule_paused_until".to_string())
.await
.context("Failed to clear paused_until for schedule")?;
}
@@ -107,6 +109,7 @@ pub async fn push_scheduled_job<'c>(
&schedule.script_path
)
.fetch_one(&mut *tx)
.warn_after_seconds_with_sql(1, "already_exists_job".to_string())
.await?
.unwrap_or(false);
@@ -141,6 +144,7 @@ pub async fn push_scheduled_job<'c>(
&schedule.script_path,
false,
)
.warn_after_seconds_with_sql(1, "get_latest_flow_version_id_for_path".to_string())
.await?;
let FlowVersionInfo {
@@ -151,6 +155,10 @@ pub async fn push_scheduled_job<'c>(
&schedule.workspace_id,
&schedule.script_path,
)
.warn_after_seconds_with_sql(
1,
"get_latest_flow_version_info_for_path_from_version".to_string(),
)
.await?;
(
JobPayload::Flow {
@@ -184,6 +192,7 @@ pub async fn push_scheduled_job<'c>(
&schedule.script_path,
false,
)
.warn_after_seconds_with_sql(1, "get_latest_hash_for_path".to_string())
.await?;
if schedule.retry.is_some() {
@@ -257,6 +266,7 @@ pub async fn push_scheduled_job<'c>(
&schedule.path
)
.execute(&mut *tx)
.warn_after_seconds_with_sql(1, "clear_schedule_error".to_string())
.await
{
tracing::error!(
@@ -272,10 +282,12 @@ pub async fn push_scheduled_job<'c>(
let is_windmill_user =
sqlx::query_scalar!("SELECT CURRENT_USER = 'windmill_user' as \"is_windmill_user!\"")
.fetch_one(&mut *tx)
.warn_after_seconds_with_sql(1, "is_windmill_user".to_string())
.await?;
if is_windmill_user {
sqlx::query!("SET LOCAL ROLE NONE")
.execute(&mut *tx)
.warn_after_seconds_with_sql(1, "set_local_role_none".to_string())
.await?;
}
(
@@ -301,6 +313,7 @@ pub async fn push_scheduled_job<'c>(
email,
None, // no token for schedules so no scopes so no scope_tags
)
.warn_after_seconds_with_sql(1, "check_tag_available_for_workspace_internal".to_string())
.await?;
}
@@ -331,12 +344,15 @@ pub async fn push_scheduled_job<'c>(
None,
push_authed,
false,
None,
)
.warn_after_seconds_with_sql(1, "push in push_scheduled_job".to_string())
.await?;
if revert_to_windmill_user {
sqlx::query!("SET LOCAL ROLE windmill_user")
.execute(&mut *tx)
.warn_after_seconds_with_sql(1, "set_local_role_windmill_user".to_string())
.await?;
}
+1
View File
@@ -117,6 +117,7 @@ pub struct AIAgentArgs {
pub output_type: Option<OutputType>,
pub user_images: Option<Vec<S3Object>>,
pub streaming: Option<bool>,
pub messages_context_length: Option<usize>,
}
#[derive(Deserialize, Debug)]
+151 -24
View File
@@ -1,3 +1,4 @@
use crate::memory_oss::{read_from_memory, write_to_memory};
use anyhow::Context;
use async_recursion::async_recursion;
use regex::Regex;
@@ -98,6 +99,20 @@ pub async fn get_flow_job_runnable_and_raw_flow(
Ok(job)
}
/// Get memory_id from parent flow's flow_status
async fn get_memory_id_from_flow_status(db: &DB, parent_job: &Uuid) -> Result<Option<Uuid>, Error> {
let result = sqlx::query_scalar!(
"SELECT (flow_status->>'memory_id')::uuid as memory_id
FROM v2_job_status
WHERE id = $1",
parent_job
)
.fetch_optional(db)
.await?;
Ok(result.flatten())
}
pub async fn handle_ai_agent_job(
// connection
conn: &Connection,
@@ -310,7 +325,7 @@ pub async fn handle_ai_agent_job(
Ok(result)
}
/// Find a unique tool name to avoid collisions with user-provided tools
/// Find a unique tool name for structured output tool to avoid collisions with user-provided tools
fn find_unique_tool_name(base_name: &str, existing_tools: Option<&[ToolDef]>) -> String {
let Some(tools) = existing_tools else {
return base_name.to_string();
@@ -445,6 +460,39 @@ pub async fn run_agent(
vec![]
};
// Load previous messages from memory for text output mode (only if context length is set)
if matches!(output_type, OutputType::Text) {
if let Some(context_length) = args.messages_context_length.filter(|&n| n > 0) {
if let Some(step_id) = job.flow_step_id.as_deref() {
// Get memory_id from flow_status
if let Ok(Some(memory_id)) = get_memory_id_from_flow_status(db, parent_job).await {
// Read messages from memory
match read_from_memory(&job.workspace_id, memory_id, step_id).await {
Ok(Some(loaded_messages)) => {
// Take the last n messages
let start_idx = loaded_messages.len().saturating_sub(context_length);
let mut messages_to_load = loaded_messages[start_idx..].to_vec();
// Remove the first message if its role is "tool" to avoid OpenAI API error
// "messages with role 'tool' must be a response to a preceeding message with 'tool_calls'"
if let Some(first_msg) = messages_to_load.first() {
if first_msg.role == "tool" {
messages_to_load.remove(0);
}
}
messages.extend(messages_to_load);
}
Ok(None) => {}
Err(e) => {
tracing::error!("Failed to read memory for step {}: {}", step_id, e);
}
}
}
}
}
}
// Create user message with optional images
let mut parts = vec![ContentPart::Text { text: args.user_message.clone() }];
if let Some(images) = &args.user_images {
@@ -808,6 +856,7 @@ pub async fn run_agent(
job_priority,
job_perms.as_ref(),
true,
None,
)
.await?;
@@ -835,30 +884,69 @@ pub async fn run_agent(
#[cfg(feature = "benchmark")]
let mut bench = windmill_common::bench::BenchmarkIter::new();
match handle_queued_job(
tool_job.clone(),
None,
None,
None,
None,
conn,
Some(job_queue_db),
client,
hostname,
worker_name,
worker_dir,
&job_dir,
None,
base_internal_url,
inner_job_completed_tx,
occupancy_metrics,
killpill_rx,
None,
// Spawn handle_queued_job on separate task to prevent tokio stack overflow
// Clone everything needed for the spawned task
let tool_job_spawn = tool_job.clone();
let conn_spawn = conn.clone();
let client_spawn = client.clone();
let hostname_spawn = hostname.to_string();
let worker_name_spawn = worker_name.to_string();
let worker_dir_spawn = worker_dir.to_string();
let job_dir_spawn = job_dir.clone();
let base_internal_url_spawn = base_internal_url.to_string();
let inner_job_completed_tx_spawn = inner_job_completed_tx.clone();
let mut occupancy_metrics_spawn = occupancy_metrics.clone();
let mut killpill_rx_spawn = killpill_rx.resubscribe();
// Spawn on separate tokio task with fresh stack
let join_handle = tokio::task::spawn(async move {
#[cfg(feature = "benchmark")]
&mut bench,
)
.await
{
let mut bench_spawn =
windmill_common::bench::BenchmarkIter::new();
let result = handle_queued_job(
tool_job_spawn,
None,
None,
None,
None,
&conn_spawn,
Some(job_queue_db),
&client_spawn,
&hostname_spawn,
&worker_name_spawn,
&worker_dir_spawn,
&job_dir_spawn,
None,
&base_internal_url_spawn,
inner_job_completed_tx_spawn,
&mut occupancy_metrics_spawn,
&mut killpill_rx_spawn,
None,
#[cfg(feature = "benchmark")]
&mut bench_spawn,
)
.await;
// Return both result and updated metrics
(result, occupancy_metrics_spawn)
});
// Await the spawned task
let (handle_result, updated_occupancy) =
join_handle.await.map_err(|e| {
Error::internal_err(format!(
"Tool execution task panicked: {}",
e
))
})?;
// Merge occupancy metrics back
occupancy_metrics.total_duration_of_running_jobs =
updated_occupancy.total_duration_of_running_jobs;
// Continue with match on handle_result
match handle_result {
Err(err) => {
let err_string =
format!("{}: {}", err.name(), err.to_string());
@@ -1040,6 +1128,45 @@ pub async fn run_agent(
}
}
// Persist complete conversation to memory at the end (only if context length is set)
// final_messages contains the complete history (old messages + new ones)
if matches!(output_type, OutputType::Text) {
if let Some(context_length) = args.messages_context_length.filter(|&n| n > 0) {
if let Some(step_id) = job.flow_step_id.as_deref() {
// Extract OpenAIMessages from final_messages
let all_messages: Vec<OpenAIMessage> =
final_messages.iter().map(|m| m.message.clone()).collect();
if !all_messages.is_empty() {
// Keep only the last n messages
let start_idx = all_messages.len().saturating_sub(context_length);
let messages_to_persist = all_messages[start_idx..].to_vec();
// Get memory_id from flow_status
if let Ok(Some(memory_id)) =
get_memory_id_from_flow_status(db, parent_job).await
{
if let Err(e) = write_to_memory(
&job.workspace_id,
memory_id,
step_id,
&messages_to_persist,
)
.await
{
tracing::error!(
"Failed to persist {} messages to memory for step {}: {}",
messages_to_persist.len(),
step_id,
e
);
}
}
}
}
}
}
Ok(to_raw_value(&AIAgentResult {
output: output_value,
messages: final_messages,
+6 -3
View File
@@ -25,7 +25,11 @@ use crate::{
DISABLE_NSJAIL, DISABLE_NUSER, HOME_ENV, NODE_BIN_PATH, NODE_PATH, NPM_CONFIG_REGISTRY,
NPM_PATH, NSJAIL_PATH, PATH_ENV, PROXY_ENVS, TZ_ENV,
};
use windmill_common::{client::AuthedClient, s3_helpers::BundleFormat, scripts::{id_to_codebase_info, CodebaseInfo}};
use windmill_common::{
client::AuthedClient,
s3_helpers::BundleFormat,
scripts::{id_to_codebase_info, CodebaseInfo},
};
#[cfg(windows)]
use crate::SYSTEM_ROOT;
@@ -910,7 +914,6 @@ pub async fn handle_bun_job(
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(Some(&base_internal_url)).await;
let main_override = job.script_entrypoint_override.as_deref();
let apply_preprocessor =
job.flow_step_id.as_deref() != Some("preprocessor") && job.preprocessed == Some(false);
@@ -1621,11 +1624,11 @@ pub async fn start_worker(
None,
None,
None,
None,
)
.await;
let context_envs = build_envs_map(context.to_vec()).await;
let mut format = BundleFormat::Cjs;
if let Some(codebase) = codebase.as_ref() {
let pulled_codebase = pull_codebase(w_id, codebase, job_dir).await?;
+1
View File
@@ -452,6 +452,7 @@ pub async fn get_reserved_variables(
Some(get_root_job_id(job).to_string()),
Some(job.scheduled_for.clone()),
job.runnable_id,
job.permissioned_as_end_user_email.clone(),
)
.await
.to_vec();
@@ -556,6 +556,7 @@ pub async fn start_worker(
None,
None,
None,
None,
)
.await;
let context_envs = build_envs_map(context.to_vec()).await;
+17 -3
View File
@@ -10,7 +10,7 @@ use serde_json::value::RawValue;
use serde_json::{json, Value};
use uuid::Uuid;
use windmill_common::error::{to_anyhow, Error, Result};
use windmill_common::s3_helpers::S3Object;
use windmill_common::s3_helpers::{S3Object, S3_PROXY_LAST_ERRORS_CACHE};
use windmill_common::utils::sanitize_string_from_password;
use windmill_common::worker::Connection;
use windmill_common::workspaces::{get_ducklake_from_db_unchecked, DucklakeCatalogResourceType};
@@ -128,7 +128,7 @@ pub async fn do_duckdb(
let base_internal_url = client.base_internal_url.clone();
let w_id = job.workspace_id.clone();
let (result, column_order) = tokio::task::spawn_blocking(move || {
let result = tokio::task::spawn_blocking(move || {
run_duckdb_ffi_safe(
query_block_list.iter().map(String::as_str),
query_block_list.len(),
@@ -139,7 +139,21 @@ pub async fn do_duckdb(
)
})
.await
.map_err(to_anyhow)??;
.map_err(|e| Error::from(to_anyhow(e)))
.and_then(|r| r);
let (result, column_order) = match result {
Ok(r) => r,
Err(e) => {
if let Some(s3_proxy_err) = S3_PROXY_LAST_ERRORS_CACHE.get(&client.token) {
return Err(Error::ExecutionErr(format!(
"{}\n\nS3 Related Error: {}",
e.to_string(),
s3_proxy_err,
)));
}
return Err(e);
}
};
drop(bigquery_credentials);
+4
View File
@@ -36,6 +36,10 @@ pub mod job_logger;
pub mod job_logger_ee;
mod job_logger_oss;
mod js_eval;
pub mod memory_common;
#[cfg(feature = "private")]
pub mod memory_ee;
pub mod memory_oss;
#[cfg(feature = "mysql")]
mod mysql_executor;
#[cfg(feature = "nu")]
@@ -0,0 +1,74 @@
use crate::ai::types::OpenAIMessage;
use std::path::PathBuf;
use tokio::{fs, io::AsyncWriteExt};
use uuid::Uuid;
use windmill_common::worker::TMP_LOGS_DIR;
/// Get the file path for storing memory for a specific AI agent step
pub fn path_for(workspace_id: &str, conversation_id: Uuid, step_id: &str) -> PathBuf {
PathBuf::from(TMP_LOGS_DIR)
.join("memory")
.join(workspace_id)
.join(conversation_id.to_string())
.join(format!("{step_id}.json"))
}
/// Read messages from disk storage
pub async fn read_from_disk(
workspace_id: &str,
conversation_id: Uuid,
step_id: &str,
) -> anyhow::Result<Option<Vec<OpenAIMessage>>> {
let path = path_for(workspace_id, conversation_id, step_id);
if !fs::try_exists(&path).await? {
return Ok(None);
}
let bytes = fs::read(&path).await?;
let messages: Vec<OpenAIMessage> = serde_json::from_slice(&bytes)?;
Ok(Some(messages))
}
/// Write messages to disk storage
pub async fn write_to_disk(
workspace_id: &str,
conversation_id: Uuid,
step_id: &str,
messages: &[OpenAIMessage],
) -> anyhow::Result<()> {
let path = path_for(workspace_id, conversation_id, step_id);
// Ensure parent directories exist
if let Some(dir) = path.parent() {
fs::create_dir_all(dir).await?;
}
// Write atomically using a temporary file
let tmp = path.with_extension("json.tmp");
let mut f = fs::File::create(&tmp).await?;
f.write_all(&serde_json::to_vec(messages)?).await?;
f.flush().await?;
drop(f);
// Atomic rename
fs::rename(tmp, &path).await?;
Ok(())
}
/// Delete all memory for a conversation from disk storage
pub async fn delete_conversation_from_disk(
workspace_id: &str,
conversation_id: Uuid,
) -> anyhow::Result<()> {
let conversation_path = PathBuf::from(TMP_LOGS_DIR)
.join("memory")
.join(workspace_id)
.join(conversation_id.to_string());
if fs::try_exists(&conversation_path).await? {
fs::remove_dir_all(&conversation_path).await?;
}
Ok(())
}
+43
View File
@@ -0,0 +1,43 @@
#[cfg(all(feature = "private", feature = "enterprise"))]
#[allow(unused)]
pub use crate::memory_ee::*;
#[cfg(not(all(feature = "private", feature = "enterprise")))]
use {crate::ai::types::OpenAIMessage, crate::memory_common, uuid::Uuid};
/// Read AI agent memory from storage
/// In OSS: always reads from disk
#[cfg(not(all(feature = "private", feature = "enterprise")))]
pub async fn read_from_memory(
workspace_id: &str,
conversation_id: Uuid,
step_id: &str,
) -> anyhow::Result<Option<Vec<OpenAIMessage>>> {
memory_common::read_from_disk(workspace_id, conversation_id, step_id).await
}
/// Write AI agent memory to storage
/// In OSS: always writes to disk
#[cfg(not(all(feature = "private", feature = "enterprise")))]
pub async fn write_to_memory(
workspace_id: &str,
conversation_id: Uuid,
step_id: &str,
messages: &[OpenAIMessage],
) -> anyhow::Result<()> {
if messages.is_empty() {
return Ok(());
}
memory_common::write_to_disk(workspace_id, conversation_id, step_id, messages).await
}
/// Delete all memory for a conversation from storage
/// In OSS: always deletes from disk
#[cfg(not(all(feature = "private", feature = "enterprise")))]
pub async fn delete_conversation_memory(
workspace_id: &str,
conversation_id: Uuid,
) -> anyhow::Result<()> {
memory_common::delete_conversation_from_disk(workspace_id, conversation_id).await
}
+1 -1
View File
@@ -88,7 +88,7 @@ fn do_postgresql_inner<'a>(
let arg_t = arg
.otyp
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Missing otzyp for pg arg"))?;
.ok_or_else(|| anyhow::anyhow!("Missing otyp for pg arg"))?;
let typ = &arg.typ;
let param = convert_val(value, arg_t, typ)?;
query_params.push(param);
@@ -2173,6 +2173,7 @@ pub async fn start_worker(
None,
None,
None,
None,
)
.await
.to_vec();
@@ -2294,6 +2295,7 @@ for line in sys.stdin:
None,
None,
None,
None,
)
.await;
+38 -11
View File
@@ -59,6 +59,7 @@ use std::{
time::Duration,
};
use windmill_parser::MainArgSignature;
use windmill_queue::PulledJobResultToJobErr;
use uuid::Uuid;
@@ -161,7 +162,7 @@ use crate::mysql_executor::do_mysql;
#[cfg(feature = "duckdb")]
use crate::duckdb_executor::do_duckdb;
#[cfg(feature = "oracledb")]
#[cfg(all(feature = "enterprise", feature = "oracledb"))]
use crate::oracledb_executor::do_oracledb;
#[cfg(feature = "enterprise")]
@@ -827,7 +828,18 @@ pub fn start_interactive_worker_shell(
)
.await;
job.map(|x| x.job.map(NextJob::Sql))
match job {
Ok(j) => match j.to_pulled_job() {
Ok(j) => Ok(j.map(NextJob::Sql)),
Err(PulledJobResultToJobErr::MissingConcurrencyKey(jc)) => {
if let Err(err) = job_completed_tx.send_job(jc, true).await {
tracing::error!("An error occurred while sending job completed (missing concurrency key): {:#?}", err)
}
Ok(None)
}
},
Err(err) => Err(err),
}
}
Connection::Http(client) => {
crate::agent_workers::pull_job(&client, None, Some(true))
@@ -1646,7 +1658,18 @@ pub async fn run_worker(
}
}
}
job.map(|x| x.job.map(NextJob::Sql))
match job {
Ok(pulled_job_result) => match pulled_job_result.to_pulled_job() {
Ok(j) => Ok(j.map(NextJob::Sql)),
Err(PulledJobResultToJobErr::MissingConcurrencyKey(jc)) => {
if let Err(err) = job_completed_tx.send_job(jc, true).await {
tracing::error!("An error occurred while sending job completed (missing concurrency key): {:#?}", err)
}
Ok(None)
}
},
Err(err) => Err(err),
}
}
Connection::Http(client) => crate::agent_workers::pull_job(&client, None, None)
.await
@@ -2388,14 +2411,16 @@ pub async fn handle_queued_job(
| JobKind::Flow
| JobKind::FlowDependencies,
x,
) => if x.map(|x| x.0).is_none_or(|x| is_special_codebase_hash(x)) {
Some(
cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow.clone())
.await?,
)
} else {
) => {
if x.map(|x| x.0).is_none_or(|x| is_special_codebase_hash(x)) {
Some(
cache::job::fetch_preview(conn, &job.id, raw_lock, raw_code, raw_flow.clone())
.await?,
)
} else {
None
},
}
}
_ => None,
};
@@ -2889,7 +2914,9 @@ async fn handle_code_execution_job(
ScriptMetadata { language, envs, codebase, schema_validator, schema },
) = match job.kind {
JobKind::Preview => {
let codebase = job.runnable_id.and_then(|x| hash_to_codebase_id(&job.id.to_string(), x.0));
let codebase = job
.runnable_id
.and_then(|x| hash_to_codebase_id(&job.id.to_string(), x.0));
if codebase.is_none() && job.runnable_id.is_some() {
(arc_data, arc_metadata) =
cache::script::fetch(conn, job.runnable_id.unwrap()).await?;
+3 -2
View File
@@ -1246,14 +1246,14 @@ pub async fn update_flow_status_after_job_completion_internal(
if let Some(t) = tag {
tag = Some(interpolate_args(t, &args, &flow_job.workspace_id));
}
} else if let Some(ck) = concurrency_key {
} else if concurrent_limit.is_some() {
let mut tx = db.begin().await?;
insert_concurrency_key(
&flow_job.workspace_id,
&PushArgs::from(&HashMap::new()),
&flow_job.runnable_path,
JobKind::Flow,
Some(ck),
concurrency_key,
&mut tx,
flow,
)
@@ -3170,6 +3170,7 @@ async fn push_next_flow_job(
new_job_priority_override,
job_perms.as_ref(),
false,
None,
)
.warn_after_seconds(2)
.await?;
@@ -833,6 +833,7 @@ pub async fn trigger_dependents_to_recompute_dependencies(
None,
None,
false,
None,
)
.await?;
tracing::info!(
+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.554.0";
export const VERSION = "v1.555.1";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+5 -1
View File
@@ -16,4 +16,8 @@ set -e
echo "Running dnt..."
deno run -A dnt.ts
echo "Build complete!"
# Remove .ts extensions after building to go back to the original state
./windmill-utils-internal/remove-ts-ext.sh
echo "Build complete!"
+1 -1
View File
@@ -68,7 +68,7 @@ export {
// }
// });
export const VERSION = "1.554.0";
export const VERSION = "1.555.1";
export const WM_FORK_PREFIX = "wm-fork";
@@ -1,4 +1,4 @@
import { newPathAssigner } from "../path-utils/path-assigner";
import { newPathAssigner, PathAssigner } from "../path-utils/path-assigner";
import { FlowModule } from "../gen/types.gen";
/**
@@ -17,20 +17,25 @@ interface InlineScript {
*
* @param modules - Array of flow modules to process
* @param mapping - Optional mapping of module IDs to custom file paths
* @param separator - Path separator to use
* @param defaultTs - Default TypeScript runtime to use ("bun" or "deno")
* @param pathAssigner - Optional path assigner to reuse (for nested calls)
* @returns Array of inline scripts with their paths and content
*/
export function extractInlineScripts(
modules: FlowModule[],
mapping: Record<string, string> = {},
separator: string = "/",
defaultTs?: "bun" | "deno"
defaultTs?: "bun" | "deno",
pathAssigner?: PathAssigner
): InlineScript[] {
const pathAssigner = newPathAssigner(defaultTs ?? "bun");
// Create pathAssigner only if not provided (top-level call), but reuse it for nested calls
const assigner = pathAssigner ?? newPathAssigner(defaultTs ?? "bun");
return modules.flatMap((m) => {
if (m.value.type == "rawscript") {
let basePath, ext;
[basePath, ext] = pathAssigner.assignPath(m.summary, m.value.language);
[basePath, ext] = assigner.assignPath(m.summary, m.value.language);
const path = mapping[m.id] ?? basePath + ext;
const content = m.value.content;
const r = [{ path: path, content: content }];
@@ -47,25 +52,39 @@ export function extractInlineScripts(
m.value.modules,
mapping,
separator,
defaultTs
defaultTs,
assigner
);
} else if (m.value.type == "branchall") {
return m.value.branches.flatMap((b) =>
extractInlineScripts(b.modules, mapping, separator, defaultTs)
extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner)
);
} else if (m.value.type == "whileloopflow") {
return extractInlineScripts(
m.value.modules,
mapping,
separator,
defaultTs
defaultTs,
assigner
);
} else if (m.value.type == "branchone") {
return [
...m.value.branches.flatMap((b) =>
extractInlineScripts(b.modules, mapping, separator, defaultTs)
extractInlineScripts(
b.modules,
mapping,
separator,
defaultTs,
assigner
)
),
...extractInlineScripts(
m.value.default,
mapping,
separator,
defaultTs,
assigner
),
...extractInlineScripts(m.value.default, mapping, separator, defaultTs),
];
} else {
return [];
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.554.0",
"version": "1.555.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.554.0",
"version": "1.555.1",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.554.0",
"version": "1.555.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",
@@ -349,8 +349,8 @@
"default": "./package/gen/index.js"
},
"./components/flows/flowStore": {
"types": "./package/components/flows/flowStore.d.ts",
"default": "./package/components/flows/flowStore.js"
"types": "./package/components/flows/flowStore.svelte.d.ts",
"default": "./package/components/flows/flowStore.svelte.js"
},
"./components/icons": {
"types": "./package/components/icons/index.d.ts",
@@ -501,7 +501,7 @@
"./package/gen/index.d.ts"
],
"components/flows/flowStore": [
"./package/components/flows/flowStore.d.ts"
"./package/components/flows/flowStore.svelte.d.ts"
],
"components/icons": [
"./package/components/icons/index.d.ts"
@@ -542,4 +542,4 @@
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
"fsevents": "^2.3.3"
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ console.log('Running postinstall for root project');
import { x } from 'tar'
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-d44b577.tar.gz'
const tarUrl = 'https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev/ui_builder-a3f259c.tar.gz'
const outputTarPath = path.join(process.cwd(), 'ui_builder.tar.gz')
const extractTo = path.join(process.cwd(), 'static/ui_builder/')
+92
View File
@@ -0,0 +1,92 @@
import { writable, get } from 'svelte/store'
import { workspaceAIClients } from './components/copilot/lib'
import { type AIProviderModel, type AIProvider, WorkspaceService, type AIConfig } from './gen'
import { COPILOT_SESSION_MODEL_SETTING_NAME, COPILOT_SESSION_PROVIDER_SETTING_NAME } from './stores'
import { getLocalSetting } from './utils'
const sessionModel = getLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME)
const sessionProvider = getLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME)
export const copilotSessionModel = writable<AIProviderModel | undefined>(
sessionModel && sessionProvider
? {
model: sessionModel,
provider: sessionProvider as AIProvider
}
: undefined
)
export const copilotInfo = writable<{
enabled: boolean
codeCompletionModel?: AIProviderModel
defaultModel?: AIProviderModel
aiModels: AIProviderModel[]
customPrompts?: Record<string, string>
maxTokensPerModel?: Record<string, number>
}>({
enabled: false,
codeCompletionModel: undefined,
defaultModel: undefined,
aiModels: [],
customPrompts: {},
maxTokensPerModel: {}
})
export async function loadCopilot(workspace: string) {
workspaceAIClients.init(workspace)
try {
const info = await WorkspaceService.getCopilotInfo({ workspace })
setCopilotInfo(info)
} catch (err) {
setCopilotInfo({})
console.error('Could not get copilot info', err)
}
}
export function setCopilotInfo(aiConfig: AIConfig) {
if (Object.keys(aiConfig.providers ?? {}).length > 0) {
const aiModels = Object.entries(aiConfig.providers ?? {}).flatMap(
([provider, providerConfig]) =>
providerConfig.models.map((m) => ({ model: m, provider: provider as AIProvider }))
)
copilotSessionModel.update((model) => {
if (
model &&
!aiModels.some((m) => m.model === model.model && m.provider === model.provider)
) {
return undefined
}
return model
})
copilotInfo.set({
enabled: true,
codeCompletionModel: aiConfig.code_completion_model,
defaultModel: aiConfig.default_model,
aiModels: aiModels,
customPrompts: aiConfig.custom_prompts ?? {},
maxTokensPerModel: aiConfig.max_tokens_per_model ?? {}
})
} else {
copilotSessionModel.set(undefined)
copilotInfo.set({
enabled: false,
codeCompletionModel: undefined,
defaultModel: undefined,
aiModels: [],
customPrompts: {},
maxTokensPerModel: {}
})
}
}
export function getCurrentModel() {
const model =
get(copilotSessionModel) ?? get(copilotInfo).defaultModel ?? get(copilotInfo).aiModels[0]
if (!model) {
throw new Error('No model selected')
}
return model
}
+1
View File
@@ -49,6 +49,7 @@ export interface SchemaProperty {
placeholder?: string
oneOf?: SchemaProperty[]
originalType?: string
disabled?: boolean
}
export interface ModalSchemaProperty {
+2 -1
View File
@@ -16,7 +16,7 @@
type TriggersCount
} from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { setCopilotInfo, userStore, workspaceStore } from '$lib/stores'
import { userStore, workspaceStore } from '$lib/stores'
import { emptySchema, readFieldsRecursively, sendUserToast, type StateStore } from '$lib/utils'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { onDestroy, onMount, setContext, untrack } from 'svelte'
@@ -51,6 +51,7 @@
import { StepsInputArgs } from './flows/stepsInputArgs.svelte'
import { ModulesTestStates } from './modulesTest.svelte'
import type { GraphModuleState } from './graph'
import { setCopilotInfo } from '$lib/aiStore'
let {
initial = undefined
+1 -1
View File
@@ -30,7 +30,6 @@
import {
dbSchemas,
type DBSchema,
copilotInfo,
codeCompletionSessionEnabled,
lspTokenStore,
formatOnSave,
@@ -91,6 +90,7 @@
import { getDbSchemas } from './apps/components/display/dbtable/utils'
import { PYTHON_PREPROCESSOR_MODULE_CODE, TS_PREPROCESSOR_MODULE_CODE } from '$lib/script_helpers'
import { setMonacoTypescriptOptions } from './monacoLanguagesOptions'
import { copilotInfo } from '$lib/aiStore'
// import EditorTheme from './EditorTheme.svelte'
let divEl: HTMLDivElement | null = $state(null)
@@ -38,6 +38,7 @@
import { getStepHistoryLoaderContext } from './stepHistoryLoader.svelte'
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
import FlowChatInterface from './flows/FlowChatInterface.svelte'
interface Props {
previewMode: 'upTo' | 'whole'
@@ -93,9 +94,9 @@
let suspendStatus: StateStore<Record<string, { job: Job; nb: number }>> = $state({ val: {} })
let isOwner: boolean = $state(false)
export function test() {
export async function test(): Promise<string | undefined> {
renderCount++
runPreview(previewArgs.val, undefined)
return await runPreview(previewArgs.val, undefined)
}
const {
@@ -139,6 +140,7 @@
args: Record<string, any>,
restartedFrom: RestartedFrom | undefined
) {
let newJobId: string | undefined = undefined
if (stepHistoryLoader?.flowJobInitial !== false) {
stepHistoryLoader?.setFlowJobInitial(false)
}
@@ -146,7 +148,8 @@
lastPreviewFlow = JSON.stringify(flowStore.val)
flowProgressBar?.reset()
const newFlow = extractFlow(previewMode)
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
newJobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
jobId = newJobId
isRunning = true
if (inputSelected) {
savedArgs = $state.snapshot(previewArgs.val)
@@ -159,6 +162,7 @@
jobId = undefined
}
schemaFormWithArgPicker?.refreshHistory()
return newJobId
}
function onKeyDown(event: KeyboardEvent) {
@@ -414,25 +418,27 @@
</Popover>
{/if}
{/if}
<Button
variant="contained"
startIcon={{ icon: isRunning ? RefreshCw : Play }}
color="dark"
size="sm"
btnClasses="w-full max-w-lg"
on:click={() => runPreview(previewArgs.val, undefined)}
id="flow-editor-test-flow-drawer"
shortCut={{ Icon: CornerDownLeft }}
>
{#if previewMode == 'upTo'}
Test up to
<Badge baseClass="ml-1" color="indigo">
{$selectedId}
</Badge>
{:else}
Test flow
{/if}
</Button>
{#if !flowStore.val.value?.chat_input_enabled}
<Button
variant="contained"
startIcon={{ icon: isRunning ? RefreshCw : Play }}
color="dark"
size="sm"
btnClasses="w-full max-w-lg"
on:click={() => runPreview(previewArgs.val, undefined)}
id="flow-editor-test-flow-drawer"
shortCut={{ Icon: CornerDownLeft }}
>
{#if previewMode == 'upTo'}
Test up to
<Badge baseClass="ml-1" color="indigo">
{$selectedId}
</Badge>
{:else}
Test flow
{/if}
</Button>
{/if}
</div>
{/if}
</div>
@@ -457,81 +463,96 @@
onscroll={(e) => handleScroll()}
>
{#if render}
<div class="border-b">
<SchemaFormWithArgPicker
bind:this={schemaFormWithArgPicker}
runnableId={$initialPathStore}
stablePathForCaptures={$initialPathStore || fakeInitialPath}
runnableType={'FlowPath'}
previewArgs={previewArgs.val}
on:openTriggers
on:select={(e) => {
selectInput(e.detail.payload, e.detail?.type)
}}
{isValid}
{jsonView}
>
<div class="w-full flex flex-row justify-between">
<InputSelectedBadge
onReject={() => schemaFormWithArgPicker?.resetSelected()}
{inputSelected}
/>
<div class="flex flex-row gap-2">
<Toggle
bind:checked={jsonView}
label="JSON View"
size="xs"
options={{
right: 'JSON',
rightTooltip: 'Fill args from JSON'
}}
lightMode
on:change={(e) => {
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
refresh()
}}
{#if flowStore.val.value?.chat_input_enabled}
<div class="flex flex-row justify-center w-full">
<FlowChatInterface
onRunFlow={async (userMessage, _conversationId) => {
await runPreview({ user_message: userMessage }, undefined)
return jobId ?? ''
}}
createConversation={async () => {
const newConversationId = crypto.randomUUID()
return newConversationId
}}
/>
</div>
{:else}
<div class="border-b">
<SchemaFormWithArgPicker
bind:this={schemaFormWithArgPicker}
runnableId={$initialPathStore}
stablePathForCaptures={$initialPathStore || fakeInitialPath}
runnableType={'FlowPath'}
previewArgs={previewArgs.val}
on:openTriggers
on:select={(e) => {
selectInput(e.detail.payload, e.detail?.type)
}}
{isValid}
{jsonView}
>
<div class="w-full flex flex-row justify-between">
<InputSelectedBadge
onReject={() => schemaFormWithArgPicker?.resetSelected()}
{inputSelected}
/>
</div>
</div>
{#if jsonView}
<div class="py-2" style="height: {Math.max(schemaHeight, 100)}px" data-schema-picker>
<JsonInputs
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
previewArgs.val = e.detail
}
}}
updateOnBlur={false}
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}`}
/>
</div>
{:else}
{#key renderCount}
<div bind:clientHeight={schemaHeight} class="min-h-[40vh]">
<SchemaForm
noVariablePicker
compact
schema={flowStore.val.schema}
bind:args={previewArgs.val}
on:change={() => {
savedArgs = $state.snapshot(previewArgs.val)
<div class="flex flex-row gap-2">
<Toggle
bind:checked={jsonView}
label="JSON View"
size="xs"
options={{
right: 'JSON',
rightTooltip: 'Fill args from JSON'
}}
lightMode
on:change={(e) => {
jsonEditor?.setCode(JSON.stringify(previewArgs.val ?? {}, null, '\t'))
refresh()
}}
bind:isValid
helperScript={flowStore.val.schema?.['x-windmill-dyn-select-code'] &&
flowStore.val.schema?.['x-windmill-dyn-select-lang']
? {
source: 'inline',
code: flowStore.val.schema['x-windmill-dyn-select-code'] as string,
lang: flowStore.val.schema['x-windmill-dyn-select-lang'] as ScriptLang
}
: undefined}
/>
</div>
{/key}
{/if}
</SchemaFormWithArgPicker>
</div>
</div>
{#if jsonView}
<div class="py-2" style="height: {Math.max(schemaHeight, 100)}px" data-schema-picker>
<JsonInputs
bind:this={jsonEditor}
on:select={(e) => {
if (e.detail) {
previewArgs.val = e.detail
}
}}
updateOnBlur={false}
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/>&nbsp;&nbsp;"foo": "12"<br/>}`}
/>
</div>
{:else}
{#key renderCount}
<div bind:clientHeight={schemaHeight} class="min-h-[40vh]">
<SchemaForm
noVariablePicker
compact
schema={flowStore.val.schema}
bind:args={previewArgs.val}
on:change={() => {
savedArgs = $state.snapshot(previewArgs.val)
}}
bind:isValid
helperScript={flowStore.val.schema?.['x-windmill-dyn-select-code'] &&
flowStore.val.schema?.['x-windmill-dyn-select-lang']
? {
source: 'inline',
code: flowStore.val.schema['x-windmill-dyn-select-code'] as string,
lang: flowStore.val.schema['x-windmill-dyn-select-lang'] as ScriptLang
}
: undefined}
/>
</div>
{/key}
{/if}
</SchemaFormWithArgPicker>
</div>
{/if}
{/if}
<div class="pt-4 flex flex-col grow relative">
<div
@@ -67,7 +67,7 @@
let globalRefreshes: Record<string, ((clear, root) => Promise<void>)[]> = $state({})
setContext<FlowStatusViewerContext>('FlowStatusViewer', {
flowState: flowState,
flowState,
suspendStatus,
retryStatus,
hideDownloadInGraph,
@@ -88,7 +88,7 @@
retryStatus.val = {}
suspendStatus.val = {}
globalRefreshes = {}
flowState = {}
for (let key in localModuleStates) delete flowState[key]
localDurationStatuses = {}
localModuleStates = {}
}
@@ -1671,7 +1671,7 @@
</div>
{#if selected == 'logs' && render}
<div
class="mx-auto"
class="mx-auto overflow-auto"
bind:clientHeight={tabsHeight.logsHeight}
style="min-height: {minTabHeight}px"
>
@@ -700,7 +700,9 @@
{/if}
{#if argName && schema?.properties?.[argName]?.description}
<div class="text-xs italic py-1 text-hint">
<pre class="font-main">{schema.properties[argName].description}</pre>
<pre class="font-main whitespace-normal"
>{schema.properties[argName].description}</pre
>
</div>
{/if}
</div>
@@ -806,6 +808,14 @@
/>
{/if}
{#if argName && schema?.properties?.[argName]?.description}
<div class="text-xs italic py-1 text-hint">
<pre class="font-main whitespace-normal"
>{schema.properties[argName].description}</pre
>
</div>
{/if}
{#if !hideHelpButton}
<DynamicInputHelpBox />
{/if}
@@ -291,10 +291,9 @@
<h5 class="mt-4 inline-flex items-center gap-4 pb-2">
File content ({resourceTypeInfo.format_extension})
</h5>
<div class="h-full w-full border p-1 rounded">
<div class="">
<SimpleEditor
autoHeight
class="editor"
lang={resourceTypeInfo.format_extension}
bind:code={textFileContent}
fixedOverflowWidgets={false}
@@ -324,14 +323,8 @@
{#if !emptyString(jsonError)}<span class="text-red-400 text-xs mb-1 flex flex-row-reverse"
>{jsonError}</span
>{:else}<div class="py-2"></div>{/if}
<div class="h-full w-full border p-1 rounded">
<SimpleEditor
autoHeight
class="editor"
lang="json"
bind:code={rawCode}
fixedOverflowWidgets={false}
/>
<div class="bg-surface-secondary rounded-md border py-2.5">
<SimpleEditor autoHeight lang="json" bind:code={rawCode} />
</div>
{/if}
</div>
@@ -27,6 +27,7 @@
defaultValues?: Record<string, any> | undefined
placeholder?: string | undefined
selectInputClass?: string
class?: string
onClear?: () => void
excludedValues?: string[]
}
@@ -44,6 +45,7 @@
defaultValues = undefined,
placeholder = undefined,
selectInputClass = '',
class: className = '',
onClear = undefined,
excludedValues = undefined
}: Props = $props()
@@ -180,7 +182,7 @@
}}
/>
<!-- {JSON.stringify({ value, collection })} -->
<div class="flex flex-col w-full items-start min-h-10">
<div class="flex flex-col w-full items-start min-h-10 {className}">
<div class="flex flex-row w-full items-center">
<Select
{disabled}
@@ -70,7 +70,7 @@
| undefined)
| undefined
workspace?: string | undefined
actions?: import('svelte').Snippet
actions?: import('svelte').Snippet<[{ item: { id: string; value: string } }]> | undefined
}
let {
@@ -414,7 +414,7 @@
{displayType}
>
{#snippet actions()}
{@render actions_render?.()}
{@render actions_render?.({ item })}
{#if linkedSecretCandidates?.includes(argName)}
<div>
<ToggleButtonGroup
@@ -86,7 +86,7 @@
let loading = false
let captureTable: CaptureTable | undefined = undefined
let historicInputs: HistoricInputs | undefined = undefined
$: selectedTab, (dropdownItems = getDropdownItems())
$: (selectedTab, (dropdownItems = getDropdownItems()))
let inputPanelSize = 70
</script>
@@ -3,7 +3,7 @@
import type { Schema, SupportedLanguage } from '$lib/common'
import { type CompletedJob, type Job, JobService, type Preview, type ScriptLang } from '$lib/gen'
import { copilotInfo, enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import { copyToClipboard, emptySchema, sendUserToast } from '$lib/utils'
import Editor from './Editor.svelte'
import { inferArgs, inferAssets } from '$lib/infer'
@@ -50,6 +50,7 @@
import { assetEq, type AssetWithAltAccessType } from './assets/lib'
import { editor as meditor } from 'monaco-editor'
import type { ReviewChangesOpts } from './copilot/chat/monaco-adapter'
import { copilotInfo } from '$lib/aiStore'
interface Props {
// Exported
+5 -1
View File
@@ -110,7 +110,11 @@
<span
class={twMerge(
'ml-2 font-normal duration-50 select-none',
bothOptions || textDisabled ? (checked ? 'text-primary' : 'text-disabled') : 'text-primary',
bothOptions || textDisabled
? checked
? 'text-secondary'
: 'text-disabled'
: 'text-secondary',
size === 'xs' || size === '2sm' ? 'text-xs' : size === '2xs' ? 'text-xs' : 'text-sm',
textClass
)}
@@ -13,9 +13,9 @@
<div class="max-w-min">
<ToggleButtonGroup id="flow-editor-flow-providers" bind:selected>
{#snippet children({ item })}
<ToggleButton value="all" label="All" light small {item} />
<ToggleButton value="hub" icon={WindmillIcon2} label="Hub" light small {item} />
<ToggleButton value="workspace" icon={Building} label="Workspace" light small {item} />
<ToggleButton value="all" label="All" light {item} />
<ToggleButton value="hub" icon={WindmillIcon2} label="Hub" light {item} />
<ToggleButton value="workspace" icon={Building} label="Workspace" light {item} />
{/snippet}
</ToggleButtonGroup>
</div>
@@ -267,60 +267,64 @@
<div class="text-red-500 text-xs">{errorsMessage}</div>
{/if}
{#key css}
<Button
on:pointerdown={(e) => e.stopPropagation()}
btnClasses={twMerge(
css?.button?.class ?? '',
isMenuItem ? 'flex items-center justify-start' : '',
isMenuItem ? '!border-0' : '',
'wm-button',
`wm-button-${resolvedConfig.color}`
)}
variant={isMenuItem ? 'border' : 'contained'}
style={css?.button?.style}
wrapperClasses={twMerge(
css?.container?.class ?? '',
resolvedConfig.fillContainer ? 'w-full h-full' : '',
isMenuItem ? 'w-full' : '',
'wm-button-container',
`wm-button-container-${resolvedConfig.color}`
)}
wrapperStyle={css?.container?.style}
disabled={resolvedConfig.disabled}
on:click={handleClick}
size={resolvedConfig.size}
color={resolvedConfig.color}
<div
class="inline-flex"
title={resolvedConfig.tooltip && String(resolvedConfig.tooltip).length > 0
? String(resolvedConfig.tooltip)
: undefined}
loading={resolvedConfig.runInBackground ? backgroundClickFeedback : loading}
>
{#if resolvedConfig.beforeIcon}
{#key resolvedConfig.beforeIcon}
<div
class={resolvedConfig.label?.toString() &&
resolvedConfig.label?.toString()?.length > 0
? 'min-w-4'
: ''}
bind:this={beforeIconComponent}
></div>
{/key}
{/if}
{#if resolvedConfig.label?.toString() && resolvedConfig.label?.toString()?.length > 0}
<div>{resolvedConfig.label.toString()}</div>
{/if}
{#if resolvedConfig.afterIcon}
{#key resolvedConfig.afterIcon}
<div
class={resolvedConfig.label?.toString() &&
resolvedConfig.label?.toString()?.length > 0
? 'min-w-4'
: ''}
bind:this={afterIconComponent}
></div>
{/key}
{/if}
</Button>
<Button
on:pointerdown={(e) => e.stopPropagation()}
btnClasses={twMerge(
css?.button?.class ?? '',
isMenuItem ? 'flex items-center justify-start' : '',
isMenuItem ? '!border-0' : '',
'wm-button',
`wm-button-${resolvedConfig.color}`
)}
variant={isMenuItem ? 'border' : 'contained'}
style={css?.button?.style}
wrapperClasses={twMerge(
css?.container?.class ?? '',
resolvedConfig.fillContainer ? 'w-full h-full' : '',
isMenuItem ? 'w-full' : '',
'wm-button-container',
`wm-button-container-${resolvedConfig.color}`
)}
wrapperStyle={css?.container?.style}
disabled={resolvedConfig.disabled}
on:click={handleClick}
size={resolvedConfig.size}
color={resolvedConfig.color}
loading={resolvedConfig.runInBackground ? backgroundClickFeedback : loading}
>
{#if resolvedConfig.beforeIcon}
{#key resolvedConfig.beforeIcon}
<div
class={resolvedConfig.label?.toString() &&
resolvedConfig.label?.toString()?.length > 0
? 'min-w-4'
: ''}
bind:this={beforeIconComponent}
></div>
{/key}
{/if}
{#if resolvedConfig.label?.toString() && resolvedConfig.label?.toString()?.length > 0}
<div>{resolvedConfig.label.toString()}</div>
{/if}
{#if resolvedConfig.afterIcon}
{#key resolvedConfig.afterIcon}
<div
class={resolvedConfig.label?.toString() &&
resolvedConfig.label?.toString()?.length > 0
? 'min-w-4'
: ''}
bind:this={afterIconComponent}
></div>
{/key}
{/if}
</Button>
</div>
{/key}
</AlignWrapper>
</RunnableWrapper>
@@ -17,6 +17,7 @@
import { argSigToJsonSchemaType } from 'windmill-utils-internal'
import SchemaForm from '$lib/components/SchemaForm.svelte'
import { untrack } from 'svelte'
import Toggle from '$lib/components/Toggle.svelte'
let schema: Schema | undefined = $state(undefined)
@@ -134,8 +135,10 @@
let fields = $derived(
columnDefs
?.filter((t) => {
const shouldFilter = t.isidentity === ColumnIdentity.Always || t?.hideInsert === true
const shouldFilter =
t.isidentity === ColumnIdentity.Always ||
t?.hideInsert === true ||
t.defaultvalue?.startsWith('nextval(') // exclude postgres serial/auto increment fields
return !shouldFilter
})
.map((column) => {
@@ -171,5 +174,32 @@
</script>
{#if schema}
<SchemaForm onlyMaskPassword {schema} bind:args />
<SchemaForm onlyMaskPassword {schema} bind:args>
{#snippet actions({ item })}
{@const disabled = fields?.[fields?.findIndex((f) => f.name === item.id)]?.nullable != 'YES'}
{#if !disabled}
<Toggle
options={{ right: 'NULL' }}
class="pl-2"
textClass="text-tertiary"
size="2sm"
bind:checked={
() => args[item.id] === null,
(v) => {
if (!schema?.properties[item.id]) return
if (v) {
schema.properties[item.id].nullable = true
schema.properties[item.id].disabled = true
args[item.id] = null
} else {
delete schema.properties[item.id].disabled
delete schema.properties[item.id].nullable
args[item.id] = schema.properties[item.id].default ?? ''
}
}
}
/>
{/if}
{/snippet}
</SchemaForm>
{/if}

Some files were not shown because too many files have changed in this diff Show More