mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-03 08:02:19 +00:00
* feat: infer a script's schema from its code when a deploy sends none Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: merge an inferred schema into the previous one the way the editor does Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: parse non-JSON TS defaults natively and keep the schema on a failed inference Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: type untyped TS params from their literal shape when the default can't be evaluated Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix: read literal TS defaults off the AST so the server types them like the editor Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: word the script schema description for both create and update Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs: note that dbt scripts derive their schema from the descriptor Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
116 lines
3.4 KiB
Rust
116 lines
3.4 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use serde_json::{json, Value};
|
|
use sqlx::{Pool, Postgres};
|
|
use windmill_api_client::types::{NewScript, ScriptLang};
|
|
use windmill_test_utils::init_client;
|
|
|
|
const PATH: &str = "u/test-user/inferred_schema";
|
|
|
|
fn bun_ns(content: &str, parent_hash: Option<String>) -> NewScript {
|
|
NewScript {
|
|
draft_only: None,
|
|
content: content.into(),
|
|
language: ScriptLang::Bun,
|
|
lock: None,
|
|
parent_hash,
|
|
path: PATH.into(),
|
|
concurrent_limit: None,
|
|
concurrency_time_window_s: None,
|
|
cache_ttl: None,
|
|
dedicated_worker: None,
|
|
description: "".to_string(),
|
|
envs: vec![],
|
|
is_template: None,
|
|
kind: None,
|
|
summary: "".to_string(),
|
|
tag: None,
|
|
// What this client sends for "no schema".
|
|
schema: HashMap::new(),
|
|
ws_error_handler_muted: Some(false),
|
|
priority: None,
|
|
delete_after_secs: None,
|
|
timeout: None,
|
|
restart_unless_cancelled: None,
|
|
deployment_message: None,
|
|
concurrency_key: None,
|
|
visible_to_runner_only: None,
|
|
auto_kind: None,
|
|
codebase: None,
|
|
has_preprocessor: None,
|
|
on_behalf_of_email: None,
|
|
assets: vec![],
|
|
modules: None,
|
|
}
|
|
}
|
|
|
|
async fn head(db: &Pool<Postgres>) -> anyhow::Result<(i64, Value)> {
|
|
Ok(sqlx::query_as(
|
|
"SELECT hash, schema::jsonb FROM script \
|
|
WHERE workspace_id = 'test-workspace' AND path = $1 AND archived = false",
|
|
)
|
|
.bind(PATH)
|
|
.fetch_one(db)
|
|
.await?)
|
|
}
|
|
|
|
/// A deploy without a schema (MCP, a bare API call) gets the one the editor would
|
|
/// have sent, and a new version keeps what the user wrote on the previous one's.
|
|
#[sqlx::test(fixtures("base"))]
|
|
async fn schemaless_deploy_infers_schema_keeping_annotations(
|
|
db: Pool<Postgres>,
|
|
) -> anyhow::Result<()> {
|
|
let (client, _port, _s) = init_client(db.clone()).await;
|
|
|
|
client
|
|
.create_script(
|
|
"test-workspace",
|
|
&bun_ns(
|
|
"export function main(n: number, name = \"x\") { return n }",
|
|
None,
|
|
),
|
|
)
|
|
.await?;
|
|
let (hash, schema) = head(&db).await?;
|
|
assert_eq!(schema["required"], json!(["n"]));
|
|
assert_eq!(schema["properties"]["n"]["type"], "number");
|
|
assert_eq!(schema["properties"]["name"]["default"], "x");
|
|
|
|
sqlx::query(
|
|
"UPDATE script SET schema = jsonb_set(schema::jsonb, '{properties,n,description}', \
|
|
'\"how many\"')::json WHERE hash = $1",
|
|
)
|
|
.bind(hash)
|
|
.execute(&db)
|
|
.await?;
|
|
|
|
client
|
|
.create_script(
|
|
"test-workspace",
|
|
&bun_ns(
|
|
"export function main(n: number, flag: boolean) { return n }",
|
|
Some(format!("{hash:016x}")),
|
|
),
|
|
)
|
|
.await?;
|
|
let (hash, schema) = head(&db).await?;
|
|
assert_eq!(schema["required"], json!(["n", "flag"]));
|
|
assert_eq!(schema["properties"]["n"]["description"], "how many");
|
|
assert_eq!(schema["properties"]["flag"]["type"], "boolean");
|
|
assert!(schema["properties"].get("name").is_none());
|
|
|
|
// Code that does not parse keeps the schema rather than losing it for good.
|
|
client
|
|
.create_script(
|
|
"test-workspace",
|
|
&bun_ns(
|
|
"export function main(n: number {",
|
|
Some(format!("{hash:016x}")),
|
|
),
|
|
)
|
|
.await?;
|
|
assert_eq!(head(&db).await?.1, schema);
|
|
|
|
Ok(())
|
|
}
|