Compare commits

..

3 Commits

Author SHA1 Message Date
Pyra
f40a3b420b Merge branch 'main' into debouncing-tests 2026-03-06 14:42:25 +01:00
Pyra
993fbcde59 Merge branch 'main' into debouncing-tests 2026-03-04 17:40:57 +01:00
pyranota
b1142421b8 nit: add more tests
Signed-off-by: pyranota <pyra@duck.com>
2026-03-04 17:39:54 +01:00
51 changed files with 1074 additions and 2205 deletions

View File

@@ -1,52 +1,113 @@
# Project display name in the dashboard
name: windmill
workspace:
mainBranch: main
worktreeRoot: ../__worktrees
defaultAgent: claude
name: Windmill
startupEnvs:
CARGO_FEATURES: "quickjs"
WM_CLONE_DB: false
USE_RUST_PLUGIN: false
lifecycleHooks:
postCreate: bash ./scripts/post-create.sh
preRemove: bash ./scripts/pre-remove.sh
# Each service defines a port env var that webmux injects into pane and agent
# process environments when creating a worktree. Ports are auto-assigned:
# base + (slot x step).
services:
- name: backend
- name: BE
portEnv: BACKEND_PORT
portStart: 8000
portStep: 10
- name: frontend
- name: FE
portEnv: FRONTEND_PORT
portStart: 3000
portStep: 10
profiles:
default:
runtime: host
envPassthrough: []
panes:
- id: agent
kind: agent
focus: true
- id: backend
kind: command
split: right
command: ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/backend" && PORT=${BACKEND_PORT:-8000} cargo watch -x "run ${CARGO_FEATURES:+--features $CARGO_FEATURES}"
- id: frontend
kind: command
split: bottom
command: ROOT="$(git rev-parse --show-toplevel)"; [ -f "$ROOT/.env.local" ] && source "$ROOT/.env.local"; cd "$ROOT/frontend" && npm run generate-backend-client && REMOTE=${REMOTE:-http://localhost:${BACKEND_PORT:-8000}} npm run dev -- --port ${FRONTEND_PORT:-3000} --host 0.0.0.0
name: default
integrations:
github:
linkedRepos: []
linear:
enabled: true
sandbox:
name: sandbox
image: windmill-sandbox
envPassthrough:
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- R2_ENDPOINT
- R2_BUCKET
- R2_PUBLIC_URL
extraMounts:
- hostPath: ~/.ssh
guestPath: /root/.ssh
writable: true
- hostPath: ~/.codex
guestPath: /root/.codex
writable: true
- hostPath: ~/windmill-ee-private
writable: true
- hostPath: ~/windmill-ee-private__worktrees
writable: true
systemPrompt: >
You are running inside a sandboxed container with full permissions.
This worktree is configured with the following ports:
- Backend: port ${BACKEND_PORT}.
Start with: cd backend && PORT=${BACKEND_PORT}
DATABASE_URL=postgres://postgres:changeme@localhost:5432/windmill
cargo watch -x run
- Frontend: port ${FRONTEND_PORT}.
Start with: cd frontend && REMOTE=http://localhost:${BACKEND_PORT}
npm run dev -- --port ${FRONTEND_PORT} --host 0.0.0.0
--- Screenshots ---
You can take screenshots of the frontend UI and upload them to R2
for use in PR descriptions.
1) Take a screenshot:
bunx playwright screenshot --browser chromium
http://localhost:${FRONTEND_PORT}/path/to/page /tmp/screenshot.png
2) Upload to R2:
aws s3 cp /tmp/screenshot.png
"s3://$(printenv R2_BUCKET)/$(git rev-parse --abbrev-ref HEAD)/screenshot.png"
--endpoint-url "$(printenv R2_ENDPOINT)"
3) The public URL will be:
$(printenv R2_PUBLIC_URL)/<branch>/screenshot.png
4) Include in PR descriptions using markdown image syntax.
--- Terminal Recordings (asciinema) ---
You can record terminal sessions and upload them for sharing.
asciinema is available on PATH.
1) Write a shell script with the commands to demo. Add sleep
delays for readable pacing:
- 0.5s after printing a "$ command" line (lets viewer read it)
- 1.5-2s after command output (lets viewer absorb the result)
- Set GIT_PAGER=cat and PAGER=cat to prevent pager hangs
2) Record headlessly:
asciinema rec --headless --overwrite \
-c "bash /tmp/demo.sh" \
--window-size 120x50 \
--title "Description of demo" \
/tmp/demo.cast
3) Upload to asciinema.org:
XDG_DATA_HOME=/tmp/.local/share \
asciinema upload --server-url https://asciinema.org /tmp/demo.cast
--- Mermaid Diagrams ---
You can render Mermaid diagrams to SVG using the pre-installed mmdc CLI.
The puppeteer config (no-sandbox + Chromium path) is at /root/.puppeteerrc.json.
1) Write a .mmd file with your diagram:
cat > /tmp/diagram.mmd << 'EOF'
graph TD
A[Start] --> B[End]
EOF
2) Render to SVG (the -p flag is required):
mmdc -i /tmp/diagram.mmd -o /tmp/diagram.svg -p /root/.puppeteerrc.json
3) Upload to R2:
aws s3 cp /tmp/diagram.svg
"s3://$(printenv R2_BUCKET)/$(git rev-parse --abbrev-ref HEAD)/diagram.svg"
--endpoint-url "$(printenv R2_ENDPOINT)"
4) The public URL will be:
$(printenv R2_PUBLIC_URL)/<branch>/diagram.svg
5) Include in PR descriptions using markdown image syntax.
IMPORTANT: Read docs/autonomous-mode.md before starting any work.
linkedRepos:
- repo: windmill-labs/windmill-ee-private
alias: ee

View File

@@ -26,27 +26,6 @@ Open-source platform for internal tools, workflows, API integrations, background
- **Login**: `admin@windmill.dev` / `changeme`
- **Instance settings**: navigate to `/#superadmin-settings`
## Banned Patterns
### `$bindable(default_value)` on optional props
Using `$bindable(default_value)` on props that can be `undefined` is **banned**. This pattern causes subtle bugs because the default value masks the `undefined` state.
**Bad:**
```svelte
let { my_prop = $bindable(default_value) }: { my_prop?: string } = $props()
```
**Correct alternatives:**
1. **Use `$derived` with nullish coalescing** — handle the potential `undefined` at the usage site:
```svelte
let { my_prop = $bindable() }: { my_prop?: string } = $props()
let effective_value = $derived(my_prop ?? default_value)
```
2. **Create a `useMyPropState()` helper** — encapsulate the undefined-handling logic in a reusable function and call it higher in the component tree, so the child component always receives a defined value.
## Core Principles
- Search for existing code to reuse before writing new code

View File

@@ -1 +1 @@
716b350bce1730b302c66ea69df618fa40f2f16b
f9549c813b3dba5324ea9d1edacc8756a6d699bf

View File

@@ -238,7 +238,7 @@ lazy_static::lazy_static! {
// used for `unsafe` sql interpolation
// -- %%name%% (type) = default
static ref RE_ARG_SQL_INTERPOLATION: Regex = Regex::new(r#"(?m)^--\s*%%([a-z_][a-z0-9_]*)%%[ \t]*([\w][\w \t\/]*)?(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
static ref RE_ARG_SQL_INTERPOLATION: Regex = Regex::new(r#"(?m)^--\s*%%([a-z_][a-z0-9_]*)%%\s*([\s\w\/]+)?(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
}
fn parsed_default(parsed_typ: &Typ, default: String) -> Option<serde_json::Value> {
@@ -1547,36 +1547,4 @@ SELECT $1::integer;
Ok(())
}
#[test]
fn test_parse_pgsql_safe_interpolated_args() -> anyhow::Result<()> {
// There was a bug where enum would be "angrycreative"/"bishop"/"test SELECT x"
let code = r#"
-- %%table_name%% angrycreative/bishop/test
SELECT x
"#;
assert_eq!(
parse_pgsql_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![Arg {
otyp: Some("__sanitized_enum__".to_string()),
name: "table_name".to_string(),
typ: Typ::Str(Some(vec![
"angrycreative".to_string(),
"bishop".to_string(),
"test".to_string()
])),
default: None,
has_default: false,
oidx: None,
},],
no_main_func: None,
has_preprocessor: None
}
);
Ok(())
}
}

View File

@@ -9,23 +9,7 @@ export async function main() {
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/leafs/ts', 500001, 'nativets', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
package main
import "fmt"
func main() {
fmt.Println("Go leaf")
}',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/leafs/go', 500002, 'go', '');
'f/leafs/ts', 500001, 'bun', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
@@ -52,3 +36,9 @@ function main() {
'',
'f/leafs/php', 500004, 'php', '');
-- Link scripts to named workspace dependencies (name: "test")
INSERT INTO dependency_map (workspace_id, importer_path, importer_kind, imported_path, importer_node_id) VALUES
('test-workspace', 'f/leafs/ts', 'script', 'dependencies/test.package.json', ''),
('test-workspace', 'f/leafs/python', 'script', 'dependencies/test.requirements.in', ''),
('test-workspace', 'f/leafs/php', 'script', 'dependencies/test.composer.json', '');

View File

@@ -3548,170 +3548,3 @@ async fn test_flow_substep_tag_availability_check(db: Pool<Postgres>) -> anyhow:
Ok(())
}
#[cfg(all(feature = "quickjs", feature = "python"))]
#[sqlx::test(fixtures("base"))]
async fn test_stop_after_all_iters_if_bad_expr_parallel_branchall(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
let port = 123;
let flow: FlowValue = serde_json::from_value(serde_json::json!({
"modules": [
{
"id": "a",
"value": {
"branches": [
{"modules": [{
"id": "b",
"value": {
"input_transforms": { "n": { "type": "javascript", "expr": "flow_input.n" } },
"type": "rawscript",
"language": "python3",
"content": "def main(n): return n",
},
}]}
],
"type": "branchall",
"parallel": true,
},
"stop_after_all_iters_if": {
"expr": "invalid!!!syntax",
"skip_if_stopped": false,
},
},
],
}))
.unwrap();
let job = JobPayload::RawFlow { value: flow, path: None, restarted_from: None };
let cjob = RunJob::from(job)
.arg("n", json!(42))
.run_until_complete(&db, false, port)
.await;
assert!(
!cjob.success,
"flow should fail when stop_after_all_iters_if has bad expression"
);
let result = cjob.json_result().unwrap();
let error_msg = result["error"]["message"].as_str().unwrap_or("");
assert!(
error_msg.contains("stop_after_all_iters_if"),
"error should mention stop_after_all_iters_if, got: {error_msg}"
);
Ok(())
}
#[cfg(all(feature = "quickjs", feature = "python"))]
#[sqlx::test(fixtures("base"))]
async fn test_stop_after_all_iters_if_bad_expr_parallel_forloop(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
let port = 123;
let flow: FlowValue = serde_json::from_value(serde_json::json!({
"modules": [
{
"id": "a",
"value": {
"type": "forloopflow",
"iterator": { "type": "javascript", "expr": "result.items" },
"skip_failures": false,
"parallel": true,
"modules": [{
"value": {
"input_transforms": {
"n": { "type": "javascript", "expr": "flow_input.iter.value" },
},
"type": "rawscript",
"language": "python3",
"content": "def main(n): return n",
},
}],
},
"stop_after_all_iters_if": {
"expr": "invalid!!!syntax",
"skip_if_stopped": false,
},
},
],
}))
.unwrap();
let job = JobPayload::RawFlow { value: flow, path: None, restarted_from: None };
let cjob = RunJob::from(job)
.arg("items", json!([1, 2, 3]))
.run_until_complete(&db, false, port)
.await;
assert!(
!cjob.success,
"flow should fail when stop_after_all_iters_if has bad expression"
);
let result = cjob.json_result().unwrap();
let error_msg = result["error"]["message"].as_str().unwrap_or("");
assert!(
error_msg.contains("stop_after_all_iters_if"),
"error should mention stop_after_all_iters_if, got: {error_msg}"
);
Ok(())
}
#[cfg(all(feature = "quickjs", feature = "python"))]
#[sqlx::test(fixtures("base"))]
async fn test_results_length_in_input_transform(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
// Step a returns a list, step b accesses results.a.length via input transform.
// This tests that the handle_full_regex fast path falls through to QuickJS
// when the SQL JSON path operator can't resolve JS properties like .length.
let flow: FlowValue = serde_json::from_value(json!({
"modules": [
{
"id": "a",
"value": {
"type": "rawscript",
"language": "python3",
"content": "def main(): return [10, 20, 30]",
},
},
{
"id": "b",
"value": {
"input_transforms": {
"v": { "type": "javascript", "expr": "results.a.length" },
},
"type": "rawscript",
"language": "python3",
"content": "def main(v): return v",
},
},
],
}))
.unwrap();
let result =
RunJob::from(JobPayload::RawFlow { value: flow, path: None, restarted_from: None })
.run_until_complete(&db, false, port)
.await
.json_result()
.unwrap();
assert_eq!(
result,
json!(3),
"results.a.length should resolve to 3, not null"
);
Ok(())
}

