Compare commits

..

2 Commits

Author SHA1 Message Date
Ruben Fiszel
741526b7b8 experiment: Add full flow executor with branch/loop support
Add flow_executor.rs that supports executing complex flows in local mode:
- ForloopFlow: iterate over arrays/ranges with sequential execution
- WhileloopFlow: execute modules while condition is true
- BranchOne: if/else branching based on conditions
- BranchAll: parallel branch execution (sequential for now)
- RawScript: inline script execution (bash, python, deno, bun)
- Identity: pass-through module

Key features:
- Uses windmill-common FlowValue types for compatibility
- Expression evaluation for input transforms with comparisons
- Proper flow status tracking with module results
- Recursive async execution with async_recursion crate

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 07:14:59 +00:00
Ruben Fiszel
73deef44ed experiment: Add windmill-local crate with libSQL/Turso support
This experimental crate demonstrates running Windmill preview endpoints
with libSQL (SQLite/Turso) instead of PostgreSQL. Key features:

- Schema: SQLite-compatible schema for jobs, queue, and results
  - ENUMs → TEXT with CHECK constraints
  - JSONB → JSON (TEXT)
  - Arrays → JSON arrays
  - No FOR UPDATE SKIP LOCKED (single worker, mutex coordination)

- Database: Supports three modes via libsql crate:
  - In-memory SQLite (for testing)
  - File-based SQLite (local persistence)
  - Remote Turso (multi-writer scenarios)

- API: Compatible preview endpoints:
  - POST /api/w/{workspace}/jobs/run/preview
  - POST /api/w/{workspace}/jobs/run_wait_result/preview
  - POST /api/w/{workspace}/jobs/run/preview_flow
  - POST /api/w/{workspace}/jobs/run_wait_result/preview_flow

- Executor: Simple script execution for bash, python3, deno, bun

- Worker: Single embedded worker that processes queue

Run with: cargo run -p windmill-local --example local_server

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 07:01:44 +00:00
36 changed files with 3778 additions and 501 deletions

View File

