Compare commits
2 Commits
v1.614.0
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
741526b7b8 | ||
|
|
73deef44ed |
98
.github/workflows/build-publish-rh-image.yml
vendored
98
.github/workflows/build-publish-rh-image.yml
vendored
@@ -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
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
657
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1549849fadc4e5634334a384bfe52343eb1e93f0
|
||||
0bfcfe263622f48c086331b628c1239d5cea0b37
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.614.0
|
||||
version: 1.613.4
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/");
|
||||
|
||||
|
||||
52
backend/windmill-local/Cargo.toml
Normal file
52
backend/windmill-local/Cargo.toml
Normal 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"
|
||||
65
backend/windmill-local/examples/local_server.rs
Normal file
65
backend/windmill-local/examples/local_server.rs
Normal 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");
|
||||
}
|
||||
161
backend/windmill-local/src/db.rs
Normal file
161
backend/windmill-local/src/db.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
29
backend/windmill-local/src/error.rs
Normal file
29
backend/windmill-local/src/error.rs
Normal 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>;
|
||||
330
backend/windmill-local/src/executor.rs
Normal file
330
backend/windmill-local/src/executor.rs
Normal 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"}));
|
||||
}
|
||||
}
|
||||
750
backend/windmill-local/src/flow_executor.rs
Normal file
750
backend/windmill-local/src/flow_executor.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
495
backend/windmill-local/src/jobs.rs
Normal file
495
backend/windmill-local/src/jobs.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
31
backend/windmill-local/src/lib.rs
Normal file
31
backend/windmill-local/src/lib.rs
Normal 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;
|
||||
296
backend/windmill-local/src/queue.rs
Normal file
296
backend/windmill-local/src/queue.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
218
backend/windmill-local/src/schema.rs
Normal file
218
backend/windmill-local/src/schema.rs
Normal 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;
|
||||
"#;
|
||||
419
backend/windmill-local/src/server.rs
Normal file
419
backend/windmill-local/src/server.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
204
backend/windmill-local/src/worker.rs
Normal file
204
backend/windmill-local/src/worker.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
174
frontend/package-lock.json
generated
174
frontend/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.614.0",
|
||||
"version": "1.613.4",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -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 = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.614.0
|
||||
version: 1.613.4
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.614.0'
|
||||
ModuleVersion = '1.613.4'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.614.0
|
||||
1.613.4
|
||||
|
||||
Reference in New Issue
Block a user