fix: parse Python datetime.datetime and datetime.date type annotations (#7856)

* fix: parse Python datetime.datetime and datetime.date type annotations correctly

The Python parser only matched ExprKind::Name for type annotations, so
`datetime.datetime` (an Attribute expression) silently fell through to
Typ::Unknown and no datetime picker was shown in the UI.

- Extend parse_expr to resolve `datetime.*` attribute access (alongside
  the existing `wmill.*` handling)
- Add Typ::Date variant for `datetime.date` → JSON schema format "date"
- Update python worker to import and convert `date.fromisoformat()`
- Update argSigToJsonSchemaType, AI types, schema validation, and SQL
  datatype wasm for the new Date variant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* all

* all

* all

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-02-09 16:17:26 +01:00
committed by GitHub
parent 76377a00a6
commit ff70a4e9d1
72 changed files with 756 additions and 459 deletions
@@ -46,11 +46,11 @@
]
},
"nullable": [
true,
true,
true,
true,
true,
false,
false,
false,
false,
false,
true,
true
]
@@ -30,8 +30,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,21 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO email_trigger (\n path, local_part, workspaced_local_part, script_path,\n is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "1074c6c98e6a0c83ac04172a39abea21c793f58947051d39931d4da0868a1d77"
}
@@ -122,8 +122,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -40,8 +40,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE email_trigger SET script_path = $1, local_part = $2 WHERE workspace_id = $3 AND path = $4",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "2031c5138a785367e5127180ccb6734efa41ee6cb3b4819c1c517798b2b23e4a"
}
@@ -0,0 +1,85 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id, runnable_path, trigger_kind AS \"trigger_kind: String\",\n args AS \"args: sqlx::types::Json<serde_json::Value>\"\n FROM v2_job\n WHERE runnable_path = $1\n AND trigger_kind = $2::job_trigger_kind\n ORDER BY created_at DESC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "trigger_kind: String",
"type_info": {
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt",
"nextcloud"
]
}
}
}
},
{
"ordinal": 3,
"name": "args: sqlx::types::Json<serde_json::Value>",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
{
"Custom": {
"name": "job_trigger_kind",
"kind": {
"Enum": [
"webhook",
"http",
"websocket",
"kafka",
"email",
"nats",
"schedule",
"app",
"ui",
"postgres",
"sqs",
"gcp",
"mqtt",
"nextcloud"
]
}
}
}
]
},
"nullable": [
false,
true,
true,
true
]
},
"hash": "212553c83e4dcdc6d045eb2fe2dadbb2860ce52d37a56b2861de1215260ecff8"
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -40,8 +40,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT value FROM variable WHERE workspace_id = $1 AND path = $2 AND is_secret = true",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "value",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "302bc55d0c227c5b12458ccde6569c4b531ff494d1ede0a655872ae05215f8a6"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3 AND is_secret = true",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "33fff66efe810ad7e92b36ca9b287938437182d7817387707e800519d9f5bafc"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM raw_app WHERE path = $1 AND workspace_id = $2",
"query": "DELETE FROM mqtt_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [],
"parameters": {
@@ -11,5 +11,5 @@
},
"nullable": []
},
"hash": "b2b2b7251be2b80207f47b10eeff78d61c84161caaf16b8fd25d82b97aac1186"
"hash": "3481e65196e500ad914f10b87884c19d8a3636ae955788c3366365194fe2ef57"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM email_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": []
},
"hash": "40971d637c5b4d2af8e67872722880058cc067fca5e807ab3e1ed17d180cb7f7"
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -30,8 +30,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -37,8 +37,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -32,8 +32,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -71,8 +70,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -245,8 +245,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,39 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT delivery_type AS \"delivery_type: String\",\n delivery_config\n FROM gcp_trigger\n WHERE workspace_id = $1 AND path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "delivery_type: String",
"type_info": {
"Custom": {
"name": "delivery_mode",
"kind": {
"Enum": [
"push",
"pull"
]
}
}
}
},
{
"ordinal": 1,
"name": "delivery_config",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
true
]
},
"hash": "60a666a7cffce2d7631682095ccfd31e99997e88374496d4800eaa7b5152a464"
}
@@ -35,8 +35,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -29,8 +29,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -40,8 +40,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -27,8 +27,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "SELECT script_path, local_part FROM email_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "local_part",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false
]
},
"hash": "8002f7f1521846056278f18e8b003fe48a4d3d827ac8b212e0f3d3bc2ed42f37"
}
@@ -1,12 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "SELECT EXISTS(SELECT 1 FROM raw_app WHERE path = $1 AND workspace_id = $2)",
"query": "SELECT COUNT(*) FROM email_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
@@ -19,5 +19,5 @@
null
]
},
"hash": "c4b3ab3d87f158b20f0557f78c6307c50620425aeb43636e19931c142b5ce0d7"
"hash": "8364505b9a4d0d6f7406e4a5425360682ac2295bbf6746c7641141ed7bf47bf6"
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -1,17 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO raw_app\n (workspace_id, path, summary, extra_perms, data)\n VALUES ($1, $2, $3, '{}', $4)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Text"
]
},
"nullable": []
},
"hash": "84bbd11f7bb0c65dcfb2e12038b8a9cb51d303480ac3c5f8a5e833a53c40ed9b"
}
@@ -35,8 +35,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,62 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT mqtt_resource_path, client_version AS \"client_version: String\",\n script_path, mode AS \"mode: String\"\n FROM mqtt_trigger\n WHERE workspace_id = $1 AND path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "mqtt_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "client_version: String",
"type_info": {
"Custom": {
"name": "mqtt_client_version",
"kind": {
"Enum": [
"v3",
"v5"
]
}
}
}
},
{
"ordinal": 2,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "mode: String",
"type_info": {
"Custom": {
"name": "trigger_mode",
"kind": {
"Enum": [
"enabled",
"disabled",
"suspended"
]
}
}
}
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "899a162192d93c7fb7d47d38f4fb0fadcafc2bb0c61efa8ee496af7252d18270"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO kafka_trigger (\n path, kafka_resource_path, topics, group_id,\n script_path, is_flow, workspace_id, edited_by, email\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"VarcharArray",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Varchar"
]
},
"nullable": []
},
"hash": "8c3cc09c1bbb6209467c75723dd02e97dddb99789422012e85e86c3151a9f2e9"
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -32,8 +32,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -30,8 +30,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT script_path FROM mqtt_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "script_path",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "a086a5fa28dcff80911c7e5de73f204fc42625e58b967b0376ff7b382e38fa11"
}
@@ -155,8 +155,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -185,8 +185,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,84 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT gcp_resource_path, topic_id, subscription_id,\n delivery_type AS \"delivery_type: String\",\n subscription_mode AS \"subscription_mode: String\",\n mode AS \"mode: String\"\n FROM gcp_trigger\n WHERE workspace_id = $1 AND path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "gcp_resource_path",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "topic_id",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "subscription_id",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "delivery_type: String",
"type_info": {
"Custom": {
"name": "delivery_mode",
"kind": {
"Enum": [
"push",
"pull"
]
}
}
}
},
{
"ordinal": 4,
"name": "subscription_mode: String",
"type_info": {
"Custom": {
"name": "gcp_subscription_mode",
"kind": {
"Enum": [
"create_update",
"existing"
]
}
}
}
},
{
"ordinal": 5,
"name": "mode: String",
"type_info": {
"Custom": {
"name": "trigger_mode",
"kind": {
"Enum": [
"enabled",
"disabled",
"suspended"
]
}
}
}
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false
]
},
"hash": "a80a17cda8ad722a97f16526d8576b76c7377066a06dba98a2921a68928aa644"
}
@@ -160,8 +160,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -105,8 +105,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -31,8 +31,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT local_part, workspaced_local_part, script_path,\n mode AS \"mode: String\"\n FROM email_trigger\n WHERE workspace_id = $1 AND path = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "local_part",
"type_info": "Varchar"
},
{
"ordinal": 1,
"name": "workspaced_local_part",
"type_info": "Bool"
},
{
"ordinal": 2,
"name": "script_path",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "mode: String",
"type_info": {
"Custom": {
"name": "trigger_mode",
"kind": {
"Enum": [
"enabled",
"disabled",
"suspended"
]
}
}
}
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "cabef30aa1e308bc656660c4ac0d3e77b020de05465f81a4fb4b0c07bd0d7439"
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -105,8 +105,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -25,8 +25,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) as count FROM variable WHERE is_secret = true AND value != 'CLEARED'",
"query": "SELECT COUNT(*) FROM variable WHERE is_secret = true",
"describe": {
"columns": [
{
@@ -16,5 +16,5 @@
null
]
},
"hash": "052d42b46d5faba6b41f1fdcbf6a012db51b9e5a255ec0da9a8a0999d668d336"
"hash": "d7d11b2c288fa0282539edde0c5450e4b5f8355821617b6c1ce3db64f6ceaf69"
}
@@ -0,0 +1,21 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO websocket_trigger (\n path, url, script_path, is_flow, workspace_id,\n edited_by, email, initial_messages\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Bool",
"Varchar",
"Varchar",
"Varchar",
"JsonbArray"
]
},
"nullable": []
},
"hash": "e485c82978d10379c6d1b7cd850f3bf764e6c5bf775f3d710ba36a79f5e424eb"
}
@@ -185,8 +185,7 @@
"sqs",
"gcp",
"mqtt",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -31,8 +31,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -24,8 +24,7 @@
"mqtt",
"gcp",
"default_email",
"nextcloud",
"google"
"nextcloud"
]
}
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE mqtt_trigger SET script_path = $1 WHERE workspace_id = $2 AND path = $3",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "f0eb9d49021668673880aa20227c24a130cada07fba3f43e32d904fde97106d3"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(*) FROM raw_app WHERE workspace_id = $1",
"query": "SELECT COUNT(*) FROM mqtt_trigger WHERE workspace_id = $1 AND path = $2",
"describe": {
"columns": [
{
@@ -11,6 +11,7 @@
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
@@ -18,5 +19,5 @@
null
]
},
"hash": "3b5295a7c4b99aefa52c9a8ae1e0dd12bf4a0be1bf755caf7a1fa863e7950562"
"hash": "f149d740301a65a62664fd407abe89c972e50837b6c54ac7a3ccd5f60ca05d3c"
}
+13 -7
View File
@@ -422,11 +422,15 @@ fn parse_expr(
match e.as_ref() {
Expr::Name(ExprName { id, .. }) => (parse_typ(id.as_ref(), enums, module), false),
Expr::Attribute(x) => {
if x.value
.as_name_expr()
.is_some_and(|x| x.id.as_str() == "wmill")
{
(parse_typ(x.attr.as_str(), enums, module), false)
if let Some(name) = x.value.as_name_expr() {
match name.id.as_str() {
"wmill" => (parse_typ(x.attr.as_str(), enums, module), false),
"datetime" => {
let full_name = format!("datetime.{}", x.attr.as_str());
(parse_typ(&full_name, enums, module), false)
}
_ => (Typ::Unknown, false),
}
} else {
(Typ::Unknown, false)
}
@@ -493,6 +497,8 @@ fn parse_typ(id: &str, enums: &HashMap<String, EnumInfo>, module: Option<&[Stmt]
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
"date" => Typ::Date,
"datetime.date" => Typ::Date,
"Sql" | "sql" => Typ::Sql,
x @ _ if x.starts_with("DynSelect_") => {
Typ::DynSelect(x.strip_prefix("DynSelect_").unwrap().to_string())
@@ -620,7 +626,7 @@ def main(test1: str, name: datetime.datetime = datetime.now(), byte: bytes = byt
Arg {
otyp: None,
name: "name".to_string(),
typ: Typ::Unknown,
typ: Typ::Datetime,
default: Some(json!("<function call>")),
has_default: true,
oidx: None
@@ -709,7 +715,7 @@ def main(test1: str,
Arg {
otyp: None,
name: "name".to_string(),
typ: Typ::Unknown,
typ: Typ::Datetime,
default: Some(json!("<function call>")),
has_default: true,
oidx: None
@@ -295,6 +295,7 @@ fn extract_field_type(
"bool" => Typ::Bool,
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"date" => Typ::Date,
"Any" => Typ::Unknown, // typing.Any maps to Unknown
// Custom class - check if it's a model
custom_type => {
@@ -65,6 +65,7 @@ pub enum Typ {
List(Box<Typ>),
Bytes,
Datetime,
Date,
Resource(String),
Email,
Sql,
@@ -10,6 +10,7 @@ fn to_str(typ: Typ) -> String {
Typ::List(t) => format!("list-{}", to_str(*t)),
Typ::Bytes => "bytes".to_string(),
Typ::Datetime => "datetime".to_string(),
Typ::Date => "date".to_string(),
_ => "unknown".to_string(),
}
}
+6 -6
View File
@@ -14,18 +14,18 @@ INSERT INTO workspace_key(workspace_id, kind, key)
VALUES ('test-workspace-2', 'cloud', 'test-key-2')
ON CONFLICT DO NOTHING;
-- Insert test secrets for workspace 1
-- Note: The 'value' column stores encrypted values in production,
-- but for tests we'll use plain text that the migration will handle
-- Insert test variables with placeholder values.
-- Secret values are encrypted by the test setup using build_crypt + encrypt
-- with the workspace key, matching production behavior.
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES
('test-workspace', 'u/test-user/db_password', 'encrypted-db-pass-123', true, 'Database password', '{}'),
('test-workspace', 'u/test-user/api_key', 'encrypted-api-key-abc', true, 'API key for external service', '{}'),
('test-workspace', 'u/test-user/db_password', 'PLACEHOLDER', true, 'Database password', '{}'),
('test-workspace', 'u/test-user/api_key', 'PLACEHOLDER', true, 'API key for external service', '{}'),
('test-workspace', 'u/test-user/public_var', 'not-a-secret', false, 'A non-secret variable', '{}')
ON CONFLICT DO NOTHING;
-- Insert test secrets for workspace 2 (to test isolation)
INSERT INTO variable (workspace_id, path, value, is_secret, description, extra_perms)
VALUES
('test-workspace-2', 'u/test-user/other_secret', 'encrypted-other-secret', true, 'Secret in workspace 2', '{}')
('test-workspace-2', 'u/test-user/other_secret', 'PLACEHOLDER', true, 'Secret in workspace 2', '{}')
ON CONFLICT DO NOTHING;
+103 -111
View File
@@ -26,28 +26,56 @@
#[cfg(all(feature = "private", feature = "enterprise"))]
mod tests {
use sqlx::{Pool, Postgres};
use std::collections::HashMap;
use windmill_common::secret_backend::{
migrate_secrets_to_database, migrate_secrets_to_vault, test_vault_connection,
SecretBackend, VaultBackend, VaultSettings,
};
use windmill_common::variables::{build_crypt, decrypt, encrypt};
/// Plaintext values for test secrets. The fixture inserts PLACEHOLDERs that
/// `encrypt_fixture_secrets` replaces with properly encrypted values.
const TEST_SECRETS: &[(&str, &str, &str)] = &[
("test-workspace", "u/test-user/db_password", "db-pass-123"),
("test-workspace", "u/test-user/api_key", "api-key-abc"),
(
"test-workspace-2",
"u/test-user/other_secret",
"other-secret-value",
),
];
/// Encrypt the PLACEHOLDER values inserted by the fixture using the real
/// workspace encryption keys, so migration tests can decrypt them correctly.
async fn encrypt_fixture_secrets(db: &Pool<Postgres>) {
for &(workspace_id, path, plaintext) in TEST_SECRETS {
let mc = build_crypt(db, workspace_id).await.unwrap();
let encrypted = encrypt(&mc, plaintext);
sqlx::query!(
"UPDATE variable SET value = $1 WHERE workspace_id = $2 AND path = $3 AND is_secret = true",
encrypted,
workspace_id,
path,
)
.execute(db)
.await
.unwrap();
}
}
/// Check if vault tests should run (requires RUN_VAULT_TESTS=1 env var)
fn should_run_vault_tests() -> bool {
std::env::var("RUN_VAULT_TESTS")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
/// Set up BASE_URL for JWT tests (required for OIDC issuer URL generation)
#[cfg(feature = "openidconnect")]
async fn setup_base_url() {
let base_url = std::env::var("BASE_URL")
.unwrap_or_else(|_| "http://localhost:8000".to_string());
let base_url =
std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
let mut url = windmill_common::BASE_URL.write().await;
*url = base_url;
}
/// Skip test if RUN_VAULT_TESTS is not set
macro_rules! skip_if_no_vault {
() => {
if !should_run_vault_tests() {
@@ -71,6 +99,7 @@ mod tests {
}
}
#[cfg(feature = "openidconnect")]
fn vault_settings_jwt() -> VaultSettings {
VaultSettings {
address: std::env::var("VAULT_ADDR")
@@ -163,6 +192,7 @@ mod tests {
// ==================== JWT Auth Tests ====================
/// Test Vault connection with JWT authentication
#[cfg(feature = "openidconnect")]
#[sqlx::test(fixtures("base", "secret_backend"))]
async fn test_vault_connection_jwt(db: Pool<Postgres>) {
skip_if_no_vault!();
@@ -236,70 +266,51 @@ mod tests {
let settings = vault_settings_static_token();
// Verify Vault connection
test_vault_connection(&settings, Some(&db))
.await
.expect("Failed to connect to Vault");
// Check initial state
let secrets_before = sqlx::query!(
"SELECT workspace_id, path, value FROM variable WHERE is_secret = true ORDER BY workspace_id, path"
)
.fetch_all(&db)
.await
.expect("Failed to query secrets");
// Encrypt fixture placeholders with real workspace keys
encrypt_fixture_secrets(&db).await;
println!(
"Found {} secrets in database before migration:",
secrets_before.len()
);
for s in &secrets_before {
println!(" - {}/{}: {} chars", s.workspace_id, s.path, s.value.len());
}
let secret_count = sqlx::query_scalar!(
"SELECT COUNT(*) FROM variable WHERE is_secret = true"
)
.fetch_one(&db)
.await
.expect("Failed to count secrets");
println!("Found {} secrets in database before migration", secret_count.unwrap_or(0));
// Run migration
println!("\nMigrating secrets to Vault...");
println!("Migrating secrets to Vault...");
let report = migrate_secrets_to_vault(&db, &settings)
.await
.expect("Migration to Vault failed");
println!("Migration report:");
println!(" Total secrets: {}", report.total_secrets);
println!(" Migrated: {}", report.migrated_count);
println!(" Failed: {}", report.failed_count);
println!("Migration report: total={}, migrated={}, failed={}",
report.total_secrets, report.migrated_count, report.failed_count);
if !report.failures.is_empty() {
println!(" Failures:");
for f in &report.failures {
println!(" - {}/{}: {}", f.workspace_id, f.path, f.error);
println!(" FAIL: {}/{}: {}", f.workspace_id, f.path, f.error);
}
}
assert_eq!(report.failed_count, 0, "Migration had failures");
assert!(report.migrated_count > 0, "No secrets were migrated");
// Verify secrets in Vault
println!("\nVerifying secrets in Vault...");
// Verify decrypted values in Vault match original plaintexts
let vault_backend = VaultBackend::new(settings.clone());
for secret in &secrets_before {
let result = vault_backend
.get_secret(&secret.workspace_id, &secret.path)
.await;
assert!(
result.is_ok(),
"Failed to read secret {}/{} from Vault: {:?}",
secret.workspace_id,
secret.path,
result.err()
);
println!(
"{}/{} exists in Vault",
secret.workspace_id, secret.path
);
for &(ws, path, expected_plaintext) in TEST_SECRETS {
let value = vault_backend
.get_secret(ws, path)
.await
.unwrap_or_else(|e| panic!("Failed to read {}/{} from Vault: {:?}", ws, path, e));
assert_eq!(value, expected_plaintext, "Vault value mismatch for {}/{}", ws, path);
println!("{}/{} correct in Vault", ws, path);
}
println!("\n✓ Migration to Vault completed successfully");
println!("✓ Migration to Vault completed successfully");
}
/// Test migration from Vault back to database
@@ -313,49 +324,50 @@ mod tests {
.await
.expect("Failed to connect to Vault");
encrypt_fixture_secrets(&db).await;
// First migrate TO Vault
println!("Setting up: migrating secrets to Vault first...");
let to_vault = migrate_secrets_to_vault(&db, &settings)
.await
.expect("Initial migration to Vault failed");
assert!(to_vault.migrated_count > 0, "No secrets to test with");
println!(" Migrated {} secrets to Vault", to_vault.migrated_count);
// Clear database values
println!("\nClearing database secret values...");
sqlx::query!("UPDATE variable SET value = 'CLEARED' WHERE is_secret = true")
.execute(&db)
.await
.expect("Failed to clear values");
// Migrate back from Vault
println!("\nMigrating secrets from Vault to database...");
println!("Migrating secrets from Vault to database...");
let report = migrate_secrets_to_database(&db, &settings)
.await
.expect("Migration to database failed");
println!("Migration report:");
println!(" Total secrets: {}", report.total_secrets);
println!(" Migrated: {}", report.migrated_count);
println!(" Failed: {}", report.failed_count);
println!("Migration report: total={}, migrated={}, failed={}",
report.total_secrets, report.migrated_count, report.failed_count);
assert_eq!(report.failed_count, 0, "Migration had failures");
assert!(report.migrated_count > 0, "No secrets were migrated");
// Verify restored
let restored = sqlx::query!(
"SELECT COUNT(*) as count FROM variable WHERE is_secret = true AND value != 'CLEARED'"
)
.fetch_one(&db)
.await
.expect("Failed to count restored");
// Verify restored values decrypt to the original plaintexts
for &(ws, path, expected_plaintext) in TEST_SECRETS {
let row = sqlx::query_scalar!(
"SELECT value FROM variable WHERE workspace_id = $1 AND path = $2 AND is_secret = true",
ws, path
)
.fetch_one(&db)
.await
.unwrap_or_else(|_| panic!("Secret {}/{} not found in DB after migration", ws, path));
assert!(
restored.count.unwrap_or(0) > 0,
"No secrets were restored in database"
);
let mc = build_crypt(&db, ws).await.unwrap();
let decrypted = decrypt(&mc, row).expect("Failed to decrypt restored value");
assert_eq!(decrypted, expected_plaintext, "Restored value mismatch for {}/{}", ws, path);
println!("{}/{} correctly restored in DB", ws, path);
}
println!("\n✓ Migration to database completed successfully");
println!("✓ Migration to database completed successfully");
}
// ==================== Variable Rename Tests ====================
@@ -443,68 +455,47 @@ mod tests {
.await
.expect("Failed to connect to Vault");
// Get original secrets
let original: HashMap<(String, String), String> = sqlx::query!(
"SELECT workspace_id, path, value FROM variable WHERE is_secret = true"
)
.fetch_all(&db)
.await
.expect("Failed to query")
.into_iter()
.map(|r| ((r.workspace_id, r.path), r.value))
.collect();
encrypt_fixture_secrets(&db).await;
println!("Original secrets: {} entries", original.len());
// Step 1: DB -> Vault
println!("\n=== Step 1: Migrate DB -> Vault ===");
println!("=== Step 1: Migrate DB -> Vault ===");
let to_vault = migrate_secrets_to_vault(&db, &settings)
.await
.expect("Migration to Vault failed");
println!("Migrated {} secrets to Vault", to_vault.migrated_count);
assert_eq!(to_vault.failed_count, 0);
println!("Migrated {} secrets to Vault", to_vault.migrated_count);
// Step 2: Clear DB
println!("\n=== Step 2: Clear database values ===");
println!("=== Step 2: Clear database values ===");
sqlx::query!("UPDATE variable SET value = 'ROUND_TRIP_CLEARED' WHERE is_secret = true")
.execute(&db)
.await
.expect("Failed to clear");
// Step 3: Vault -> DB
println!("\n=== Step 3: Migrate Vault -> DB ===");
println!("=== Step 3: Migrate Vault -> DB ===");
let to_db = migrate_secrets_to_database(&db, &settings)
.await
.expect("Migration to database failed");
println!("Migrated {} secrets to database", to_db.migrated_count);
assert_eq!(to_db.failed_count, 0);
println!("Migrated {} secrets to database", to_db.migrated_count);
// Step 4: Verify
println!("\n=== Step 4: Verify round-trip integrity ===");
let restored: HashMap<(String, String), String> = sqlx::query!(
"SELECT workspace_id, path, value FROM variable WHERE is_secret = true"
)
.fetch_all(&db)
.await
.expect("Failed to query")
.into_iter()
.map(|r| ((r.workspace_id, r.path), r.value))
.collect();
for ((ws, path), _) in &original {
let restored_value = restored
.get(&(ws.clone(), path.clone()))
.expect(&format!("Secret {}/{} not found after round-trip", ws, path));
assert_ne!(
restored_value, "ROUND_TRIP_CLEARED",
"Secret {}/{} was not restored",
println!("=== Step 4: Verify round-trip integrity ===");
for &(ws, path, expected_plaintext) in TEST_SECRETS {
let encrypted = sqlx::query_scalar!(
"SELECT value FROM variable WHERE workspace_id = $1 AND path = $2 AND is_secret = true",
ws, path
);
println!("{}/{}: restored", ws, path);
)
.fetch_one(&db)
.await
.unwrap_or_else(|_| panic!("Secret {}/{} not found after round-trip", ws, path));
assert_ne!(encrypted, "ROUND_TRIP_CLEARED", "Secret {}/{} was not restored", ws, path);
let mc = build_crypt(&db, ws).await.unwrap();
let decrypted = decrypt(&mc, encrypted).expect("Failed to decrypt");
assert_eq!(decrypted, expected_plaintext, "Round-trip value mismatch for {}/{}", ws, path);
println!("{}/{}: round-trip OK", ws, path);
}
println!("\n✓ Full round-trip completed successfully!");
println!("✓ Full round-trip completed successfully!");
}
// ==================== Workspace Isolation Test ====================
@@ -517,7 +508,8 @@ mod tests {
let settings = vault_settings_static_token();
let backend = VaultBackend::new(settings.clone());
// First migrate secrets to Vault
encrypt_fixture_secrets(&db).await;
migrate_secrets_to_vault(&db, &settings)
.await
.expect("Migration failed");
@@ -542,7 +534,7 @@ mod tests {
assert!(ws1.is_ok(), "Same-workspace access should work");
println!("✓ Same-workspace access works");
println!("\n✓ Workspace isolation verified!");
println!("✓ Workspace isolation verified!");
}
}
+5
View File
@@ -460,6 +460,11 @@ impl OpenAPISchema {
Typ::Bool => Self::from_str("boolean"),
Typ::Bytes => Self::from_str("string"),
Typ::Datetime => Self::datetime(),
Typ::Date => Self {
r#type: Some(SchemaType::Single("string".to_string())),
format: Some("date".to_string()),
..Default::default()
},
Typ::Resource(_) => Self::from_str("string"),
Typ::Email => Self::from_str("string"),
Typ::Sql => Self::from_str("string"),
+26 -6
View File
@@ -1016,6 +1016,21 @@ async fn prepare_wrapper(
kwargs[\"{name}\"] = datetime.fromisoformat(kwargs[\"{name}\"])\n",
)
}
windmill_parser::Typ::Date => {
let name = &x.name;
format!(
"if \"{name}\" in kwargs and kwargs[\"{name}\"] is not None:\n \
try:\n \
kwargs[\"{name}\"] = date.fromisoformat(kwargs[\"{name}\"])\n \
except ValueError:\n \
for _fmt in (\"%d-%m-%Y\", \"%m/%d/%Y\", \"%d/%m/%Y\", \"%Y/%m/%d\"):\n \
try:\n \
kwargs[\"{name}\"] = datetime.strptime(kwargs[\"{name}\"], _fmt).date()\n \
break\n \
except ValueError:\n \
continue\n",
)
}
_ => "".to_string(),
})
.collect::<Vec<String>>()
@@ -1035,14 +1050,19 @@ async fn prepare_wrapper(
} else {
""
};
let import_datetime = if init_sig
let has_datetime = init_sig
.args
.iter()
.any(|x| x.typ == windmill_parser::Typ::Datetime)
{
"from datetime import datetime"
} else {
""
.any(|x| x.typ == windmill_parser::Typ::Datetime);
let has_date = init_sig
.args
.iter()
.any(|x| x.typ == windmill_parser::Typ::Date);
let import_datetime = match (has_datetime, has_date) {
(true, true) => "from datetime import datetime, date",
(true, false) => "from datetime import datetime",
(false, true) => "from datetime import datetime, date",
(false, false) => "",
};
let spread = if sig.star_kwargs {
"args = kwargs".to_string()
+1 -1
View File
@@ -34,7 +34,7 @@ fn make_rules_for_arg_typ(typ: &Typ) -> Vec<SchemaValidationRule> {
rules.push(SchemaValidationRule::IsString);
rules.push(SchemaValidationRule::IsBytes);
}
Typ::Datetime => {
Typ::Datetime | Typ::Date => {
rules.push(SchemaValidationRule::IsString);
rules.push(SchemaValidationRule::IsDatetime);
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "windmill-utils-internal",
"version": "1.3.2",
"version": "1.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-utils-internal",
"version": "1.3.2",
"version": "1.3.4",
"license": "Apache 2.0",
"devDependencies": {
"@types/node": "^24.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "windmill-utils-internal",
"version": "1.3.3",
"version": "1.3.4",
"description": "Internal utility functions for Windmill",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
@@ -108,6 +108,9 @@ export function argSigToJsonSchemaType(
} else if (t === "datetime") {
newS.type = "string";
newS.format = "date-time";
} else if (t === "date") {
newS.type = "string";
newS.format = "date";
} else if (typeof t !== "string" && "oneof" in t) {
newS.type = "object";
if (t.oneof) {
+9 -212
View File
@@ -79,14 +79,14 @@
"windmill-parser-wasm-java": "1.510.1",
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.574.1",
"windmill-parser-wasm-py": "1.623.1",
"windmill-parser-wasm-py": "^1.628.3",
"windmill-parser-wasm-regex": "1.625.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.558.1",
"windmill-parser-wasm-ts": "1.623.1",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-sql-datatype-parser-wasm": "1.512.0",
"windmill-utils-internal": "^1.3.2",
"windmill-utils-internal": "^1.3.4",
"xterm": "^5.3.0",
"xterm-readline": "^1.1.2",
"y-monaco": "^0.1.4",
@@ -834,7 +834,6 @@
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -846,7 +845,6 @@
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -857,7 +855,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1347,7 +1344,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz",
"integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1502,7 +1498,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1519,7 +1514,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1536,7 +1530,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1553,7 +1546,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1570,7 +1562,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1587,7 +1578,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1604,7 +1594,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1621,7 +1610,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1638,7 +1626,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1655,7 +1642,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1672,7 +1658,6 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1689,7 +1674,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1706,7 +1690,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2327,7 +2310,6 @@
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -2955,115 +2937,6 @@
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"license": "ISC"
},
"node_modules/@vitest/browser": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.15.tgz",
"integrity": "sha512-zedtczX688KehaIaAv7m25CeDLb0gBtAOa2Oi1G1cqvSO5aLSVfH6lpZMJLW8BKYuWMxLQc9/5GYoM+jgvGIrw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@vitest/mocker": "4.0.15",
"@vitest/utils": "4.0.15",
"magic-string": "^0.30.21",
"pixelmatch": "7.1.0",
"pngjs": "^7.0.0",
"sirv": "^3.0.2",
"tinyrainbow": "^3.0.3",
"ws": "^8.18.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "4.0.15"
}
},
"node_modules/@vitest/browser-playwright": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.15.tgz",
"integrity": "sha512-94yVpDbb+ykiT7mK6ToonGnq2GIHEQGBTZTAzGxBGQXcVNCh54YKC2/WkfaDzxy0m6Kgw05kq3FYHKHu+wRdIA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@vitest/browser": "4.0.15",
"@vitest/mocker": "4.0.15",
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"playwright": "*",
"vitest": "4.0.15"
},
"peerDependenciesMeta": {
"playwright": {
"optional": false
}
}
},
"node_modules/@vitest/browser-playwright/node_modules/@vitest/mocker": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz",
"integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@vitest/spy": "4.0.15",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/browser/node_modules/@vitest/mocker": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz",
"integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@vitest/spy": "4.0.15",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/expect": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz",
@@ -7185,7 +7058,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -7684,7 +7557,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7705,7 +7577,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7726,7 +7597,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7747,7 +7617,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7768,7 +7637,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7789,7 +7657,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7810,7 +7677,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7831,7 +7697,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7852,7 +7717,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7873,7 +7737,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7894,7 +7757,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -9950,21 +9812,6 @@
"node": ">= 6"
}
},
"node_modules/pixelmatch": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz",
"integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"dependencies": {
"pngjs": "^7.0.0"
},
"bin": {
"pixelmatch": "bin/pixelmatch"
}
},
"node_modules/pkg-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
@@ -10040,18 +9887,6 @@
"node": ">=4"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -12548,21 +12383,6 @@
}
}
},
"node_modules/svelte-check/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/svelte-eslint-parser": {
"version": "0.43.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
@@ -14603,9 +14423,9 @@
"integrity": "sha512-COyid6B1RYs+bpzUCInsA4HY/WZkpDLfkQ90+AqU/TVTpzYSbAC2JCbIwy0cRElBvlhI4bQ+9Wg6hSQKMpEkpA=="
},
"node_modules/windmill-parser-wasm-py": {
"version": "1.623.1",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.623.1.tgz",
"integrity": "sha512-lFBlZg6hvhHzsU5oPJq0478UyMTZ9UKVd8Hc8ggmxPIHZaJBeJ+56NR75hmGwg0VJcRff8ed+zEm1PgmjVhD+w=="
"version": "1.628.3",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.628.3.tgz",
"integrity": "sha512-TlluqknZpg8cZ+A3m6JFLPseY2PpKtDsxdj26fAnCUzKPtse8TxQR+n0dwC80rfW5TwdWSulvNGRDgcNuf7CTw=="
},
"node_modules/windmill-parser-wasm-regex": {
"version": "1.625.0",
@@ -14638,9 +14458,9 @@
"integrity": "sha512-uHNL8F72/Tf96xF3hOHnPDjkEyqXw7fNjcPJiUhth9sTQkcwUIoJMOdwm8/cs+j9kKVRJ4tgNYMHEBLylazp6g=="
},
"node_modules/windmill-utils-internal": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/windmill-utils-internal/-/windmill-utils-internal-1.3.2.tgz",
"integrity": "sha512-sLE7sfmN3vBwPReqKorBrusnn83Qnm8vVFqh/7+kz8mf98ojMVDMgBginjlh9KBtVfWu9NhyjGS+WXRcnWalzA==",
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/windmill-utils-internal/-/windmill-utils-internal-1.3.4.tgz",
"integrity": "sha512-XVypDKIZ6P4fwIjZwvuvq1m+j0rtAA7BDp1rI2F7hQ+VBKZUHsLskP+jgstXs+kN1LqGGsQJj4ecMYDImpIZ6A==",
"license": "Apache 2.0"
},
"node_modules/word-wrap": {
@@ -14785,29 +14605,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-utils": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
+2 -2
View File
@@ -149,14 +149,14 @@
"windmill-parser-wasm-java": "1.510.1",
"windmill-parser-wasm-nu": "1.510.1",
"windmill-parser-wasm-php": "1.574.1",
"windmill-parser-wasm-py": "1.623.1",
"windmill-parser-wasm-py": "^1.628.3",
"windmill-parser-wasm-regex": "1.625.0",
"windmill-parser-wasm-ruby": "1.526.1",
"windmill-parser-wasm-rust": "1.558.1",
"windmill-parser-wasm-ts": "1.623.1",
"windmill-parser-wasm-yaml": "1.593.0",
"windmill-sql-datatype-parser-wasm": "1.512.0",
"windmill-utils-internal": "^1.3.2",
"windmill-utils-internal": "^1.3.4",
"xterm": "^5.3.0",
"xterm-readline": "^1.1.2",
"y-monaco": "^0.1.4",
+5 -2
View File
@@ -258,7 +258,11 @@
nvalue = structuredClone($state.snapshot(defaultValue))
if (defaultValue === undefined || defaultValue === null) {
if (inputCat === 'string') {
nvalue = nullable ? null : format === 'uuid' && extra?.['x-auto-generate'] ? randomUUID() : ''
nvalue = nullable
? null
: format === 'uuid' && extra?.['x-auto-generate']
? randomUUID()
: ''
} else if (inputCat == 'enum' && required) {
let firstV = enum_?.[0]
if (typeof firstV === 'string') {
@@ -1177,7 +1181,6 @@
titleClass="text-2xs"
/>
{/if}
{:else if disabled}
<textarea disabled></textarea>
{:else}
@@ -899,11 +899,6 @@ class AIChatManager {
}
saveAndClear = async () => {
console.log('saveAndClear called', {
hasAbortController: !!this.abortController,
isLoading: this.loading,
stack: new Error().stack
})
this.cancel('saveAndClear')
await this.historyManager.save(this.displayMessages, this.messages)
this.displayMessages = []