View File

@@ -1,21 +1,17 @@
mod workspace_dependencies {
use windmill_test_utils::in_test_worker;
use windmill_test_utils::init_client;
use windmill_test_utils::listen_for_completed_jobs;
use sqlx::{Pool, Postgres};
use tokio_stream::StreamExt;
use windmill_common::scripts::ScriptLang;
use windmill_common::workspace_dependencies::WorkspaceDependencies;
use windmill_dep_map::workspace_dependencies::NewWorkspaceDependencies;
use windmill_test_utils::in_test_worker;
use windmill_test_utils::init_client;
use windmill_test_utils::listen_for_completed_jobs;
mod deps {
pub const REQUIREMENTS_IN: &'static str = "tiny==0.1.3";
// pub const GO_MOD: &'static str = r##"
// module example.com/project
// go 1.20
// require github.com/gin-gonic/gin v1.8.1
// "##;
pub const REQUIREMENTS_IN_V2: &'static str = "tiny==0.2.0";
pub const PACKAGE_JSON: &'static str = r##"
{
@@ -25,6 +21,18 @@ mod workspace_dependencies {
"express": "^4.17.1"
}
}
"##;
#[allow(dead_code)]
pub const PACKAGE_JSON_V2: &'static str = r##"
{
"name": "example-project",
"version": "2.0.0",
"dependencies": {
"express": "^4.18.0",
"axios": "^1.0.0"
}
}
"##;
pub const COMPOSER_JSON: &'static str = r##"
@@ -37,9 +45,510 @@ mod workspace_dependencies {
"##;
}
// =========================================================================
// CRUD Tests
// =========================================================================
/// Test: Create workspace dependencies and verify they are stored correctly.
#[sqlx::test(fixtures("base"))]
async fn test_create_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
let id = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("test-deps".to_owned()),
description: Some("Test dependencies".to_owned()),
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
assert!(id > 0, "Should return a valid ID");
// Verify it was stored correctly
let stored = WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db).await?;
assert_eq!(stored.name, Some("test-deps".to_owned()));
assert_eq!(stored.content, deps::REQUIREMENTS_IN);
assert_eq!(stored.language, ScriptLang::Python3);
Ok(())
}
/// Test: Create unnamed (default) workspace dependencies.
#[sqlx::test(fixtures("base"))]
async fn test_create_unnamed_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
let id = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Bun,
content: deps::PACKAGE_JSON.into(),
name: None, // Unnamed = default
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
assert!(id > 0, "Should return a valid ID");
// Verify it was stored correctly
let stored = WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db).await?;
assert_eq!(stored.name, None);
assert_eq!(stored.language, ScriptLang::Bun);
Ok(())
}
/// Test: List workspace dependencies returns all active entries.
#[sqlx::test(fixtures("base"))]
async fn test_list_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create multiple workspace dependencies
for (lang, content, name) in [
(ScriptLang::Python3, deps::REQUIREMENTS_IN, Some("python-deps")),
(ScriptLang::Bun, deps::PACKAGE_JSON, Some("bun-deps")),
(ScriptLang::Bun, deps::PACKAGE_JSON, None), // Default bun deps
] {
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: lang,
content: content.into(),
name: name.map(|s| s.to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
}
let list = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list.len(), 3, "Should have 3 workspace dependencies");
Ok(())
}
/// Test: Archive workspace dependencies marks them as archived.
#[sqlx::test(fixtures("base"))]
async fn test_archive_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create workspace dependencies
let _id = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("to-archive".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Verify it exists
let list_before = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list_before.len(), 1);
// Archive it
WorkspaceDependencies::archive(
Some("to-archive".to_owned()),
ScriptLang::Python3,
"test-workspace",
&db,
)
.await?;
// Verify it's no longer in the active list
let list_after = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list_after.len(), 0, "Archived deps should not appear in list");
Ok(())
}
/// Test: Delete workspace dependencies permanently removes them.
#[sqlx::test(fixtures("base"))]
async fn test_delete_workspace_dependencies(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create workspace dependencies
let id = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("to-delete".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Verify it exists
assert!(
WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db)
.await
.is_ok()
);
// Delete it
WorkspaceDependencies::delete(
Some("to-delete".to_owned()),
ScriptLang::Python3,
"test-workspace",
&db,
)
.await?;
// Verify it's gone (should error)
let result = WorkspaceDependencies::get(id, "test-workspace".to_owned(), &db).await;
assert!(result.is_err(), "Deleted deps should not be retrievable");
Ok(())
}
// =========================================================================
// Version History Tests
// =========================================================================
/// Test: Creating new version archives the old one.
#[sqlx::test(fixtures("base"))]
async fn test_versioning_archives_previous(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create first version
let id1 = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("versioned".to_owned()),
description: Some("Version 1".to_owned()),
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Create second version with same name
let id2 = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN_V2.into(),
name: Some("versioned".to_owned()),
description: Some("Version 2".to_owned()),
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
assert_ne!(id1, id2, "Should create a new entry");
// List should only show the active (latest) version
let list = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list.len(), 1, "Should only have 1 active entry");
assert_eq!(list[0].content, deps::REQUIREMENTS_IN_V2);
// History should show both versions
let history = WorkspaceDependencies::get_history(
Some("versioned".to_owned()),
ScriptLang::Python3,
"test-workspace",
&db,
)
.await?;
assert_eq!(history.len(), 2, "Should have 2 versions in history");
Ok(())
}
/// Test: Description is inherited from previous version if not provided.
#[sqlx::test(fixtures("base"))]
async fn test_description_inheritance(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create first version with description
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("inherit-desc".to_owned()),
description: Some("Original description".to_owned()),
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Create second version without description
let id2 = NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN_V2.into(),
name: Some("inherit-desc".to_owned()),
description: None, // Should inherit
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
let stored = WorkspaceDependencies::get(id2, "test-workspace".to_owned(), &db).await?;
assert_eq!(
stored.description,
Some("Original description".to_owned()),
"Description should be inherited from previous version"
);
Ok(())
}
// =========================================================================
// Workspace Isolation Tests
// =========================================================================
/// Test: Workspace dependencies are isolated between workspaces.
#[sqlx::test(fixtures("base"))]
async fn test_workspace_isolation(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create another workspace
sqlx::query!(
"INSERT INTO workspace (id, name, owner) VALUES ('other-workspace', 'other', 'test-user')"
)
.execute(&db)
.await?;
sqlx::query!("INSERT INTO workspace_settings (workspace_id) VALUES ('other-workspace')")
.execute(&db)
.await?;
// Create deps in test-workspace
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("shared-name".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Create deps in other-workspace with same name
NewWorkspaceDependencies {
workspace_id: "other-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN_V2.into(),
name: Some("shared-name".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Each workspace should have exactly 1 entry
let list1 = WorkspaceDependencies::list("test-workspace", &db).await?;
let list2 = WorkspaceDependencies::list("other-workspace", &db).await?;
assert_eq!(list1.len(), 1);
assert_eq!(list2.len(), 1);
// Content should be different
assert_eq!(list1[0].content, deps::REQUIREMENTS_IN);
assert_eq!(list2[0].content, deps::REQUIREMENTS_IN_V2);
Ok(())
}
// =========================================================================
// Language-specific Tests
// =========================================================================
/// Test: Different languages can have same-named workspace dependencies.
#[sqlx::test(fixtures("base"))]
async fn test_same_name_different_languages(db: Pool<Postgres>) -> anyhow::Result<()> {
// Create Python deps
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Python3,
content: deps::REQUIREMENTS_IN.into(),
name: Some("common".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Create Bun deps with same name
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Bun,
content: deps::PACKAGE_JSON.into(),
name: Some("common".to_owned()),
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
let list = WorkspaceDependencies::list("test-workspace", &db).await?;
assert_eq!(list.len(), 2, "Should have 2 entries (different languages)");
let python_deps: Vec<_> = list
.iter()
.filter(|d| d.language == ScriptLang::Python3)
.collect();
let bun_deps: Vec<_> = list
.iter()
.filter(|d| d.language == ScriptLang::Bun)
.collect();
assert_eq!(python_deps.len(), 1);
assert_eq!(bun_deps.len(), 1);
Ok(())
}
/// Test: Nativets and Bunnative use Bun workspace dependencies.
#[sqlx::test(fixtures("base"))]
async fn test_nativets_uses_bun_deps(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::worker::Connection;
// Create Bun deps (which Nativets should use)
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: ScriptLang::Bun,
content: deps::PACKAGE_JSON.into(),
name: None,
description: None,
}
.create(
(
"test@test.com".to_owned(),
"u/test".to_owned(),
"test".to_owned(),
),
db.clone(),
)
.await?;
// Query for Nativets should return Bun deps
let result = WorkspaceDependencies::get_latest(
None,
ScriptLang::Nativets,
"test-workspace",
Connection::Sql(db.clone()),
)
.await?;
assert!(result.is_some(), "Nativets should find Bun deps");
assert_eq!(result.unwrap().language, ScriptLang::Bun);
Ok(())
}
// =========================================================================
// Path Generation Tests
// =========================================================================
/// Test: to_path generates correct paths for named and unnamed deps.
#[test]
fn test_to_path_generation() {
// Unnamed (default) deps
let path = WorkspaceDependencies::to_path(&None, ScriptLang::Python3).unwrap();
assert_eq!(path, "dependencies/requirements.in");
let path = WorkspaceDependencies::to_path(&None, ScriptLang::Bun).unwrap();
assert_eq!(path, "dependencies/package.json");
let path = WorkspaceDependencies::to_path(&None, ScriptLang::Php).unwrap();
assert_eq!(path, "dependencies/composer.json");
// Named deps
let path =
WorkspaceDependencies::to_path(&Some("custom".to_owned()), ScriptLang::Python3).unwrap();
assert_eq!(path, "dependencies/custom.requirements.in");
let path =
WorkspaceDependencies::to_path(&Some("custom".to_owned()), ScriptLang::Bun).unwrap();
assert_eq!(path, "dependencies/custom.package.json");
}
/// Test: to_path returns error for unsupported languages.
#[test]
fn test_to_path_unsupported_language() {
// Deno doesn't support workspace dependencies
let result = WorkspaceDependencies::to_path(&None, ScriptLang::Deno);
assert!(result.is_err(), "Deno should not support workspace deps");
}
/// Test E2E: Creating named workspace dependencies triggers re-lock jobs for dependent scripts.
///
/// This test:
/// 1. Uses fixture with Python, Bun, PHP scripts linked to named workspace deps via dependency_map
/// 2. Creates named workspace dependencies for each language
/// 3. Verifies dependency jobs are triggered for all linked scripts
#[cfg(feature = "python")]
#[sqlx::test(fixtures("base", "workspace_dependencies_leafs"))]
#[ignore]
async fn basic_manual_named(db: Pool<Postgres>) -> anyhow::Result<()> {
let ((_client, port, _s), db, mut completed) = (
init_client(db.clone()).await,
@@ -47,67 +556,69 @@ mod workspace_dependencies {
listen_for_completed_jobs(&db).await,
);
for (idx, (l, c)) in [
// Create named workspace dependencies for Python, Bun, and PHP
// These will trigger dependency jobs for scripts linked via dependency_map
for (lang, content) in [
(ScriptLang::Python3, deps::REQUIREMENTS_IN),
(ScriptLang::Bun, deps::PACKAGE_JSON),
(ScriptLang::Php, deps::COMPOSER_JSON),
// (ScriptLang::Go, deps::GO_MOD),
]
.iter()
.enumerate()
{
let id = NewWorkspaceDependencies {
] {
NewWorkspaceDependencies {
workspace_id: "test-workspace".into(),
language: *l,
content: (*c).into(),
language: lang,
content: content.into(),
name: Some("test".to_owned()),
description: None,
}
.create(("".to_owned(), "".to_owned(), "".to_owned()), db.clone())
.await
.unwrap();
assert_eq!(idx + 1, id as usize);
.create(
(
"test@test.com".to_owned(),
"u/test-user".to_owned(),
"test-user".to_owned(),
),
db.clone(),
)
.await?;
}
// Wait for 4 jobs.
// Creating those dependencies will trigger redeployment of all scripts in workspace_dependencies_leafs.sql
in_test_worker(
db,
async {
completed.next().await;
completed.next().await;
completed.next().await;
// completed.next().await;
},
port,
)
.await;
// Wait for 3 dependency jobs (one per script in fixture)
let mut completed_paths = vec![];
for _ in 0..3 {
let job_id = in_test_worker(db, async { completed.next().await }, port)
.await
.expect("Expected a dependency job to complete");
// Verify all scripts have correct locks
// let mut langs = vec![];
// for r in sqlx::query!(
// r#"SELECT language AS "language: ScriptLang",lock FROM script WHERE archived = false"#
// )
// .fetch_all(db)
// .await
// .unwrap()
// {
// match r.language {
// ScriptLang::Python3 => assert_eq!("", &r.lock.unwrap()),
// ScriptLang::Go => todo!(),
// ScriptLang::Bun => todo!(),
// ScriptLang::Bunnative => todo!(),
// ScriptLang::Php => todo!(),
// _ => panic!("Unsupported language"),
// }
let job_path = sqlx::query_scalar!(
"SELECT runnable_path FROM v2_job WHERE id = $1",
job_id
)
.fetch_one(db)
.await?;
// langs.push(r.language);
// }
if let Some(path) = job_path {
completed_paths.push(path);
}
}
// langs.sort();
// // Just tiny additional verification for peace of mind.
// assert_eq!(langs.as_slice(), &[]);
// Verify all 3 scripts received dependency jobs
completed_paths.sort();
let expected = vec![
"f/leafs/php".to_string(),
"f/leafs/python".to_string(),
"f/leafs/ts".to_string(),
];
assert_eq!(
completed_paths, expected,
"All scripts should have received dependency jobs"
);
// Verify no extra jobs were created
let total_jobs = sqlx::query_scalar!("SELECT COUNT(*) FROM v2_job")
.fetch_one(db)
.await?;
assert_eq!(total_jobs, Some(3), "Should have exactly 3 jobs");
Ok(())
}

View File

@@ -1877,15 +1877,6 @@ pub struct ExecuteApp {
pub run_query_params: Option<RunJobQuery>,
}
fn maybe_replace_internal_db_script(mut raw_code: RawCode) -> RawCode {
if let Some(replaced) =
crate::db_studio_scripts::maybe_replace_internal_script(&raw_code.content)
{
raw_code.content = replaced;
}
raw_code
}
fn digest(code: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(code);
@@ -2147,30 +2138,13 @@ async fn execute_component(
// flow or script:
(Some(path), None, None) => get_payload_tag_from_prefixed_path(&path, &db, &w_id).await?,
// inline script: in "preview" mode or without entry in the `app_script` table.
(None, Some(raw_code), None) => {
let raw_code = maybe_replace_internal_db_script(raw_code);
(JobPayload::Code(raw_code), None, None)
}
(None, Some(raw_code), None) => (JobPayload::Code(raw_code), None, None),
// inline script: in "run" mode and with an entry in the `app_script` table.
(None, Some(raw_code), Some(id)) => {
// Check if this is an internal DB script marker — if so, replace content and
// execute as Code (preview-style) since the content is server-controlled.
if raw_code
.content
.trim_start()
.starts_with(crate::db_studio_scripts::WM_INTERNAL_PREFIX)
{
let raw_code = maybe_replace_internal_db_script(raw_code);
(JobPayload::Code(raw_code), None, None)
} else {
let RawCode { language, path, cache_ttl, .. } = raw_code;
(
JobPayload::AppScript { id: AppScriptId(id), cache_ttl, language, path },
None,
None,
)
}
}
(None, Some(RawCode { language, path, cache_ttl, .. }), Some(id)) => (
JobPayload::AppScript { id: AppScriptId(id), cache_ttl, language, path },
None,
None,
),
_ => unreachable!(),
};
let tx = PushIsolationLevel::IsolatedRoot(db.clone());

View File

@@ -284,9 +284,6 @@ pub async fn migrate(
20260207000004,
];
for m in migrator.migrations.iter() {
if m.migration_type.is_down_migration() {
continue;
}
if potentially_stale.contains(&m.version) {
if let Err(err) =
sqlx::query("DELETE FROM _sqlx_migrations WHERE version = $1 AND checksum != $2")

File diff suppressed because it is too large Load Diff

View File

@@ -4612,26 +4612,21 @@ async fn run_preview_script(
match preview.kind {
Some(PreviewKind::Identity) => JobPayload::Identity,
Some(PreviewKind::Noop) => JobPayload::Noop,
_ => {
let content = preview.content.unwrap_or_default();
let content = crate::db_studio_scripts::maybe_replace_internal_script(&content)
.unwrap_or(content);
JobPayload::Code(RawCode {
hash: preview
.script_hash
.as_ref()
.and_then(|s| windmill_common::scripts::to_i64(s).ok()),
content,
path: preview.path,
language: preview.language.unwrap_or(ScriptLang::Deno),
lock: preview.lock,
concurrency_settings: ConcurrencySettingsWithCustom::default(), // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here
debouncing_settings: DebouncingSettings::default(), // TODO(pyra): same as for concurrency limits.
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: preview.dedicated_worker,
})
}
_ => JobPayload::Code(RawCode {
hash: preview
.script_hash
.as_ref()
.and_then(|s| windmill_common::scripts::to_i64(s).ok()),
content: preview.content.unwrap_or_default(),
path: preview.path,
language: preview.language.unwrap_or(ScriptLang::Deno),
lock: preview.lock,
concurrency_settings: ConcurrencySettingsWithCustom::default(), // TODO(gbouv): once I find out how to store limits in the content of a script, should be easy to plug limits here
debouncing_settings: DebouncingSettings::default(), // TODO(pyra): same as for concurrency limits.
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: preview.dedicated_worker,
}),
},
push_args,
authed.display_username(),

View File

@@ -76,7 +76,6 @@ mod bedrock;
mod capture;
mod concurrency_groups;
mod db;
mod db_studio_scripts;
mod drafts;
#[cfg(feature = "private")]

View File

@@ -44,7 +44,7 @@ pub struct EnvRefWrapper {
///
/// `Literal` serializes back to a plain JSON string, preserving backwards
/// compatibility with existing consumers.
#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone, Debug)]
#[cfg_attr(feature = "instance_config_schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum StringOrSecretRef {
@@ -53,16 +53,6 @@ pub enum StringOrSecretRef {
EnvRef(EnvRefWrapper),
}
impl fmt::Debug for StringOrSecretRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Literal(_) => f.write_str("Literal(****)"),
Self::SecretRef(w) => f.debug_tuple("SecretRef").field(w).finish(),
Self::EnvRef(w) => f.debug_tuple("EnvRef").field(w).finish(),
}
}
}
impl StringOrSecretRef {
/// Returns the literal string value, or `None` if this is an unresolved ref.
pub fn as_literal(&self) -> Option<&str> {
@@ -265,25 +255,25 @@ pub struct GlobalSettings {
#[serde(skip_serializing_if = "Option::is_none")]
pub instance_python_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pip_index_url: Option<StringOrSecretRef>,
pub pip_index_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pip_extra_index_url: Option<StringOrSecretRef>,
pub pip_extra_index_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub npm_config_registry: Option<StringOrSecretRef>,
pub npm_config_registry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bunfig_install_scopes: Option<StringOrSecretRef>,
pub bunfig_install_scopes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub npmrc: Option<StringOrSecretRef>,
pub npmrc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nuget_config: Option<StringOrSecretRef>,
pub nuget_config: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maven_repos: Option<StringOrSecretRef>,
pub maven_repos: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ruby_repos: Option<StringOrSecretRef>,
pub ruby_repos: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub powershell_repo_url: Option<StringOrSecretRef>,
pub powershell_repo_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub powershell_repo_pat: Option<StringOrSecretRef>,
pub powershell_repo_pat: Option<String>,
// Array settings
#[serde(skip_serializing_if = "Option::is_none")]

View File

@@ -152,21 +152,6 @@ pub fn try_exact_property_access(
None
}
/// JS runtime properties (not methods) that cannot be resolved by PostgreSQL's
/// #> JSON path operator. Function calls like .map(...) already don't match the
/// RE_FULL regex due to parentheses, so only property accesses need listing here.
const JS_ONLY_PROPERTIES: &[&str] = &["length"];
fn ends_with_js_only_property(rest: Option<&str>) -> bool {
match rest {
None => false,
Some(rest) => {
let last_segment = rest.rsplit('.').next().unwrap_or("");
JS_ONLY_PROPERTIES.contains(&last_segment)
}
}
}
pub async fn handle_full_regex(
expr: &str,
authed_client: &AuthedClient,
@@ -177,13 +162,6 @@ pub async fn handle_full_regex(
let obj_key = captures.get(2).unwrap().as_str();
let idx_o = captures.get(3).map(|y| y.as_str());
let rest = captures.get(4).map(|y| y.as_str());
// Skip the SQL fast path when the expression accesses a JS runtime
// property (e.g. .length) that the PostgreSQL #> operator can't resolve.
if ends_with_js_only_property(rest) {
return None;
}
let query = if let Some(idx) = idx_o {
match rest {
Some(rest) => Some(format!("{}{}", idx, rest)),

View File

@@ -94,7 +94,7 @@ pub struct OAuthConfig {
}
/// OAuth client credentials
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OAuthClient {
#[serde(default = "empty_string")]
pub id: String,
@@ -110,21 +110,6 @@ pub struct OAuthClient {
pub grant_types: Vec<String>,
}
impl std::fmt::Debug for OAuthClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OAuthClient")
.field("id", &self.id)
.field("secret", &"***")
.field("display_name", &self.display_name)
.field("allowed_domains", &self.allowed_domains)
.field("connect_config", &self.connect_config)
.field("login_config", &self.login_config)
.field("tenant", &self.tenant)
.field("grant_types", &self.grant_types)
.finish()
}
}
fn empty_string() -> String {
"".to_string()
}
@@ -623,18 +608,7 @@ pub async fn refresh_token<'c>(
.await?;
let account = windmill_common::utils::not_found_if_none(account, "Account", &id.to_string())?;
refresh_token_for_account(
tx,
path,
w_id,
id,
db,
account,
oauth_clients,
http_client,
connect_configs_json,
)
.await
refresh_token_for_account(tx, path, w_id, id, db, account, oauth_clients, http_client, connect_configs_json).await
}
/// Refresh an OAuth token given pre-fetched account info (no additional SELECT).

View File

@@ -215,10 +215,7 @@ exit $exit_status
.current_dir(job_dir)
.env_clear()
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::Bash, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Bash).await?)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.args(cmd_args)
@@ -244,10 +241,7 @@ exit $exit_status
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::Bash, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Bash).await?)
.env("PATH", PATH_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
.env("HOME", HOME_ENV.as_str())

View File

@@ -1564,9 +1564,7 @@ try {{
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::Bun, &job.id, &job.workspace_id, conn).await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Bun).await?)
.envs(common_bun_proc_envs)
.env("PATH", PATH_ENV.as_str())
.args(args)
@@ -1584,10 +1582,7 @@ try {{
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::Bun, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Bun).await?)
.envs(common_bun_proc_envs)
.stdin(Stdio::null())
.stdout(Stdio::piped())
@@ -1618,10 +1613,7 @@ try {{
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::Bun, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Bun).await?)
.envs(common_bun_proc_envs)
.stdin(Stdio::null())
.stdout(Stdio::piped())

View File

@@ -600,10 +600,7 @@ pub async fn handle_csharp_job(
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::CSharp, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::CSharp).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
@@ -636,10 +633,7 @@ pub async fn handle_csharp_job(
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::CSharp, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::CSharp).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)

View File

@@ -121,13 +121,11 @@ async fn get_common_deno_proc_envs(
}
// Add proxy envs (including OTEL tracing proxy if enabled for deno)
if let Some(conn) = conn {
for (k, v) in get_proxy_envs_for_lang(&ScriptLang::Deno, job_id, w_id, conn)
.await
.unwrap_or_default()
{
deno_envs.insert(k.to_string(), v);
}
for (k, v) in get_proxy_envs_for_lang(&ScriptLang::Deno)
.await
.unwrap_or_default()
{
deno_envs.insert(k.to_string(), v);
}
return deno_envs;

View File

@@ -354,7 +354,7 @@ func Run(req Req) (interface{{}}, error){{
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(get_proxy_envs_for_lang(&ScriptLang::Go, &job.id, &job.workspace_id, conn).await?)
.envs(get_proxy_envs_for_lang(&ScriptLang::Go).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
@@ -375,7 +375,7 @@ func Run(req Req) (interface{{}}, error){{
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(get_proxy_envs_for_lang(&ScriptLang::Go, &job.id, &job.workspace_id, conn).await?)
.envs(get_proxy_envs_for_lang(&ScriptLang::Go).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)

View File

@@ -264,7 +264,7 @@ async fn run<'a>(
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables)
.envs(get_proxy_envs_for_lang(&ScriptLang::Nu, &job.id, &job.workspace_id, conn).await?)
.envs(get_proxy_envs_for_lang(&ScriptLang::Nu).await?)
.args(vec![
"--config",
"run.config.proto",
@@ -303,7 +303,7 @@ async fn run<'a>(
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(envs)
.envs(reserved_variables)
.envs(get_proxy_envs_for_lang(&ScriptLang::Nu, &job.id, &job.workspace_id, conn).await?)
.envs(get_proxy_envs_for_lang(&ScriptLang::Nu).await?)
// TODO(v1):
// "--plugins",
// &format!(

View File

@@ -841,10 +841,7 @@ mount {{
.env_clear()
// inject PYTHONPATH here - for some reason I had to do it in nsjail conf
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::Python3, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Python3).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
@@ -870,10 +867,7 @@ mount {{
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::Python3, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Python3).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)

View File

@@ -812,10 +812,7 @@ mount {{
.envs(envs)
.envs(reserved_variables)
.envs(RUBY_PROXY_ENVS.clone())
.envs(
get_proxy_envs_for_lang(&ScriptLang::Ruby, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Ruby).await?)
.args(vec![
"--config",
"run.config.proto",
@@ -854,10 +851,7 @@ mount {{
.env("BASE_INTERNAL_URL", base_internal_url)
.envs(reserved_variables)
.envs(RUBY_PROXY_ENVS.clone())
.envs(
get_proxy_envs_for_lang(&ScriptLang::Ruby, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Ruby).await?)
.envs(envs);
cmd.stdin(Stdio::null())

View File

@@ -700,10 +700,7 @@ pub async fn handle_rust_job(
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::Rust, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Rust).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)
@@ -719,10 +716,7 @@ pub async fn handle_rust_job(
.env_clear()
.envs(envs)
.envs(reserved_variables)
.envs(
get_proxy_envs_for_lang(&ScriptLang::Rust, &job.id, &job.workspace_id, conn)
.await?,
)
.envs(get_proxy_envs_for_lang(&ScriptLang::Rust).await?)
.env("PATH", PATH_ENV.as_str())
.env("TZ", TZ_ENV.as_str())
.env("BASE_INTERNAL_URL", base_internal_url)

View File

@@ -745,37 +745,21 @@ pub async fn is_otel_tracing_proxy_enabled_for_lang(lang: &ScriptLang) -> bool {
/// Otherwise, uses the standard HTTP_PROXY/HTTPS_PROXY from environment.
pub async fn get_proxy_envs_for_lang(
lang: &ScriptLang,
job_id: &uuid::Uuid,
w_id: &str,
conn: &Connection,
) -> anyhow::Result<Vec<(&'static str, String)>> {
#[cfg(all(feature = "private", feature = "enterprise"))]
if is_otel_tracing_proxy_enabled_for_lang(lang).await {
return get_otel_tracing_proxy_envs(job_id, w_id, conn).await;
return get_otel_tracing_proxy_envs().await;
}
let _ = (lang, job_id, w_id, conn);
let _ = lang;
Ok(PROXY_ENVS.clone())
}
#[cfg(all(feature = "private", feature = "enterprise"))]
async fn get_otel_tracing_proxy_envs(
job_id: &uuid::Uuid,
w_id: &str,
conn: &Connection,
) -> anyhow::Result<Vec<(&'static str, String)>> {
let port = match *crate::otel_tracing_proxy_ee::TRACING_PROXY_PORT
async fn get_otel_tracing_proxy_envs() -> anyhow::Result<Vec<(&'static str, String)>> {
let port = crate::otel_tracing_proxy_ee::TRACING_PROXY_PORT
.read()
.await
{
Some(p) => p,
None => {
let reason = "OTEL tracing proxy is enabled but not available (not initialized yet, or NUM_WORKERS > 1). \
This job's HTTP requests will not be traced.";
tracing::warn!("{}", reason);
append_logs(job_id, w_id, format!("\n[warning] {reason}\n"), conn).await;
return Ok(PROXY_ENVS.clone());
}
};
.ok_or_else(|| anyhow::anyhow!("OTEL tracing proxy port not initialized"))?;
let proxy_url = format!("http://127.0.0.1:{}", port);
Ok(vec![
("HTTP_PROXY", proxy_url.clone()),
@@ -3899,7 +3883,7 @@ pub async fn run_language_executor(
run_inline: bool,
) -> error::Result<Box<RawValue>> {
if language == Some(ScriptLang::Postgresql) {
return Box::pin(do_postgresql(
return do_postgresql(
job,
&client,
&code,
@@ -3911,7 +3895,7 @@ pub async fn run_language_executor(
occupancy_metrics,
parent_runnable_path,
run_inline,
))
)
.await;
} else if language == Some(ScriptLang::Mysql) {
#[cfg(not(feature = "mysql"))]
@@ -3926,7 +3910,7 @@ pub async fn run_language_executor(
"Inline execution is not yet supported for this language".to_string(),
));
}
return Box::pin(do_mysql(
return do_mysql(
job,
&client,
&code,
@@ -3937,7 +3921,7 @@ pub async fn run_language_executor(
column_order,
occupancy_metrics,
parent_runnable_path,
))
)
.await;
}
} else if language == Some(ScriptLang::Bigquery) {
@@ -3963,7 +3947,7 @@ pub async fn run_language_executor(
"Inline execution is not yet supported for this language".to_string(),
));
}
return Box::pin(do_bigquery(
return do_bigquery(
job,
&client,
&code,
@@ -3974,7 +3958,7 @@ pub async fn run_language_executor(
column_order,
occupancy_metrics,
parent_runnable_path,
))
)
.await;
}
} else if language == Some(ScriptLang::Snowflake) {
@@ -3992,7 +3976,7 @@ pub async fn run_language_executor(
"Inline execution is not yet supported for this language".to_string(),
));
}
return Box::pin(do_snowflake(
return do_snowflake(
job,
&client,
&code,
@@ -4003,7 +3987,7 @@ pub async fn run_language_executor(
column_order,
occupancy_metrics,
parent_runnable_path,
))
)
.await;
}
} else if language == Some(ScriptLang::Mssql) {
@@ -4029,7 +4013,7 @@ pub async fn run_language_executor(
"Inline execution is not yet supported for this language".to_string(),
));
}
return Box::pin(do_mssql(
return do_mssql(
job,
&client,
&code,
@@ -4040,7 +4024,7 @@ pub async fn run_language_executor(
occupancy_metrics,
job_dir,
parent_runnable_path,
))
)
.await;
}
} else if language == Some(ScriptLang::OracleDB) {
@@ -4066,7 +4050,7 @@ pub async fn run_language_executor(
"Inline execution is not yet supported for this language".to_string(),
));
}
return Box::pin(do_oracledb(
return do_oracledb(
job,
&client,
&code,
@@ -4077,7 +4061,7 @@ pub async fn run_language_executor(
column_order,
occupancy_metrics,
parent_runnable_path,
))
)
.await;
}
} else if language == Some(ScriptLang::DuckDb) {
@@ -4091,7 +4075,7 @@ pub async fn run_language_executor(
#[cfg(feature = "duckdb")]
{
return Box::pin(do_duckdb(
return do_duckdb(
job,
&client,
&code,
@@ -4103,7 +4087,7 @@ pub async fn run_language_executor(
occupancy_metrics,
parent_runnable_path,
run_inline,
))
)
.await;
}
} else if language == Some(ScriptLang::Graphql) {
@@ -4112,7 +4096,7 @@ pub async fn run_language_executor(
"Inline execution is not yet supported for this language".to_string(),
));
}
return Box::pin(do_graphql(
return do_graphql(
job,
&client,
&code,
@@ -4121,7 +4105,7 @@ pub async fn run_language_executor(
canceled_by,
worker_name,
occupancy_metrics,
))
)
.await;
} else if language == Some(ScriptLang::Nativets) {
if run_inline {
@@ -4148,7 +4132,7 @@ pub async fn run_language_executor(
.collect::<Vec<String>>()
.join("\n"));
let result = Box::pin(do_nativets(
let result = do_nativets(
job,
&client,
env_code,
@@ -4159,7 +4143,7 @@ pub async fn run_language_executor(
worker_name,
occupancy_metrics,
has_stream,
))
)
.await?;
return Ok(result);
}

View File

@@ -865,7 +865,7 @@ pub async fn update_flow_status_after_job_completion_internal(
.and_then(|x| x.stop_after_all_iters_if.as_ref())
{
let args = from_result_to_args(args.as_ref().await.get_ref())?;
if let Err(e) = evaluate_stop_after_all_iters_if(
evaluate_stop_after_all_iters_if(
db,
stop_after_all_iters_if,
module_status,
@@ -879,16 +879,7 @@ pub async fn update_flow_status_after_job_completion_internal(
flow,
&old_status,
)
.await
{
tracing::error!("error evaluating stop_after_all_iters_if: {e:#}");
stop_early = true;
skip_if_stop_early = false;
stop_early_err_msg = Some(format!(
"Error evaluating stop_after_all_iters_if expression `{}`: {e:#}",
stop_after_all_iters_if.expr
));
}
.await?;
}
let new_status = if
@@ -1083,7 +1074,7 @@ pub async fn update_flow_status_after_job_completion_internal(
{
let args = from_result_to_args(args.as_ref().await.get_ref())?;
if let Err(e) = evaluate_stop_after_all_iters_if(
evaluate_stop_after_all_iters_if(
db,
stop_after_all_iters_if,
module_status,
@@ -1097,15 +1088,7 @@ pub async fn update_flow_status_after_job_completion_internal(
flow,
&old_status,
)
.await
{
stop_early = true;
skip_if_stop_early = false;
stop_early_err_msg = Some(format!(
"Error evaluating stop_after_all_iters_if expression `{}`: {e:#}",
stop_after_all_iters_if.expr
));
}
.await?;
}
}

View File

@@ -15,7 +15,14 @@
};
}
let { connect_config = $bindable() }: Props = $props();
let { connect_config = $bindable({
scopes: ['offline_access'],
auth_url: '',
token_url: '',
req_body_auth: true,
extra_params: { tenant_id: '' },
extra_params_callback: {}
}) }: Props = $props();
run(() => {
if (!connect_config) {
@@ -31,14 +38,13 @@
});
run(() => {
if (connect_config?.extra_params?.tenant_id) {
if (connect_config.extra_params.tenant_id) {
connect_config.auth_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/authorize`
connect_config.token_url = `https://login.microsoftonline.com/${connect_config.extra_params.tenant_id}/oauth2/v2.0/token`
}
});
</script>
{#if connect_config}
<label class="flex flex-col gap-1" for="tenant-id">
<span class="text-primary font-semibold text-xs flex gap-2 items-center"> Azure tenant id </span>
<span class="text-secondary font-normal text-xs">
@@ -67,4 +73,3 @@
<OauthScopes bind:scopes={connect_config.scopes} />
</div>
</label>
{/if}

View File

@@ -6,7 +6,14 @@
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
let { connect_config = $bindable() } = $props();
let { connect_config = $bindable({
scopes: [],
auth_url: '',
token_url: '',
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
}) } = $props();
run(() => {
if (!connect_config) {

View File

@@ -6,7 +6,15 @@
import Toggle from './Toggle.svelte'
import Tooltip from './Tooltip.svelte'
let { login_config = $bindable() } = $props();
let { login_config = $bindable({
scopes: [],
auth_url: '',
token_url: '',
userinfo_url: '',
req_body_auth: false,
extra_params: {},
extra_params_callback: {}
}) } = $props();
run(() => {
if (!login_config) {

View File

@@ -109,7 +109,7 @@
}
let darkModeToggle: DarkModeToggle | undefined = $state()
let darkMode: boolean = $state(document.documentElement.classList.contains('dark'))
let darkMode: boolean | undefined = $state(undefined)
let modeInitialized = $state(false)
function initializeMode() {
modeInitialized = true

View File

@@ -6,15 +6,9 @@
extra_params?: Record<string, string>;
}
let { extra_params = $bindable() }: Props = $props();
let { extra_params = $bindable({}) }: Props = $props();
$effect.pre(() => {
if (!extra_params) {
extra_params = {}
}
})
let extra_params_vec: [string, string][] = $state(Object.entries(extra_params ?? {}))
let extra_params_vec: [string, string][] = $state(Object.entries(extra_params))
function sync() {
extra_params = Object.fromEntries(extra_params_vec)

View File

@@ -6,13 +6,7 @@
scopes?: string[]
}
let { scopes = $bindable() }: Props = $props()
$effect.pre(() => {
if (!scopes) {
scopes = []
}
})
let { scopes = $bindable([]) }: Props = $props()
</script>
{#if scopes && Array.isArray(scopes)}
@@ -24,7 +18,7 @@
size="xs"
btnClasses="mx-6"
on:click={() => {
scopes = scopes?.filter((el) => el != v)
scopes = scopes.filter((el) => el != v)
}}
startIcon={{ icon: Minus }}
iconOnly

View File

@@ -438,6 +438,7 @@
let cip
let extraModel
let width = $state(0)
// let widgets: HTMLElement | undefined = document.getElementById('monaco-widgets-root') ?? undefined
let initialized = $state(false)
@@ -544,6 +545,9 @@
if (divEl) {
divEl.style.height = `${contentHeight}px`
}
try {
editor?.layout({ width, height: contentHeight })
} catch {}
}
editor.onDidContentSizeChange(updateHeight)
updateHeight()
@@ -714,6 +718,7 @@
bind:this={divEl}
style="height: 18px;"
class="template nonmain-editor rounded-md overflow-clip {!editor ? 'hidden' : ''}"
bind:clientWidth={width}
></div>
</div>

View File

@@ -1,4 +1,5 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import type { DbType, DbInput } from '$lib/components/dbTypes'
import { buildParameters } from '../utils'
import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils'
@@ -141,28 +142,6 @@ export function makeCountQuery(
return query
}
export function buildCountMarker(
table: string,
columnDefs: ColumnDef[],
whereClause: string | undefined,
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
column_defs: columnDefs.map((c) => ({
field: c.field,
datatype: c.datatype,
isprimarykey: c.isprimarykey,
ignored: c.ignored ?? false
})),
where_clause: whereClause ?? null,
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_COUNT_SCRIPT\n${JSON.stringify(params)}`
}
export function getCountInput(
dbInput: DbInput,
table: string,
@@ -178,8 +157,8 @@ export function getCountInput(
return undefined
}
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
const query = buildCountMarker(table, columnDefs, whereClause, dbType, ducklake)
let query = makeCountQuery(dbType, table, whereClause, columnDefs)
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
const updateRunnable: RunnableByName = {
name: 'AppDbExplorer',

View File

@@ -1,5 +1,6 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import type { DbType, DbInput } from '$lib/components/dbTypes'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import { getLanguageByResourceType, type ColumnDef, buildParameters } from '../utils'
export function makeDeleteQuery(table: string, columns: ColumnDef[], dbType: DbType) {
@@ -65,21 +66,6 @@ export function makeDeleteQuery(table: string, columns: ColumnDef[], dbType: DbT
}
}
export function buildDeleteMarker(
table: string,
columns: ColumnDef[],
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
columns: columns.map((c) => ({ field: c.field, datatype: c.datatype })),
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_DELETE_SCRIPT\n${JSON.stringify(params)}`
}
export function getDeleteInput(
dbInput: DbInput,
table: string,
@@ -93,8 +79,8 @@ export function getDeleteInput(
return undefined
}
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
const query = buildDeleteMarker(table, columns, dbType, ducklake)
let query = makeDeleteQuery(table, columns, dbType)
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
const deleteRunnable: RunnableByName = {
name: 'AppDbExplorer',
type: 'inline',

View File

@@ -1,4 +1,5 @@
import type { AppInput } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import type { DbType, DbInput } from '$lib/components/dbTypes'
import { buildParameters, ColumnIdentity } from '../utils'
import { getLanguageByResourceType, type ColumnDef } from '../utils'
@@ -105,37 +106,10 @@ export function makeInsertQuery(table: string, columns: ColumnDef[], dbType: DbT
return query
}
export function buildInsertMarker(
table: string,
columns: ColumnDef[],
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
columns: columns.map((c) => ({
field: c.field,
datatype: c.datatype,
isprimarykey: c.isprimarykey,
ignored: c.ignored ?? false,
isnullable: c.isnullable ?? 'YES',
isidentity: c.isidentity ?? 'No',
defaultvalue: c.defaultvalue ?? null,
hideInsert: c.hideInsert ?? false,
overrideDefaultValue: c.overrideDefaultValue ?? false,
defaultUserValue: c.defaultUserValue ?? null,
defaultValueNull: c.defaultValueNull ?? false
})),
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_INSERT_SCRIPT\n${JSON.stringify(params)}`
}
export function getInsertInput(dbInput: DbInput, table: string, columns: ColumnDef[]): AppInput {
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
const query = buildInsertMarker(table, columns, dbType, ducklake)
let query = makeInsertQuery(table, columns, dbType)
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
return {
runnable: {
name: 'AppDbExplorer',

View File

@@ -1,4 +1,5 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import type { DbType, DbInput } from '$lib/components/dbTypes'
import { buildParameters } from '../utils'
import { getLanguageByResourceType, type ColumnDef, buildVisibleFieldList } from '../utils'
@@ -317,33 +318,6 @@ function coerceToNumber(value: any): number {
return 0
}
export function buildSelectMarker(
table: string,
columnDefs: ColumnDef[],
whereClause: string | undefined,
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
column_defs: columnDefs.map((c) => ({
field: c.field,
datatype: c.datatype,
isprimarykey: c.isprimarykey,
ignored: c.ignored ?? false,
editable: c.editable ?? false,
isnullable: c.isnullable ?? 'YES',
isidentity: c.isidentity ?? 'No',
defaultvalue: c.defaultvalue ?? null,
hideInsert: c.hideInsert ?? false
})),
where_clause: whereClause ?? null,
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_SELECT_SCRIPT\n${JSON.stringify(params)}`
}
export function getSelectInput(
dbInput: DbInput,
table: string | undefined,
@@ -361,8 +335,8 @@ export function getSelectInput(
}
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
const content = buildSelectMarker(table, columnDefs, whereClause, dbType, ducklake)
let content = makeSelectQuery(table, columnDefs, whereClause, dbType, options)
if (dbInput.type === 'ducklake') content = wrapDucklakeQuery(content, dbInput.ducklake)
const getRunnable: RunnableByName = {
name: 'AppDbExplorer',
type: 'inline',

View File

@@ -1,4 +1,5 @@
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
import { wrapDucklakeQuery } from '../../../../../ducklake'
import type { DbInput, DbType } from '$lib/components/dbTypes'
import { getLanguageByResourceType, type ColumnDef, buildParameters } from '../utils'
@@ -75,23 +76,6 @@ export function makeUpdateQuery(
}
}
export function buildUpdateMarker(
table: string,
column: { datatype: string; field: string },
columns: { datatype: string; field: string }[],
dbType: DbType,
ducklake?: string
): string {
const params: Record<string, unknown> = {
table,
column: { field: column.field, datatype: column.datatype },
columns: columns.map((c) => ({ field: c.field, datatype: c.datatype })),
db_type: dbType
}
if (ducklake) params.ducklake = ducklake
return `-- WM_INTERNAL_DB_UPDATE_SCRIPT\n${JSON.stringify(params)}`
}
export function getUpdateInput(
dbInput: DbInput,
table: string,
@@ -106,8 +90,8 @@ export function getUpdateInput(
return undefined
}
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
const query = buildUpdateMarker(table, column, columns, dbType, ducklake)
let query = makeUpdateQuery(table, column, columns, dbType)
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
const updateRunnable: RunnableByName = {
name: 'AppDbExplorer',

View File

@@ -4,12 +4,7 @@
import { type GridApi, createGrid, type IDatasource } from 'ag-grid-community'
import { sendUserToast } from '$lib/utils'
import { createEventDispatcher, getContext, mount, unmount, untrack } from 'svelte'
import {
type AppEditorContext,
type AppViewerContext,
type ComponentCustomCSS,
type ContextPanelContext
} from '../../../types'
import type { AppViewerContext, ComponentCustomCSS, ContextPanelContext } from '../../../types'
import type { TableAction, components } from '$lib/components/apps/editor/component'
import { deepEqual } from 'fast-equals'
@@ -67,15 +62,9 @@
const context = getContext<AppViewerContext>('AppViewerContext')
const contextPanel = getContext<ContextPanelContext>('ContextPanel')
const editorContext = getContext<AppEditorContext>('AppEditorContext')
const { app, selectedComponent, componentControl, darkMode, mode } = context
let css = $state(
initCss(
$app.css?.aggridcomponent,
untrack(() => customCss)
)
)
let css = $state(initCss($app.css?.aggridcomponent, untrack(() => customCss)))
let selectedRowIndex = -1
@@ -162,8 +151,7 @@
const componentContext = new Map<string, any>([
['AppViewerContext', context],
['ContextPanel', contextPanel],
['AppEditorContext', editorContext]
['ContextPanel', contextPanel]
])
const taComponent = withProps(AppAggridTableActions, {

View File

@@ -280,7 +280,10 @@
}
} catch {}
} else {
if ($focusedGrid?.parentComponentId !== befSelected) {
const drawerAlreadyHandledFocusedGrid =
item?.data.type === 'drawercomponent' &&
$focusedGrid?.parentComponentId === befSelected
if (!drawerAlreadyHandledFocusedGrid) {
$focusedGrid = undefined
}
}

View File

@@ -4159,16 +4159,14 @@ This is a paragraph.
type: 'static',
fieldType: 'resource',
subFieldType: 'mysql',
value: '',
allowTypeChange: false
value: ''
} as StaticAppInput,
table: {
fieldType: 'select',
subFieldType: 'db-table',
type: 'static',
selectOptions: [],
value: undefined,
allowTypeChange: false
value: undefined
}
},
ms_sql_server: {
@@ -4176,16 +4174,14 @@ This is a paragraph.
type: 'static',
fieldType: 'resource',
subFieldType: 'ms_sql_server',
value: '',
allowTypeChange: false
value: ''
} as StaticAppInput,
table: {
fieldType: 'select',
subFieldType: 'db-table',
type: 'static',
selectOptions: [],
value: undefined,
allowTypeChange: false
value: undefined
}
},
snowflake: {
@@ -4193,16 +4189,14 @@ This is a paragraph.
type: 'static',
fieldType: 'resource',
subFieldType: 'snowflake',
value: '',
allowTypeChange: false
value: ''
} as StaticAppInput,
table: {
fieldType: 'select',
subFieldType: 'db-table',
type: 'static',
selectOptions: [],
value: undefined,
allowTypeChange: false
value: undefined
}
},
bigquery: {
@@ -4210,16 +4204,14 @@ This is a paragraph.
type: 'static',
fieldType: 'resource',
subFieldType: 'bigquery',
value: '',
allowTypeChange: false
value: ''
} as StaticAppInput,
table: {
fieldType: 'select',
subFieldType: 'db-table',
type: 'static',
selectOptions: [],
value: undefined,
allowTypeChange: false
value: undefined
}
},
ducklake: {
@@ -4227,16 +4219,14 @@ This is a paragraph.
type: 'static',
fieldType: 'ducklake',
subFieldType: 'ducklake',
value: '',
allowTypeChange: false
value: ''
} as StaticAppInput,
table: {
fieldType: 'select',
subFieldType: 'db-table',
type: 'static',
selectOptions: [],
value: undefined,
allowTypeChange: false
value: undefined
}
},
datatable: {
@@ -4244,16 +4234,14 @@ This is a paragraph.
type: 'static',
fieldType: 'datatable',
subFieldType: 'datatable',
value: '',
allowTypeChange: false
value: ''
} as StaticAppInput,
table: {
fieldType: 'select',
subFieldType: 'db-table',
type: 'static',
selectOptions: [],
value: undefined,
allowTypeChange: false
value: undefined
}
}
}

View File

@@ -221,7 +221,7 @@
const pointerdown = ({ clientX, clientY }) => {
dragClosure = () => {
dragClosure = undefined
ctx?.componentActive.set(true)
ctx.componentActive.set(true)
initX = (clientX / $scale) * 100
initY = (clientY / $scale) * 100
@@ -401,7 +401,7 @@
}, 50)
const pointerup = (e) => {
ctx?.componentActive.set(false)
ctx.componentActive.set(false)
stopAutoscroll()
window.removeEventListener('pointerdown', pointerdown)

View File

@@ -3,12 +3,12 @@ import {
type ColumnDef,
type TableMetadata
} from './apps/components/display/dbtable/utils'
import { buildSelectMarker } from './apps/components/display/dbtable/queries/select'
import { makeSelectQuery } from './apps/components/display/dbtable/queries/select'
import { runScriptAndPollResult } from './jobs/utils'
import { buildCountMarker } from './apps/components/display/dbtable/queries/count'
import { buildUpdateMarker } from './apps/components/display/dbtable/queries/update'
import { buildDeleteMarker } from './apps/components/display/dbtable/queries/delete'
import { buildInsertMarker } from './apps/components/display/dbtable/queries/insert'
import { makeCountQuery } from './apps/components/display/dbtable/queries/count'
import { makeUpdateQuery } from './apps/components/display/dbtable/queries/update'
import { makeDeleteQuery } from './apps/components/display/dbtable/queries/delete'
import { makeInsertQuery } from './apps/components/display/dbtable/queries/insert'
import { makeDeleteTableQuery } from './apps/components/display/dbtable/queries/deleteTable'
import type { DBSchema, SQLSchema } from '$lib/stores'
import { stringifySchema } from './copilot/lib'
@@ -68,8 +68,8 @@ export function dbTableOpsWithPreviewScripts({
tableKey,
colDefs,
getCount: async ({ quicksearch }) => {
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
const countQuery = buildCountMarker(tableKey, colDefs, undefined, dbType, ducklake)
let countQuery = makeCountQuery(dbType, tableKey, undefined, colDefs)
if (input.type === 'ducklake') countQuery = wrapDucklakeQuery(countQuery, input.ducklake)
const result = await runScriptAndPollResult({
workspace,
requestBody: { args: { ...dbArg, quicksearch }, language, content: countQuery }
@@ -78,8 +78,10 @@ export function dbTableOpsWithPreviewScripts({
return count
},
getRows: async (params) => {
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
const query = buildSelectMarker(tableKey, colDefs, undefined, dbType, ducklake)
let query = makeSelectQuery(tableKey, colDefs, undefined, dbType, undefined, {
fixPgIntTypes: true
})
if (input.type === 'ducklake') query = wrapDucklakeQuery(query, input.ducklake)
let items = (await runScriptAndPollResult({
workspace,
requestBody: { args: { ...dbArg, ...params }, language, content: query }
@@ -90,8 +92,8 @@ export function dbTableOpsWithPreviewScripts({
return items
},
onUpdate: async ({ values }, colDef, newValue) => {
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
const updateQuery = buildUpdateMarker(tableKey, colDef, colDefs, dbType, ducklake)
let updateQuery = makeUpdateQuery(tableKey, colDef, colDefs, dbType)
if (input.type === 'ducklake') updateQuery = wrapDucklakeQuery(updateQuery, input.ducklake)
await runScriptAndPollResult({
workspace,
requestBody: {
@@ -102,16 +104,16 @@ export function dbTableOpsWithPreviewScripts({
})
},
onDelete: async ({ values }) => {
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
const deleteQuery = buildDeleteMarker(tableKey, colDefs, dbType, ducklake)
let deleteQuery = makeDeleteQuery(tableKey, colDefs, dbType)
if (input.type === 'ducklake') deleteQuery = wrapDucklakeQuery(deleteQuery, input.ducklake)
await runScriptAndPollResult({
workspace,
requestBody: { args: { ...dbArg, ...values }, language, content: deleteQuery }
})
},
onInsert: async ({ values }) => {
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
const insertQuery = buildInsertMarker(tableKey, colDefs, dbType, ducklake)
let insertQuery = makeInsertQuery(tableKey, colDefs, dbType)
if (input.type === 'ducklake') insertQuery = wrapDucklakeQuery(insertQuery, input.ducklake)
await runScriptAndPollResult({
workspace,
requestBody: { args: { ...dbArg, ...values }, language, content: insertQuery }

View File

@@ -130,8 +130,7 @@
const old = v.assets?.find((a) => assetEq(a, asset))
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
}
const normalizedAssets = newAssets.length > 0 ? newAssets : undefined
if (!deepEqual(v.assets, normalizedAssets)) v.assets = normalizedAssets
if (!deepEqual(v.assets, newAssets)) v.assets = newAssets
}
// Check for raw script modules whose assets were not parsed. Useful for flows created

View File

@@ -142,7 +142,6 @@
<div class="max-h-[300px]">
{#key items}
{#if items.length > 0}
<VirtualList height={300} width="100%" itemCount={items.length} itemSize={24}>
{#snippet header()}{/snippet}
{#snippet footer()}{/snippet}
@@ -171,9 +170,6 @@
</div>
{/snippet}
</VirtualList>
{:else}
<div class="text-xs text-tertiary py-2 px-2">No iterations</div>
{/if}
{/key}
<!-- {#each flowJobs ?? [] as id, idx (id)}

View File

@@ -52,7 +52,7 @@
}
let darkModeToggle: DarkModeToggle | undefined = $state()
let darkMode: boolean = $state(document.documentElement.classList.contains('dark'))
let darkMode: boolean | undefined = $state(undefined)
let modeInitialized = $state(false)
function initializeMode() {
modeInitialized = true

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/worktree-common.sh"
backend_port="${BACKEND_PORT:-}"
frontend_port="${FRONTEND_PORT:-}"
if [[ -z "$backend_port" || -z "$frontend_port" ]]; then
echo "Missing BACKEND_PORT or FRONTEND_PORT in hook environment" >&2
exit 1
fi
cat > .env.local <<EOF
BACKEND_PORT=$backend_port
FRONTEND_PORT=$frontend_port
REMOTE=http://localhost:$backend_port
EOF
if [[ -n "${CARGO_FEATURES:-}" ]]; then
echo "CARGO_FEATURES=$CARGO_FEATURES" >> .env.local
fi
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
wm_shared_post_create "$(pwd)"

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/worktree-common.sh"
wm_kill_processes_from_env_file "$(pwd)/.env.local"
wm_shared_pre_remove "$(pwd)"

View File

@@ -1,8 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/worktree-common.sh"
# Use WM_WORKTREE_PATH (set by workmux) so this works regardless of cwd
wt_dir="${WM_WORKTREE_PATH:-.}"
wm_kill_processes_from_env_file "${wt_dir}/.env.local"
wm_shared_pre_remove "$wt_dir"
echo "[cleanup] cwd=$(pwd) WM_WORKTREE_PATH=${WM_WORKTREE_PATH:-<unset>} wt_dir=$wt_dir"
# Kill backend/frontend processes using this worktree's ports
if [ -f "$wt_dir/.env.local" ]; then
source "$wt_dir/.env.local"
echo "[cleanup] .env.local found: BACKEND_PORT=${BACKEND_PORT:-<unset>} FRONTEND_PORT=${FRONTEND_PORT:-<unset>}"
for port in "${BACKEND_PORT:-}" "${FRONTEND_PORT:-}"; do
[ -z "$port" ] && continue
pid=$(lsof -ti "TCP:${port}" -sTCP:LISTEN 2>/dev/null || true)
if [ -n "$pid" ]; then
kill "$pid" 2>/dev/null && echo "[cleanup] Killed process $pid on port $port" \
|| echo "[cleanup] Warning: Could not kill process $pid on port $port"
else
echo "[cleanup] No process listening on port $port"
fi
done
else
echo "[cleanup] No .env.local at $wt_dir/.env.local"
fi
# Drop per-worktree database
if [ -n "${WM_DB_NAME:-}" ]; then
db_conn="postgres://postgres:changeme@127.0.0.1:5432"
if command -v psql &>/dev/null; then
psql "$db_conn/postgres" -c "DROP DATABASE IF EXISTS ${WM_DB_NAME} WITH (FORCE)" 2>/dev/null \
&& echo "[cleanup] Dropped database $WM_DB_NAME" \
|| echo "[cleanup] Warning: Could not drop database $WM_DB_NAME"
else
echo "[cleanup] psql not found, skipping database cleanup for $WM_DB_NAME"
fi
else
echo "[cleanup] No WM_DB_NAME in .env.local, skipping database cleanup"
fi
# Remove the matching windmill-ee-private worktree if one exists
wt_basename=$(basename "$wt_dir")
# Find ee repo using same discovery logic as worktree-env
main_repo_root="$(cd "$(git -C "$wt_dir" rev-parse --git-common-dir 2>/dev/null)/.." && pwd)"
parent_dir="$(cd "$wt_dir/.." && pwd)"
echo "[cleanup] wt_basename=$wt_basename main_repo_root=$main_repo_root parent_dir=$parent_dir"
ee_repo=""
for candidate in \
"${main_repo_root:+${main_repo_root}/../windmill-ee-private}" \
"${parent_dir}/windmill-ee-private" \
"${HOME}/windmill-ee-private" \
"${HOME}/projects/windmill-ee-private"; do
if [ -n "$candidate" ] && [ -d "$candidate" ]; then
ee_repo="$(cd "$candidate" && pwd)"
break
fi
done
if [ -z "$ee_repo" ]; then
echo "[cleanup] Could not find windmill-ee-private repo, skipping EE worktree cleanup"
fi
ee_worktree_dir="${ee_repo:+${ee_repo}__worktrees/${wt_basename}}"
echo "[cleanup] ee_repo=${ee_repo:-<not found>} ee_worktree_dir=${ee_worktree_dir:-<none>} exists=$([ -n "$ee_worktree_dir" ] && [ -d "$ee_worktree_dir" ] && echo yes || echo no)"
if [ -n "$ee_worktree_dir" ] && [ -d "$ee_worktree_dir" ]; then
git -C "$ee_repo" worktree remove "$ee_worktree_dir" --force 2>/dev/null \
&& echo "[cleanup] Removed EE worktree at $ee_worktree_dir" \
|| echo "[cleanup] Warning: Could not remove EE worktree at $ee_worktree_dir"
fi
# Clean up Cursor grouped tmux session
tmux kill-session -t "cursor-${wt_basename}" 2>/dev/null || true

View File

@@ -1,265 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
wm_is_true() {
case "${1:-}" in
1|true|TRUE|yes|YES|on|ON) return 0 ;;
*) return 1 ;;
esac
}
wm_main_repo_root() {
local repo_root=${1:-.}
cd "$(git -C "$repo_root" rev-parse --git-common-dir 2>/dev/null)/.." && pwd
}
wm_setup_database() {
local repo_root=$1
local env_file=$2
local wt_basename db_name db_conn db_url license_key
wt_basename="$(basename "$repo_root")"
db_name="windmill_${wt_basename//-/_}"
db_conn="postgres://postgres:changeme@127.0.0.1:5432"
if ! command -v psql >/dev/null 2>&1; then
echo "WARNING: psql not found, skipping per-worktree database creation" >&2
return
fi
if psql "$db_conn/postgres" -tc "SELECT 1 FROM pg_database WHERE datname = '${db_name}'" 2>/dev/null | grep -q 1; then
echo "Database $db_name already exists"
else
if wm_is_true "${WM_CLONE_DB:-}"; then
psql "$db_conn/postgres" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'windmill' AND pid <> pg_backend_pid();" 2>/dev/null || true
psql "$db_conn/postgres" -c "CREATE DATABASE ${db_name} TEMPLATE windmill" 2>/dev/null \
&& echo "Created database $db_name (template: windmill)" \
|| echo "WARNING: Could not create database $db_name from template windmill" >&2
else
psql "$db_conn/postgres" -c "CREATE DATABASE ${db_name}" 2>/dev/null \
&& echo "Created database $db_name" \
|| echo "WARNING: Could not create database $db_name (is PostgreSQL running?)" >&2
fi
fi
db_url="${db_conn}/${db_name}?sslmode=disable"
DATABASE_URL="$db_url" sqlx migrate run --source "${repo_root}/backend/migrations" \
&& echo "Migrations applied to $db_name" \
|| echo "WARNING: Could not run migrations on $db_name" >&2
license_key="$(psql "$db_conn/windmill" -t -A -c "SELECT value FROM global_settings WHERE name = 'license_key'" 2>/dev/null || true)"
if [[ -n "$license_key" ]]; then
psql "$db_url" -c "INSERT INTO global_settings (name, value) VALUES ('license_key', '${license_key}'::jsonb) ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value" 2>/dev/null \
&& echo "Copied license_key to $db_name" \
|| echo "WARNING: Could not copy license_key to $db_name" >&2
fi
cat >> "$env_file" <<EOF
export DATABASE_URL=$db_url
WM_DB_NAME=$db_name
EOF
echo "Added DATABASE_URL for database $db_name to .env.local"
}
wm_copy_dependencies() {
local repo_root=$1
local main_repo_root=$2
if [[ -d "${main_repo_root}/frontend/node_modules" ]]; then
cp -a "${main_repo_root}/frontend/node_modules" "${repo_root}/frontend/"
echo "Copied frontend/node_modules (with symlinks preserved)"
fi
if [[ -d "${main_repo_root}/cli/node_modules" ]]; then
cp -a "${main_repo_root}/cli/node_modules" "${repo_root}/cli/"
echo "Copied cli/node_modules (with symlinks preserved)"
fi
if [[ -d "${repo_root}/cli" ]]; then
(cd "${repo_root}/cli" && npm install && npm run gen-client) \
&& echo "CLI deps installed and client generated" \
|| echo "WARNING: CLI setup failed" >&2
fi
}
wm_allow_direnv() {
local repo_root=$1
if command -v direnv >/dev/null 2>&1 && [[ -f "${repo_root}/.envrc" ]]; then
(cd "$repo_root" && direnv allow)
echo "direnv allowed"
fi
}
wm_trust_claude() {
local repo_root=$1
local claude_json="${HOME}/.claude.json"
if [[ ! -f "$claude_json" ]] || ! command -v python3 >/dev/null 2>&1; then
return
fi
REPO_ROOT="$repo_root" CLAUDE_JSON="$claude_json" python3 - <<'PY' \
&& echo "Added $repo_root to Claude Code trusted directories" \
|| echo "Warning: Could not update Claude Code trusted directories"
import json
import os
path = os.environ["REPO_ROOT"]
claude_json = os.environ["CLAUDE_JSON"]
with open(claude_json, "r") as f:
data = json.load(f)
projects = data.setdefault("projects", {})
proj = projects.setdefault(path, {})
proj["hasTrustDialogAccepted"] = True
proj["hasCompletedProjectOnboarding"] = True
with open(claude_json, "w") as f:
json.dump(data, f, indent=2)
PY
}
wm_find_ee_repo() {
local repo_root=$1
local main_repo_root=$2
local candidate
for candidate in \
"${main_repo_root}/../windmill-ee-private" \
"${repo_root}/../windmill-ee-private" \
"${HOME}/windmill-ee-private" \
"${HOME}/projects/windmill-ee-private"; do
if [[ -d "$candidate" ]]; then
cd "$candidate" && pwd
return 0
fi
done
return 1
}
wm_setup_ee_worktree() {
local repo_root=$1
local main_repo_root=$2
local ee_repo branch wt_basename ee_worktree_dir ee_rel rust_plugin
if ! ee_repo="$(wm_find_ee_repo "$repo_root" "$main_repo_root")"; then
return
fi
branch="$(git -C "$repo_root" branch --show-current 2>/dev/null || true)"
wt_basename="$(basename "$repo_root")"
ee_worktree_dir="${ee_repo}__worktrees/${wt_basename}"
if [[ -n "$branch" && ! -d "$ee_worktree_dir" ]]; then
mkdir -p "$(dirname "$ee_worktree_dir")"
git -C "$ee_repo" fetch --quiet 2>/dev/null || true
if git -C "$ee_repo" worktree add "$ee_worktree_dir" "$branch" 2>/dev/null; then
echo "Created EE worktree at $ee_worktree_dir (branch: $branch)"
elif git -C "$ee_repo" worktree add -b "$branch" "$ee_worktree_dir" main 2>/dev/null; then
echo "Created EE worktree at $ee_worktree_dir (new branch: $branch from main)"
else
echo "Warning: Could not create EE worktree for branch $branch"
fi
elif [[ -d "$ee_worktree_dir" ]]; then
echo "EE worktree already exists at $ee_worktree_dir"
fi
if [[ ! -d "$ee_worktree_dir" ]]; then
return
fi
ee_rel="$(REPO_ROOT="$repo_root" EE_WORKTREE_DIR="$ee_worktree_dir" python3 - <<'PY' 2>/dev/null || echo "$ee_worktree_dir"
import os
print(os.path.relpath(os.environ["EE_WORKTREE_DIR"], os.environ["REPO_ROOT"]))
PY
)"
mkdir -p "${repo_root}/.claude"
rust_plugin=""
if wm_is_true "${USE_RUST_PLUGIN:-}"; then
rust_plugin=',
"enabledPlugins": {
"rust-analyzer-lsp@claude-plugins-official": true
}'
fi
cat > "${repo_root}/.claude/settings.local.json" <<EOF
{
"permissions": {
"additionalDirectories": [
"$ee_rel"
]
}${rust_plugin}
}
EOF
echo "Created .claude/settings.local.json with EE path: $ee_rel"
if [[ -x "${repo_root}/backend/substitute_ee_code.sh" ]]; then
"${repo_root}/backend/substitute_ee_code.sh" -d "$ee_worktree_dir"
fi
}
wm_shared_post_create() {
local repo_root=$1
local main_repo_root
main_repo_root="$(wm_main_repo_root "$repo_root")"
wm_setup_database "$repo_root" "${repo_root}/.env.local"
wm_copy_dependencies "$repo_root" "$main_repo_root"
wm_allow_direnv "$repo_root"
wm_trust_claude "$repo_root"
wm_setup_ee_worktree "$repo_root" "$main_repo_root"
}
wm_kill_processes_from_env_file() {
local env_file=$1
local pid port
if [[ ! -f "$env_file" ]]; then
return
fi
# shellcheck disable=SC1090
source "$env_file"
for port in "${BACKEND_PORT:-}" "${FRONTEND_PORT:-}"; do
[[ -z "$port" ]] && continue
pid="$(lsof -ti "TCP:${port}" -sTCP:LISTEN 2>/dev/null || true)"
if [[ -n "$pid" ]]; then
kill "$pid" 2>/dev/null && echo "Killed process $pid on port $port" \
|| echo "Warning: Could not kill process $pid on port $port"
fi
done
}
wm_shared_pre_remove() {
local repo_root=$1
local env_file="${repo_root}/.env.local"
local db_conn wt_basename main_repo_root ee_repo ee_worktree_dir
if [[ -f "$env_file" ]]; then
# shellcheck disable=SC1090
source "$env_file"
fi
if [[ -n "${WM_DB_NAME:-}" ]]; then
db_conn="postgres://postgres:changeme@127.0.0.1:5432"
if command -v psql >/dev/null 2>&1; then
psql "$db_conn/postgres" -c "DROP DATABASE IF EXISTS ${WM_DB_NAME} WITH (FORCE)" 2>/dev/null \
&& echo "Dropped database $WM_DB_NAME" \
|| echo "Warning: Could not drop database $WM_DB_NAME"
else
echo "psql not found, skipping database cleanup for $WM_DB_NAME"
fi
fi
main_repo_root="$(wm_main_repo_root "$repo_root")"
wt_basename="$(basename "$repo_root")"
if ee_repo="$(wm_find_ee_repo "$repo_root" "$main_repo_root")"; then
ee_worktree_dir="${ee_repo}__worktrees/${wt_basename}"
if [[ -d "$ee_worktree_dir" ]]; then
git -C "$ee_repo" worktree remove "$ee_worktree_dir" --force 2>/dev/null \
&& echo "Removed EE worktree at $ee_worktree_dir" \
|| echo "Warning: Could not remove EE worktree at $ee_worktree_dir"
fi
fi
tmux kill-session -t "cursor-${wt_basename}" 2>/dev/null || true
}

View File

@@ -1,8 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/worktree-common.sh"
port_in_use() {
lsof -nP -iTCP:"$1" -sTCP:LISTEN &>/dev/null
}
@@ -53,4 +51,154 @@ if [[ -n "${CARGO_FEATURES:-}" ]]; then
fi
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
wm_shared_post_create "$(pwd)"
# --- Create per-worktree database ---
wt_basename=$(basename "$(pwd)")
db_name="windmill_${wt_basename//-/_}"
db_conn="postgres://postgres:changeme@127.0.0.1:5432"
if command -v psql &>/dev/null; then
if psql "$db_conn/postgres" -tc "SELECT 1 FROM pg_database WHERE datname = '${db_name}'" 2>/dev/null | grep -q 1; then
echo "Database $db_name already exists"
else
if [[ "${WM_CLONE_DB:-}" == "1" || "${WM_CLONE_DB:-}" == "true" ]]; then
# Terminate active connections so CREATE DATABASE ... TEMPLATE works
psql "$db_conn/postgres" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'windmill' AND pid <> pg_backend_pid();" 2>/dev/null || true
psql "$db_conn/postgres" -c "CREATE DATABASE ${db_name} TEMPLATE windmill" 2>/dev/null \
&& echo "Created database $db_name (template: windmill)" \
|| echo "WARNING: Could not create database $db_name from template windmill" >&2
else
psql "$db_conn/postgres" -c "CREATE DATABASE ${db_name}" 2>/dev/null \
&& echo "Created database $db_name" \
|| echo "WARNING: Could not create database $db_name (is PostgreSQL running?)" >&2
fi
fi
db_url="${db_conn}/${db_name}?sslmode=disable"
# Run migrations against the new database
DATABASE_URL="$db_url" sqlx migrate run --source backend/migrations \
&& echo "Migrations applied to $db_name" \
|| echo "WARNING: Could not run migrations on $db_name" >&2
# Copy license_key from the main windmill database to the new database
license_key=$(psql "$db_conn/windmill" -t -A -c "SELECT value FROM global_settings WHERE name = 'license_key'" 2>/dev/null || true)
if [[ -n "$license_key" ]]; then
psql "$db_url" -c "INSERT INTO global_settings (name, value) VALUES ('license_key', '${license_key}'::jsonb) ON CONFLICT (name) DO UPDATE SET value = EXCLUDED.value" 2>/dev/null \
&& echo "Copied license_key to $db_name" \
|| echo "WARNING: Could not copy license_key to $db_name" >&2
fi
# Use export so DATABASE_URL overrides the nix devshell value for child processes
cat >> .env.local <<EOF
export DATABASE_URL=$db_url
WM_DB_NAME=$db_name
EOF
echo "Added DATABASE_URL for database $db_name to .env.local"
else
echo "WARNING: psql not found, skipping per-worktree database creation" >&2
fi
# --- Copy frontend/node_modules preserving symlinks ---
# cp -a preserves .bin/ symlinks that cp -r would dereference, breaking require() paths
main_repo_root="$(cd "$(git rev-parse --git-common-dir 2>/dev/null)/.." && pwd)"
if [[ -n "$main_repo_root" && -d "$main_repo_root/frontend/node_modules" ]]; then
cp -a "$main_repo_root/frontend/node_modules" frontend/
echo "Copied frontend/node_modules (with symlinks preserved)"
fi
# --- Install cli deps and generate client ---
if [[ -n "$main_repo_root" && -d "$main_repo_root/cli/node_modules" ]]; then
cp -a "$main_repo_root/cli/node_modules" cli/
echo "Copied cli/node_modules (with symlinks preserved)"
fi
(cd cli && npm install && npm run gen-client) \
&& echo "CLI deps installed and client generated" \
|| echo "WARNING: CLI setup failed" >&2
# --- Allow direnv so the nix devshell activates in pane commands ---
if command -v direnv &>/dev/null && [ -f .envrc ]; then
direnv allow
echo "direnv allowed"
fi
# --- Trust worktree directory in Claude Code ---
claude_json="$HOME/.claude.json"
if [ -f "$claude_json" ]; then
wt_path="$(pwd)"
python3 -c "
import json, sys
path = '$wt_path'
with open('$claude_json', 'r') as f:
data = json.load(f)
projects = data.setdefault('projects', {})
proj = projects.setdefault(path, {})
proj['hasTrustDialogAccepted'] = True
proj['hasCompletedProjectOnboarding'] = True
with open('$claude_json', 'w') as f:
json.dump(data, f, indent=2)
" && echo "Added $wt_path to Claude Code trusted directories" \
|| echo "Warning: Could not update Claude Code trusted directories"
fi
# --- Create matching windmill-ee-private worktree ---
# Find ee repo: sibling to the main worktree (git toplevel of the main checkout),
# then try parent of cwd, then fall back to home
ee_repo=""
for candidate in \
"${main_repo_root:+${main_repo_root}/../windmill-ee-private}" \
"$(pwd)/../windmill-ee-private" \
"${HOME}/windmill-ee-private" \
"${HOME}/projects/windmill-ee-private"; do
if [ -n "$candidate" ] && [ -d "$candidate" ]; then
ee_repo="$(cd "$candidate" && pwd)"
break
fi
done
if [ -n "$ee_repo" ]; then
branch=$(git branch --show-current 2>/dev/null || true)
wt_basename=$(basename "$(pwd)")
ee_worktree_dir="${ee_repo}__worktrees/${wt_basename}"
if [ -n "$branch" ] && [ ! -d "$ee_worktree_dir" ]; then
mkdir -p "$(dirname "$ee_worktree_dir")"
# Fetch latest so we can check out remote branches
git -C "$ee_repo" fetch --quiet 2>/dev/null || true
# Try: existing branch, then new branch from main
if git -C "$ee_repo" worktree add "$ee_worktree_dir" "$branch" 2>/dev/null; then
echo "Created EE worktree at $ee_worktree_dir (branch: $branch)"
elif git -C "$ee_repo" worktree add -b "$branch" "$ee_worktree_dir" main 2>/dev/null; then
echo "Created EE worktree at $ee_worktree_dir (new branch: $branch from main)"
else
echo "Warning: Could not create EE worktree for branch $branch"
fi
elif [ -d "$ee_worktree_dir" ]; then
echo "EE worktree already exists at $ee_worktree_dir"
fi
# Point Claude Code additionalDirectories at the EE worktree
if [ -d "$ee_worktree_dir" ]; then
ee_rel=$(python3 -c "import os; print(os.path.relpath('$ee_worktree_dir', '$(pwd)'))" 2>/dev/null || echo "$ee_worktree_dir")
mkdir -p .claude
rust_plugin=""
if [[ "${USE_RUST_PLUGIN:-}" == "1" || "${USE_RUST_PLUGIN:-}" == "true" ]]; then
rust_plugin=',
"enabledPlugins": {
"rust-analyzer-lsp@claude-plugins-official": true
}'
fi
cat > .claude/settings.local.json <<EOFCLAUDE
{
"permissions": {
"additionalDirectories": [
"$ee_rel"
]
}${rust_plugin}
}
EOFCLAUDE
echo "Created .claude/settings.local.json with EE path: $ee_rel"
# Create symlinks from backend crates to the EE worktree
if [ -x "./backend/substitute_ee_code.sh" ]; then
./backend/substitute_ee_code.sh -d "$ee_worktree_dir"
fi
fi
fi