diff --git a/backend/.sqlx/query-a750630d79166f5b6d5be0e049c36135212666ce74dbf290d701b6402b800f13.json b/backend/.sqlx/query-a750630d79166f5b6d5be0e049c36135212666ce74dbf290d701b6402b800f13.json new file mode 100644 index 0000000000..ebf92515e1 --- /dev/null +++ b/backend/.sqlx/query-a750630d79166f5b6d5be0e049c36135212666ce74dbf290d701b6402b800f13.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n jsonb_strip_nulls(jsonb_build_object(\n 'path', path,\n 'kind', kind,\n 'access_type', usage_access_type,\n 'columns', columns\n )) as \"list!: _\"\n FROM asset\n WHERE workspace_id = $1 AND usage_path = $2 AND usage_kind = $3\n ORDER BY path, kind", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "list!: _", + "type_info": "Jsonb" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + { + "Custom": { + "name": "asset_usage_kind", + "kind": { + "Enum": [ + "script", + "flow", + "job" + ] + } + } + } + ] + }, + "nullable": [ + null + ] + }, + "hash": "a750630d79166f5b6d5be0e049c36135212666ce74dbf290d701b6402b800f13" +} diff --git a/backend/.sqlx/query-a9e29764b5b9d94269e2b8aa755c71b61774c8ff8ae218d7a8d6ed0ac0169366.json b/backend/.sqlx/query-a9e29764b5b9d94269e2b8aa755c71b61774c8ff8ae218d7a8d6ed0ac0169366.json new file mode 100644 index 0000000000..b39c1b5b31 --- /dev/null +++ b/backend/.sqlx/query-a9e29764b5b9d94269e2b8aa755c71b61774c8ff8ae218d7a8d6ed0ac0169366.json @@ -0,0 +1,55 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO asset (workspace_id, path, kind, usage_access_type, usage_path, usage_kind, columns)\n VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + { + "Custom": { + "name": "asset_kind", + "kind": { + "Enum": [ + "s3object", + "resource", + "variable", + "ducklake", + "datatable" + ] + } + } + }, + { + "Custom": { + "name": "asset_access_type", + "kind": { + "Enum": [ + "r", + "w", + "rw" + ] + } + } + }, + "Varchar", + { + "Custom": { + "name": "asset_usage_kind", + "kind": { + "Enum": [ + "script", + "flow", + "job" + ] + } + } + }, + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "a9e29764b5b9d94269e2b8aa755c71b61774c8ff8ae218d7a8d6ed0ac0169366" +} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 818af9b666..153181e90c 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -88e49a7c9746080a8a95e30828655d5783a616d6 \ No newline at end of file +88e49a7c9746080a8a95e30828655d5783a616d6 diff --git a/backend/migrations/20260203122047_asset_columns.down.sql b/backend/migrations/20260203122047_asset_columns.down.sql new file mode 100644 index 0000000000..a97fc56c19 --- /dev/null +++ b/backend/migrations/20260203122047_asset_columns.down.sql @@ -0,0 +1,2 @@ +-- Remove columns field from asset table +ALTER TABLE asset DROP COLUMN columns; diff --git a/backend/migrations/20260203122047_asset_columns.up.sql b/backend/migrations/20260203122047_asset_columns.up.sql new file mode 100644 index 0000000000..2133b8da7d --- /dev/null +++ b/backend/migrations/20260203122047_asset_columns.up.sql @@ -0,0 +1,3 @@ +-- Add columns field to asset table to store column-level access information +-- This is a JSONB map of column name to access type (r, w, or rw) +ALTER TABLE asset ADD COLUMN columns JSONB; diff --git a/backend/parsers/windmill-parser-py/src/asset_parser.rs b/backend/parsers/windmill-parser-py/src/asset_parser.rs index f94c09ac7f..3818eb2a1b 100644 --- a/backend/parsers/windmill-parser-py/src/asset_parser.rs +++ b/backend/parsers/windmill-parser-py/src/asset_parser.rs @@ -19,9 +19,12 @@ pub fn parse_assets(input: &str) -> anyhow::Result { // if a db = wmill.datatable() was never used (e.g db.query(...)), // we still want to register the asset as unknown access type if asset_was_used(&assets_finder.assets, (kind, &path)) == false { - assets_finder - .assets - .push(ParseAssetsResult { kind, access_type: None, path }); + assets_finder.assets.push(ParseAssetsResult { + kind, + path, + access_type: None, + columns: None, + }); } } @@ -48,8 +51,12 @@ impl Visitor for AssetsFinder { match removed { Some((kind, path, _)) => { if !asset_was_used(&self.assets, (kind, &path)) { - self.assets - .push(ParseAssetsResult { kind, access_type: None, path }); + self.assets.push(ParseAssetsResult { + kind, + path, + access_type: None, + columns: None, + }); } } None => {} @@ -76,6 +83,7 @@ impl Visitor for AssetsFinder { kind, path: path.to_string(), access_type: None, + columns: None, }); } } @@ -97,6 +105,7 @@ impl Visitor for AssetsFinder { kind, path: path.to_string(), access_type: None, + columns: None, }); } } @@ -252,8 +261,12 @@ impl AssetsFinder { let path = parse_asset_syntax(&value, false) .map(|(_, p)| p) .unwrap_or(&value); - self.assets - .push(ParseAssetsResult { kind, path: path.to_string(), access_type }); + self.assets.push(ParseAssetsResult { + kind, + path: path.to_string(), + access_type, + columns: None, + }); } _ => return Err(()), }; @@ -281,7 +294,8 @@ def main(): Ok(vec![ParseAssetsResult { kind: AssetKind::S3Object, path: "/test.csv".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, },]) ); } @@ -299,7 +313,8 @@ def main(): Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "main".to_string(), - access_type: None + access_type: None, + columns: None, },]) ); } @@ -318,7 +333,8 @@ def main(x: int): Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/friends".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, },]) ); } @@ -340,12 +356,14 @@ def main(x: int): ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/analytics".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, }, ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/friends".to_string(), - access_type: Some(RW) + access_type: Some(RW), + columns: None, }, ]) ); @@ -372,17 +390,20 @@ def g(): ParseAssetsResult { kind: AssetKind::DataTable, path: "another1/customers".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: None, }, ParseAssetsResult { kind: AssetKind::Ducklake, path: "another2".to_string(), - access_type: None + access_type: None, + columns: None, }, ParseAssetsResult { kind: AssetKind::DataTable, path: "main/friends".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, }, ]) ); @@ -404,12 +425,14 @@ def g(): ParseAssetsResult { kind: AssetKind::DataTable, path: "another1".to_string(), - access_type: None + access_type: None, + columns: None, }, ParseAssetsResult { kind: AssetKind::Ducklake, path: "main".to_string(), - access_type: None + access_type: None, + columns: None, }, ]) ); @@ -429,7 +452,8 @@ def main(x: int): Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/public.friends".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, },]) ); } @@ -448,7 +472,8 @@ def main(): Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "lake1/analytics.metrics".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, },]) ); } @@ -468,7 +493,8 @@ def main(x: int): Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/public.users".to_string(), - access_type: Some(RW) + access_type: Some(RW), + columns: None, },]) ); } @@ -486,7 +512,8 @@ def main(): Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "dt".to_string(), - access_type: None + access_type: None, + columns: None, },]) ); } diff --git a/backend/parsers/windmill-parser-sql/src/asset_parser.rs b/backend/parsers/windmill-parser-sql/src/asset_parser.rs index 489f9c661f..51b90b379a 100644 --- a/backend/parsers/windmill-parser-sql/src/asset_parser.rs +++ b/backend/parsers/windmill-parser-sql/src/asset_parser.rs @@ -1,9 +1,9 @@ -use std::collections::HashMap; +use std::collections::BTreeMap; use sqlparser::{ ast::{ - CopyTarget, Expr, ObjectName, TableFactor, TableObject, Value, ValueWithSpan, Visit, - Visitor, + CopyTarget, Expr, ObjectName, ObjectNamePart, SelectItem, TableFactor, TableObject, Value, + ValueWithSpan, Visit, Visitor, }, dialect::DuckDbDialect, parser::Parser, @@ -24,9 +24,12 @@ pub fn parse_assets(input: &str) -> anyhow::Result { for (_, (kind, path)) in collector.var_identifiers { if !asset_was_used(&collector.assets, (kind, &path)) { - collector - .assets - .push(ParseAssetsResult { kind, access_type: None, path: path }); + collector.assets.push(ParseAssetsResult { + kind, + access_type: None, + path: path, + columns: None, + }); } } @@ -39,7 +42,7 @@ struct AssetCollector { // e.g set to Read when we are inside a SELECT ... FROM ... statement current_access_type_stack: Vec, // e.g ATTACH 'ducklake://a' AS dl; => { "dl": (Ducklake, "a") } - var_identifiers: HashMap, + var_identifiers: BTreeMap, // e.g USE dl; currently_used_asset: Option<(AssetKind, String)>, } @@ -49,15 +52,19 @@ impl AssetCollector { Self { assets: Vec::new(), current_access_type_stack: Vec::with_capacity(8), - var_identifiers: HashMap::new(), + var_identifiers: BTreeMap::new(), currently_used_asset: None, } } // Detect when we do 'a.b' and 'a' is associated with an asset in var_identifiers // Or when we access 'b' and we did USE a; - fn get_associated_asset_from_obj_name(&self, name: &ObjectName) -> Option { - let access_type = self.current_access_type_stack.last().copied(); + fn get_associated_asset_from_obj_name( + &self, + name: &ObjectName, + access_type: Option, + ) -> Option { + let access_type = access_type.or_else(|| self.current_access_type_stack.last().copied()); if let Some((kind, path)) = &self.currently_used_asset { // We don't want to infer that any simple identifier refers to an asset if // we are not in a known R/W context @@ -81,7 +88,7 @@ impl AssetCollector { .collect::>>()? .join("."); let path = format!("{}/{}", path, specific_table); - return Some(ParseAssetsResult { kind: *kind, access_type, path }); + return Some(ParseAssetsResult { kind: *kind, access_type, path, columns: None }); } } @@ -101,7 +108,7 @@ impl AssetCollector { } else { path.clone() }; - Some(ParseAssetsResult { kind: *kind, access_type, path }) + Some(ParseAssetsResult { kind: *kind, access_type, path, columns: None }) } fn handle_string_literal(&mut self, s: &str) { @@ -112,6 +119,7 @@ impl AssetCollector { kind, path: path.to_string(), access_type: self.current_access_type_stack.last().copied(), + columns: None, }); } } @@ -126,13 +134,6 @@ impl AssetCollector { if let Some(str_lit) = get_str_lit_from_obj_name(name) { self.handle_string_literal(str_lit); } - - // Writes to tables should be handled directly when visiting the statement - if self.current_access_type_stack.last() == Some(&R) { - if let Some(asset) = self.get_associated_asset_from_obj_name(name) { - self.assets.push(asset); - } - } } fn handle_obj_name_post(&mut self, name: &ObjectName) { @@ -146,20 +147,144 @@ impl AssetCollector { } } - fn handle_table_with_joins(&mut self, table_with_joins: &sqlparser::ast::TableWithJoins) { - if let TableFactor::Table { name, .. } = &table_with_joins.relation { - if let Some(asset) = self.get_associated_asset_from_obj_name(name) { + fn handle_table_with_joins( + &mut self, + table_with_joins: &sqlparser::ast::TableWithJoins, + access_type: Option, + ) { + if let TableFactor::Table { name, args, .. } = &table_with_joins.relation { + if args.is_some() && args.as_ref().map_or(0, |a| a.args.len()) > 0 { + return; + } + if let Some(asset) = self.get_associated_asset_from_obj_name(name, access_type) { self.assets.push(asset); } } for join in &table_with_joins.joins { if let TableFactor::Table { name, .. } = &join.relation { - if let Some(asset) = self.get_associated_asset_from_obj_name(name) { + if let Some(asset) = self.get_associated_asset_from_obj_name(name, access_type) { self.assets.push(asset); } } } } + + // Extract columns from SELECT items and create individual asset results for each column + // Only processes columns that reference known assets to avoid false positives + fn extract_column_assets( + &mut self, + projection: &[SelectItem], + from_tables: &[sqlparser::ast::TableWithJoins], + ) { + // Check if this is a single-table SELECT (to avoid ambiguity) + let single_table = if from_tables.len() == 1 { + if let TableFactor::Table { name, args, .. } = &from_tables[0].relation { + if args.is_some() && args.as_ref().map_or(0, |a| a.args.len()) > 0 { + return; // Skip table functions + } + self.get_associated_asset_from_obj_name(name, Some(R)) + } else { + None + } + } else { + None + }; + + // Build a map of table aliases/names to assets for multi-table queries + let mut table_to_asset: BTreeMap = BTreeMap::new(); + for table_with_joins in from_tables { + if let TableFactor::Table { name, alias, args, .. } = &table_with_joins.relation { + if args.is_some() && args.as_ref().map_or(0, |a| a.args.len()) > 0 { + continue; // Skip table functions + } + if let Some(asset) = self.get_associated_asset_from_obj_name(name, Some(R)) { + // Use alias if present, otherwise use the table name + let table_key = if let Some(alias) = alias { + alias.name.value.clone() + } else { + // For qualified names like "dl.table1", use just the last part + name.0 + .last() + .and_then(|id| id.as_ident()) + .map(|id| id.value.clone()) + .unwrap_or_default() + }; + table_to_asset.insert(table_key, asset); + } + } + } + + // Process each SELECT item + for item in projection { + match item { + SelectItem::UnnamedExpr(Expr::Identifier(ident)) + | SelectItem::ExprWithAlias { expr: Expr::Identifier(ident), .. } => { + // Simple column: SELECT a + // Only add if we have a single table (unambiguous) + if let Some(asset) = &single_table { + let mut columns = BTreeMap::new(); + columns.insert(ident.value.clone(), R); + self.assets.push(ParseAssetsResult { + kind: asset.kind, + path: asset.path.clone(), + access_type: Some(R), + columns: Some(columns), + }); + } + } + SelectItem::UnnamedExpr(Expr::CompoundIdentifier(parts)) + | SelectItem::ExprWithAlias { expr: Expr::CompoundIdentifier(parts), .. } => { + // Qualified column: SELECT table1.a or SELECT x.table1.a + if parts.len() >= 2 { + let column_name = parts.last().map(|id| id.value.clone()); + + if let Some(column_name) = column_name { + // Check if the prefix matches a known table + let table_prefix = parts.first().map(|id| id.value.clone()); + + if let Some(table_prefix) = table_prefix { + if let Some(asset) = table_to_asset.get(&table_prefix) { + // Found a matching table, add column asset + let mut columns = BTreeMap::new(); + columns.insert(column_name.clone(), R); + self.assets.push(ParseAssetsResult { + kind: asset.kind, + path: asset.path.clone(), + access_type: Some(R), + columns: Some(columns), + }); + } else if parts.len() >= 3 { + // Could be x.table1.column format or db.schema.table.column + // Convert Idents to ObjectNameParts + let obj_parts: Vec = parts[..parts.len() - 1] + .iter() + .cloned() + .map(|ident| ObjectNamePart::Identifier(ident)) + .collect(); + let obj_name = ObjectName(obj_parts); + if let Some(asset) = + self.get_associated_asset_from_obj_name(&obj_name, Some(R)) + { + let mut columns = BTreeMap::new(); + columns.insert(column_name.clone(), R); + self.assets.push(ParseAssetsResult { + kind: asset.kind, + path: asset.path.clone(), + access_type: Some(R), + columns: Some(columns), + }); + } + } + } + } + } + } + _ => { + // Ignore wildcards, expressions, etc. + } + } + } + } } impl Visitor for AssetCollector { @@ -218,51 +343,154 @@ impl Visitor for AssetCollector { statement: &sqlparser::ast::Statement, ) -> std::ops::ControlFlow { match statement { - sqlparser::ast::Statement::Query(_) => { - // don't forget pop() in post_visit_statement - self.current_access_type_stack.push(R); + sqlparser::ast::Statement::Query(q) => { + if let Some(select) = q.body.as_select() { + // First, handle table references (adds table-level assets) + for t in &select.from { + self.handle_table_with_joins(t, Some(R)); + } + // Then, extract column-level assets + self.extract_column_assets(&select.projection, &select.from); + } } sqlparser::ast::Statement::Insert(insert) => { let access_type = if insert.returning.is_some() { RW } else { W }; - self.current_access_type_stack.push(access_type); match insert.table { TableObject::TableName(ref name) => { - if let Some(asset) = self.get_associated_asset_from_obj_name(name) { - self.assets.push(asset); + if let Some(asset) = + self.get_associated_asset_from_obj_name(name, Some(access_type)) + { + // Add table-level asset + self.assets.push(ParseAssetsResult { + kind: asset.kind, + path: asset.path.clone(), + access_type: asset.access_type, + columns: None, + }); + + // Extract column information for INSERT with explicit columns (Write access) + if !insert.columns.is_empty() { + for col in &insert.columns { + let columns = BTreeMap::from([(col.value.clone(), W)]); + self.assets.push(ParseAssetsResult { + kind: asset.kind, + path: asset.path.clone(), + access_type: Some(W), + columns: Some(columns), + }); + } + } + + // Extract column information from RETURNING clause (Read access) + if let Some(returning) = &insert.returning { + for item in returning { + match item { + SelectItem::UnnamedExpr(Expr::Identifier(ident)) + | SelectItem::ExprWithAlias { + expr: Expr::Identifier(ident), + .. + } => { + let mut col_map = BTreeMap::new(); + col_map.insert(ident.value.clone(), R); + self.assets.push(ParseAssetsResult { + kind: asset.kind, + path: asset.path.clone(), + access_type: Some(R), + columns: Some(col_map), + }); + } + _ => { + // Ignore wildcards and complex expressions + } + } + } + } } } _ => {} } - self.current_access_type_stack.pop(); } - sqlparser::ast::Statement::Update { returning, table, from, .. } => { + sqlparser::ast::Statement::Update { returning, table, from, assignments, .. } => { if let Some(from_tables) = from { let from_tables = match from_tables { sqlparser::ast::UpdateTableFromKind::AfterSet(tables) => tables, sqlparser::ast::UpdateTableFromKind::BeforeSet(tables) => tables, }; - self.current_access_type_stack.push(R); for table_with_joins in from_tables { - self.handle_table_with_joins(table_with_joins); + self.handle_table_with_joins(table_with_joins, Some(R)); } - self.current_access_type_stack.pop(); } let access_type = if returning.is_some() { RW } else { W }; - self.current_access_type_stack.push(access_type); + self.handle_table_with_joins(table, Some(access_type)); - self.handle_table_with_joins(table); + // Extract column information from UPDATE SET clauses (Write access) + // Only process if it's a single table update + if let TableFactor::Table { name, .. } = &table.relation { + if let Some(asset) = + self.get_associated_asset_from_obj_name(name, Some(access_type)) + { + // Process each assignment to extract column names + for assignment in assignments { + // assignment.target is an AssignmentTarget enum + // We only handle simple column names (ColumnName variant) + if let sqlparser::ast::AssignmentTarget::ColumnName(col_name) = + &assignment.target + { + // For simple column updates, this is typically a single ident + if col_name.0.len() == 1 { + if let Some(col_ident) = + col_name.0.first().and_then(|p| p.as_ident()) + { + let mut col_map = BTreeMap::new(); + col_map.insert(col_ident.value.clone(), W); + self.assets.push(ParseAssetsResult { + kind: asset.kind, + path: asset.path.clone(), + access_type: Some(W), + columns: Some(col_map), + }); + } + } + } + } - self.current_access_type_stack.pop(); + // Extract column information from RETURNING clause (Read access) + if let Some(returning_items) = returning { + for item in returning_items { + match item { + SelectItem::UnnamedExpr(Expr::Identifier(ident)) + | SelectItem::ExprWithAlias { + expr: Expr::Identifier(ident), + .. + } => { + let mut col_map = BTreeMap::new(); + col_map.insert(ident.value.clone(), R); + self.assets.push(ParseAssetsResult { + kind: asset.kind, + path: asset.path.clone(), + access_type: Some(R), + columns: Some(col_map), + }); + } + _ => { + // Ignore wildcards and complex expressions + } + } + } + } + } + } } sqlparser::ast::Statement::Delete(delete) => { let access_type = if delete.returning.is_some() { RW } else { W }; - self.current_access_type_stack.push(access_type); for name in &delete.tables { - if let Some(asset) = self.get_associated_asset_from_obj_name(name) { + if let Some(asset) = + self.get_associated_asset_from_obj_name(name, Some(access_type)) + { self.assets.push(asset); } } @@ -271,25 +499,22 @@ impl Visitor for AssetCollector { sqlparser::ast::FromTable::WithoutKeyword(tables) => tables, }; for table_with_joins in tables { - self.handle_table_with_joins(table_with_joins); + self.handle_table_with_joins(table_with_joins, Some(access_type)); } - self.current_access_type_stack.pop(); } sqlparser::ast::Statement::CreateTable(create_table) => { - self.current_access_type_stack.push(W); - if let Some(asset) = self.get_associated_asset_from_obj_name(&create_table.name) { + if let Some(asset) = + self.get_associated_asset_from_obj_name(&create_table.name, Some(W)) + { self.assets.push(asset); } - self.current_access_type_stack.pop(); } sqlparser::ast::Statement::CreateView { name, .. } => { - self.current_access_type_stack.push(W); - if let Some(asset) = self.get_associated_asset_from_obj_name(name) { + if let Some(asset) = self.get_associated_asset_from_obj_name(name, Some(W)) { self.assets.push(asset); } - self.current_access_type_stack.pop(); } sqlparser::ast::Statement::Copy { target: CopyTarget::File { filename }, .. } => { @@ -339,14 +564,8 @@ impl Visitor for AssetCollector { fn post_visit_statement( &mut self, - statement: &sqlparser::ast::Statement, + _statement: &sqlparser::ast::Statement, ) -> std::ops::ControlFlow { - match statement { - sqlparser::ast::Statement::Query(_) => { - self.current_access_type_stack.pop(); - } - _ => {} - } std::ops::ControlFlow::Continue(()) } @@ -409,17 +628,20 @@ mod tests { ParseAssetsResult { kind: AssetKind::S3Object, path: "/a.parquet".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None }, ParseAssetsResult { kind: AssetKind::S3Object, path: "/c.parquet".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: None }, ParseAssetsResult { kind: AssetKind::S3Object, path: "snd/b.parquet".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None }, ]) ); @@ -438,7 +660,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "my_dl".to_string(), - access_type: None + access_type: None, + columns: None },]) ); } @@ -455,7 +678,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "my_dl/table1".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None },]) ); } @@ -473,7 +697,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "my_dt/table1".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: None },]) ); } @@ -504,7 +729,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "my_dl/table1".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: None },]) ); } @@ -521,7 +747,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "main/table1".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: None },]) ); } @@ -543,7 +770,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "main/friends".to_string(), - access_type: Some(RW) + access_type: Some(RW), + columns: None },]) ); } @@ -561,7 +789,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "main".to_string(), - access_type: None + access_type: None, + columns: None },]) ); } @@ -579,7 +808,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "main/table1".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: None },]) ); } @@ -597,7 +827,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "main/table1".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: Some(BTreeMap::from([("id".to_string(), W)])), },]) ); } @@ -615,7 +846,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Resource, path: "u/user/pg_resource/table1".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None },]) ); } @@ -632,7 +864,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "main/table1".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: Some(BTreeMap::from([("id".to_string(), W)])), },]) ); } @@ -650,7 +883,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "main/sch.table1".to_string(), - access_type: Some(RW) + access_type: Some(RW), + columns: Some(BTreeMap::from([("id".to_string(), W)])), },]) ); } @@ -669,8 +903,289 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::Ducklake, path: "main/sch.table1".to_string(), - access_type: Some(RW) + access_type: Some(RW), + columns: Some(BTreeMap::from([("id".to_string(), W)])), },]) ); } + + #[test] + fn test_sql_asset_parser_single_table_column_detection() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + SELECT a, b FROM dl.table1; + "#; + let s = parse_assets(input).map(|s| s.assets); + + let result = s.unwrap(); + + // Should have one asset with merged columns + assert_eq!(result.len(), 1); + assert_eq!(result[0].path, "my_dl/table1"); + assert_eq!(result[0].access_type, Some(R)); + + // Check that both columns are present in the merged asset + let columns = result[0].columns.as_ref().expect("Should have columns"); + assert_eq!(columns.len(), 2); + assert_eq!(columns.get("a"), Some(&R)); + assert_eq!(columns.get("b"), Some(&R)); + } + + #[test] + fn test_sql_asset_parser_explicit_table_prefix_columns() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + SELECT dl.table1.a, dl.table1.b FROM dl.table1; + "#; + let s = parse_assets(input).map(|s| s.assets); + + // Should detect columns with explicit table prefix + let result = s.unwrap(); + // Check we have the table asset + + // Check we have column assets + assert!(result.iter().any(|a| { + a.path == "my_dl/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("a")) + })); + assert!(result.iter().any(|a| { + a.path == "my_dl/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("b")) + })); + } + + #[test] + fn test_sql_asset_parser_multi_table_no_simple_columns() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + SELECT a, b FROM dl.table1, dl.table2; + "#; + let s = parse_assets(input).map(|s| s.assets); + + // Simple columns (a, b) should NOT be detected with multiple tables + // Only table-level assets should be present + let result = s.unwrap(); + + // Should have 2 table assets + assert_eq!(result.iter().filter(|a| a.columns.is_none()).count(), 2); + + // Should have NO column assets (ambiguous which table they belong to) + assert_eq!(result.iter().filter(|a| a.columns.is_some()).count(), 0); + } + + #[test] + fn test_sql_asset_parser_multi_table_with_qualified_columns() { + let input = r#" + ATTACH 'ducklake://my_dl1' AS dl1; + ATTACH 'ducklake://my_dl2' AS dl2; + SELECT table1.a, table2.b FROM dl1.table1, dl2.table2; + "#; + let s = parse_assets(input).map(|s| s.assets); + + // Qualified columns should be detected even with multiple tables + let result = s.unwrap(); + + // Check we have column assets for both tables + assert!(result.iter().any(|a| { + a.path == "my_dl1/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("a")) + })); + assert!(result.iter().any(|a| { + a.path == "my_dl2/table2" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("b")) + })); + } + + #[test] + fn test_sql_asset_parser_use_with_simple_columns() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + USE dl; + SELECT a, b, c FROM table1; + "#; + let s = parse_assets(input).map(|s| s.assets); + + let result = s.unwrap(); + + // Should detect columns since it's a single table + assert!(result.iter().any(|a| { + a.path == "my_dl/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("a")) + })); + assert!(result.iter().any(|a| { + a.path == "my_dl/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("b")) + })); + assert!(result.iter().any(|a| { + a.path == "my_dl/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("c")) + })); + } + + #[test] + fn test_sql_asset_parser_wildcard_no_columns() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + SELECT * FROM dl.table1; + "#; + let s = parse_assets(input).map(|s| s.assets); + + let result = s.unwrap(); + + // Wildcard should NOT create column assets, only table asset + assert_eq!(result.len(), 1); + assert!(result[0].columns.is_none()); + } + + #[test] + fn test_sql_asset_parser_columns_with_alias() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + SELECT a AS column_a, b AS column_b FROM dl.table1; + "#; + let s = parse_assets(input).map(|s| s.assets); + + let result = s.unwrap(); + + // Should detect columns even when aliased + assert!(result.iter().any(|a| { + a.path == "my_dl/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("a")) + })); + assert!(result.iter().any(|a| { + a.path == "my_dl/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("b")) + })); + } + + #[test] + fn test_sql_asset_parser_columns_with_table_alias() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + SELECT t.a, t.b FROM dl.table1 AS t; + "#; + let s = parse_assets(input).map(|s| s.assets); + + let result = s.unwrap(); + + // Should detect columns using the table alias + assert!(result.iter().any(|a| { + a.path == "my_dl/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("a")) + })); + assert!(result.iter().any(|a| { + a.path == "my_dl/table1" + && a.columns + .as_ref() + .map_or(false, |cols| cols.contains_key("b")) + })); + } + + #[test] + fn test_sql_asset_parser_insert_with_columns() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + INSERT INTO dl.table1 (name, age, email) VALUES ('John', 30, 'john@example.com'); + "#; + let s = parse_assets(input).map(|s| s.assets); + + let result = s.unwrap(); + + // Should have one asset with merged columns + assert_eq!(result.len(), 1); + assert_eq!(result[0].path, "my_dl/table1"); + assert_eq!(result[0].access_type, Some(W)); + + // Check that all columns are present in the merged asset + let columns = result[0].columns.as_ref().expect("Should have columns"); + assert_eq!(columns.len(), 3); + assert_eq!(columns.get("name"), Some(&W)); + assert_eq!(columns.get("age"), Some(&W)); + assert_eq!(columns.get("email"), Some(&W)); + } + + #[test] + fn test_sql_asset_parser_insert_without_columns() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + INSERT INTO dl.table1 VALUES ('John', 30); + "#; + let s = parse_assets(input).map(|s| s.assets); + + let result = s.unwrap(); + + // Should have one asset without column information + assert_eq!(result.len(), 1); + assert_eq!(result[0].path, "my_dl/table1"); + assert_eq!(result[0].access_type, Some(W)); + assert!(result[0].columns.is_none()); + } + + #[test] + fn test_sql_asset_parser_update_multiple_columns() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + UPDATE dl.table1 SET name = 'Jane', age = 25, active = true; + "#; + let s = parse_assets(input).map(|s| s.assets); + + let result = s.unwrap(); + + // Should have one asset with merged columns + assert_eq!(result.len(), 1); + assert_eq!(result[0].path, "my_dl/table1"); + assert_eq!(result[0].access_type, Some(W)); + + // Check that all columns are present + let columns = result[0].columns.as_ref().expect("Should have columns"); + assert_eq!(columns.len(), 3); + assert_eq!(columns.get("name"), Some(&W)); + assert_eq!(columns.get("age"), Some(&W)); + assert_eq!(columns.get("active"), Some(&W)); + } + + #[test] + fn test_sql_asset_parser_update_returning() { + let input = r#" + ATTACH 'ducklake://my_dl' AS dl; + UPDATE dl.table1 SET name = 'Jane', age = 26 RETURNING id, name; + "#; + let s = parse_assets(input).map(|s| s.assets); + + let result = s.unwrap(); + + // Should have RW access type when RETURNING is used + assert_eq!(result.len(), 1); + assert_eq!(result[0].path, "my_dl/table1"); + assert_eq!(result[0].access_type, Some(RW)); + + // Check that columns are present with correct access types + // name and age are written (W), id and name are read (R) + // name should be RW (both written and read) + let columns = result[0].columns.as_ref().expect("Should have columns"); + assert_eq!(columns.len(), 3); + assert_eq!(columns.get("name"), Some(&RW)); // Written in SET, read in RETURNING + assert_eq!(columns.get("age"), Some(&W)); // Only written + assert_eq!(columns.get("id"), Some(&R)); // Only read + } } diff --git a/backend/parsers/windmill-parser-ts/src/asset_parser.rs b/backend/parsers/windmill-parser-ts/src/asset_parser.rs index 2b85070584..66bda658fe 100644 --- a/backend/parsers/windmill-parser-ts/src/asset_parser.rs +++ b/backend/parsers/windmill-parser-ts/src/asset_parser.rs @@ -117,6 +117,7 @@ impl Visit for AssetsFinder { kind, path: path.to_string(), access_type: None, + columns: None, }); } } @@ -177,8 +178,12 @@ impl Visit for AssetsFinder { if asset_was_used(&self.assets, (kind, path)) { continue; } - self.assets - .push(ParseAssetsResult { kind, access_type: None, path: path.clone() }); + self.assets.push(ParseAssetsResult { + kind, + access_type: None, + path: path.clone(), + columns: None, + }); } // Restore state - identifiers declared in this block go out of scope @@ -294,8 +299,12 @@ impl AssetsFinder { let path = parse_asset_syntax(&value, false) .map(|(_, p)| p) .unwrap_or(&value); - self.assets - .push(ParseAssetsResult { kind, path: path.to_string(), access_type }); + self.assets.push(ParseAssetsResult { + kind, + path: path.to_string(), + access_type, + columns: None, + }); } _ => return Err(()), } @@ -321,7 +330,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::S3Object, path: "/test.csv".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, },]) ); } @@ -340,7 +350,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "dt".to_string(), - access_type: None + access_type: None, + columns: None, },]) ); } @@ -360,7 +371,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/friends".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, },]) ); } @@ -383,12 +395,14 @@ mod tests { ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/analytics".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, }, ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/friends".to_string(), - access_type: Some(RW) + access_type: Some(RW), + columns: None, }, ]) ); @@ -418,17 +432,20 @@ mod tests { ParseAssetsResult { kind: AssetKind::DataTable, path: "another1/customers".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: None, }, ParseAssetsResult { kind: AssetKind::Ducklake, path: "another2".to_string(), - access_type: None + access_type: None, + columns: None, }, ParseAssetsResult { kind: AssetKind::DataTable, path: "main/friends".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, }, ]) ); @@ -452,12 +469,14 @@ mod tests { ParseAssetsResult { kind: AssetKind::DataTable, path: "another1".to_string(), - access_type: None + access_type: None, + columns: None, }, ParseAssetsResult { kind: AssetKind::Ducklake, path: "main".to_string(), - access_type: None + access_type: None, + columns: None, }, ]) ); @@ -478,7 +497,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "main/myschema.friends".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, },]) ); } @@ -499,7 +519,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/public.users".to_string(), - access_type: Some(RW) + access_type: Some(RW), + columns: None, },]) ); } @@ -518,7 +539,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "dt".to_string(), - access_type: None + access_type: None, + columns: None, },]) ); } @@ -539,7 +561,8 @@ mod tests { Ok(vec![ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/users".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, },]) ); } @@ -562,12 +585,14 @@ mod tests { ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/private.users".to_string(), - access_type: Some(R) + access_type: Some(R), + columns: None, }, ParseAssetsResult { kind: AssetKind::DataTable, path: "dt/test".to_string(), - access_type: Some(W) + access_type: Some(W), + columns: None, }, ]) ); diff --git a/backend/parsers/windmill-parser-yaml/src/asset_parser.rs b/backend/parsers/windmill-parser-yaml/src/asset_parser.rs index 9e4cedf08d..7e67d563ff 100644 --- a/backend/parsers/windmill-parser-yaml/src/asset_parser.rs +++ b/backend/parsers/windmill-parser-yaml/src/asset_parser.rs @@ -12,6 +12,7 @@ pub fn parse_assets(input: &str) -> anyhow::Result { kind: AssetKind::Resource, path: delegate_to_git_repo_details.resource, access_type: Some(AssetUsageAccessType::R), + columns: None, }) } @@ -21,6 +22,7 @@ pub fn parse_assets(input: &str) -> anyhow::Result { kind: AssetKind::Resource, path: pinned_res, access_type: Some(AssetUsageAccessType::R), + columns: None, }) } } @@ -31,6 +33,7 @@ pub fn parse_assets(input: &str) -> anyhow::Result { kind: AssetKind::Resource, path: resource, access_type: Some(AssetUsageAccessType::R), + columns: None, }) } } diff --git a/backend/parsers/windmill-parser/src/asset_parser.rs b/backend/parsers/windmill-parser/src/asset_parser.rs index 4c287974ed..4f88fe17c1 100644 --- a/backend/parsers/windmill-parser/src/asset_parser.rs +++ b/backend/parsers/windmill-parser/src/asset_parser.rs @@ -1,4 +1,5 @@ use serde::Serialize; +use std::collections::BTreeMap; #[derive(Serialize, PartialEq, Clone, Copy, Debug)] #[serde(rename_all(serialize = "lowercase"))] @@ -19,12 +20,14 @@ pub enum AssetKind { DataTable, } -#[derive(Serialize, Debug, PartialEq)] +#[derive(Serialize, Debug, PartialEq, Clone)] pub struct ParseAssetsResult { pub kind: AssetKind, pub path: String, #[serde(skip_serializing_if = "Option::is_none")] pub access_type: Option, // None in case of ambiguity + #[serde(skip_serializing_if = "Option::is_none")] + pub columns: Option>, // Map column name to access type, "*" represents wildcard } #[derive(Serialize, Debug, PartialEq)] @@ -66,6 +69,8 @@ pub fn merge_assets(assets: Vec) -> Vec { (Some(R), Some(R)) => Some(R), (Some(W), Some(W)) => Some(W), }; + // merge columns: union the column sets and merge access types per column + existing.columns = merge_column_maps(existing.columns.take(), asset.columns); } else { arr.push(asset); } @@ -74,6 +79,36 @@ pub fn merge_assets(assets: Vec) -> Vec { arr } +fn merge_column_maps( + existing: Option>, + new: Option>, +) -> Option> { + match (existing, new) { + (None, None) => None, + (Some(map), None) | (None, Some(map)) => Some(map), + (Some(mut existing_map), Some(new_map)) => { + for (col_name, new_access) in new_map { + existing_map + .entry(col_name) + .and_modify(|existing_access| { + *existing_access = merge_access_types(*existing_access, new_access); + }) + .or_insert(new_access); + } + Some(existing_map) + } + } +} + +fn merge_access_types(a: AssetUsageAccessType, b: AssetUsageAccessType) -> AssetUsageAccessType { + match (a, b) { + (R, W) | (W, R) => RW, + (RW, _) | (_, RW) => RW, + (R, R) => R, + (W, W) => W, + } +} + // Will return false if the user assigned an asset to a variable like: // let sql = wmill.datatable('main') // But never used it. In that case we don't know which table is being used, diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index d3375a9f84..41ef3babb4 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -16526,6 +16526,11 @@ paths: $ref: "#/components/schemas/AssetUsageKind" access_type: $ref: "#/components/schemas/AssetUsageAccessType" + columns: + type: object + description: The columns used (for tables) + additionalProperties: + $ref: "#/components/schemas/AssetUsageAccessType" created_at: type: string format: date-time diff --git a/backend/windmill-api/src/assets.rs b/backend/windmill-api/src/assets.rs index 94d4b3877e..7afea4038f 100644 --- a/backend/windmill-api/src/assets.rs +++ b/backend/windmill-api/src/assets.rs @@ -158,6 +158,7 @@ async fn list_assets( 'path', asset.usage_path, 'kind', asset.usage_kind, 'access_type', asset.usage_access_type, + 'columns', asset.columns, 'created_at', asset.created_at, 'metadata', (CASE WHEN asset.usage_kind = 'job' THEN @@ -266,11 +267,12 @@ async fn list_assets_by_usages( for usage in body.usages { let assets = sqlx::query_scalar!( r#"SELECT - jsonb_build_object( + jsonb_strip_nulls(jsonb_build_object( 'path', path, 'kind', kind, - 'access_type', usage_access_type - ) as "list!: _" + 'access_type', usage_access_type, + 'columns', columns + )) as "list!: _" FROM asset WHERE workspace_id = $1 AND usage_path = $2 AND usage_kind = $3 ORDER BY path, kind"#, diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 48cfb80182..15adf46caa 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -6378,10 +6378,18 @@ fn register_potential_assets_on_inline_execution( match assets { Some(Ok(assets)) => { for asset in assets { + let columns = asset.columns.as_ref().map(|cols| { + cols.iter() + .map(|(col_name, col_access_type)| { + (col_name.clone(), (*col_access_type).into()) + }) + .collect() + }); register_runtime_asset(InsertRuntimeAssetParams { access_type: asset.access_type.map(|a| a.into()), asset_kind: asset.kind.into(), asset_path: asset.path, + columns, job_id, workspace_id: w_id.to_string(), created_at: None, diff --git a/backend/windmill-common/src/assets.rs b/backend/windmill-common/src/assets.rs index a27070feee..eb4c482c67 100644 --- a/backend/windmill-common/src/assets.rs +++ b/backend/windmill-common/src/assets.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use sqlx::PgExecutor; +use std::collections::BTreeMap; use crate::{error, scripts::ScriptHash}; @@ -37,23 +38,15 @@ pub enum AssetUsageAccessType { RW, } -pub struct Asset { - pub path: String, - pub kind: AssetKind, -} - -pub struct AssetUsage { - pub path: String, - pub kind: AssetUsageKind, - pub access_type: AssetUsageAccessType, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Hash, sqlx::Type)] +#[derive(Serialize, Deserialize, Debug, Clone, Hash)] pub struct AssetWithAltAccessType { pub path: String, pub kind: AssetKind, pub access_type: Option, pub alt_access_type: Option, + /// Map of column name to access type for column-level access tracking + #[serde(skip_serializing_if = "Option::is_none")] + pub columns: Option>, } pub async fn insert_static_asset_usage<'e>( @@ -63,15 +56,22 @@ pub async fn insert_static_asset_usage<'e>( usage_path: &str, usage_kind: AssetUsageKind, ) -> error::Result<()> { + // Convert columns BTreeMap to JSONB format + let columns_json = asset + .columns + .as_ref() + .map(|cols| serde_json::to_value(cols).unwrap_or(serde_json::Value::Null)); + sqlx::query!( - r#"INSERT INTO asset (workspace_id, path, kind, usage_access_type, usage_path, usage_kind) - VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT DO NOTHING"#, + r#"INSERT INTO asset (workspace_id, path, kind, usage_access_type, usage_path, usage_kind, columns) + VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING"#, workspace_id, asset.path, asset.kind as AssetKind, (asset.access_type.or(asset.alt_access_type)) as Option, usage_path, - usage_kind as AssetUsageKind + usage_kind as AssetUsageKind, + columns_json as Option ) .execute(executor) .await?; @@ -125,6 +125,28 @@ pub fn merge_asset_usage_access_types( } } +pub fn merge_asset_columns( + a: &Option>, + b: &Option>, +) -> Option> { + match (a, b) { + (None, None) => None, + (Some(cols), None) | (None, Some(cols)) => Some(cols.clone()), + (Some(cols_a), Some(cols_b)) => { + let mut merged = cols_a.clone(); + for (col, access_b) in cols_b { + let access_a = merged.get(col); + let merged_access = + merge_asset_usage_access_types(access_a.cloned(), Some(*access_b)); + if let Some(access) = merged_access { + merged.insert(col.clone(), access); + } + } + Some(merged) + } + } +} + impl From for AssetKind { fn from(parser_kind: windmill_parser::asset_parser::AssetKind) -> Self { match parser_kind { diff --git a/backend/windmill-common/src/runtime_assets.rs b/backend/windmill-common/src/runtime_assets.rs index 0060495371..811e62e9d8 100644 --- a/backend/windmill-common/src/runtime_assets.rs +++ b/backend/windmill-common/src/runtime_assets.rs @@ -1,13 +1,19 @@ -use std::{collections::HashMap, sync::OnceLock}; +use std::{ + collections::{BTreeMap, HashMap}, + sync::OnceLock, +}; use itertools::Itertools; use serde_json::value::RawValue; -use sqlx::{Pool, Postgres, QueryBuilder}; +use sqlx::{types::Json, Pool, Postgres, QueryBuilder}; use tokio::sync::mpsc; use windmill_parser::asset_parser::parse_asset_syntax; use crate::{ - assets::{merge_asset_usage_access_types, AssetKind, AssetUsageAccessType, AssetUsageKind}, + assets::{ + merge_asset_columns, merge_asset_usage_access_types, AssetKind, AssetUsageAccessType, + AssetUsageKind, + }, error, }; @@ -59,6 +65,7 @@ pub struct InsertRuntimeAssetParams { pub job_id: uuid::Uuid, pub access_type: Option, pub created_at: Option>, + pub columns: Option>, } async fn insert_runtime_assets( @@ -66,13 +73,14 @@ async fn insert_runtime_assets( assets: &[InsertRuntimeAssetParams], ) -> error::Result<()> { for chunk in assets.chunks(1000) { - let mut query_builder = QueryBuilder::new("INSERT INTO asset (workspace_id, path, kind, usage_access_type, usage_path, usage_kind, created_at) "); + let mut query_builder = QueryBuilder::new("INSERT INTO asset (workspace_id, path, kind, usage_access_type, usage_path, columns, usage_kind, created_at) "); query_builder.push_values(chunk, |mut b, asset| { b.push_bind(&asset.workspace_id) .push_bind(&asset.asset_path) .push_bind(&asset.asset_kind) .push_bind(&asset.access_type) .push_bind(asset.job_id.to_string()) + .push_bind(Json(&asset.columns)) .push_bind(&AssetUsageKind::Job) .push_bind(&asset.created_at); }); @@ -104,6 +112,7 @@ async fn prune_runtime_assets( // Same job used the same asset multiple times last_same_job.access_type = merge_asset_usage_access_types(last_same_job.access_type, asset.access_type); + last_same_job.columns = merge_asset_columns(&last_same_job.columns, &asset.columns); } else if v.len() < max_n { v.push(asset); } diff --git a/backend/windmill-worker/src/worker.rs b/backend/windmill-worker/src/worker.rs index ec7e67ae40..1bc84df1cb 100644 --- a/backend/windmill-worker/src/worker.rs +++ b/backend/windmill-worker/src/worker.rs @@ -1533,7 +1533,11 @@ pub async fn run_worker( let is_dedicated_worker: bool = { let config = WORKER_CONFIG.read().await; - config.dedicated_worker.is_some() || config.dedicated_workers.as_ref().is_some_and(|dws| !dws.is_empty()) + config.dedicated_worker.is_some() + || config + .dedicated_workers + .as_ref() + .is_some_and(|dws| !dws.is_empty()) }; #[cfg(feature = "benchmark")] @@ -2045,7 +2049,9 @@ pub async fn run_worker( dedicated_workers.get(&key) }) } else { - job.runnable_path.as_ref().and_then(|path| dedicated_workers.get(path)) + job.runnable_path + .as_ref() + .and_then(|path| dedicated_workers.get(path)) }; if let Some(dedicated_worker_tx) = dedicated_worker_tx { let dedicated_job = DedicatedWorkerJob { @@ -2773,6 +2779,7 @@ async fn detect_and_store_runtime_assets_from_job_args( asset_kind: asset.kind, access_type: None, created_at: None, + columns: None, }; register_runtime_asset(asset); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d52eeaed61..a882369b69 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -78,11 +78,11 @@ "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.601.1", - "windmill-parser-wasm-regex": "1.593.0", + "windmill-parser-wasm-py": "1.623.1", + "windmill-parser-wasm-regex": "1.623.1", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.558.1", - "windmill-parser-wasm-ts": "1.593.0", + "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", @@ -258,7 +258,6 @@ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", @@ -274,7 +273,6 @@ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -820,7 +818,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": "^14 || ^16 || >=18" }, @@ -1330,6 +1327,7 @@ "integrity": "sha512-Jer+M7DgIwT5IHfTayb4Iw/fkkxWNmC/mqn/nMh9JrbPbkxmyabfLQnhJ+JDn5HK77f84j34lubO3iqFtYAfMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@floating-ui/core": "^1.3.1", "@floating-ui/dom": "^1.4.5", @@ -2105,6 +2103,7 @@ "integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -2182,6 +2181,7 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -2708,8 +2708,7 @@ "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/ms": { "version": "2.1.0", @@ -2722,8 +2721,7 @@ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/semver": { "version": "7.7.1", @@ -2793,6 +2791,7 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -2961,7 +2960,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@vitest/mocker": "4.0.15", "@vitest/utils": "4.0.15", @@ -2986,7 +2984,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@vitest/browser": "4.0.15", "@vitest/mocker": "4.0.15", @@ -3012,7 +3009,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@vitest/spy": "4.0.15", "estree-walker": "^3.0.3", @@ -3041,7 +3037,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@vitest/spy": "4.0.15", "estree-walker": "^3.0.3", @@ -3273,6 +3268,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3326,6 +3322,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3471,7 +3468,6 @@ "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3499,7 +3495,6 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -3592,8 +3587,7 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", @@ -3720,6 +3714,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3917,7 +3912,6 @@ "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "camelcase": "^6.3.0", "map-obj": "^4.1.0", @@ -3937,7 +3931,6 @@ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -3951,7 +3944,6 @@ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -3965,7 +3957,6 @@ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -4074,6 +4065,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -4314,7 +4306,6 @@ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", @@ -4379,7 +4370,6 @@ "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12 || >=16" } @@ -4645,6 +4635,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -4698,6 +4689,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -4732,7 +4724,6 @@ "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -4746,7 +4737,6 @@ "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" @@ -4764,7 +4754,6 @@ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4775,7 +4764,6 @@ "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5261,7 +5249,6 @@ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -5349,6 +5336,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5895,7 +5883,6 @@ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 4.9.1" } @@ -6381,7 +6368,6 @@ "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "global-prefix": "^3.0.0" }, @@ -6395,7 +6381,6 @@ "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ini": "^1.3.5", "kind-of": "^6.0.2", @@ -6411,7 +6396,6 @@ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -6461,8 +6445,7 @@ "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/gopd": { "version": "1.2.0", @@ -6539,7 +6522,6 @@ "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -6761,7 +6743,6 @@ "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -6775,7 +6756,6 @@ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -6788,8 +6768,7 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/html-tags": { "version": "3.3.1", @@ -6797,7 +6776,6 @@ "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" }, @@ -6881,7 +6859,6 @@ "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -6902,7 +6879,6 @@ "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6974,8 +6950,7 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -7080,7 +7055,6 @@ "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7091,7 +7065,6 @@ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7190,8 +7163,7 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -7227,8 +7199,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-refs": { "version": "3.0.15", @@ -7347,7 +7318,6 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7926,8 +7896,7 @@ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.uniq": { "version": "4.5.0", @@ -7995,7 +7964,6 @@ "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" }, @@ -8046,7 +8014,6 @@ "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -8287,7 +8254,6 @@ "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/minimist": "^1.2.2", "camelcase-keys": "^7.0.0", @@ -8315,7 +8281,6 @@ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -9009,7 +8974,6 @@ "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", @@ -9088,6 +9052,7 @@ "resolved": "https://registry.npmjs.org/@codingame/monaco-vscode-editor-api/-/monaco-vscode-editor-api-25.0.0.tgz", "integrity": "sha512-uiY06RTWFo2WZdh6OybkLlDhuG+8LlkjUDpr9/wW55uucqHo4X8fx4XKEtD98cscC+6FKQkbG2yyUiOJ/npHOw==", "license": "MIT", + "peer": true, "dependencies": { "@codingame/monaco-vscode-api": "25.0.0" } @@ -9324,7 +9289,6 @@ "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", @@ -9670,7 +9634,6 @@ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -9882,7 +9845,6 @@ "dev": true, "license": "ISC", "optional": true, - "peer": true, "dependencies": { "pngjs": "^7.0.0" }, @@ -9972,7 +9934,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">=14.19.0" } @@ -9997,6 +9958,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10185,6 +10147,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -10574,8 +10537,7 @@ "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/postcss-safe-parser": { "version": "6.0.0", @@ -10751,6 +10713,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11055,7 +11018,6 @@ "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^3.0.2", @@ -11075,7 +11037,6 @@ "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "find-up": "^5.0.0", "read-pkg": "^6.0.0", @@ -11094,7 +11055,6 @@ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -11108,7 +11068,6 @@ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -11151,7 +11110,6 @@ "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "indent-string": "^5.0.0", "strip-indent": "^4.0.0" @@ -11788,7 +11746,6 @@ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -11865,7 +11822,6 @@ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -11876,8 +11832,7 @@ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, - "license": "CC-BY-3.0", - "peer": true + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", @@ -11885,7 +11840,6 @@ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -11896,8 +11850,7 @@ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", "dev": true, - "license": "CC0-1.0", - "peer": true + "license": "CC0-1.0" }, "node_modules/sprintf-js": { "version": "1.0.3", @@ -11991,7 +11944,6 @@ "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12017,8 +11969,7 @@ "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/style-to-object": { "version": "0.4.4", @@ -12067,7 +12018,6 @@ "integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^2.3.1", "@csstools/css-tokenizer": "^2.2.0", @@ -12150,7 +12100,6 @@ } ], "license": "MIT-0", - "peer": true, "engines": { "node": "^14 || ^16 || >=18" }, @@ -12164,7 +12113,6 @@ "integrity": "sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flat-cache": "^3.2.0" }, @@ -12177,8 +12125,7 @@ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/stylelint/node_modules/postcss-selector-parser": { "version": "6.1.2", @@ -12201,7 +12148,6 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -12336,7 +12282,6 @@ "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" @@ -12366,6 +12311,7 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.12.tgz", "integrity": "sha512-CEzwxFuEycokU8K8CE/OuwVbmei+ivu2HvBGYIdASfMa1hCRSNr4RRkzNSvbAvu6h+BOig2CsZTAEY+WKvwZpA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -12461,21 +12407,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", @@ -12663,8 +12594,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", - "dev": true, - "peer": true + "dev": true }, "node_modules/svgo": { "version": "3.3.2", @@ -12715,7 +12645,6 @@ "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", @@ -12743,6 +12672,7 @@ "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -12985,6 +12915,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13047,7 +12978,6 @@ "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -13146,6 +13076,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13360,7 +13291,6 @@ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -13415,6 +13345,7 @@ "integrity": "sha512-5hI5NCJwKBGtzWtdKB3c2fOEpI77Iaa0z4mSzZPU1cJ/OqrGbFafm90edVCd7T9Snz+Sh09TMAv4EQqyVLzuEg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/runtime": "0.101.0", "fdir": "^6.5.0", @@ -13527,6 +13458,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13540,6 +13472,7 @@ "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.15", "@vitest/mocker": "4.0.15", @@ -14168,6 +14101,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14191,6 +14125,7 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -14517,14 +14452,14 @@ "integrity": "sha512-COyid6B1RYs+bpzUCInsA4HY/WZkpDLfkQ90+AqU/TVTpzYSbAC2JCbIwy0cRElBvlhI4bQ+9Wg6hSQKMpEkpA==" }, "node_modules/windmill-parser-wasm-py": { - "version": "1.601.1", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-py/-/windmill-parser-wasm-py-1.601.1.tgz", - "integrity": "sha512-xcNZE/8B29yfl6UuQDPSXMD+83/W2Hzt2uhn+WrNvy0+qzk6nLh/vJGrf2srLBngYX1TxhUI5Jgseg0PK9yvNw==" + "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==" }, "node_modules/windmill-parser-wasm-regex": { - "version": "1.593.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.593.0.tgz", - "integrity": "sha512-m8BvTGJc2710YODmKKDXiASfssoJ/YFJGfYRhRnQvltENvaRee2NZuf4XoUkOVJESDdCSEX6U6fVkFvF9rXp2Q==" + "version": "1.623.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-regex/-/windmill-parser-wasm-regex-1.623.1.tgz", + "integrity": "sha512-rW3pl4ysIXmVAmwxSTKhR2sUfScXKfTqxrUebDg0ZNcUzY97S8mNRdQGZfaQ1NWhWPRQNXfL/z/yaUOQh5ConQ==" }, "node_modules/windmill-parser-wasm-ruby": { "version": "1.526.1", @@ -14537,9 +14472,9 @@ "integrity": "sha512-21S7lm1KF8zO1187rbq14hzPHII2RdM2+D44MoAh1F6VoaScj+Puq0z5B1O/hwn/95R/a9jBlL2D8jbkXtlD1A==" }, "node_modules/windmill-parser-wasm-ts": { - "version": "1.593.0", - "resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.593.0.tgz", - "integrity": "sha512-NFY9gaEIpJOwGZJeGYDS3+/16QiPYdxFgmb1bKhXyEAdWjYMQ+otrTezf+K09lmvPzB+len37GlNCU72OgyQ6A==" + "version": "1.623.1", + "resolved": "https://registry.npmjs.org/windmill-parser-wasm-ts/-/windmill-parser-wasm-ts-1.623.1.tgz", + "integrity": "sha512-FBwi/zXxjhZcCvi04oFdNivazru1ynIqSbafHSArfaaBWesBO3nye9UO/WXUlWZm5a7BExbU+3R/eVJrGaornw==" }, "node_modules/windmill-parser-wasm-yaml": { "version": "1.593.0", @@ -14690,7 +14625,6 @@ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" @@ -14699,29 +14633,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", @@ -14909,7 +14820,6 @@ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=10" } @@ -14929,6 +14839,7 @@ "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", "license": "MIT", + "peer": true, "dependencies": { "lib0": "^0.2.99" }, @@ -14964,6 +14875,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index 84ff0e3fc0..3c3b559ab1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -148,11 +148,11 @@ "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.601.1", - "windmill-parser-wasm-regex": "1.593.0", + "windmill-parser-wasm-py": "1.623.1", + "windmill-parser-wasm-regex": "1.623.1", "windmill-parser-wasm-ruby": "1.526.1", "windmill-parser-wasm-rust": "1.558.1", - "windmill-parser-wasm-ts": "1.593.0", + "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", diff --git a/frontend/src/lib/components/TooltipInner.svelte b/frontend/src/lib/components/TooltipInner.svelte index 28dab0595f..437fbc1c02 100644 --- a/frontend/src/lib/components/TooltipInner.svelte +++ b/frontend/src/lib/components/TooltipInner.svelte @@ -2,13 +2,19 @@ import Markdown from 'svelte-exmarkdown' import { ExternalLink } from 'lucide-svelte' import { gfmPlugin } from 'svelte-exmarkdown/gfm' + import { twMerge } from 'tailwind-merge' + export let documentationLink: string | undefined = undefined export let markdownTooltip: string | undefined = undefined + export let customBgClass: string | undefined = undefined const plugins = [gfmPlugin()]
{#if markdownTooltip}
diff --git a/frontend/src/lib/components/assets/AssetColumnBadges.svelte b/frontend/src/lib/components/assets/AssetColumnBadges.svelte new file mode 100644 index 0000000000..8bb02bd456 --- /dev/null +++ b/frontend/src/lib/components/assets/AssetColumnBadges.svelte @@ -0,0 +1,44 @@ + + +{#if entries?.length} +
+ {#each entries as [columnName, accessType]} + {@const accessType2 = formatAssetAccessType(accessType)} + {#snippet badge()} +
+ {columnName} +
+ {/snippet} + {#if disableTooltip} + {@render badge()} + {:else} + + {@render badge()} + + {accessType2} access to column "{columnName}" + + + {/if} + {/each} +
+{/if} diff --git a/frontend/src/lib/components/assets/AssetsUsageDrawer.svelte b/frontend/src/lib/components/assets/AssetsUsageDrawer.svelte index e6e6f78fc9..177032341b 100644 --- a/frontend/src/lib/components/assets/AssetsUsageDrawer.svelte +++ b/frontend/src/lib/components/assets/AssetsUsageDrawer.svelte @@ -6,8 +6,9 @@ import Tooltip from '../meltComponents/Tooltip.svelte' import Tooltip2 from '../Tooltip.svelte' import { twMerge } from 'tailwind-merge' - import { displayDate } from '$lib/utils' + import { capitalize, displayDate } from '$lib/utils' import Alert from '../common/alert/Alert.svelte' + import AssetColumnBadges from './AssetColumnBadges.svelte' let usagesDrawerData: | { @@ -64,14 +65,10 @@ -{#snippet badge(text: string | undefined, tooltip?: string)} +{#snippet rightBadge(text: string | undefined, tooltip?: string)} {#if text} -
+
{text}
@@ -92,7 +89,7 @@ -
- - {u.kind == 'job' ? (u.metadata?.runnable_path ?? 'Unknown job') : u.path} +
+ + + {u.kind == 'job' ? (u.metadata?.runnable_path ?? 'Unknown job') : u.path} + - {u.kind == 'job' ? u.path : u.kind} + + {u.kind == 'job' ? u.path : capitalize(u.kind)} + +
- {@render badge(displayDate(u.created_at), 'Asset detection time')} - {@render badge(accessType)} + {@render rightBadge(displayDate(u.created_at), 'Asset detection time')} + {@render rightBadge(accessType)}
{/each} diff --git a/frontend/src/lib/components/assets/lib.ts b/frontend/src/lib/components/assets/lib.ts index 228a2fabe5..2475e2b126 100644 --- a/frontend/src/lib/components/assets/lib.ts +++ b/frontend/src/lib/components/assets/lib.ts @@ -12,6 +12,7 @@ export type AssetKind = _AssetKind export type AssetWithAccessType = Asset & { access_type?: AssetUsageAccessType } export type AssetWithAltAccessType = AssetWithAccessType & { alt_access_type?: AssetUsageAccessType + columns?: Record } export type AssetUsage = ListAssetsResponse['assets'][number]['usages'][number] diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index d57aab0b65..0ad3ca837d 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -948,7 +948,7 @@ {height} {width} minZoom={0.2} - maxZoom={1.2} + maxZoom={1.6} connectionLineType={ConnectionLineType.SmoothStep} defaultEdgeOptions={{ type: 'smoothstep' }} preventScrolling={scroll} diff --git a/frontend/src/lib/components/graph/graphBuilder.svelte.ts b/frontend/src/lib/components/graph/graphBuilder.svelte.ts index e5f2b03315..0d7b104138 100644 --- a/frontend/src/lib/components/graph/graphBuilder.svelte.ts +++ b/frontend/src/lib/components/graph/graphBuilder.svelte.ts @@ -297,6 +297,7 @@ export type AssetN = { type: 'asset' data: { asset: AssetWithAltAccessType + displayedAccessType: 'r' | 'w' } } @@ -304,6 +305,7 @@ export type AssetsOverflowedN = { type: 'assetsOverflowed' data: { overflowedAssets: AssetWithAltAccessType[] + displayedAccessType: 'r' | 'w' } } diff --git a/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte index f4ded7dfa2..161c6c8dd9 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte @@ -57,7 +57,7 @@ // All asset nodes displayed on top const inputAssetNodes: (Node & AssetN)[] = displayedInputAssets.map((asset, i) => { let inputAssetXGap = 12 - let inputAssetWidth = 150 + let inputAssetWidth = 165 const targetRowW = MAX_ASSET_ROW_WIDTH - @@ -73,7 +73,7 @@ return { type: 'asset' as const, parentId: node.id, - data: { asset }, + data: { asset, displayedAccessType: 'r' }, id: `${node.id}-asset-in-${asset.kind}-${asset.path}`, width: inputAssetWidth, position: { @@ -94,7 +94,7 @@ // All asset nodes displayed on the bottom const outputAssetNodes: (Node & AssetN)[] = displayedOutputAssets.map((asset, i) => { let outputAssetXGap = 12 - let outputAssetWidth = 150 + let outputAssetWidth = 165 const targetRowW = MAX_ASSET_ROW_WIDTH - @@ -110,7 +110,7 @@ return { type: 'asset' as const, parentId: node.id, - data: { asset }, + data: { asset, displayedAccessType: 'w' }, id: `${node.id}-asset-out-${asset.kind}-${asset.path}`, width: outputAssetWidth, position: { @@ -150,7 +150,7 @@ if (overflowedInputAssets.length) allAssetNodes.push({ type: 'assetsOverflowed', - data: { overflowedAssets: overflowedInputAssets }, + data: { overflowedAssets: overflowedInputAssets, displayedAccessType: 'r' }, id: `${node.id}-assets-overflowed-in`, parentId: node.id, width: ASSETS_OVERFLOWED_NODE_WIDTH, @@ -169,7 +169,7 @@ if (overflowedOutputAssets.length) allAssetNodes.push({ type: 'assetsOverflowed', - data: { overflowedAssets: overflowedOutputAssets }, + data: { overflowedAssets: overflowedOutputAssets, displayedAccessType: 'w' }, id: `${node.id}-assets-overflowed-out`, parentId: node.id, width: ASSETS_OVERFLOWED_NODE_WIDTH, @@ -237,6 +237,7 @@ import { userStore } from '$lib/stores' import { deepEqual } from 'fast-equals' import { slide } from 'svelte/transition' + import AssetColumnBadges from '$lib/components/assets/AssetColumnBadges.svelte' interface Props { data: AssetN['data'] @@ -254,15 +255,24 @@ }) const usageCount = $derived(flowGraphAssetsCtx?.val.computeAssetsCount?.(data.asset)) const colors = $derived(getNodeColorClasses(undefined, isSelected)) + + let assetColumns = $derived( + data.asset.columns && + Object.fromEntries( + Object.entries(data.asset.columns).filter( + ([_, accessType]) => accessType && accessType === data.displayedAccessType + ) + ) + ) {#snippet children({ darkMode })} - +
- + {formatShortAssetPath(data.asset)} + {#if data.asset.kind === 'resource' && cachedResourceMetadata === undefined} @@ -319,6 +337,7 @@ {formatAssetKind({ ...data.asset, metadata: cachedResourceMetadata })} + {/snippet} diff --git a/frontend/src/lib/components/graph/renderers/nodes/AssetsOverflowedNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/AssetsOverflowedNode.svelte index 2353d37aa8..623a725b01 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/AssetsOverflowedNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/AssetsOverflowedNode.svelte @@ -60,7 +60,7 @@
    {#each data.overflowedAssets as asset}
  • - +
  • {/each}
diff --git a/frontend/src/lib/components/meltComponents/Tooltip.svelte b/frontend/src/lib/components/meltComponents/Tooltip.svelte index 297709b672..ea464fa123 100644 --- a/frontend/src/lib/components/meltComponents/Tooltip.svelte +++ b/frontend/src/lib/components/meltComponents/Tooltip.svelte @@ -16,6 +16,7 @@ export let openDelay: number = 300 export let closeDelay: number = 0 export let portal: string | undefined | null = 'body' + export let customBgClass: string | undefined = undefined const { elements: { trigger, content }, @@ -47,7 +48,7 @@ {#if $open && !disablePopup}
- +
diff --git a/frontend/src/lib/infer.ts b/frontend/src/lib/infer.ts index b12ca1e671..4360e912c5 100644 --- a/frontend/src/lib/infer.ts +++ b/frontend/src/lib/infer.ts @@ -1,4 +1,10 @@ -import { ScriptService, type MainArgSignature, FlowService, type Script } from '$lib/gen' +import { + ScriptService, + type MainArgSignature, + FlowService, + type Script, + type AssetUsageAccessType +} from '$lib/gen' import { get, writable } from 'svelte/store' import type { Schema, SupportedLanguage } from './common.js' import { emptySchema, sortObject } from './utils.js' @@ -90,8 +96,18 @@ async function initWasmRuby() { } type InferAssetsResult = - | { status: 'ok'; assets: AssetWithAccessType[]; sql_queries?: InferAssetsSqlQueryDetails[] } - | { status: 'error'; error: string; assets?: undefined; sql_queries?: undefined } + | { + status: 'ok' + assets: AssetWithAccessType[] + sql_queries?: InferAssetsSqlQueryDetails[] + columns?: Record + } + | { + status: 'error' + error: string + assets?: undefined + sql_queries?: undefined + } export type InferAssetsSqlQueryDetails = { query_string: string // SQL query with $1 placeholders for interpolations