@@ -27,6 +27,8 @@ jobs:
token: ${{ secrets.WINDMILL_EE_PRIVATE_ACCESS }}
fetch-depth: 0
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v2
- uses: depot/setup-action@v1
- name: Docker meta
@@ -55,11 +57,11 @@ jobs:
run: |
cp ./docker/RHEL9/Dockerfile ./Dockerfile
- name: Build and push EE (multi-arch)
- name: Build and push publicly ee amd64
uses: depot/build-push-action@v1
with:
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,deno_core,mcp,private
@@ -67,50 +69,72 @@ jobs:
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
tags: |
${{ steps.meta-ee-public.outputs.tags }}
${{ steps.meta-ee-public.outputs.tags }}-amd64
labels: |
${{ steps.meta-ee-public.outputs.labels }}
${{ steps.meta-ee-public.outputs.labels }}-amd64
org.opencontainers.image.licenses=Windmill-Enterprise-License
- name: Install crane
uses: imjasonh/setup-crane@v0.4
- name: Extract binaries with crane
run: |
mkdir -p extracted
# Extract arm64 binary (include deps/ for hard link resolution)
mkdir -p /tmp/arm64
crane export --platform linux/arm64 ${{ steps.meta-ee-public.outputs.tags }} - \
| tar -xf - -C /tmp/arm64 windmill/target/release/ usr/src/app/libwindmill_duckdb_ffi_internal.so
cp /tmp/arm64/windmill/target/release/windmill extracted/windmill-ee-arm64-rhel9
cp /tmp/arm64/usr/src/app/libwindmill_duckdb_ffi_internal.so extracted/libwindmill_duckdb_ffi_internal-arm64.so
rm -rf /tmp/arm64
# Extract amd64 binary
mkdir -p /tmp/amd64
crane export --platform linux/amd64 ${{ steps.meta-ee-public.outputs.tags }} - \
| tar -xf - -C /tmp/amd64 windmill/target/release/ usr/src/app/libwindmill_duckdb_ffi_internal.so
cp /tmp/amd64/windmill/target/release/windmill extracted/windmill-ee-amd64-rhel9
cp /tmp/amd64/usr/src/app/libwindmill_duckdb_ffi_internal.so extracted/libwindmill_duckdb_ffi_internal-amd64.so
rm -rf /tmp/amd64
- uses: actions/upload-artifact@v4
- name: Build and push publicly ee arm64
uses: depot/build-push-action@v1
with:
name: RHEL9-arm64 build
path: extracted/windmill-ee-arm64-rhel9
context: .
platforms: linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml,stripe,embedding,parquet,prometheus,openidconnect,cloud,jemalloc,license,otel,http_trigger,zip,oauth2,kafka,sqs_trigger,nats,postgres_trigger,gcp_trigger,mqtt_trigger,websocket,smtp,static_frontend,all_languages,deno_core,mcp,private
secrets: |
rh_username=${{ secrets.RH_USERNAME }}
rh_password=${{ secrets.RH_PASSWORD }}
tags: |
${{ steps.meta-ee-public.outputs.tags }}-arm64
labels: |
${{ steps.meta-ee-public.outputs.labels }}-arm64
org.opencontainers.image.licenses=Windmill-Enterprise-License
- uses: shrink/actions-docker-extract@v3
id: extract-ee-amd64
with:
image: ${{ steps.meta-ee-public.outputs.tags}}-amd64
path: "/windmill/target/release/windmill"
- uses: shrink/actions-docker-extract@v3
id: extract-duckdb-ffi-internal
with:
image: ${{ steps.meta-ee-public.outputs.tags}}-amd64
path: "/usr/src/app/libwindmill_duckdb_ffi_internal.so"
# - uses: shrink/actions-docker-extract@v3
# id: extract-ee-arm64
# with:
# image: ${{ steps.meta-ee-public.outputs.tags}}-arm64
# path: "/windmill/target/release/windmill"
- name: Rename binary with corresponding architecture
run: |
mv "${{ steps.extract-ee-amd64.outputs.destination }}/windmill" "${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9"
# mv "${{ steps.extract-ee-arm64.outputs.destination }}/windmill" "${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel9"
- uses: actions/upload-artifact@v4
with:
name: RHEL9-amd64 build
path: extracted/windmill-ee-amd64-rhel9
- uses: actions/upload-artifact@v4
with:
name: RHEL9-arm64 dynamic libraries build
path: extracted/libwindmill_duckdb_ffi_internal-arm64.so
path: ${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9
- uses: actions/upload-artifact@v4
with:
name: RHEL9-amd64 dynamic libraries build
path: extracted/libwindmill_duckdb_ffi_internal-amd64.so
path: ${{ steps.extract-duckdb-ffi-internal.outputs.destination }}/libwindmill_duckdb_ffi_internal.so
# - uses: actions/upload-artifact@v4
# with:
# name: RHEL9-arm64 build
# path:
# ${{ steps.extract-ee-arm64.outputs.destination
# }}/windmill-ee-arm64-rhel9
# - name: Attach binary to release
# uses: softprops/action-gh-release@v2
# if: startsWith(github.ref, 'refs/tags/')
# with:
# files: |
# ${{ steps.extract-ee-arm64.outputs.destination }}/windmill-ee-arm64-rhel9
# ${{ steps.extract-ee-amd64.outputs.destination }}/windmill-ee-amd64-rhel9

View File

@@ -1,24 +1,5 @@
# Changelog
## [1.614.0](https://github.com/windmill-labs/windmill/compare/v1.613.4...v1.614.0) (2026-01-23)
### Features
* add cache-rt command and SYNC_CACHED_RT env variable for resource types ([#7666](https://github.com/windmill-labs/windmill/issues/7666)) ([85e460d](https://github.com/windmill-labs/windmill/commit/85e460d853cbd9f8d245efb9009690c0bb468bfc))
* **aichat:** handle codestral from any provider ([#7649](https://github.com/windmill-labs/windmill/issues/7649)) ([389499e](https://github.com/windmill-labs/windmill/commit/389499e57696dc4805080a9e6b737b1aef4566be))
* **ai:** handle google vertex for claude models + base url overrides ([#7654](https://github.com/windmill-labs/windmill/issues/7654)) ([0797e89](https://github.com/windmill-labs/windmill/commit/0797e89aa00e57b4e162df32d7b0a041ad6db71e))
* better mixed versions handling ([#7628](https://github.com/windmill-labs/windmill/issues/7628)) ([7249b82](https://github.com/windmill-labs/windmill/commit/7249b82dbaee2d14cb5768233c2026a6ab44231f))
### Bug Fixes
* add support for OIDC session tokens in S3 proxy headers ([#7652](https://github.com/windmill-labs/windmill/issues/7652)) ([3b8a99e](https://github.com/windmill-labs/windmill/commit/3b8a99e174682ad90a9a0d3957902e09c9e0a195))
* Avoid logout when using deploy ui and no access to some deps ([#7655](https://github.com/windmill-labs/windmill/issues/7655)) ([bb21486](https://github.com/windmill-labs/windmill/commit/bb2148639441b86c6c966119df4711066fc94c85))
* **frontend:** improve ai chat ui ([#7648](https://github.com/windmill-labs/windmill/issues/7648)) ([af14b09](https://github.com/windmill-labs/windmill/commit/af14b0941581eec98061d4cbae159a061f1d5eee))
* **frontend:** Improve flow detail page ([#7647](https://github.com/windmill-labs/windmill/issues/7647)) ([7385726](https://github.com/windmill-labs/windmill/commit/738572674123a00a5e37a0c875b2649608c70f0a))
* use pgoptions for iam rds connection ([#7660](https://github.com/windmill-labs/windmill/issues/7660)) ([08b483e](https://github.com/windmill-labs/windmill/commit/08b483eacafcd161537b9a09f63640eeacad087f))
## [1.613.4](https://github.com/windmill-labs/windmill/compare/v1.613.3...v1.613.4) (2026-01-21)

View File

@@ -254,7 +254,7 @@ COPY ./frontend/src/lib/hubPaths.json ${APP}/hubPaths.json
RUN windmill cache ${APP}/hubPaths.json && rm ${APP}/hubPaths.json
RUN windmill cache-rt
# Create a non-root user 'windmill' with UID and GID 1000
RUN addgroup --gid 1000 windmill && \

657
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.614.0"
version = "1.613.4"
authors.workspace = true
edition.workspace = true
@@ -18,6 +18,7 @@ members = [
"./windmill-indexer",
"./windmill-macros",
"./windmill-oauth",
"./windmill-local",
"./parsers/windmill-parser",
"./parsers/windmill-parser-ts",
"./parsers/windmill-parser-go",
@@ -35,7 +36,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal"]
[workspace.package]
version = "1.614.0"
version = "1.613.4"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -1 +1 @@
1549849fadc4e5634334a384bfe52343eb1e93f0
0bfcfe263622f48c086331b628c1239d5cea0b37

View File

@@ -16,7 +16,7 @@ use monitor::{
send_current_log_file_to_object_store, send_logs_to_object_store, WORKERS_NAMES,
};
use rand::Rng;
use sqlx::{postgres::PgListener, Pool, Postgres};
use sqlx::postgres::PgListener;
use std::{
collections::HashMap,
fs::{create_dir_all, DirBuilder},
@@ -35,6 +35,7 @@ use windmill_common::ee_oss::{
use windmill_common::{
agent_workers::build_agent_http_client,
get_database_url,
global_settings::{
APP_WORKSPACED_ROUTE_SETTING, BASE_URL_SETTING, BUNFIG_INSTALL_SCOPES_SETTING,
CRITICAL_ALERTS_ON_DB_OVERSIZE_SETTING, CRITICAL_ALERT_MUTE_UI_SETTING,
@@ -59,10 +60,9 @@ use windmill_common::{
MODE_AND_ADDONS,
},
worker::{
reload_custom_tags_setting, Connection, HUB_CACHE_DIR, HUB_RT_CACHE_DIR, TMP_DIR,
TMP_LOGS_DIR, WORKER_GROUP,
reload_custom_tags_setting, Connection, HUB_CACHE_DIR, TMP_DIR, TMP_LOGS_DIR, WORKER_GROUP,
},
KillpillSender, DEFAULT_HUB_BASE_URL, METRICS_ENABLED,
KillpillSender, METRICS_ENABLED,
};
#[cfg(feature = "enterprise")]
@@ -320,160 +320,6 @@ async fn cache_hub_scripts(file_path: Option<String>) -> anyhow::Result<()> {
Ok(())
}
/// Raw resource type from hub API (schema is a JSON string)
#[derive(serde::Deserialize)]
struct HubResourceTypeRaw {
pub id: i64,
pub name: String,
pub schema: Option<String>,
pub app: String,
pub description: Option<String>,
}
/// Processed resource type with parsed schema
#[derive(serde::Deserialize, serde::Serialize, Clone)]
pub struct HubResourceType {
pub id: i64,
pub name: String,
pub schema: Option<serde_json::Value>,
pub app: String,
pub description: Option<String>,
}
const HUB_RT_CACHE_FILE: &str = "resource_types.json";
async fn cache_hub_resource_types() -> anyhow::Result<()> {
println!("Caching resource types from hub...");
let response = HTTP_CLIENT
.get(format!("{}/resource_types/list", DEFAULT_HUB_BASE_URL))
.header("Accept", "application/json")
.send()
.await
.with_context(|| "Failed to fetch resource types from hub")?;
if !response.status().is_success() {
anyhow::bail!(
"Failed to fetch resource types from hub: {}",
response.status()
);
}
let raw_types: Vec<HubResourceTypeRaw> = response
.json::<Vec<HubResourceTypeRaw>>()
.await
.with_context(|| "Failed to parse resource types from hub")?;
// Parse schema strings into JSON values
let resource_types: Vec<HubResourceType> = raw_types
.into_iter()
.filter_map(|rt| {
let schema = match rt.schema {
Some(s) => match serde_json::from_str(&s) {
Ok(v) => Some(v),
Err(e) => {
println!("Warning: failed to parse schema for {}: {}", rt.name, e);
return None;
}
},
None => None,
};
Some(HubResourceType {
id: rt.id,
name: rt.name,
schema,
app: rt.app,
description: rt.description,
})
})
.collect();
println!("Fetched {} resource types from hub", resource_types.len());
create_dir_all(HUB_RT_CACHE_DIR)?;
let cache_path = format!("{}/{}", HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
let content = serde_json::to_string_pretty(&resource_types)
.with_context(|| "Failed to serialize resource types")?;
std::fs::write(&cache_path, content)
.with_context(|| format!("Failed to write cache file to {}", cache_path))?;
println!("Cached resource types to {}", cache_path);
Ok(())
}
pub async fn sync_cached_resource_types(db: &sqlx::Pool<sqlx::Postgres>) -> anyhow::Result<()> {
let cache_path = format!("{}/{}", HUB_RT_CACHE_DIR, HUB_RT_CACHE_FILE);
if tokio::fs::metadata(&cache_path).await.is_err() {
tracing::info!("No cached resource types found at {}, skipping sync", cache_path);
return Ok(());
}
tracing::info!("Syncing cached resource types to admins workspace...");
let content = tokio::fs::read_to_string(&cache_path)
.await
.with_context(|| format!("Failed to read cache file from {}", cache_path))?;
let cached_types: Vec<HubResourceType> = serde_json::from_str(&content)
.with_context(|| "Failed to parse cached resource types")?;
tracing::info!("Found {} cached resource types", cached_types.len());
// Get existing resource types in admins workspace
let existing_types: Vec<(String, Option<serde_json::Value>, Option<String>)> = sqlx::query_as(
"SELECT name, schema, description FROM resource_type WHERE workspace_id = 'admins'",
)
.fetch_all(db)
.await
.with_context(|| "Failed to fetch existing resource types")?;
let existing_map: std::collections::HashMap<String, (Option<serde_json::Value>, Option<String>)> =
existing_types
.into_iter()
.map(|(name, schema, desc)| (name, (schema, desc)))
.collect();
let mut synced_count = 0;
let mut skipped_count = 0;
for rt in cached_types {
// Check if resource type already exists with same schema and description
if let Some((existing_schema, existing_desc)) = existing_map.get(&rt.name) {
if existing_schema == &rt.schema && existing_desc == &rt.description {
skipped_count += 1;
continue;
}
}
// Insert or update resource type
sqlx::query(
"INSERT INTO resource_type (workspace_id, name, schema, description, edited_at)
VALUES ('admins', $1, $2, $3, now())
ON CONFLICT (workspace_id, name) DO UPDATE
SET schema = EXCLUDED.schema, description = EXCLUDED.description, edited_at = now()",
)
.bind(&rt.name)
.bind(&rt.schema)
.bind(&rt.description)
.execute(db)
.await
.with_context(|| format!("Failed to upsert resource type {}", rt.name))?;
synced_count += 1;
}
tracing::info!(
"Synced {} resource types to admins workspace ({} skipped as unchanged)",
synced_count,
skipped_count
);
Ok(())
}
fn print_help() {
println!("Windmill - a fast, open-source workflow engine and job runner.");
println!();
@@ -484,7 +330,6 @@ fn print_help() {
println!(" help | -h | --help Show this help information and exit");
println!(" version Show Windmill version and exit");
println!(" cache [hubPaths.json] Pre-cache hub scripts (default: ./hubPaths.json)");
println!(" cache-rt Pre-cache hub resource types");
println!();
println!("Environment variables (name = default):");
println!(" DATABASE_URL = <required> The Postgres database url.");
@@ -500,7 +345,6 @@ fn print_help() {
println!(" LICENSE_KEY = None (EE only) Enterprise license key (workers require valid key)");
println!(" RUN_UPDATE_CA_CERTIFICATE_AT_START = false Run system CA update at startup");
println!(" RUN_UPDATE_CA_CERTIFICATE_PATH = /usr/sbin/update-ca-certificates Path to CA update tool");
println!(" SYNC_CACHED_RT = false Sync cached resource types to admins workspace on server start");
println!();
println!("Notes:");
println!("- Advanced and less commonly used settings are managed via the database and are omitted here.");
@@ -595,10 +439,6 @@ async fn windmill_main() -> anyhow::Result<()> {
windmill_worker::run_prepare_deps_cli().await?;
return Ok(());
}
"cache-rt" => {
cache_hub_resource_types().await?;
return Ok(());
}
_ => {}
}
@@ -703,17 +543,6 @@ async fn windmill_main() -> anyhow::Result<()> {
} else {
tracing::info!("SKIP_MIGRATION set, skipping db migration...")
}
// Sync cached resource types to admins workspace if SYNC_CACHED_RT is set
if std::env::var("SYNC_CACHED_RT")
.ok()
.map(|v| v.to_lowercase() == "true" || v == "1")
.unwrap_or(false)
{
if let Err(e) = sync_cached_resource_types(db).await {
tracing::warn!("Failed to sync cached resource types: {:#}", e);
}
}
}
}
@@ -1072,9 +901,10 @@ Windmill Community Edition {GIT_VERSION}
match conn {
Connection::Sql(ref db) => {
let base_internal_url = base_internal_url.to_string();
let db_url = get_database_url().await?;
let db = db.clone();
let h = tokio::spawn(async move {
let mut listener = retry_listen_pg(&db).await;
let mut listener = retry_listen_pg(&db_url.as_str().await).await;
let mut last_listener_refresh = Instant::now();
let mut monitor_iteration: u64 = 0;
let rd_shift: u8 = rand::rng().random_range(0..200);
@@ -1415,14 +1245,14 @@ Windmill Community Edition {GIT_VERSION}
},
Err(e) => {
tracing::error!(error = %e, "Could not receive notification, attempting to reconnect listener");
let db = db.clone();
let db_url = db_url.clone();
tokio::select! {
biased;
_ = monitor_killpill_rx.recv() => {
tracing::info!("received killpill for monitor job");
break;
},
new_listener = async move { retry_listen_pg(&db).await } => {
new_listener = async move { retry_listen_pg(&db_url.as_str().await).await } => {
listener = new_listener;
continue;
}
@@ -1436,7 +1266,7 @@ Windmill Community Edition {GIT_VERSION}
if let Err(e) = listener.unlisten_all().await {
tracing::error!(error = %e, "Could not unlisten to database");
}
listener = retry_listen_pg(&db).await;
listener = retry_listen_pg(&db_url.as_str().await).await;
initial_load(
&conn,
tx.clone(),
@@ -1621,8 +1451,8 @@ Windmill Community Edition {GIT_VERSION}
std::process::exit(0);
}
async fn listen_pg(db: &Pool<Postgres>) -> Option<PgListener> {
let mut listener = match PgListener::connect_with(db).await {
async fn listen_pg(url: &str) -> Option<PgListener> {
let mut listener = match PgListener::connect(url).await {
Ok(l) => l,
Err(e) => {
tracing::error!(error = %e, "Could not connect to database");
@@ -1655,13 +1485,13 @@ async fn listen_pg(db: &Pool<Postgres>) -> Option<PgListener> {
return Some(listener);
}
async fn retry_listen_pg(db: &Pool<Postgres>) -> PgListener {
let mut listener = listen_pg(db).await;
async fn retry_listen_pg(url: &str) -> PgListener {
let mut listener = listen_pg(url).await;
loop {
if listener.is_none() {
tracing::info!("Retrying listening to pg listen in 5 seconds");
tokio::time::sleep(Duration::from_secs(5)).await;
listener = listen_pg(db).await;
listener = listen_pg(url).await;
} else {
tracing::info!("Successfully connected to pg listen");
return listener.unwrap();

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.614.0
version: 1.613.4
title: Windmill API
contact:

View File

@@ -515,9 +515,6 @@ pub enum DatabaseUrl {
}
impl DatabaseUrl {
/// Get the database URL as a string.
/// Note: For IAM RDS, this returns the original URL (for metadata extraction).
/// For actual database connections, use connect_options() instead.
pub async fn as_str(&self) -> String {
match self {
#[cfg(all(feature = "enterprise", feature = "private"))]
@@ -529,24 +526,6 @@ impl DatabaseUrl {
}
}
/// Get PgConnectOptions for this database URL.
/// For IAM RDS, this returns options built directly from the token to avoid double-encoding
/// issues with temporary credentials (IRSA/Pod Identity).
/// For static URLs, this parses the URL string.
pub async fn connect_options(&self) -> Result<sqlx::postgres::PgConnectOptions, Error> {
match self {
#[cfg(all(feature = "enterprise", feature = "private"))]
DatabaseUrl::IamRds(rds_url) => {
let guard = rds_url.read().await;
Ok(guard.connect_options())
}
DatabaseUrl::Static(url) => {
sqlx::postgres::PgConnectOptions::from_str(url)
.map_err(|e| Error::InternalErr(format!("Failed to parse database URL: {}", e)))
}
}
}
pub async fn refresh(&self) -> anyhow::Result<()> {
match self {
#[cfg(all(feature = "enterprise", feature = "private"))]
@@ -643,10 +622,10 @@ pub async fn get_database_url() -> Result<DatabaseUrl, Error> {
}
pub async fn initial_connection() -> Result<sqlx::Pool<sqlx::Postgres>, error::Error> {
let connect_options = get_database_url().await?.connect_options().await?;
let database_url = get_database_url().await?.as_str().await;
sqlx::postgres::PgPoolOptions::new()
.max_connections(2)
.connect_with(connect_options)
.connect_with(sqlx::postgres::PgConnectOptions::from_str(&database_url)?)
.await
.map_err(|err| Error::ConnectingToDatabase(err.to_string()))
}
@@ -700,16 +679,14 @@ pub async fn connect_db(
let new_url = tokio::time::timeout(std::time::Duration::from_secs(10), get_database_url()).await;
match new_url {
Ok(Ok(new_url)) => {
match new_url.connect_options().await {
Ok(connect_options) => {
pool2.set_connect_options(connect_options);
tracing::info!("Refreshed IAM RDS URL successfully");
}
Err(e) => {
tracing::error!("Error getting IAM RDS connect options, retrying in 10s: {}", e);
continue;
}
let new_url = new_url.as_str().await;
let connect_options = sqlx::postgres::PgConnectOptions::from_str(&new_url);
if let Err(e) = connect_options {
tracing::error!("Error parsing IAM RDS URL as connect options, retrying in 10s: {}", e);
continue;
}
pool2.set_connect_options(connect_options.unwrap());
tracing::info!("Refreshed IAM RDS URL successfully");
}
Ok(Err(e)) => {
tracing::error!("Error refreshing IAM RDS URL, trying again in 10s: {}", e);
@@ -780,7 +757,7 @@ pub async fn connect(
}
})
.connect_with(
database_url.connect_options().await?
sqlx::postgres::PgConnectOptions::from_str(&database_url.as_str().await)?
.statement_cache_capacity(400),
)
.await

View File

@@ -486,7 +486,6 @@ pub const TMP_DIR: &str = "/tmp/windmill";
pub const TMP_LOGS_DIR: &str = concatcp!(TMP_DIR, "/logs");
pub const HUB_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub");
pub const HUB_RT_CACHE_DIR: &str = concatcp!(ROOT_CACHE_DIR, "hub_rt");
pub const ROOT_CACHE_DIR: &str = concatcp!(TMP_DIR, "/cache/");

View File

@@ -0,0 +1,52 @@
[package]
name = "windmill-local"
version = "0.1.0"
edition = "2021"
description = "Windmill local mode with libSQL/Turso support for preview execution"
[dependencies]
# libSQL - Turso's SQLite fork (used for local and remote Turso connections)
# Note: libsql IS the Turso database driver - Turso is built on libSQL
libsql = "0.9"
# Async runtime
tokio = { version = "1", features = ["full", "sync", "macros"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Error handling
anyhow = "1"
thiserror = "1"
# UUID for job IDs
uuid = { version = "1", features = ["v4", "serde"] }
# Timestamps
chrono = { version = "0.4", features = ["serde"] }
# Tracing
tracing = "0.1"
# HTTP server
axum = "0.7"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
# Windmill types (flows, scripts, etc.)
windmill-common = { path = "../windmill-common", default-features = false }
# For input transforms (JavaScript evaluation)
rquickjs = { version = "0.8", features = ["bindgen", "classes", "loader", "array-buffer", "futures"] }
# For recursive async functions
async-recursion = "1"
[dev-dependencies]
tokio-test = "0.4"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[[example]]
name = "local_server"
path = "examples/local_server.rs"

View File

@@ -0,0 +1,65 @@
//! Example: Run the Windmill local server
//!
//! This demonstrates running a local Windmill server with libSQL/SQLite backend.
//!
//! Run with:
//! cargo run -p windmill-local --example local_server
//!
//! Test with:
//! # Health check
//! curl http://localhost:8000/health
//!
//! # Run a bash preview (async)
//! curl -X POST http://localhost:8000/api/w/local/jobs/run/preview \
//! -H "Content-Type: application/json" \
//! -d '{"content": "echo Hello World", "language": "bash"}'
//!
//! # Run a bash preview and wait for result
//! curl -X POST http://localhost:8000/api/w/local/jobs/run_wait_result/preview \
//! -H "Content-Type: application/json" \
//! -d '{"content": "echo 42", "language": "bash"}'
//!
//! # Run a Python preview
//! curl -X POST http://localhost:8000/api/w/local/jobs/run_wait_result/preview \
//! -H "Content-Type: application/json" \
//! -d '{"content": "def main(x=1): return x * 2", "language": "python3", "args": {"x": 21}}'
//!
//! # Run a flow preview (linear flow with two steps)
//! curl -X POST http://localhost:8000/api/w/local/jobs/run_wait_result/preview_flow \
//! -H "Content-Type: application/json" \
//! -d '{
//! "value": {
//! "modules": [
//! {"id": "step1", "value": {"type": "rawscript", "language": "bash", "content": "echo 10"}},
//! {"id": "step2", "value": {"type": "identity"}}
//! ]
//! },
//! "args": {}
//! }'
use std::net::SocketAddr;
use windmill_local::LocalServer;
#[tokio::main]
async fn main() {
// Initialize tracing
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("windmill_local=info".parse().unwrap()),
)
.init();
let addr: SocketAddr = "0.0.0.0:8000".parse().unwrap();
println!("Starting Windmill Local Server on {}", addr);
println!();
println!("Test endpoints:");
println!(" Health: curl http://localhost:8000/health");
println!(" Preview: curl -X POST http://localhost:8000/api/w/local/jobs/run_wait_result/preview \\");
println!(" -H 'Content-Type: application/json' \\");
println!(" -d '{{\"content\": \"echo Hello\", \"language\": \"bash\"}}'");
println!();
let server = LocalServer::new(addr).await.expect("Failed to create server");
server.run().await.expect("Server error");
}

View File

@@ -0,0 +1,161 @@
//! Database connection and initialization for local mode
use libsql::{Builder, Connection, Database};
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::error::Result;
use crate::schema;
/// Local database wrapper
///
/// For local mode, we use a single connection with in-process coordination.
/// This simplifies the implementation since we don't need `FOR UPDATE SKIP LOCKED`.
pub struct LocalDb {
#[allow(dead_code)]
db: Database,
/// Single connection for all operations (simplifies transaction handling)
conn: Arc<Mutex<Connection>>,
}
impl LocalDb {
/// Create an in-memory database (for testing/ephemeral use)
pub async fn in_memory() -> Result<Self> {
let db = Builder::new_local(":memory:").build().await?;
let conn = db.connect()?;
let local_db = Self {
db,
conn: Arc::new(Mutex::new(conn)),
};
local_db.init_schema().await?;
Ok(local_db)
}
/// Create a file-based database
pub async fn file(path: &str) -> Result<Self> {
let db = Builder::new_local(path).build().await?;
let conn = db.connect()?;
let local_db = Self {
db,
conn: Arc::new(Mutex::new(conn)),
};
local_db.init_schema().await?;
Ok(local_db)
}
/// Create a Turso remote database connection
/// This would be used for the multi-writer scenario
pub async fn turso_remote(url: &str, auth_token: &str) -> Result<Self> {
let db = Builder::new_remote(url.to_string(), auth_token.to_string())
.build()
.await?;
let conn = db.connect()?;
let local_db = Self {
db,
conn: Arc::new(Mutex::new(conn)),
};
local_db.init_schema().await?;
Ok(local_db)
}
/// Initialize the schema
async fn init_schema(&self) -> Result<()> {
let conn = self.conn.lock().await;
// Execute schema as multiple statements
conn.execute_batch(schema::SCHEMA).await?;
Ok(())
}
/// Reset the database (drop and recreate all tables)
pub async fn reset(&self) -> Result<()> {
let conn = self.conn.lock().await;
conn.execute_batch(schema::DROP_SCHEMA).await?;
conn.execute_batch(schema::SCHEMA).await?;
Ok(())
}
/// Get a reference to the connection (locked)
pub async fn conn(&self) -> tokio::sync::MutexGuard<'_, Connection> {
self.conn.lock().await
}
/// Execute a simple query that returns no rows
pub async fn execute(&self, sql: &str, params: impl libsql::params::IntoParams) -> Result<u64> {
let conn = self.conn.lock().await;
let rows_affected = conn.execute(sql, params).await?;
Ok(rows_affected)
}
/// Execute a query and return all rows
pub async fn query(
&self,
sql: &str,
params: impl libsql::params::IntoParams,
) -> Result<libsql::Rows> {
let conn = self.conn.lock().await;
let rows = conn.query(sql, params).await?;
Ok(rows)
}
/// Execute a batch of statements (for transactions)
pub async fn execute_batch(&self, sql: &str) -> Result<()> {
let conn = self.conn.lock().await;
conn.execute_batch(sql).await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_in_memory_db() {
let db = LocalDb::in_memory().await.unwrap();
// Verify tables exist
let rows = db
.query(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
(),
)
.await
.unwrap();
let mut tables = Vec::new();
let mut rows = rows;
while let Some(row) = rows.next().await.unwrap() {
let name: String = row.get(0).unwrap();
tables.push(name);
}
assert!(tables.contains(&"v2_job".to_string()));
assert!(tables.contains(&"v2_job_queue".to_string()));
assert!(tables.contains(&"v2_job_completed".to_string()));
}
#[tokio::test]
async fn test_reset_db() {
let db = LocalDb::in_memory().await.unwrap();
// Insert a job
db.execute(
"INSERT INTO v2_job (id, kind) VALUES ('test-uuid', 'preview')",
(),
)
.await
.unwrap();
// Reset
db.reset().await.unwrap();
// Verify job is gone
let mut rows = db
.query("SELECT COUNT(*) FROM v2_job", ())
.await
.unwrap();
let row = rows.next().await.unwrap().unwrap();
let count: i64 = row.get(0).unwrap();
assert_eq!(count, 0);
}
}

View File

@@ -0,0 +1,29 @@
//! Error types for local mode
use thiserror::Error;
#[derive(Error, Debug)]
pub enum LocalError {
#[error("Database error: {0}")]
Database(#[from] libsql::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Job not found: {0}")]
JobNotFound(uuid::Uuid),
#[error("Invalid job state: {0}")]
InvalidJobState(String),
#[error("Queue is empty")]
QueueEmpty,
#[error("Execution error: {0}")]
Execution(String),
#[error("Timeout")]
Timeout,
}
pub type Result<T> = std::result::Result<T, LocalError>;

View File

@@ -0,0 +1,330 @@
//! Simple script executor for local mode
//!
//! This is a minimal executor that supports a few languages for demonstration.
//! A full implementation would integrate with windmill-worker's execution logic.
use std::process::Stdio;
use tokio::process::Command;
use tokio::io::AsyncReadExt;
use crate::error::{LocalError, Result};
use crate::jobs::ScriptLang;
/// Result of script execution
#[derive(Debug)]
pub struct ExecutionResult {
pub success: bool,
pub result: serde_json::Value,
pub logs: String,
}
/// Execute a script with the given language and arguments
pub async fn execute_script(
language: ScriptLang,
code: &str,
args: &serde_json::Value,
) -> Result<ExecutionResult> {
match language {
ScriptLang::Bash => execute_bash(code, args).await,
ScriptLang::Python3 => execute_python(code, args).await,
ScriptLang::Deno => execute_deno(code, args).await,
ScriptLang::Bun => execute_bun(code, args).await,
_ => Ok(ExecutionResult {
success: false,
result: serde_json::json!({
"error": format!("Language {:?} not supported in local mode yet", language)
}),
logs: format!("Language {:?} not supported in local mode", language),
}),
}
}
/// Execute a bash script
async fn execute_bash(code: &str, args: &serde_json::Value) -> Result<ExecutionResult> {
// Create environment variables from args
let mut env_vars = Vec::new();
if let serde_json::Value::Object(map) = args {
for (key, value) in map {
let val_str = match value {
serde_json::Value::String(s) => s.clone(),
_ => value.to_string(),
};
env_vars.push((key.to_uppercase(), val_str));
}
}
let mut child = Command::new("bash")
.arg("-c")
.arg(code)
.envs(env_vars)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| LocalError::Execution(format!("Failed to spawn bash: {}", e)))?;
let status = child
.wait()
.await
.map_err(|e| LocalError::Execution(format!("Failed to wait for bash: {}", e)))?;
let mut stdout = String::new();
let mut stderr = String::new();
if let Some(mut out) = child.stdout.take() {
out.read_to_string(&mut stdout).await.ok();
}
if let Some(mut err) = child.stderr.take() {
err.read_to_string(&mut stderr).await.ok();
}
let logs = format!("{}{}", stdout, stderr);
let success = status.success();
// Try to parse stdout as JSON, otherwise use as string
let result = if success {
let trimmed = stdout.trim();
serde_json::from_str(trimmed).unwrap_or_else(|_| serde_json::json!(trimmed))
} else {
serde_json::json!({ "error": stderr.trim() })
};
Ok(ExecutionResult {
success,
result,
logs,
})
}
/// Execute a Python script
async fn execute_python(code: &str, args: &serde_json::Value) -> Result<ExecutionResult> {
// Wrap the code to handle args and return JSON result
let wrapped_code = format!(
r#"
import json
import sys
# Args passed as JSON
args = json.loads('''{}''')
# User code
{}
# Call main if it exists
if 'main' in dir():
result = main(**args)
print(json.dumps(result))
"#,
serde_json::to_string(args)?,
code
);
let mut child = Command::new("python3")
.arg("-c")
.arg(&wrapped_code)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| LocalError::Execution(format!("Failed to spawn python3: {}", e)))?;
let status = child
.wait()
.await
.map_err(|e| LocalError::Execution(format!("Failed to wait for python3: {}", e)))?;
let mut stdout = String::new();
let mut stderr = String::new();
if let Some(mut out) = child.stdout.take() {
out.read_to_string(&mut stdout).await.ok();
}
if let Some(mut err) = child.stderr.take() {
err.read_to_string(&mut stderr).await.ok();
}
let logs = format!("{}{}", stdout, stderr);
let success = status.success();
let result = if success {
let trimmed = stdout.trim();
// Get the last line as result (in case there's debug output)
let last_line = trimmed.lines().last().unwrap_or("");
serde_json::from_str(last_line).unwrap_or_else(|_| serde_json::json!(trimmed))
} else {
serde_json::json!({ "error": stderr.trim() })
};
Ok(ExecutionResult {
success,
result,
logs,
})
}
/// Execute a Deno/TypeScript script
async fn execute_deno(code: &str, args: &serde_json::Value) -> Result<ExecutionResult> {
// Wrap the code to handle args and return JSON result
let wrapped_code = format!(
r#"
const args = {};
{}
// Call main if it exists
if (typeof main === 'function') {{
const result = await main(args);
console.log(JSON.stringify(result));
}}
"#,
serde_json::to_string(args)?,
code
);
let mut child = Command::new("deno")
.arg("eval")
.arg("--unstable")
.arg(&wrapped_code)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| LocalError::Execution(format!("Failed to spawn deno: {}", e)))?;
let status = child
.wait()
.await
.map_err(|e| LocalError::Execution(format!("Failed to wait for deno: {}", e)))?;
let mut stdout = String::new();
let mut stderr = String::new();
if let Some(mut out) = child.stdout.take() {
out.read_to_string(&mut stdout).await.ok();
}
if let Some(mut err) = child.stderr.take() {
err.read_to_string(&mut stderr).await.ok();
}
let logs = format!("{}{}", stdout, stderr);
let success = status.success();
let result = if success {
let trimmed = stdout.trim();
let last_line = trimmed.lines().last().unwrap_or("");
serde_json::from_str(last_line).unwrap_or_else(|_| serde_json::json!(trimmed))
} else {
serde_json::json!({ "error": stderr.trim() })
};
Ok(ExecutionResult {
success,
result,
logs,
})
}
/// Execute a Bun/TypeScript script
async fn execute_bun(code: &str, args: &serde_json::Value) -> Result<ExecutionResult> {
// Similar to Deno but using Bun
let wrapped_code = format!(
r#"
const args = {};
{}
// Call main if it exists
if (typeof main === 'function') {{
const result = await main(args);
console.log(JSON.stringify(result));
}}
"#,
serde_json::to_string(args)?,
code
);
let mut child = Command::new("bun")
.arg("eval")
.arg(&wrapped_code)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| LocalError::Execution(format!("Failed to spawn bun: {}", e)))?;
let status = child
.wait()
.await
.map_err(|e| LocalError::Execution(format!("Failed to wait for bun: {}", e)))?;
let mut stdout = String::new();
let mut stderr = String::new();
if let Some(mut out) = child.stdout.take() {
out.read_to_string(&mut stdout).await.ok();
}
if let Some(mut err) = child.stderr.take() {
err.read_to_string(&mut stderr).await.ok();
}
let logs = format!("{}{}", stdout, stderr);
let success = status.success();
let result = if success {
let trimmed = stdout.trim();
let last_line = trimmed.lines().last().unwrap_or("");
serde_json::from_str(last_line).unwrap_or_else(|_| serde_json::json!(trimmed))
} else {
serde_json::json!({ "error": stderr.trim() })
};
Ok(ExecutionResult {
success,
result,
logs,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_bash_execution() {
let result = execute_script(
ScriptLang::Bash,
"echo 42",
&serde_json::json!({}),
)
.await
.unwrap();
assert!(result.success);
// Output "42" is parsed as JSON number
assert_eq!(result.result, serde_json::json!(42));
}
#[tokio::test]
async fn test_bash_with_args() {
let result = execute_script(
ScriptLang::Bash,
"echo $NAME",
&serde_json::json!({"name": "world"}),
)
.await
.unwrap();
assert!(result.success);
assert_eq!(result.result, serde_json::json!("world"));
}
#[tokio::test]
async fn test_bash_json_output() {
let result = execute_script(
ScriptLang::Bash,
r#"echo '{"key": "value"}'"#,
&serde_json::json!({}),
)
.await
.unwrap();
assert!(result.success);
assert_eq!(result.result, serde_json::json!({"key": "value"}));
}
}

View File

@@ -0,0 +1,750 @@
//! Flow executor for local mode
//!
//! This module implements flow execution using the real Windmill flow types
//! from windmill-common, but with libSQL as the backend.
use std::collections::HashMap;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
use uuid::Uuid;
use windmill_common::flows::{
Branch, FlowModule, FlowModuleValue, FlowValue, InputTransform,
};
use windmill_common::scripts::ScriptLang as WmScriptLang;
use crate::db::LocalDb;
use crate::error::{LocalError, Result};
use crate::executor::{execute_script, ExecutionResult};
use crate::jobs::ScriptLang;
/// Flow execution state
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowStatus {
pub step: usize,
pub modules: Vec<ModuleStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failure_module: Option<FailureModule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry: Option<RetryStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleStatus {
pub id: String,
#[serde(rename = "type")]
pub status_type: ModuleStatusType,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iterator: Option<IteratorStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch_chosen: Option<BranchChosen>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branchall: Option<BranchAllStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum ModuleStatusType {
WaitingForPriorSteps,
WaitingForEvents,
WaitingForExecutor,
InProgress,
Success,
Failure,
Skipped,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IteratorStatus {
pub index: usize,
pub itered: Vec<serde_json::Value>,
pub args: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BranchChosen {
#[serde(rename = "type")]
pub branch_type: String,
pub branch: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BranchAllStatus {
pub branch: usize,
pub len: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FailureModule {
pub id: String,
pub error: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryStatus {
pub fail_count: u32,
}
/// Context passed through flow execution
#[derive(Debug, Clone)]
pub struct FlowContext {
pub flow_input: serde_json::Value,
pub previous_result: serde_json::Value,
pub results_by_id: HashMap<String, serde_json::Value>,
}
impl FlowContext {
pub fn new(flow_input: serde_json::Value) -> Self {
Self {
flow_input: flow_input.clone(),
previous_result: flow_input,
results_by_id: HashMap::new(),
}
}
/// Get the evaluation context for input transforms
pub fn to_eval_context(&self) -> serde_json::Value {
serde_json::json!({
"flow_input": self.flow_input,
"previous_result": self.previous_result,
"results": self.results_by_id,
})
}
}
/// Execute a flow and return the result
pub async fn execute_flow(
db: &LocalDb,
flow_value: &FlowValue,
flow_input: serde_json::Value,
) -> Result<(serde_json::Value, FlowStatus)> {
let mut ctx = FlowContext::new(flow_input);
let mut status = FlowStatus {
step: 0,
modules: Vec::new(),
failure_module: None,
retry: None,
};
// Execute modules sequentially
for (idx, module) in flow_value.modules.iter().enumerate() {
status.step = idx;
let module_status = ModuleStatus {
id: module.id.clone(),
status_type: ModuleStatusType::InProgress,
result: None,
iterator: None,
branch_chosen: None,
branchall: None,
};
status.modules.push(module_status);
tracing::info!("Executing flow module {}: {}", idx, module.id);
match execute_module(db, module, &mut ctx).await {
Ok(result) => {
// Update context with result
ctx.results_by_id.insert(module.id.clone(), result.clone());
ctx.previous_result = result.clone();
// Update status
if let Some(ms) = status.modules.last_mut() {
ms.status_type = ModuleStatusType::Success;
ms.result = Some(result);
}
}
Err(e) => {
// Module failed
tracing::error!("Flow module {} failed: {}", module.id, e);
if let Some(ms) = status.modules.last_mut() {
ms.status_type = ModuleStatusType::Failure;
ms.result = Some(serde_json::json!({"error": e.to_string()}));
}
status.failure_module = Some(FailureModule {
id: module.id.clone(),
error: e.to_string(),
});
// Check if continue_on_error is set
if !module.continue_on_error.unwrap_or(false) {
return Ok((serde_json::json!({"error": e.to_string()}), status));
}
}
}
}
Ok((ctx.previous_result, status))
}
/// Execute a single flow module
#[async_recursion::async_recursion]
async fn execute_module(
db: &LocalDb,
module: &FlowModule,
ctx: &mut FlowContext,
) -> Result<serde_json::Value> {
// Check skip_if condition
if let Some(skip_if) = &module.skip_if {
let should_skip = evaluate_expr(&skip_if.expr, &ctx.to_eval_context())?;
if should_skip.as_bool().unwrap_or(false) {
tracing::info!("Skipping module {} due to skip_if condition", module.id);
return Ok(ctx.previous_result.clone());
}
}
// Parse the module value
let module_value: FlowModuleValue = serde_json::from_str(module.value.get())
.map_err(|e| LocalError::Execution(format!("Failed to parse module value: {}", e)))?;
match module_value {
FlowModuleValue::Identity => {
Ok(ctx.previous_result.clone())
}
FlowModuleValue::RawScript { content, language, input_transforms, .. } => {
let args = resolve_input_transforms(&input_transforms, ctx)?;
let lang = convert_script_lang(&language);
let result = execute_script(lang, &content, &args).await?;
if result.success {
Ok(result.result)
} else {
Err(LocalError::Execution(result.result.to_string()))
}
}
FlowModuleValue::Script { path, input_transforms, .. } => {
// In local mode, we don't have access to saved scripts
Err(LocalError::Execution(format!(
"Script references (path: {}) are not supported in local mode. Use rawscript instead.",
path
)))
}
FlowModuleValue::Flow { path, .. } => {
// In local mode, we don't have access to saved flows
Err(LocalError::Execution(format!(
"Flow references (path: {}) are not supported in local mode. Use inline modules instead.",
path
)))
}
FlowModuleValue::ForloopFlow { iterator, modules, skip_failures, parallel, .. } => {
execute_forloop(db, &iterator, &modules, skip_failures, parallel, ctx).await
}
FlowModuleValue::WhileloopFlow { modules, skip_failures, .. } => {
execute_whileloop(db, &modules, skip_failures, ctx).await
}
FlowModuleValue::BranchOne { branches, default, .. } => {
execute_branch_one(db, &branches, &default, ctx).await
}
FlowModuleValue::BranchAll { branches, parallel } => {
execute_branch_all(db, &branches, parallel, ctx).await
}
FlowModuleValue::FlowScript { .. } => {
Err(LocalError::Execution(
"FlowScript (internal reference) is not supported in local mode".to_string()
))
}
FlowModuleValue::AIAgent { .. } => {
Err(LocalError::Execution(
"AIAgent is not supported in local mode".to_string()
))
}
}
}
/// Execute a for loop
#[async_recursion::async_recursion]
async fn execute_forloop(
db: &LocalDb,
iterator: &InputTransform,
modules: &[FlowModule],
skip_failures: bool,
_parallel: bool, // TODO: implement parallel execution
ctx: &mut FlowContext,
) -> Result<serde_json::Value> {
// Evaluate the iterator expression
let iter_value = evaluate_input_transform(iterator, ctx)?;
let items = match iter_value.as_array() {
Some(arr) => arr.clone(),
None => {
return Err(LocalError::Execution(
"For loop iterator must evaluate to an array".to_string()
));
}
};
let mut results = Vec::new();
for (idx, item) in items.iter().enumerate() {
tracing::debug!("For loop iteration {} of {}", idx + 1, items.len());
// Create iteration context
let mut iter_ctx = FlowContext {
flow_input: ctx.flow_input.clone(),
previous_result: item.clone(),
results_by_id: ctx.results_by_id.clone(),
};
// Add iter context
iter_ctx.results_by_id.insert("iter".to_string(), serde_json::json!({
"index": idx,
"value": item,
}));
// Execute modules in sequence
let mut iter_result = item.clone();
let mut had_error = false;
for module in modules {
match execute_module(db, module, &mut iter_ctx).await {
Ok(result) => {
iter_ctx.results_by_id.insert(module.id.clone(), result.clone());
iter_ctx.previous_result = result.clone();
iter_result = result;
}
Err(e) => {
had_error = true;
if skip_failures {
tracing::warn!("For loop iteration {} failed (skipping): {}", idx, e);
iter_result = serde_json::json!({"error": e.to_string()});
} else {
return Err(e);
}
break;
}
}
}
if !had_error || skip_failures {
results.push(iter_result);
}
}
Ok(serde_json::Value::Array(results))
}
/// Execute a while loop
#[async_recursion::async_recursion]
async fn execute_whileloop(
db: &LocalDb,
modules: &[FlowModule],
skip_failures: bool,
ctx: &mut FlowContext,
) -> Result<serde_json::Value> {
const MAX_ITERATIONS: usize = 1000;
let mut results = Vec::new();
let mut iteration = 0;
loop {
if iteration >= MAX_ITERATIONS {
return Err(LocalError::Execution(format!(
"While loop exceeded maximum iterations ({})", MAX_ITERATIONS
)));
}
// Execute modules
let mut iter_result = ctx.previous_result.clone();
let mut should_continue = true;
for module in modules {
match execute_module(db, module, ctx).await {
Ok(result) => {
ctx.results_by_id.insert(module.id.clone(), result.clone());
ctx.previous_result = result.clone();
iter_result = result;
}
Err(e) => {
if skip_failures {
tracing::warn!("While loop iteration {} failed (skipping): {}", iteration, e);
iter_result = serde_json::json!({"error": e.to_string()});
} else {
return Err(e);
}
should_continue = false;
break;
}
}
}
results.push(iter_result);
iteration += 1;
// Check stop condition (result should be truthy to continue)
if !should_continue {
break;
}
// Check if the result indicates we should stop
let continue_loop = match &ctx.previous_result {
serde_json::Value::Bool(b) => *b,
serde_json::Value::Null => false,
serde_json::Value::Object(obj) => {
// Check for a "continue" field
obj.get("continue")
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
_ => false,
};
if !continue_loop {
break;
}
}
Ok(serde_json::Value::Array(results))
}
/// Execute branch-one (if/else)
#[async_recursion::async_recursion]
async fn execute_branch_one(
db: &LocalDb,
branches: &[Branch],
default: &[FlowModule],
ctx: &mut FlowContext,
) -> Result<serde_json::Value> {
// Find the first matching branch
for (idx, branch) in branches.iter().enumerate() {
let condition = evaluate_expr(&branch.expr, &ctx.to_eval_context())?;
if condition.as_bool().unwrap_or(false) {
tracing::debug!("Branch {} matched", idx);
return execute_branch_modules(db, &branch.modules, ctx).await;
}
}
// No branch matched, execute default
tracing::debug!("No branch matched, executing default");
execute_branch_modules(db, default, ctx).await
}
/// Execute branch-all (parallel branches)
#[async_recursion::async_recursion]
async fn execute_branch_all(
db: &LocalDb,
branches: &[Branch],
_parallel: bool, // TODO: implement true parallel execution
ctx: &mut FlowContext,
) -> Result<serde_json::Value> {
let mut results = Vec::new();
// Execute all branches (sequentially for now)
for (idx, branch) in branches.iter().enumerate() {
tracing::debug!("Executing branch {}", idx);
let mut branch_ctx = ctx.clone();
match execute_branch_modules(db, &branch.modules, &mut branch_ctx).await {
Ok(result) => {
results.push(result);
}
Err(e) => {
if branch.skip_failure {
tracing::warn!("Branch {} failed (skipping): {}", idx, e);
results.push(serde_json::json!({"error": e.to_string()}));
} else {
return Err(e);
}
}
}
}
Ok(serde_json::Value::Array(results))
}
/// Execute a sequence of modules in a branch
#[async_recursion::async_recursion]
async fn execute_branch_modules(
db: &LocalDb,
modules: &[FlowModule],
ctx: &mut FlowContext,
) -> Result<serde_json::Value> {
let mut result = ctx.previous_result.clone();
for module in modules {
result = execute_module(db, module, ctx).await?;
ctx.results_by_id.insert(module.id.clone(), result.clone());
ctx.previous_result = result.clone();
}
Ok(result)
}
/// Resolve input transforms to concrete arguments
fn resolve_input_transforms(
transforms: &HashMap<String, InputTransform>,
ctx: &FlowContext,
) -> Result<serde_json::Value> {
let mut args = serde_json::Map::new();
for (key, transform) in transforms {
let value = evaluate_input_transform(transform, ctx)?;
args.insert(key.clone(), value);
}
Ok(serde_json::Value::Object(args))
}
/// Evaluate an input transform
fn evaluate_input_transform(
transform: &InputTransform,
ctx: &FlowContext,
) -> Result<serde_json::Value> {
match transform {
InputTransform::Static { value } => {
serde_json::from_str(value.get())
.map_err(|e| LocalError::Execution(format!("Invalid static value: {}", e)))
}
InputTransform::Javascript { expr } => {
evaluate_expr(expr, &ctx.to_eval_context())
}
InputTransform::Ai => {
Err(LocalError::Execution("AI input transforms are not supported in local mode".to_string()))
}
}
}
/// Evaluate a JavaScript expression
fn evaluate_expr(expr: &str, context: &serde_json::Value) -> Result<serde_json::Value> {
let expr = expr.trim();
// Handle comparison operators
if let Some(result) = try_evaluate_comparison(expr, context) {
return Ok(result);
}
// Handle simple variable references
if let Some(val) = resolve_path(expr, context) {
return Ok(val);
}
// Handle boolean literals
if expr == "true" {
return Ok(serde_json::Value::Bool(true));
}
if expr == "false" {
return Ok(serde_json::Value::Bool(false));
}
// Handle numeric literals
if let Ok(n) = expr.parse::<i64>() {
return Ok(serde_json::json!(n));
}
if let Ok(n) = expr.parse::<f64>() {
return Ok(serde_json::json!(n));
}
// Handle string literals
if (expr.starts_with('"') && expr.ends_with('"')) ||
(expr.starts_with('\'') && expr.ends_with('\'')) {
return Ok(serde_json::json!(&expr[1..expr.len()-1]));
}
// For complex expressions, we'd need a full JS runtime
tracing::warn!("Complex expression not evaluated: {}", expr);
Ok(serde_json::json!(expr))
}
/// Try to resolve a path like "flow_input.x" or "results.a.b"
fn resolve_path(path: &str, context: &serde_json::Value) -> Option<serde_json::Value> {
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
return None;
}
let mut current = context.get(parts[0])?;
for part in &parts[1..] {
current = current.get(*part)?;
}
Some(current.clone())
}
/// Try to evaluate a comparison expression
fn try_evaluate_comparison(expr: &str, context: &serde_json::Value) -> Option<serde_json::Value> {
// Supported operators: >, <, >=, <=, ==, !=, ===, !==
let operators = ["===", "!==", ">=", "<=", "==", "!=", ">", "<"];
for op in operators {
if let Some(pos) = expr.find(op) {
let left = expr[..pos].trim();
let right = expr[pos + op.len()..].trim();
let left_val = evaluate_expr(left, context).ok()?;
let right_val = evaluate_expr(right, context).ok()?;
let result = match op {
">" => compare_values(&left_val, &right_val, |a, b| a > b),
"<" => compare_values(&left_val, &right_val, |a, b| a < b),
">=" => compare_values(&left_val, &right_val, |a, b| a >= b),
"<=" => compare_values(&left_val, &right_val, |a, b| a <= b),
"==" | "===" => Some(left_val == right_val),
"!=" | "!==" => Some(left_val != right_val),
_ => None,
};
return result.map(serde_json::Value::Bool);
}
}
// Try logical operators
if let Some(pos) = expr.find("&&") {
let left = expr[..pos].trim();
let right = expr[pos + 2..].trim();
let left_val = evaluate_expr(left, context).ok()?;
let right_val = evaluate_expr(right, context).ok()?;
return Some(serde_json::Value::Bool(
is_truthy(&left_val) && is_truthy(&right_val)
));
}
if let Some(pos) = expr.find("||") {
let left = expr[..pos].trim();
let right = expr[pos + 2..].trim();
let left_val = evaluate_expr(left, context).ok()?;
let right_val = evaluate_expr(right, context).ok()?;
return Some(serde_json::Value::Bool(
is_truthy(&left_val) || is_truthy(&right_val)
));
}
None
}
/// Compare two JSON values numerically
fn compare_values<F>(left: &serde_json::Value, right: &serde_json::Value, cmp: F) -> Option<bool>
where
F: Fn(f64, f64) -> bool,
{
let left_num = value_to_number(left)?;
let right_num = value_to_number(right)?;
Some(cmp(left_num, right_num))
}
/// Convert a JSON value to a number
fn value_to_number(val: &serde_json::Value) -> Option<f64> {
match val {
serde_json::Value::Number(n) => n.as_f64(),
serde_json::Value::String(s) => s.parse().ok(),
serde_json::Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
_ => None,
}
}
/// Check if a value is truthy (JavaScript semantics)
fn is_truthy(val: &serde_json::Value) -> bool {
match val {
serde_json::Value::Null => false,
serde_json::Value::Bool(b) => *b,
serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
serde_json::Value::String(s) => !s.is_empty(),
serde_json::Value::Array(a) => !a.is_empty(),
serde_json::Value::Object(_) => true,
}
}
/// Convert windmill-common ScriptLang to local ScriptLang
fn convert_script_lang(lang: &WmScriptLang) -> ScriptLang {
match lang {
WmScriptLang::Deno => ScriptLang::Deno,
WmScriptLang::Python3 => ScriptLang::Python3,
WmScriptLang::Bash => ScriptLang::Bash,
WmScriptLang::Go => ScriptLang::Go,
WmScriptLang::Bun => ScriptLang::Bun,
WmScriptLang::Nativets => ScriptLang::Deno, // Fallback to Deno
_ => ScriptLang::Bash, // Default fallback
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_evaluate_simple_expr() {
let ctx = serde_json::json!({
"flow_input": {"x": 10},
"previous_result": 42,
"results": {"a": "hello"},
});
assert_eq!(
evaluate_expr("flow_input", &ctx).unwrap(),
serde_json::json!({"x": 10})
);
assert_eq!(
evaluate_expr("previous_result", &ctx).unwrap(),
serde_json::json!(42)
);
assert_eq!(
evaluate_expr("results.a", &ctx).unwrap(),
serde_json::json!("hello")
);
assert_eq!(
evaluate_expr("flow_input.x", &ctx).unwrap(),
serde_json::json!(10)
);
}
#[test]
fn test_evaluate_comparison_expr() {
let ctx = serde_json::json!({
"flow_input": {"x": 10, "y": 5},
"previous_result": 42,
});
assert_eq!(
evaluate_expr("flow_input.x > 5", &ctx).unwrap(),
serde_json::Value::Bool(true)
);
assert_eq!(
evaluate_expr("flow_input.x < 5", &ctx).unwrap(),
serde_json::Value::Bool(false)
);
assert_eq!(
evaluate_expr("flow_input.x >= 10", &ctx).unwrap(),
serde_json::Value::Bool(true)
);
assert_eq!(
evaluate_expr("flow_input.y == 5", &ctx).unwrap(),
serde_json::Value::Bool(true)
);
assert_eq!(
evaluate_expr("previous_result > 40", &ctx).unwrap(),
serde_json::Value::Bool(true)
);
}
#[test]
fn test_evaluate_logical_expr() {
let ctx = serde_json::json!({
"flow_input": {"a": true, "b": false},
});
// Simple boolean logic (complex expressions with mixed operators need parentheses support)
assert_eq!(
evaluate_expr("flow_input.a && flow_input.a", &ctx).unwrap(),
serde_json::Value::Bool(true)
);
assert_eq!(
evaluate_expr("flow_input.a && flow_input.b", &ctx).unwrap(),
serde_json::Value::Bool(false)
);
assert_eq!(
evaluate_expr("flow_input.b || flow_input.a", &ctx).unwrap(),
serde_json::Value::Bool(true)
);
}
}

View File

@@ -0,0 +1,495 @@
//! Job types and operations for local mode
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::db::LocalDb;
use crate::error::{LocalError, Result};
/// Job kind (mirrors windmill-common JobKind)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JobKind {
Script,
Preview,
Flow,
FlowPreview,
Dependencies,
FlowDependencies,
ScriptHub,
Identity,
Http,
Graphql,
Postgresql,
Noop,
AppDependencies,
DeploymentCallback,
SingleScriptFlow,
FlowScript,
FlowNode,
AppScript,
}
impl JobKind {
pub fn as_str(&self) -> &'static str {
match self {
JobKind::Script => "script",
JobKind::Preview => "preview",
JobKind::Flow => "flow",
JobKind::FlowPreview => "flowpreview",
JobKind::Dependencies => "dependencies",
JobKind::FlowDependencies => "flowdependencies",
JobKind::ScriptHub => "script_hub",
JobKind::Identity => "identity",
JobKind::Http => "http",
JobKind::Graphql => "graphql",
JobKind::Postgresql => "postgresql",
JobKind::Noop => "noop",
JobKind::AppDependencies => "appdependencies",
JobKind::DeploymentCallback => "deploymentcallback",
JobKind::SingleScriptFlow => "singlescriptflow",
JobKind::FlowScript => "flowscript",
JobKind::FlowNode => "flownode",
JobKind::AppScript => "appscript",
}
}
}
/// Script language
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScriptLang {
Python3,
Deno,
Go,
Bash,
Postgresql,
Nativets,
Bun,
Mysql,
Bigquery,
Snowflake,
Graphql,
Powershell,
Mssql,
Php,
Bunnative,
Rust,
Ansible,
Csharp,
Oracledb,
Nu,
Java,
Duckdb,
}
impl ScriptLang {
pub fn as_str(&self) -> &'static str {
match self {
ScriptLang::Python3 => "python3",
ScriptLang::Deno => "deno",
ScriptLang::Go => "go",
ScriptLang::Bash => "bash",
ScriptLang::Postgresql => "postgresql",
ScriptLang::Nativets => "nativets",
ScriptLang::Bun => "bun",
ScriptLang::Mysql => "mysql",
ScriptLang::Bigquery => "bigquery",
ScriptLang::Snowflake => "snowflake",
ScriptLang::Graphql => "graphql",
ScriptLang::Powershell => "powershell",
ScriptLang::Mssql => "mssql",
ScriptLang::Php => "php",
ScriptLang::Bunnative => "bunnative",
ScriptLang::Rust => "rust",
ScriptLang::Ansible => "ansible",
ScriptLang::Csharp => "csharp",
ScriptLang::Oracledb => "oracledb",
ScriptLang::Nu => "nu",
ScriptLang::Java => "java",
ScriptLang::Duckdb => "duckdb",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"python3" => Some(ScriptLang::Python3),
"deno" => Some(ScriptLang::Deno),
"go" => Some(ScriptLang::Go),
"bash" => Some(ScriptLang::Bash),
"postgresql" => Some(ScriptLang::Postgresql),
"nativets" => Some(ScriptLang::Nativets),
"bun" => Some(ScriptLang::Bun),
"mysql" => Some(ScriptLang::Mysql),
"bigquery" => Some(ScriptLang::Bigquery),
"snowflake" => Some(ScriptLang::Snowflake),
"graphql" => Some(ScriptLang::Graphql),
"powershell" => Some(ScriptLang::Powershell),
"mssql" => Some(ScriptLang::Mssql),
"php" => Some(ScriptLang::Php),
"bunnative" => Some(ScriptLang::Bunnative),
"rust" => Some(ScriptLang::Rust),
"ansible" => Some(ScriptLang::Ansible),
"csharp" => Some(ScriptLang::Csharp),
"oracledb" => Some(ScriptLang::Oracledb),
"nu" => Some(ScriptLang::Nu),
"java" => Some(ScriptLang::Java),
"duckdb" => Some(ScriptLang::Duckdb),
_ => None,
}
}
}
/// Job status for completed jobs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JobStatus {
Success,
Failure,
Canceled,
Skipped,
}
impl JobStatus {
pub fn as_str(&self) -> &'static str {
match self {
JobStatus::Success => "success",
JobStatus::Failure => "failure",
JobStatus::Canceled => "canceled",
JobStatus::Skipped => "skipped",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"success" => Some(JobStatus::Success),
"failure" => Some(JobStatus::Failure),
"canceled" => Some(JobStatus::Canceled),
"skipped" => Some(JobStatus::Skipped),
_ => None,
}
}
}
/// Preview job request (simplified from windmill-api Preview struct)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreviewRequest {
pub content: String,
pub language: ScriptLang,
#[serde(default)]
pub args: serde_json::Value,
pub lock: Option<String>,
pub tag: Option<String>,
}
/// Flow preview request
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowPreviewRequest {
pub value: serde_json::Value, // FlowValue as JSON
#[serde(default)]
pub args: serde_json::Value,
pub tag: Option<String>,
}
/// A queued job (combines v2_job and v2_job_queue)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueuedJob {
pub id: Uuid,
pub workspace_id: String,
pub kind: JobKind,
pub script_lang: Option<ScriptLang>,
pub raw_code: Option<String>,
pub raw_lock: Option<String>,
pub raw_flow: Option<serde_json::Value>,
pub args: serde_json::Value,
pub tag: String,
pub created_at: DateTime<Utc>,
pub scheduled_for: DateTime<Utc>,
pub running: bool,
pub parent_job: Option<Uuid>,
pub root_job: Option<Uuid>,
pub flow_step_id: Option<String>,
}
/// A completed job
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletedJob {
pub id: Uuid,
pub workspace_id: String,
pub status: JobStatus,
pub result: serde_json::Value,
pub started_at: Option<DateTime<Utc>>,
pub completed_at: DateTime<Utc>,
pub duration_ms: Option<i64>,
}
/// Push a preview job to the queue
pub async fn push_preview(db: &LocalDb, req: PreviewRequest) -> Result<Uuid> {
let id = Uuid::new_v4();
let now = Utc::now().to_rfc3339();
let args_json = serde_json::to_string(&req.args)?;
let tag = req.tag.as_deref().unwrap_or("deno");
// Insert into v2_job
db.execute(
r#"
INSERT INTO v2_job (id, kind, script_lang, raw_code, raw_lock, args, tag, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
"#,
libsql::params![
id.to_string(),
JobKind::Preview.as_str(),
req.language.as_str(),
req.content,
req.lock,
args_json,
tag,
now.clone(),
],
)
.await?;
// Insert into v2_job_queue
db.execute(
r#"
INSERT INTO v2_job_queue (id, tag, created_at, scheduled_for, running)
VALUES (?1, ?2, ?3, ?4, 0)
"#,
libsql::params![id.to_string(), tag, now.clone(), now],
)
.await?;
// Insert into v2_job_runtime (for heartbeat tracking)
db.execute(
"INSERT INTO v2_job_runtime (id) VALUES (?1)",
libsql::params![id.to_string()],
)
.await?;
// Insert into job_perms (simplified)
db.execute(
"INSERT INTO job_perms (job_id) VALUES (?1)",
libsql::params![id.to_string()],
)
.await?;
tracing::info!("Pushed preview job: {}", id);
Ok(id)
}
/// Push a flow preview job to the queue
pub async fn push_flow_preview(db: &LocalDb, req: FlowPreviewRequest) -> Result<Uuid> {
let id = Uuid::new_v4();
let now = Utc::now().to_rfc3339();
let args_json = serde_json::to_string(&req.args)?;
let flow_json = serde_json::to_string(&req.value)?;
let tag = req.tag.as_deref().unwrap_or("flow");
// Insert into v2_job
db.execute(
r#"
INSERT INTO v2_job (id, kind, raw_flow, args, tag, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
"#,
libsql::params![
id.to_string(),
JobKind::FlowPreview.as_str(),
flow_json,
args_json,
tag,
now.clone(),
],
)
.await?;
// Insert into v2_job_queue
db.execute(
r#"
INSERT INTO v2_job_queue (id, tag, created_at, scheduled_for, running)
VALUES (?1, ?2, ?3, ?4, 0)
"#,
libsql::params![id.to_string(), tag, now.clone(), now],
)
.await?;
// Insert into v2_job_runtime
db.execute(
"INSERT INTO v2_job_runtime (id) VALUES (?1)",
libsql::params![id.to_string()],
)
.await?;
// Insert into job_perms
db.execute(
"INSERT INTO job_perms (job_id) VALUES (?1)",
libsql::params![id.to_string()],
)
.await?;
// Insert initial flow status
db.execute(
"INSERT INTO v2_job_status (id, flow_status) VALUES (?1, '{}')",
libsql::params![id.to_string()],
)
.await?;
tracing::info!("Pushed flow preview job: {}", id);
Ok(id)
}
/// Get a completed job result (for polling)
pub async fn get_completed_job(db: &LocalDb, id: Uuid) -> Result<Option<CompletedJob>> {
let mut rows = db
.query(
r#"
SELECT id, workspace_id, status, result, started_at, completed_at, duration_ms
FROM v2_job_completed
WHERE id = ?1
"#,
libsql::params![id.to_string()],
)
.await?;
if let Some(row) = rows.next().await? {
let status_str: String = row.get(2)?;
let status = JobStatus::from_str(&status_str)
.ok_or_else(|| LocalError::InvalidJobState(status_str))?;
let result_str: Option<String> = row.get(3)?;
let result: serde_json::Value = result_str
.map(|s| serde_json::from_str(&s))
.transpose()?
.unwrap_or(serde_json::Value::Null);
let started_at_str: Option<String> = row.get(4)?;
let started_at = started_at_str
.map(|s| DateTime::parse_from_rfc3339(&s).map(|dt| dt.with_timezone(&Utc)))
.transpose()
.ok()
.flatten();
let completed_at_str: String = row.get(5)?;
let completed_at = DateTime::parse_from_rfc3339(&completed_at_str)
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(|_| Utc::now());
let duration_ms: Option<i64> = row.get(6)?;
Ok(Some(CompletedJob {
id,
workspace_id: row.get(1)?,
status,
result,
started_at,
completed_at,
duration_ms,
}))
} else {
Ok(None)
}
}
/// Mark a job as completed with result
pub async fn complete_job(
db: &LocalDb,
id: Uuid,
status: JobStatus,
result: serde_json::Value,
started_at: DateTime<Utc>,
) -> Result<()> {
let now = Utc::now();
let duration_ms = (now - started_at).num_milliseconds();
let result_json = serde_json::to_string(&result)?;
db.execute(
r#"
INSERT INTO v2_job_completed (id, workspace_id, status, result, started_at, completed_at, duration_ms)
SELECT ?1, workspace_id, ?2, ?3, ?4, ?5, ?6
FROM v2_job WHERE id = ?1
"#,
libsql::params![
id.to_string(),
status.as_str(),
result_json,
started_at.to_rfc3339(),
now.to_rfc3339(),
duration_ms,
],
)
.await?;
// Remove from queue
db.execute(
"DELETE FROM v2_job_queue WHERE id = ?1",
libsql::params![id.to_string()],
)
.await?;
tracing::info!("Completed job {}: {:?}", id, status);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_push_preview() {
let db = LocalDb::in_memory().await.unwrap();
let req = PreviewRequest {
content: "export function main() { return 42; }".to_string(),
language: ScriptLang::Deno,
args: serde_json::json!({}),
lock: None,
tag: None,
};
let id = push_preview(&db, req).await.unwrap();
// Verify job exists in queue
let mut rows = db
.query(
"SELECT running FROM v2_job_queue WHERE id = ?1",
libsql::params![id.to_string()],
)
.await
.unwrap();
let row = rows.next().await.unwrap().unwrap();
let running: i64 = row.get(0).unwrap();
assert_eq!(running, 0);
}
#[tokio::test]
async fn test_complete_job() {
let db = LocalDb::in_memory().await.unwrap();
let req = PreviewRequest {
content: "export function main() { return 42; }".to_string(),
language: ScriptLang::Deno,
args: serde_json::json!({}),
lock: None,
tag: None,
};
let id = push_preview(&db, req).await.unwrap();
let started_at = Utc::now();
complete_job(
&db,
id,
JobStatus::Success,
serde_json::json!(42),
started_at,
)
.await
.unwrap();
// Verify job is in completed
let completed = get_completed_job(&db, id).await.unwrap().unwrap();
assert_eq!(completed.status, JobStatus::Success);
assert_eq!(completed.result, serde_json::json!(42));
}
}

View File

@@ -0,0 +1,31 @@
//! Windmill Local Mode
//!
//! This crate provides a minimal local mode for Windmill using libSQL (SQLite/Turso)
//! instead of PostgreSQL. The goal is to support preview execution end-to-end
//! with a lightweight, embedded database.
//!
//! ## Scope
//! - Script preview execution
//! - Flow preview execution
//! - In-memory or file-based SQLite storage
//! - Remote Turso database support for multi-writer scenarios
//!
//! ## Non-goals (for this experiment)
//! - Full feature parity with PostgreSQL mode
//! - Multi-worker support (single embedded worker)
//! - Persistence of scripts/flows (only jobs)
pub mod db;
pub mod schema;
pub mod jobs;
pub mod queue;
pub mod executor;
pub mod flow_executor;
pub mod worker;
pub mod server;
pub mod error;
pub use db::LocalDb;
pub use error::LocalError;
pub use worker::Worker;
pub use server::LocalServer;

View File

@@ -0,0 +1,296 @@
//! Queue operations for local mode
//!
//! Since local mode uses a single worker, we don't need the complex
//! `FOR UPDATE SKIP LOCKED` mechanism. Instead, we use simple atomic
//! operations with the database lock.
use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::db::LocalDb;
use crate::error::{LocalError, Result};
use crate::jobs::{JobKind, QueuedJob, ScriptLang};
/// Pull the next job from the queue
///
/// This is simplified from the PostgreSQL version since we have a single worker
/// and use the connection mutex for coordination.
pub async fn pull_job(db: &LocalDb) -> Result<Option<QueuedJob>> {
let now = Utc::now().to_rfc3339();
// Get the next job (ordered by priority, then scheduled_for)
// We use a transaction-like approach: SELECT then UPDATE
let mut rows = db
.query(
r#"
SELECT q.id, j.workspace_id, j.kind, j.script_lang, j.raw_code, j.raw_lock,
j.raw_flow, j.args, q.tag, j.created_at, q.scheduled_for,
j.parent_job, j.root_job, j.flow_step_id
FROM v2_job_queue q
JOIN v2_job j ON q.id = j.id
WHERE q.running = 0 AND q.scheduled_for <= ?1
ORDER BY q.priority DESC, q.scheduled_for ASC
LIMIT 1
"#,
libsql::params![now],
)
.await?;
let Some(row) = rows.next().await? else {
return Ok(None);
};
let id_str: String = row.get(0)?;
let id = Uuid::parse_str(&id_str).map_err(|e| LocalError::InvalidJobState(e.to_string()))?;
// Mark as running
let started_at = Utc::now().to_rfc3339();
db.execute(
"UPDATE v2_job_queue SET running = 1, started_at = ?2 WHERE id = ?1",
libsql::params![id_str.clone(), started_at],
)
.await?;
// Parse the job fields
let kind_str: String = row.get(2)?;
let kind = match kind_str.as_str() {
"preview" => JobKind::Preview,
"flowpreview" => JobKind::FlowPreview,
"script" => JobKind::Script,
"flow" => JobKind::Flow,
"flowscript" => JobKind::FlowScript,
"flownode" => JobKind::FlowNode,
_ => JobKind::Preview, // Default
};
let lang_str: Option<String> = row.get(3)?;
let script_lang = lang_str.and_then(|s| ScriptLang::from_str(&s));
let raw_code: Option<String> = row.get(4)?;
let raw_lock: Option<String> = row.get(5)?;
let raw_flow_str: Option<String> = row.get(6)?;
let raw_flow = raw_flow_str
.map(|s| serde_json::from_str(&s))
.transpose()?;
let args_str: Option<String> = row.get(7)?;
let args: serde_json::Value = args_str
.map(|s| serde_json::from_str(&s))
.transpose()?
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
let tag: String = row.get(8)?;
let created_at_str: String = row.get(9)?;
let created_at = DateTime::parse_from_rfc3339(&created_at_str)
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(|_| Utc::now());
let scheduled_for_str: String = row.get(10)?;
let scheduled_for = DateTime::parse_from_rfc3339(&scheduled_for_str)
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(|_| Utc::now());
let parent_job_str: Option<String> = row.get(11)?;
let parent_job = parent_job_str.and_then(|s| Uuid::parse_str(&s).ok());
let root_job_str: Option<String> = row.get(12)?;
let root_job = root_job_str.and_then(|s| Uuid::parse_str(&s).ok());
let flow_step_id: Option<String> = row.get(13)?;
Ok(Some(QueuedJob {
id,
workspace_id: row.get(1)?,
kind,
script_lang,
raw_code,
raw_lock,
raw_flow,
args,
tag,
created_at,
scheduled_for,
running: true,
parent_job,
root_job,
flow_step_id,
}))
}
/// Get queue statistics
pub async fn queue_stats(db: &LocalDb) -> Result<QueueStats> {
let mut rows = db
.query(
r#"
SELECT
COUNT(*) as total,
SUM(CASE WHEN running = 1 THEN 1 ELSE 0 END) as running,
SUM(CASE WHEN running = 0 THEN 1 ELSE 0 END) as pending
FROM v2_job_queue
"#,
(),
)
.await?;
let row = rows.next().await?.ok_or(LocalError::QueueEmpty)?;
Ok(QueueStats {
total: row.get::<i64>(0)? as u64,
running: row.get::<i64>(1).unwrap_or(0) as u64,
pending: row.get::<i64>(2).unwrap_or(0) as u64,
})
}
#[derive(Debug, Clone)]
pub struct QueueStats {
pub total: u64,
pub running: u64,
pub pending: u64,
}
/// Update job heartbeat (ping)
pub async fn ping_job(db: &LocalDb, id: Uuid) -> Result<()> {
let now = Utc::now().to_rfc3339();
db.execute(
"UPDATE v2_job_runtime SET ping = ?2 WHERE id = ?1",
libsql::params![id.to_string(), now],
)
.await?;
Ok(())
}
/// Update flow status for a running flow job
pub async fn update_flow_status(
db: &LocalDb,
id: Uuid,
flow_status: &serde_json::Value,
) -> Result<()> {
let status_json = serde_json::to_string(flow_status)?;
db.execute(
"UPDATE v2_job_status SET flow_status = ?2 WHERE id = ?1",
libsql::params![id.to_string(), status_json],
)
.await?;
Ok(())
}
/// Push a child job for flow execution
pub async fn push_flow_child_job(
db: &LocalDb,
parent_id: Uuid,
root_id: Uuid,
step_id: &str,
kind: JobKind,
script_lang: Option<ScriptLang>,
raw_code: Option<&str>,
args: &serde_json::Value,
) -> Result<Uuid> {
let id = Uuid::new_v4();
let now = Utc::now().to_rfc3339();
let args_json = serde_json::to_string(args)?;
// Insert into v2_job
db.execute(
r#"
INSERT INTO v2_job (id, kind, script_lang, raw_code, args, parent_job, root_job, flow_step_id, created_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
"#,
libsql::params![
id.to_string(),
kind.as_str(),
script_lang.map(|l| l.as_str()),
raw_code,
args_json,
parent_id.to_string(),
root_id.to_string(),
step_id,
now.clone(),
],
)
.await?;
// Insert into v2_job_queue
db.execute(
r#"
INSERT INTO v2_job_queue (id, tag, created_at, scheduled_for, running)
VALUES (?1, 'flow', ?2, ?3, 0)
"#,
libsql::params![id.to_string(), now.clone(), now],
)
.await?;
// Insert into v2_job_runtime
db.execute(
"INSERT INTO v2_job_runtime (id) VALUES (?1)",
libsql::params![id.to_string()],
)
.await?;
Ok(id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jobs::{push_preview, PreviewRequest};
#[tokio::test]
async fn test_pull_job() {
let db = LocalDb::in_memory().await.unwrap();
// Push a job
let req = PreviewRequest {
content: "export function main() { return 42; }".to_string(),
language: ScriptLang::Deno,
args: serde_json::json!({}),
lock: None,
tag: None,
};
let pushed_id = push_preview(&db, req).await.unwrap();
// Pull it
let job = pull_job(&db).await.unwrap().unwrap();
assert_eq!(job.id, pushed_id);
assert!(job.running);
assert_eq!(job.kind, JobKind::Preview);
// Queue should now be empty (job is running)
let job2 = pull_job(&db).await.unwrap();
assert!(job2.is_none());
}
#[tokio::test]
async fn test_queue_stats() {
let db = LocalDb::in_memory().await.unwrap();
// Initially empty
let stats = queue_stats(&db).await.unwrap();
assert_eq!(stats.total, 0);
// Push two jobs
let req = PreviewRequest {
content: "test".to_string(),
language: ScriptLang::Deno,
args: serde_json::json!({}),
lock: None,
tag: None,
};
push_preview(&db, req.clone()).await.unwrap();
push_preview(&db, req).await.unwrap();
let stats = queue_stats(&db).await.unwrap();
assert_eq!(stats.total, 2);
assert_eq!(stats.pending, 2);
assert_eq!(stats.running, 0);
// Pull one
pull_job(&db).await.unwrap();
let stats = queue_stats(&db).await.unwrap();
assert_eq!(stats.total, 2);
assert_eq!(stats.pending, 1);
assert_eq!(stats.running, 1);
}
}

View File

@@ -0,0 +1,218 @@
//! SQLite schema for local mode
//!
//! This is a minimal schema supporting preview job execution.
//! Key differences from PostgreSQL:
//! - ENUMs are TEXT with CHECK constraints
//! - JSONB is JSON (stored as TEXT in SQLite)
//! - Arrays are JSON arrays
//! - No FOR UPDATE SKIP LOCKED (single worker, in-process coordination)
/// SQL to create the minimal schema for local mode preview execution
pub const SCHEMA: &str = r#"
-- Job kinds (equivalent to PostgreSQL ENUM)
-- Values: script, preview, flow, flowpreview, dependencies, flowdependencies,
-- script_hub, identity, http, graphql, postgresql, noop, appdependencies,
-- deploymentcallback, singlescriptflow, flowscript, flownode, appscript
-- Job status (equivalent to PostgreSQL ENUM)
-- Values: success, failure, canceled, skipped
-- Script languages (equivalent to PostgreSQL ENUM)
-- Values: python3, deno, go, bash, postgresql, nativets, bun, mysql, bigquery,
-- snowflake, graphql, powershell, mssql, php, bunnative, rust, ansible,
-- csharp, oracledb, nu, java, duckdb
-- Main job table (minimal for preview)
CREATE TABLE IF NOT EXISTS v2_job (
id TEXT PRIMARY KEY, -- UUID as TEXT
workspace_id TEXT NOT NULL DEFAULT 'local',
-- Raw code for preview jobs
raw_code TEXT,
raw_lock TEXT,
raw_flow TEXT, -- JSON for flow definitions
-- Job metadata
tag TEXT DEFAULT 'deno',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
created_by TEXT NOT NULL DEFAULT 'local_user',
-- Permission context (simplified for local mode)
permissioned_as TEXT NOT NULL DEFAULT 'u/local_user',
permissioned_as_email TEXT DEFAULT 'local@windmill.local',
-- Job type info
kind TEXT NOT NULL DEFAULT 'preview' CHECK (kind IN (
'script', 'preview', 'flow', 'flowpreview', 'dependencies',
'flowdependencies', 'script_hub', 'identity', 'http', 'graphql',
'postgresql', 'noop', 'appdependencies', 'deploymentcallback',
'singlescriptflow', 'flowscript', 'flownode', 'appscript'
)),
-- Script execution details
script_lang TEXT CHECK (script_lang IN (
'python3', 'deno', 'go', 'bash', 'postgresql', 'nativets', 'bun',
'mysql', 'bigquery', 'snowflake', 'graphql', 'powershell', 'mssql',
'php', 'bunnative', 'rust', 'ansible', 'csharp', 'oracledb', 'nu',
'java', 'duckdb'
)),
-- Flow execution details
parent_job TEXT, -- UUID reference
root_job TEXT, -- UUID reference
flow_step INTEGER,
flow_step_id TEXT,
flow_innermost_root_job TEXT,
-- Execution settings
timeout INTEGER,
priority INTEGER DEFAULT 0,
same_worker INTEGER DEFAULT 0, -- BOOLEAN as INTEGER
visible_to_owner INTEGER DEFAULT 1,
-- Arguments (JSON)
args TEXT, -- JSON object
-- Pre-run error if validation failed
pre_run_error TEXT
);
-- Job queue table
CREATE TABLE IF NOT EXISTS v2_job_queue (
id TEXT PRIMARY KEY, -- UUID, references v2_job.id
workspace_id TEXT NOT NULL DEFAULT 'local',
-- Timestamps
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
started_at TEXT,
scheduled_for TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
-- Queue state
running INTEGER NOT NULL DEFAULT 0, -- BOOLEAN
canceled_by TEXT,
canceled_reason TEXT,
-- Suspend state (for approval flows)
suspend INTEGER DEFAULT 0,
suspend_until TEXT,
-- Execution settings
tag TEXT DEFAULT 'deno',
priority INTEGER DEFAULT 0,
worker TEXT,
FOREIGN KEY (id) REFERENCES v2_job(id)
);
-- Index for queue ordering (simulates queue_sort_v2)
CREATE INDEX IF NOT EXISTS idx_queue_sort ON v2_job_queue (
priority DESC, scheduled_for ASC, tag
) WHERE running = 0;
-- Job runtime tracking (heartbeat/ping)
CREATE TABLE IF NOT EXISTS v2_job_runtime (
id TEXT PRIMARY KEY, -- UUID, references v2_job.id
ping TEXT, -- Timestamp
memory_peak INTEGER,
FOREIGN KEY (id) REFERENCES v2_job(id)
);
-- Completed jobs with results
CREATE TABLE IF NOT EXISTS v2_job_completed (
id TEXT PRIMARY KEY, -- UUID, references v2_job.id
workspace_id TEXT NOT NULL DEFAULT 'local',
-- Timing
started_at TEXT,
completed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
duration_ms INTEGER,
-- Result
result TEXT, -- JSON
result_columns TEXT, -- JSON array of column names
-- Status
status TEXT NOT NULL DEFAULT 'success' CHECK (status IN (
'success', 'failure', 'canceled', 'skipped'
)),
-- Cancellation details
canceled_by TEXT,
canceled_reason TEXT,
-- Flow status (for flow jobs)
flow_status TEXT, -- JSON
-- Execution details
memory_peak INTEGER,
worker TEXT,
deleted INTEGER DEFAULT 0, -- BOOLEAN
FOREIGN KEY (id) REFERENCES v2_job(id)
);
-- Index for completed job lookup by workspace and time
CREATE INDEX IF NOT EXISTS idx_completed_workspace_time ON v2_job_completed (
workspace_id, completed_at DESC
);
-- Flow status tracking (separate from completed to allow updates during execution)
CREATE TABLE IF NOT EXISTS v2_job_status (
id TEXT PRIMARY KEY, -- UUID, references v2_job.id
flow_status TEXT, -- JSON object tracking flow module execution
flow_leaf_jobs TEXT, -- JSON object
workflow_as_code_status TEXT, -- JSON object
FOREIGN KEY (id) REFERENCES v2_job(id)
);
-- Simplified job permissions (for local mode, mostly unused)
CREATE TABLE IF NOT EXISTS job_perms (
job_id TEXT PRIMARY KEY,
email TEXT DEFAULT 'local@windmill.local',
username TEXT DEFAULT 'local_user',
is_admin INTEGER DEFAULT 1, -- BOOLEAN
is_operator INTEGER DEFAULT 0, -- BOOLEAN
workspace_id TEXT DEFAULT 'local',
groups TEXT, -- JSON array
folders TEXT, -- JSON array of objects
FOREIGN KEY (job_id) REFERENCES v2_job(id)
);
-- Simple audit log (optional for local mode)
CREATE TABLE IF NOT EXISTS audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workspace_id TEXT DEFAULT 'local',
timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
username TEXT DEFAULT 'local_user',
operation TEXT NOT NULL,
action_kind TEXT CHECK (action_kind IN ('create', 'update', 'delete', 'execute')),
resource TEXT,
parameters TEXT -- JSON
);
-- Job logs storage
CREATE TABLE IF NOT EXISTS job_logs (
job_id TEXT PRIMARY KEY,
workspace_id TEXT DEFAULT 'local',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
logs TEXT,
log_offset INTEGER DEFAULT 0,
FOREIGN KEY (job_id) REFERENCES v2_job(id)
);
"#;
/// SQL to drop all tables (for testing/reset)
pub const DROP_SCHEMA: &str = r#"
DROP TABLE IF EXISTS job_logs;
DROP TABLE IF EXISTS audit;
DROP TABLE IF EXISTS job_perms;
DROP TABLE IF EXISTS v2_job_status;
DROP TABLE IF EXISTS v2_job_completed;
DROP TABLE IF EXISTS v2_job_runtime;
DROP TABLE IF EXISTS v2_job_queue;
DROP TABLE IF EXISTS v2_job;
"#;

View File

@@ -0,0 +1,419 @@
//! HTTP server for local mode
//!
//! Provides a minimal API compatible with Windmill's preview endpoints.
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use tokio::sync::watch;
use tower_http::cors::{Any, CorsLayer};
use tower_http::trace::TraceLayer;
use uuid::Uuid;
use crate::db::LocalDb;
use crate::error::Result;
use crate::jobs::{
get_completed_job, push_flow_preview, push_preview, FlowPreviewRequest,
JobStatus, PreviewRequest, ScriptLang,
};
use crate::worker::Worker;
/// Application state shared across handlers
pub struct AppState {
pub db: Arc<LocalDb>,
}
/// Local server that runs the API and embedded worker
pub struct LocalServer {
db: Arc<LocalDb>,
addr: SocketAddr,
}
impl LocalServer {
/// Create a new local server
pub async fn new(addr: SocketAddr) -> Result<Self> {
let db = Arc::new(LocalDb::in_memory().await?);
Ok(Self { db, addr })
}
/// Create a local server with a file-based database
pub async fn with_file(addr: SocketAddr, db_path: &str) -> Result<Self> {
let db = Arc::new(LocalDb::file(db_path).await?);
Ok(Self { db, addr })
}
/// Create a local server connected to a remote Turso database
pub async fn with_turso(addr: SocketAddr, url: &str, auth_token: &str) -> Result<Self> {
let db = Arc::new(LocalDb::turso_remote(url, auth_token).await?);
Ok(Self { db, addr })
}
/// Run the server
pub async fn run(self) -> Result<()> {
let (shutdown_tx, shutdown_rx) = watch::channel(false);
// Start the embedded worker
let worker_db = self.db.clone();
let worker_handle = tokio::spawn(async move {
let mut worker = Worker::new(worker_db, shutdown_rx);
if let Err(e) = worker.run().await {
tracing::error!("Worker error: {}", e);
}
});
// Build the router
let state = Arc::new(AppState { db: self.db });
let app = create_router(state);
// Run the server
tracing::info!("Local server listening on {}", self.addr);
let listener = tokio::net::TcpListener::bind(self.addr).await.unwrap();
// Handle graceful shutdown
let shutdown_signal = async move {
tokio::signal::ctrl_c()
.await
.expect("Failed to install CTRL+C signal handler");
tracing::info!("Shutdown signal received");
shutdown_tx.send(true).ok();
};
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal)
.await
.unwrap();
// Wait for worker to finish
worker_handle.await.ok();
Ok(())
}
}
/// Create the API router
fn create_router(state: Arc<AppState>) -> Router {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
Router::new()
// Health check
.route("/health", get(health_check))
// Preview endpoints (mimics windmill-api)
.route("/api/w/:workspace/jobs/run/preview", post(run_preview))
.route(
"/api/w/:workspace/jobs/run_wait_result/preview",
post(run_wait_result_preview),
)
.route(
"/api/w/:workspace/jobs/run/preview_flow",
post(run_preview_flow),
)
.route(
"/api/w/:workspace/jobs/run_wait_result/preview_flow",
post(run_wait_result_preview_flow),
)
// Get job result
.route(
"/api/w/:workspace/jobs_u/completed/get_result/:job_id",
get(get_job_result),
)
.layer(TraceLayer::new_for_http())
.layer(cors)
.with_state(state)
}
// === Request/Response Types ===
#[derive(Debug, Deserialize)]
struct PreviewPayload {
content: String,
language: String,
#[serde(default)]
args: serde_json::Value,
lock: Option<String>,
tag: Option<String>,
}
#[derive(Debug, Deserialize)]
struct FlowPreviewPayload {
value: serde_json::Value,
#[serde(default)]
args: serde_json::Value,
tag: Option<String>,
}
#[derive(Debug, Serialize)]
struct JobCreatedResponse {
job_id: String,
}
#[derive(Debug, Serialize)]
struct ErrorResponse {
error: String,
}
// === Handlers ===
async fn health_check() -> &'static str {
"OK"
}
/// Run a preview job (async - returns job ID)
async fn run_preview(
State(state): State<Arc<AppState>>,
Path(_workspace): Path<String>,
Json(payload): Json<PreviewPayload>,
) -> impl IntoResponse {
let lang = match ScriptLang::from_str(&payload.language) {
Some(l) => l,
None => {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("Unknown language: {}", payload.language),
}),
)
.into_response();
}
};
let req = PreviewRequest {
content: payload.content,
language: lang,
args: payload.args,
lock: payload.lock,
tag: payload.tag,
};
match push_preview(&state.db, req).await {
Ok(job_id) => (
StatusCode::CREATED,
Json(JobCreatedResponse {
job_id: job_id.to_string(),
}),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: e.to_string(),
}),
)
.into_response(),
}
}
/// Run a preview job and wait for result
async fn run_wait_result_preview(
State(state): State<Arc<AppState>>,
Path(_workspace): Path<String>,
Json(payload): Json<PreviewPayload>,
) -> impl IntoResponse {
let lang = match ScriptLang::from_str(&payload.language) {
Some(l) => l,
None => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": format!("Unknown language: {}", payload.language)})),
)
.into_response();
}
};
let req = PreviewRequest {
content: payload.content,
language: lang,
args: payload.args,
lock: payload.lock,
tag: payload.tag,
};
let job_id = match push_preview(&state.db, req).await {
Ok(id) => id,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
)
.into_response();
}
};
// Poll for result with timeout
wait_for_result(&state.db, job_id, Duration::from_secs(60)).await
}
/// Run a flow preview job (async - returns job ID)
async fn run_preview_flow(
State(state): State<Arc<AppState>>,
Path(_workspace): Path<String>,
Json(payload): Json<FlowPreviewPayload>,
) -> impl IntoResponse {
let req = FlowPreviewRequest {
value: payload.value,
args: payload.args,
tag: payload.tag,
};
match push_flow_preview(&state.db, req).await {
Ok(job_id) => (
StatusCode::CREATED,
Json(JobCreatedResponse {
job_id: job_id.to_string(),
}),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: e.to_string(),
}),
)
.into_response(),
}
}
/// Run a flow preview job and wait for result
async fn run_wait_result_preview_flow(
State(state): State<Arc<AppState>>,
Path(_workspace): Path<String>,
Json(payload): Json<FlowPreviewPayload>,
) -> impl IntoResponse {
let req = FlowPreviewRequest {
value: payload.value,
args: payload.args,
tag: payload.tag,
};
let job_id = match push_flow_preview(&state.db, req).await {
Ok(id) => id,
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
)
.into_response();
}
};
// Poll for result with timeout
wait_for_result(&state.db, job_id, Duration::from_secs(120)).await
}
/// Get the result of a completed job
async fn get_job_result(
State(state): State<Arc<AppState>>,
Path((_workspace, job_id)): Path<(String, String)>,
) -> impl IntoResponse {
let job_id = match Uuid::parse_str(&job_id) {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid job ID"})),
)
.into_response();
}
};
match get_completed_job(&state.db, job_id).await {
Ok(Some(job)) => {
if job.status == JobStatus::Success {
(StatusCode::OK, Json(job.result)).into_response()
} else {
(StatusCode::INTERNAL_SERVER_ERROR, Json(job.result)).into_response()
}
}
Ok(None) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Job not found or not completed"})),
)
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
)
.into_response(),
}
}
/// Poll for job completion with timeout
async fn wait_for_result(
db: &LocalDb,
job_id: Uuid,
timeout: Duration,
) -> axum::response::Response {
let start = std::time::Instant::now();
let fast_poll_duration = Duration::from_secs(2);
let fast_poll_interval = Duration::from_millis(50);
let slow_poll_interval = Duration::from_millis(200);
loop {
if start.elapsed() > timeout {
return (
StatusCode::REQUEST_TIMEOUT,
Json(serde_json::json!({"error": "Timeout waiting for job result"})),
)
.into_response();
}
match get_completed_job(db, job_id).await {
Ok(Some(job)) => {
if job.status == JobStatus::Success {
return (StatusCode::OK, Json(job.result)).into_response();
} else {
return (StatusCode::INTERNAL_SERVER_ERROR, Json(job.result)).into_response();
}
}
Ok(None) => {
// Job not completed yet, keep polling
let interval = if start.elapsed() < fast_poll_duration {
fast_poll_interval
} else {
slow_poll_interval
};
tokio::time::sleep(interval).await;
}
Err(e) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
)
.into_response();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
#[tokio::test]
async fn test_health_check() {
let db = Arc::new(LocalDb::in_memory().await.unwrap());
let state = Arc::new(AppState { db });
let app = create_router(state);
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}

View File

@@ -0,0 +1,204 @@
//! Worker for local mode
//!
//! A single embedded worker that pulls jobs from the queue and executes them.
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::watch;
use chrono::Utc;
use crate::db::LocalDb;
use crate::error::Result;
use crate::executor::{execute_script, ExecutionResult};
use crate::flow_executor;
use crate::jobs::{complete_job, JobKind, JobStatus, QueuedJob};
use crate::queue::pull_job;
use windmill_common::flows::FlowValue;
/// Worker that processes jobs from the queue
pub struct Worker {
db: Arc<LocalDb>,
/// Channel to signal shutdown
shutdown_rx: watch::Receiver<bool>,
}
impl Worker {
/// Create a new worker
pub fn new(db: Arc<LocalDb>, shutdown_rx: watch::Receiver<bool>) -> Self {
Self { db, shutdown_rx }
}
/// Run the worker loop
pub async fn run(&mut self) -> Result<()> {
tracing::info!("Worker started");
loop {
// Check for shutdown signal
if *self.shutdown_rx.borrow() {
tracing::info!("Worker received shutdown signal");
break;
}
// Try to pull a job
match pull_job(&self.db).await {
Ok(Some(job)) => {
tracing::info!("Processing job: {} (kind: {:?})", job.id, job.kind);
if let Err(e) = self.process_job(job).await {
tracing::error!("Error processing job: {}", e);
}
}
Ok(None) => {
// No jobs available, wait a bit before polling again
tokio::select! {
_ = tokio::time::sleep(Duration::from_millis(100)) => {}
_ = self.shutdown_rx.changed() => {}
}
}
Err(e) => {
tracing::error!("Error pulling job: {}", e);
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
tracing::info!("Worker stopped");
Ok(())
}
/// Process a single job
async fn process_job(&self, job: QueuedJob) -> Result<()> {
let started_at = Utc::now();
match job.kind {
JobKind::Preview => {
self.process_preview_job(job, started_at).await
}
JobKind::FlowPreview => {
self.process_flow_preview_job(job, started_at).await
}
_ => {
// Unsupported job kind
let error_result = serde_json::json!({
"error": format!("Unsupported job kind in local mode: {:?}", job.kind)
});
complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await
}
}
}
/// Process a script preview job
async fn process_preview_job(&self, job: QueuedJob, started_at: chrono::DateTime<Utc>) -> Result<()> {
let Some(code) = &job.raw_code else {
let error_result = serde_json::json!({"error": "No code provided for preview"});
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
};
let Some(lang) = job.script_lang else {
let error_result = serde_json::json!({"error": "No language specified for preview"});
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
};
// Execute the script
let exec_result = execute_script(lang, code, &job.args).await;
match exec_result {
Ok(ExecutionResult { success, result, logs }) => {
tracing::debug!("Job {} logs:\n{}", job.id, logs);
let status = if success { JobStatus::Success } else { JobStatus::Failure };
complete_job(&self.db, job.id, status, result, started_at).await
}
Err(e) => {
let error_result = serde_json::json!({"error": e.to_string()});
complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await
}
}
}
/// Process a flow preview job using the full flow executor
async fn process_flow_preview_job(&self, job: QueuedJob, started_at: chrono::DateTime<Utc>) -> Result<()> {
let Some(flow_json) = &job.raw_flow else {
let error_result = serde_json::json!({"error": "No flow definition provided"});
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
};
// Parse the flow value using windmill-common types
let flow_value: FlowValue = match serde_json::from_value(flow_json.clone()) {
Ok(fv) => fv,
Err(e) => {
let error_result = serde_json::json!({"error": format!("Failed to parse flow: {}", e)});
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
}
};
if flow_value.modules.is_empty() {
let error_result = serde_json::json!({"error": "Flow has no modules"});
return complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await;
}
tracing::info!("Executing flow with {} modules", flow_value.modules.len());
// Execute the flow using the full flow executor
match flow_executor::execute_flow(&self.db, &flow_value, job.args.clone()).await {
Ok((result, status)) => {
let is_failure = status.failure_module.is_some();
let final_result = serde_json::json!({
"result": result,
"flow_status": status
});
let job_status = if is_failure {
JobStatus::Failure
} else {
JobStatus::Success
};
complete_job(&self.db, job.id, job_status, final_result, started_at).await
}
Err(e) => {
let error_result = serde_json::json!({"error": e.to_string()});
complete_job(&self.db, job.id, JobStatus::Failure, error_result, started_at).await
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jobs::{get_completed_job, push_preview, PreviewRequest, ScriptLang};
#[tokio::test]
async fn test_worker_processes_bash_preview() {
let db = Arc::new(LocalDb::in_memory().await.unwrap());
let (shutdown_tx, shutdown_rx) = watch::channel(false);
// Push a bash preview job
let req = PreviewRequest {
content: "echo 42".to_string(),
language: ScriptLang::Bash,
args: serde_json::json!({}),
lock: None,
tag: None,
};
let job_id = push_preview(&db, req).await.unwrap();
// Create and run worker for one iteration
let mut worker = Worker::new(db.clone(), shutdown_rx);
// Process one job then shutdown
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(500)).await;
shutdown_tx.send(true).unwrap();
});
worker.run().await.unwrap();
// Check the job completed
let completed = get_completed_job(&db, job_id).await.unwrap();
assert!(completed.is_some());
let completed = completed.unwrap();
assert_eq!(completed.status, JobStatus::Success);
// Output "42" is parsed as JSON number
assert_eq!(completed.result, serde_json::json!(42));
}
}

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.614.0";
export const VERSION = "v1.613.4";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -70,7 +70,7 @@ export {
// }
// });
export const VERSION = "1.614.0";
export const VERSION = "1.613.4";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";

View File

@@ -51,8 +51,6 @@ RUN windmill cache ${APP}/hubPaths.json
RUN rm ${APP}/hubPaths.json
RUN windmill cache-rt
# Create directories and make world-accessible for arbitrary UID support
RUN mkdir -p -m 777 /tmp/windmill/logs /tmp/windmill/search /tmp/.cache && \
chmod 777 /tmp/.cache && \

View File

@@ -51,8 +51,6 @@ RUN windmill cache ${APP}/hubPaths.json
RUN rm ${APP}/hubPaths.json
RUN windmill cache-rt
# Create directories and make world-accessible for arbitrary UID support
RUN mkdir -p -m 777 /tmp/windmill/logs /tmp/windmill/search /tmp/.cache && \
chmod 777 /tmp/.cache && \

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.614.0",
"version": "1.613.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.614.0",
"version": "1.613.4",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -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",
@@ -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"
}

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.614.0",
"version": "1.613.4",
"scripts": {
"dev": "vite dev",
"build": "vite build",

View File

@@ -4,8 +4,8 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.614.0"
wmill_pg = ">=1.614.0"
wmill = ">=1.613.4"
wmill_pg = ">=1.613.4"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.614.0
version: 1.613.4
title: OpenFlow Spec
contact:
name: Ruben Fiszel

View File

@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.614.0'
ModuleVersion = '1.613.4'
# Supported PSEditions
# CompatiblePSEditions = @()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.614.0"
version = "1.613.4"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill-pg"
version = "1.614.0"
version = "1.613.4"
description = "An extension client for the wmill client library focused on pg"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.614.0",
"version": "1.613.4",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]

View File

@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.614.0",
"version": "1.613.4",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"devDependencies": {

View File

@@ -1 +1 @@
1.614.0
1.613.4