Compare commits
16 Commits
debounce-a
...
di/db-mana
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bafef661c | ||
|
|
2828616a79 | ||
|
|
73d27e92dd | ||
|
|
41e523f827 | ||
|
|
8b1fe8f9de | ||
|
|
c97cf604ab | ||
|
|
5ba4029d86 | ||
|
|
e75763dbe5 | ||
|
|
ce8ac9cf52 | ||
|
|
7e7d7645e2 | ||
|
|
037035e094 | ||
|
|
24078d736c | ||
|
|
3a2258745d | ||
|
|
0330993cb6 | ||
|
|
1d78589940 | ||
|
|
822723ae69 |
139
.webmux.yaml
139
.webmux.yaml
@@ -1,113 +1,52 @@
|
||||
name: Windmill
|
||||
# Project display name in the dashboard
|
||||
name: windmill
|
||||
|
||||
workspace:
|
||||
mainBranch: main
|
||||
worktreeRoot: ../__worktrees
|
||||
defaultAgent: claude
|
||||
|
||||
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: BE
|
||||
- name: backend
|
||||
portEnv: BACKEND_PORT
|
||||
- name: FE
|
||||
portStart: 8000
|
||||
portStep: 10
|
||||
- name: frontend
|
||||
portEnv: FRONTEND_PORT
|
||||
portStart: 3000
|
||||
portStep: 10
|
||||
|
||||
profiles:
|
||||
default:
|
||||
name: 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
|
||||
|
||||
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
|
||||
integrations:
|
||||
github:
|
||||
linkedRepos: []
|
||||
linear:
|
||||
enabled: true
|
||||
|
||||
21
CLAUDE.md
21
CLAUDE.md
@@ -26,6 +26,27 @@ 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
|
||||
|
||||
@@ -1 +1 @@
|
||||
f9549c813b3dba5324ea9d1edacc8756a6d699bf
|
||||
716b350bce1730b302c66ea69df618fa40f2f16b
|
||||
|
||||
@@ -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_]*)%%\s*([\s\w\/]+)?(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
|
||||
static ref RE_ARG_SQL_INTERPOLATION: Regex = Regex::new(r#"(?m)^--\s*%%([a-z_][a-z0-9_]*)%%[ \t]*([\w][\w \t\/]*)?(?: ?\= ?(.+))? *(?:\r|\n|$)"#).unwrap();
|
||||
}
|
||||
|
||||
fn parsed_default(parsed_typ: &Typ, default: String) -> Option<serde_json::Value> {
|
||||
@@ -1547,4 +1547,36 @@ 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3548,3 +3548,170 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -1877,6 +1877,15 @@ 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);
|
||||
@@ -2138,13 +2147,30 @@ 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) => (JobPayload::Code(raw_code), None, None),
|
||||
(None, Some(raw_code), None) => {
|
||||
let raw_code = maybe_replace_internal_db_script(raw_code);
|
||||
(JobPayload::Code(raw_code), None, None)
|
||||
}
|
||||
// inline script: in "run" mode and with an entry in the `app_script` table.
|
||||
(None, Some(RawCode { language, path, cache_ttl, .. }), Some(id)) => (
|
||||
JobPayload::AppScript { id: AppScriptId(id), cache_ttl, language, path },
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(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,
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let tx = PushIsolationLevel::IsolatedRoot(db.clone());
|
||||
|
||||
@@ -284,6 +284,9 @@ 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")
|
||||
|
||||
1109
backend/windmill-api/src/db_studio_scripts.rs
Normal file
1109
backend/windmill-api/src/db_studio_scripts.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4612,21 +4612,26 @@ async fn run_preview_script(
|
||||
match preview.kind {
|
||||
Some(PreviewKind::Identity) => JobPayload::Identity,
|
||||
Some(PreviewKind::Noop) => JobPayload::Noop,
|
||||
_ => 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,
|
||||
}),
|
||||
_ => {
|
||||
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,
|
||||
})
|
||||
}
|
||||
},
|
||||
push_args,
|
||||
authed.display_username(),
|
||||
|
||||
@@ -76,6 +76,7 @@ mod bedrock;
|
||||
mod capture;
|
||||
mod concurrency_groups;
|
||||
mod db;
|
||||
mod db_studio_scripts;
|
||||
|
||||
mod drafts;
|
||||
#[cfg(feature = "private")]
|
||||
|
||||
@@ -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, Debug)]
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[cfg_attr(feature = "instance_config_schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum StringOrSecretRef {
|
||||
@@ -53,6 +53,16 @@ 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> {
|
||||
@@ -255,25 +265,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<String>,
|
||||
pub pip_index_url: Option<StringOrSecretRef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pip_extra_index_url: Option<String>,
|
||||
pub pip_extra_index_url: Option<StringOrSecretRef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub npm_config_registry: Option<String>,
|
||||
pub npm_config_registry: Option<StringOrSecretRef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bunfig_install_scopes: Option<String>,
|
||||
pub bunfig_install_scopes: Option<StringOrSecretRef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub npmrc: Option<String>,
|
||||
pub npmrc: Option<StringOrSecretRef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nuget_config: Option<String>,
|
||||
pub nuget_config: Option<StringOrSecretRef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub maven_repos: Option<String>,
|
||||
pub maven_repos: Option<StringOrSecretRef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ruby_repos: Option<String>,
|
||||
pub ruby_repos: Option<StringOrSecretRef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub powershell_repo_url: Option<String>,
|
||||
pub powershell_repo_url: Option<StringOrSecretRef>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub powershell_repo_pat: Option<String>,
|
||||
pub powershell_repo_pat: Option<StringOrSecretRef>,
|
||||
|
||||
// Array settings
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -152,6 +152,21 @@ 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,
|
||||
@@ -162,6 +177,13 @@ 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)),
|
||||
|
||||
@@ -94,7 +94,7 @@ pub struct OAuthConfig {
|
||||
}
|
||||
|
||||
/// OAuth client credentials
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct OAuthClient {
|
||||
#[serde(default = "empty_string")]
|
||||
pub id: String,
|
||||
@@ -110,6 +110,21 @@ 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()
|
||||
}
|
||||
@@ -608,7 +623,18 @@ 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).
|
||||
|
||||
@@ -215,7 +215,10 @@ exit $exit_status
|
||||
.current_dir(job_dir)
|
||||
.env_clear()
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Bash).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Bash, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
.args(cmd_args)
|
||||
@@ -241,7 +244,10 @@ exit $exit_status
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Bash).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Bash, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
.env("HOME", HOME_ENV.as_str())
|
||||
|
||||
@@ -1564,7 +1564,9 @@ try {{
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Bun).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Bun, &job.id, &job.workspace_id, conn).await?,
|
||||
)
|
||||
.envs(common_bun_proc_envs)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.args(args)
|
||||
@@ -1582,7 +1584,10 @@ try {{
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Bun).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Bun, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.envs(common_bun_proc_envs)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
@@ -1613,7 +1618,10 @@ try {{
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Bun).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Bun, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.envs(common_bun_proc_envs)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
|
||||
@@ -600,7 +600,10 @@ pub async fn handle_csharp_job(
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::CSharp).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::CSharp, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("TZ", TZ_ENV.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
@@ -633,7 +636,10 @@ pub async fn handle_csharp_job(
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::CSharp).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::CSharp, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("TZ", TZ_ENV.as_str())
|
||||
.env("DOTNET_CLI_HOME", &*CSHARP_CACHE_DIR)
|
||||
|
||||
@@ -121,11 +121,13 @@ async fn get_common_deno_proc_envs(
|
||||
}
|
||||
|
||||
// Add proxy envs (including OTEL tracing proxy if enabled for deno)
|
||||
for (k, v) in get_proxy_envs_for_lang(&ScriptLang::Deno)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
deno_envs.insert(k.to_string(), v);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return deno_envs;
|
||||
|
||||
@@ -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).await?)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Go, &job.id, &job.workspace_id, conn).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).await?)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Go, &job.id, &job.workspace_id, conn).await?)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("TZ", TZ_ENV.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
|
||||
@@ -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).await?)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Nu, &job.id, &job.workspace_id, conn).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).await?)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Nu, &job.id, &job.workspace_id, conn).await?)
|
||||
// TODO(v1):
|
||||
// "--plugins",
|
||||
// &format!(
|
||||
|
||||
@@ -841,7 +841,10 @@ 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).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Python3, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("TZ", TZ_ENV.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
@@ -867,7 +870,10 @@ mount {{
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Python3).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Python3, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("TZ", TZ_ENV.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
|
||||
@@ -812,7 +812,10 @@ mount {{
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(RUBY_PROXY_ENVS.clone())
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Ruby).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Ruby, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.args(vec![
|
||||
"--config",
|
||||
"run.config.proto",
|
||||
@@ -851,7 +854,10 @@ mount {{
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
.envs(reserved_variables)
|
||||
.envs(RUBY_PROXY_ENVS.clone())
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Ruby).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Ruby, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.envs(envs);
|
||||
|
||||
cmd.stdin(Stdio::null())
|
||||
|
||||
@@ -700,7 +700,10 @@ pub async fn handle_rust_job(
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Rust).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Rust, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("TZ", TZ_ENV.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
@@ -716,7 +719,10 @@ pub async fn handle_rust_job(
|
||||
.env_clear()
|
||||
.envs(envs)
|
||||
.envs(reserved_variables)
|
||||
.envs(get_proxy_envs_for_lang(&ScriptLang::Rust).await?)
|
||||
.envs(
|
||||
get_proxy_envs_for_lang(&ScriptLang::Rust, &job.id, &job.workspace_id, conn)
|
||||
.await?,
|
||||
)
|
||||
.env("PATH", PATH_ENV.as_str())
|
||||
.env("TZ", TZ_ENV.as_str())
|
||||
.env("BASE_INTERNAL_URL", base_internal_url)
|
||||
|
||||
@@ -745,21 +745,37 @@ 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().await;
|
||||
return get_otel_tracing_proxy_envs(job_id, w_id, conn).await;
|
||||
}
|
||||
let _ = lang;
|
||||
let _ = (lang, job_id, w_id, conn);
|
||||
Ok(PROXY_ENVS.clone())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
async fn get_otel_tracing_proxy_envs() -> anyhow::Result<Vec<(&'static str, String)>> {
|
||||
let port = crate::otel_tracing_proxy_ee::TRACING_PROXY_PORT
|
||||
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
|
||||
.read()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("OTEL tracing proxy port not initialized"))?;
|
||||
{
|
||||
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());
|
||||
}
|
||||
};
|
||||
let proxy_url = format!("http://127.0.0.1:{}", port);
|
||||
Ok(vec![
|
||||
("HTTP_PROXY", proxy_url.clone()),
|
||||
@@ -3883,7 +3899,7 @@ pub async fn run_language_executor(
|
||||
run_inline: bool,
|
||||
) -> error::Result<Box<RawValue>> {
|
||||
if language == Some(ScriptLang::Postgresql) {
|
||||
return do_postgresql(
|
||||
return Box::pin(do_postgresql(
|
||||
job,
|
||||
&client,
|
||||
&code,
|
||||
@@ -3895,7 +3911,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"))]
|
||||
@@ -3910,7 +3926,7 @@ pub async fn run_language_executor(
|
||||
"Inline execution is not yet supported for this language".to_string(),
|
||||
));
|
||||
}
|
||||
return do_mysql(
|
||||
return Box::pin(do_mysql(
|
||||
job,
|
||||
&client,
|
||||
&code,
|
||||
@@ -3921,7 +3937,7 @@ pub async fn run_language_executor(
|
||||
column_order,
|
||||
occupancy_metrics,
|
||||
parent_runnable_path,
|
||||
)
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else if language == Some(ScriptLang::Bigquery) {
|
||||
@@ -3947,7 +3963,7 @@ pub async fn run_language_executor(
|
||||
"Inline execution is not yet supported for this language".to_string(),
|
||||
));
|
||||
}
|
||||
return do_bigquery(
|
||||
return Box::pin(do_bigquery(
|
||||
job,
|
||||
&client,
|
||||
&code,
|
||||
@@ -3958,7 +3974,7 @@ pub async fn run_language_executor(
|
||||
column_order,
|
||||
occupancy_metrics,
|
||||
parent_runnable_path,
|
||||
)
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else if language == Some(ScriptLang::Snowflake) {
|
||||
@@ -3976,7 +3992,7 @@ pub async fn run_language_executor(
|
||||
"Inline execution is not yet supported for this language".to_string(),
|
||||
));
|
||||
}
|
||||
return do_snowflake(
|
||||
return Box::pin(do_snowflake(
|
||||
job,
|
||||
&client,
|
||||
&code,
|
||||
@@ -3987,7 +4003,7 @@ pub async fn run_language_executor(
|
||||
column_order,
|
||||
occupancy_metrics,
|
||||
parent_runnable_path,
|
||||
)
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else if language == Some(ScriptLang::Mssql) {
|
||||
@@ -4013,7 +4029,7 @@ pub async fn run_language_executor(
|
||||
"Inline execution is not yet supported for this language".to_string(),
|
||||
));
|
||||
}
|
||||
return do_mssql(
|
||||
return Box::pin(do_mssql(
|
||||
job,
|
||||
&client,
|
||||
&code,
|
||||
@@ -4024,7 +4040,7 @@ pub async fn run_language_executor(
|
||||
occupancy_metrics,
|
||||
job_dir,
|
||||
parent_runnable_path,
|
||||
)
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else if language == Some(ScriptLang::OracleDB) {
|
||||
@@ -4050,7 +4066,7 @@ pub async fn run_language_executor(
|
||||
"Inline execution is not yet supported for this language".to_string(),
|
||||
));
|
||||
}
|
||||
return do_oracledb(
|
||||
return Box::pin(do_oracledb(
|
||||
job,
|
||||
&client,
|
||||
&code,
|
||||
@@ -4061,7 +4077,7 @@ pub async fn run_language_executor(
|
||||
column_order,
|
||||
occupancy_metrics,
|
||||
parent_runnable_path,
|
||||
)
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else if language == Some(ScriptLang::DuckDb) {
|
||||
@@ -4075,7 +4091,7 @@ pub async fn run_language_executor(
|
||||
|
||||
#[cfg(feature = "duckdb")]
|
||||
{
|
||||
return do_duckdb(
|
||||
return Box::pin(do_duckdb(
|
||||
job,
|
||||
&client,
|
||||
&code,
|
||||
@@ -4087,7 +4103,7 @@ pub async fn run_language_executor(
|
||||
occupancy_metrics,
|
||||
parent_runnable_path,
|
||||
run_inline,
|
||||
)
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else if language == Some(ScriptLang::Graphql) {
|
||||
@@ -4096,7 +4112,7 @@ pub async fn run_language_executor(
|
||||
"Inline execution is not yet supported for this language".to_string(),
|
||||
));
|
||||
}
|
||||
return do_graphql(
|
||||
return Box::pin(do_graphql(
|
||||
job,
|
||||
&client,
|
||||
&code,
|
||||
@@ -4105,7 +4121,7 @@ pub async fn run_language_executor(
|
||||
canceled_by,
|
||||
worker_name,
|
||||
occupancy_metrics,
|
||||
)
|
||||
))
|
||||
.await;
|
||||
} else if language == Some(ScriptLang::Nativets) {
|
||||
if run_inline {
|
||||
@@ -4132,7 +4148,7 @@ pub async fn run_language_executor(
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"));
|
||||
|
||||
let result = do_nativets(
|
||||
let result = Box::pin(do_nativets(
|
||||
job,
|
||||
&client,
|
||||
env_code,
|
||||
@@ -4143,7 +4159,7 @@ pub async fn run_language_executor(
|
||||
worker_name,
|
||||
occupancy_metrics,
|
||||
has_stream,
|
||||
)
|
||||
))
|
||||
.await?;
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
@@ -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())?;
|
||||
evaluate_stop_after_all_iters_if(
|
||||
if let Err(e) = evaluate_stop_after_all_iters_if(
|
||||
db,
|
||||
stop_after_all_iters_if,
|
||||
module_status,
|
||||
@@ -879,7 +879,16 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
flow,
|
||||
&old_status,
|
||||
)
|
||||
.await?;
|
||||
.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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let new_status = if
|
||||
@@ -1074,7 +1083,7 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
{
|
||||
let args = from_result_to_args(args.as_ref().await.get_ref())?;
|
||||
|
||||
evaluate_stop_after_all_iters_if(
|
||||
if let Err(e) = evaluate_stop_after_all_iters_if(
|
||||
db,
|
||||
stop_after_all_iters_if,
|
||||
module_status,
|
||||
@@ -1088,7 +1097,15 @@ pub async fn update_flow_status_after_job_completion_internal(
|
||||
flow,
|
||||
&old_status,
|
||||
)
|
||||
.await?;
|
||||
.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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,14 +15,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
let { connect_config = $bindable({
|
||||
scopes: ['offline_access'],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: true,
|
||||
extra_params: { tenant_id: '' },
|
||||
extra_params_callback: {}
|
||||
}) }: Props = $props();
|
||||
let { connect_config = $bindable() }: Props = $props();
|
||||
|
||||
run(() => {
|
||||
if (!connect_config) {
|
||||
@@ -38,13 +31,14 @@
|
||||
});
|
||||
|
||||
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">
|
||||
@@ -73,3 +67,4 @@
|
||||
<OauthScopes bind:scopes={connect_config.scopes} />
|
||||
</div>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
@@ -6,14 +6,7 @@
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
let { connect_config = $bindable({
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
}) } = $props();
|
||||
let { connect_config = $bindable() } = $props();
|
||||
|
||||
run(() => {
|
||||
if (!connect_config) {
|
||||
|
||||
@@ -6,15 +6,7 @@
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
|
||||
let { login_config = $bindable({
|
||||
scopes: [],
|
||||
auth_url: '',
|
||||
token_url: '',
|
||||
userinfo_url: '',
|
||||
req_body_auth: false,
|
||||
extra_params: {},
|
||||
extra_params_callback: {}
|
||||
}) } = $props();
|
||||
let { login_config = $bindable() } = $props();
|
||||
|
||||
run(() => {
|
||||
if (!login_config) {
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
}
|
||||
|
||||
let darkModeToggle: DarkModeToggle | undefined = $state()
|
||||
let darkMode: boolean | undefined = $state(undefined)
|
||||
let darkMode: boolean = $state(document.documentElement.classList.contains('dark'))
|
||||
let modeInitialized = $state(false)
|
||||
function initializeMode() {
|
||||
modeInitialized = true
|
||||
|
||||
@@ -6,9 +6,15 @@
|
||||
extra_params?: Record<string, string>;
|
||||
}
|
||||
|
||||
let { extra_params = $bindable({}) }: Props = $props();
|
||||
let { extra_params = $bindable() }: Props = $props();
|
||||
|
||||
let extra_params_vec: [string, string][] = $state(Object.entries(extra_params))
|
||||
$effect.pre(() => {
|
||||
if (!extra_params) {
|
||||
extra_params = {}
|
||||
}
|
||||
})
|
||||
|
||||
let extra_params_vec: [string, string][] = $state(Object.entries(extra_params ?? {}))
|
||||
|
||||
function sync() {
|
||||
extra_params = Object.fromEntries(extra_params_vec)
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
scopes?: string[]
|
||||
}
|
||||
|
||||
let { scopes = $bindable([]) }: Props = $props()
|
||||
let { scopes = $bindable() }: Props = $props()
|
||||
|
||||
$effect.pre(() => {
|
||||
if (!scopes) {
|
||||
scopes = []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if scopes && Array.isArray(scopes)}
|
||||
@@ -18,7 +24,7 @@
|
||||
size="xs"
|
||||
btnClasses="mx-6"
|
||||
on:click={() => {
|
||||
scopes = scopes.filter((el) => el != v)
|
||||
scopes = scopes?.filter((el) => el != v)
|
||||
}}
|
||||
startIcon={{ icon: Minus }}
|
||||
iconOnly
|
||||
|
||||
@@ -438,7 +438,6 @@
|
||||
let cip
|
||||
let extraModel
|
||||
|
||||
let width = $state(0)
|
||||
// let widgets: HTMLElement | undefined = document.getElementById('monaco-widgets-root') ?? undefined
|
||||
|
||||
let initialized = $state(false)
|
||||
@@ -545,9 +544,6 @@
|
||||
if (divEl) {
|
||||
divEl.style.height = `${contentHeight}px`
|
||||
}
|
||||
try {
|
||||
editor?.layout({ width, height: contentHeight })
|
||||
} catch {}
|
||||
}
|
||||
editor.onDidContentSizeChange(updateHeight)
|
||||
updateHeight()
|
||||
@@ -718,7 +714,6 @@
|
||||
bind:this={divEl}
|
||||
style="height: 18px;"
|
||||
class="template nonmain-editor rounded-md overflow-clip {!editor ? 'hidden' : ''}"
|
||||
bind:clientWidth={width}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -142,6 +141,28 @@ 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,
|
||||
@@ -157,8 +178,8 @@ export function getCountInput(
|
||||
return undefined
|
||||
}
|
||||
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
|
||||
let query = makeCountQuery(dbType, table, whereClause, columnDefs)
|
||||
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
|
||||
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
|
||||
const query = buildCountMarker(table, columnDefs, whereClause, dbType, ducklake)
|
||||
|
||||
const updateRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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) {
|
||||
@@ -66,6 +65,21 @@ 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,
|
||||
@@ -79,8 +93,8 @@ export function getDeleteInput(
|
||||
return undefined
|
||||
}
|
||||
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
|
||||
let query = makeDeleteQuery(table, columns, dbType)
|
||||
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
|
||||
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
|
||||
const query = buildDeleteMarker(table, columns, dbType, ducklake)
|
||||
const deleteRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'inline',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -106,10 +105,37 @@ 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
|
||||
let query = makeInsertQuery(table, columns, dbType)
|
||||
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
|
||||
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
|
||||
const query = buildInsertMarker(table, columns, dbType, ducklake)
|
||||
return {
|
||||
runnable: {
|
||||
name: 'AppDbExplorer',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
@@ -318,6 +317,33 @@ 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,
|
||||
@@ -335,8 +361,8 @@ export function getSelectInput(
|
||||
}
|
||||
|
||||
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
|
||||
let content = makeSelectQuery(table, columnDefs, whereClause, dbType, options)
|
||||
if (dbInput.type === 'ducklake') content = wrapDucklakeQuery(content, dbInput.ducklake)
|
||||
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
|
||||
const content = buildSelectMarker(table, columnDefs, whereClause, dbType, ducklake)
|
||||
const getRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'inline',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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'
|
||||
|
||||
@@ -76,6 +75,23 @@ 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,
|
||||
@@ -90,8 +106,8 @@ export function getUpdateInput(
|
||||
return undefined
|
||||
}
|
||||
const dbType = dbInput.type === 'ducklake' ? 'duckdb' : dbInput.resourceType
|
||||
let query = makeUpdateQuery(table, column, columns, dbType)
|
||||
if (dbInput.type === 'ducklake') query = wrapDucklakeQuery(query, dbInput.ducklake)
|
||||
const ducklake = dbInput.type === 'ducklake' ? dbInput.ducklake : undefined
|
||||
const query = buildUpdateMarker(table, column, columns, dbType, ducklake)
|
||||
|
||||
const updateRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
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 { AppViewerContext, ComponentCustomCSS, ContextPanelContext } from '../../../types'
|
||||
import {
|
||||
type AppEditorContext,
|
||||
type AppViewerContext,
|
||||
type ComponentCustomCSS,
|
||||
type ContextPanelContext
|
||||
} from '../../../types'
|
||||
|
||||
import type { TableAction, components } from '$lib/components/apps/editor/component'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
@@ -62,9 +67,15 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -151,7 +162,8 @@
|
||||
|
||||
const componentContext = new Map<string, any>([
|
||||
['AppViewerContext', context],
|
||||
['ContextPanel', contextPanel]
|
||||
['ContextPanel', contextPanel],
|
||||
['AppEditorContext', editorContext]
|
||||
])
|
||||
|
||||
const taComponent = withProps(AppAggridTableActions, {
|
||||
|
||||
@@ -280,10 +280,7 @@
|
||||
}
|
||||
} catch {}
|
||||
} else {
|
||||
const drawerAlreadyHandledFocusedGrid =
|
||||
item?.data.type === 'drawercomponent' &&
|
||||
$focusedGrid?.parentComponentId === befSelected
|
||||
if (!drawerAlreadyHandledFocusedGrid) {
|
||||
if ($focusedGrid?.parentComponentId !== befSelected) {
|
||||
$focusedGrid = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4159,14 +4159,16 @@ This is a paragraph.
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'mysql',
|
||||
value: ''
|
||||
value: '',
|
||||
allowTypeChange: false
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subFieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
value: undefined,
|
||||
allowTypeChange: false
|
||||
}
|
||||
},
|
||||
ms_sql_server: {
|
||||
@@ -4174,14 +4176,16 @@ This is a paragraph.
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'ms_sql_server',
|
||||
value: ''
|
||||
value: '',
|
||||
allowTypeChange: false
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subFieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
value: undefined,
|
||||
allowTypeChange: false
|
||||
}
|
||||
},
|
||||
snowflake: {
|
||||
@@ -4189,14 +4193,16 @@ This is a paragraph.
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'snowflake',
|
||||
value: ''
|
||||
value: '',
|
||||
allowTypeChange: false
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subFieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
value: undefined,
|
||||
allowTypeChange: false
|
||||
}
|
||||
},
|
||||
bigquery: {
|
||||
@@ -4204,14 +4210,16 @@ This is a paragraph.
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'bigquery',
|
||||
value: ''
|
||||
value: '',
|
||||
allowTypeChange: false
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subFieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
value: undefined,
|
||||
allowTypeChange: false
|
||||
}
|
||||
},
|
||||
ducklake: {
|
||||
@@ -4219,14 +4227,16 @@ This is a paragraph.
|
||||
type: 'static',
|
||||
fieldType: 'ducklake',
|
||||
subFieldType: 'ducklake',
|
||||
value: ''
|
||||
value: '',
|
||||
allowTypeChange: false
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subFieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
value: undefined,
|
||||
allowTypeChange: false
|
||||
}
|
||||
},
|
||||
datatable: {
|
||||
@@ -4234,14 +4244,16 @@ This is a paragraph.
|
||||
type: 'static',
|
||||
fieldType: 'datatable',
|
||||
subFieldType: 'datatable',
|
||||
value: ''
|
||||
value: '',
|
||||
allowTypeChange: false
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subFieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
value: undefined,
|
||||
allowTypeChange: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,12 +3,12 @@ import {
|
||||
type ColumnDef,
|
||||
type TableMetadata
|
||||
} from './apps/components/display/dbtable/utils'
|
||||
import { makeSelectQuery } from './apps/components/display/dbtable/queries/select'
|
||||
import { buildSelectMarker } from './apps/components/display/dbtable/queries/select'
|
||||
import { runScriptAndPollResult } from './jobs/utils'
|
||||
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 { 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 { 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 }) => {
|
||||
let countQuery = makeCountQuery(dbType, tableKey, undefined, colDefs)
|
||||
if (input.type === 'ducklake') countQuery = wrapDucklakeQuery(countQuery, input.ducklake)
|
||||
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
|
||||
const countQuery = buildCountMarker(tableKey, colDefs, undefined, dbType, ducklake)
|
||||
const result = await runScriptAndPollResult({
|
||||
workspace,
|
||||
requestBody: { args: { ...dbArg, quicksearch }, language, content: countQuery }
|
||||
@@ -78,10 +78,8 @@ export function dbTableOpsWithPreviewScripts({
|
||||
return count
|
||||
},
|
||||
getRows: async (params) => {
|
||||
let query = makeSelectQuery(tableKey, colDefs, undefined, dbType, undefined, {
|
||||
fixPgIntTypes: true
|
||||
})
|
||||
if (input.type === 'ducklake') query = wrapDucklakeQuery(query, input.ducklake)
|
||||
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
|
||||
const query = buildSelectMarker(tableKey, colDefs, undefined, dbType, ducklake)
|
||||
let items = (await runScriptAndPollResult({
|
||||
workspace,
|
||||
requestBody: { args: { ...dbArg, ...params }, language, content: query }
|
||||
@@ -92,8 +90,8 @@ export function dbTableOpsWithPreviewScripts({
|
||||
return items
|
||||
},
|
||||
onUpdate: async ({ values }, colDef, newValue) => {
|
||||
let updateQuery = makeUpdateQuery(tableKey, colDef, colDefs, dbType)
|
||||
if (input.type === 'ducklake') updateQuery = wrapDucklakeQuery(updateQuery, input.ducklake)
|
||||
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
|
||||
const updateQuery = buildUpdateMarker(tableKey, colDef, colDefs, dbType, ducklake)
|
||||
await runScriptAndPollResult({
|
||||
workspace,
|
||||
requestBody: {
|
||||
@@ -104,16 +102,16 @@ export function dbTableOpsWithPreviewScripts({
|
||||
})
|
||||
},
|
||||
onDelete: async ({ values }) => {
|
||||
let deleteQuery = makeDeleteQuery(tableKey, colDefs, dbType)
|
||||
if (input.type === 'ducklake') deleteQuery = wrapDucklakeQuery(deleteQuery, input.ducklake)
|
||||
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
|
||||
const deleteQuery = buildDeleteMarker(tableKey, colDefs, dbType, ducklake)
|
||||
await runScriptAndPollResult({
|
||||
workspace,
|
||||
requestBody: { args: { ...dbArg, ...values }, language, content: deleteQuery }
|
||||
})
|
||||
},
|
||||
onInsert: async ({ values }) => {
|
||||
let insertQuery = makeInsertQuery(tableKey, colDefs, dbType)
|
||||
if (input.type === 'ducklake') insertQuery = wrapDucklakeQuery(insertQuery, input.ducklake)
|
||||
const ducklake = input.type === 'ducklake' ? input.ducklake : undefined
|
||||
const insertQuery = buildInsertMarker(tableKey, colDefs, dbType, ducklake)
|
||||
await runScriptAndPollResult({
|
||||
workspace,
|
||||
requestBody: { args: { ...dbArg, ...values }, language, content: insertQuery }
|
||||
|
||||
@@ -130,7 +130,8 @@
|
||||
const old = v.assets?.find((a) => assetEq(a, asset))
|
||||
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
|
||||
}
|
||||
if (!deepEqual(v.assets, newAssets)) v.assets = newAssets
|
||||
const normalizedAssets = newAssets.length > 0 ? newAssets : undefined
|
||||
if (!deepEqual(v.assets, normalizedAssets)) v.assets = normalizedAssets
|
||||
}
|
||||
|
||||
// Check for raw script modules whose assets were not parsed. Useful for flows created
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
|
||||
<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}
|
||||
@@ -170,6 +171,9 @@
|
||||
</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)}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
let darkModeToggle: DarkModeToggle | undefined = $state()
|
||||
let darkMode: boolean | undefined = $state(undefined)
|
||||
let darkMode: boolean = $state(document.documentElement.classList.contains('dark'))
|
||||
let modeInitialized = $state(false)
|
||||
function initializeMode() {
|
||||
modeInitialized = true
|
||||
|
||||
25
scripts/post-create.sh
Executable file
25
scripts/post-create.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/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)"
|
||||
7
scripts/pre-remove.sh
Executable file
7
scripts/pre-remove.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/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)"
|
||||
@@ -1,73 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Use WM_WORKTREE_PATH (set by workmux) so this works regardless of cwd
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/worktree-common.sh"
|
||||
|
||||
wt_dir="${WM_WORKTREE_PATH:-.}"
|
||||
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
|
||||
wm_kill_processes_from_env_file "${wt_dir}/.env.local"
|
||||
wm_shared_pre_remove "$wt_dir"
|
||||
|
||||
265
scripts/worktree-common.sh
Executable file
265
scripts/worktree-common.sh
Executable file
@@ -0,0 +1,265 @@
|
||||
#!/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
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/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
|
||||
}
|
||||
@@ -51,154 +53,4 @@ if [[ -n "${CARGO_FEATURES:-}" ]]; then
|
||||
fi
|
||||
|
||||
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
|
||||
|
||||
# --- 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
|
||||
wm_shared_post_create "$(pwd)"
|
||||
|
||||
Reference in New Issue
Block a user