mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
d578e40101
* feat: add selfApproval option to WAC waitForApproval + inline approval buttons Add self-approval configuration to WAC workflows and inline approve/reject buttons in WorkflowTimeline. - TS SDK: add selfApproval option to waitForApproval() - Python SDK: add self_approval param to wait_for_approval() - Backend: store approval_conditions in flow_status for WAC, enforce self-approval checks on resume endpoints - Frontend: show Approve/Reject buttons in timeline with form support (EE), gated by user permissions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert sqlx query change + regenerate system prompts - Revert get_suspended_flow_info to use original sqlx::query_as! with COALESCE to avoid sqlx offline cache mismatch in CI - Detect WAC by checking if FlowStatus parsing fails + suspend > 0 - Re-fetch flow_status column separately for WAC approval conditions - Regenerate auto-generated system prompt files for SDK changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: use resume URLs for WAC inline approval buttons - Backend generates HMAC-signed resume/cancel URLs when creating WAC approval, stores them in timeline entry and approval meta - Frontend uses anonymous resume endpoint (like classic flows) with fallback to resumeSuspendedFlowAsOwner for admins - Buttons show for everyone when URLs are present; server-side self_approval_disabled check enforces restrictions - Show warning for admins/owners when self-approval is disabled - selfApproval: false requires EE (errors at dispatch on CE) - self_approval_disabled check moved outside user_auth_required gate so it works independently - WAC detection no longer requires task import Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add resume_suspended and approval_info endpoints - New approval_token DB table for token-based approval access - New POST /jobs_u/flow/resume_suspended/{job_id} endpoint: - OptAuthed: works with login or approval_token - Checks approval_conditions (self_approval, groups, auth) - Admins/owners bypass rules - New GET /jobs_u/flow/approval_info/{job_id} endpoint: - Returns form, rules, can_approve status - HMAC anonymous endpoint now bypasses all approval_conditions (secret = full capability) - getResumeUrls approvalPage URL now uses token format - WAC approval dispatch generates and stores approval tokens - Mark resumeSuspendedFlowAsOwner as legacy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: simplify frontend to use resume_suspended endpoint - OpenAPI spec updated with resume_suspended and approval_info endpoints - WorkflowTimeline: removed URL parsing, now calls single resumeSuspended endpoint for both approve and reject - Buttons show for any logged-in user viewing the job (backend enforces authorization rules) - Kept self-approval warning for admins Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: stateless approval tokens, new approval page, FlowStatusWaitingForEvents update - Replace DB-stored approval tokens with stateless HMAC derivation: token = HMAC(workspace_key, job_id + "approval_token") Verifiable without DB lookup, not reversible to resume secret - Drop approval_token migration (no DB table needed) - FlowStatusWaitingForEvents: use resumeSuspended endpoint instead of URL parsing + resumeSuspendedFlowAsOwner - New approval page route /approve/{ws}/{job}?token= that uses approval_info and resume_suspended endpoints - Old approval page route kept for back-compat Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: match old approval page content in new approval page - Add FlowMetadata, JobArgs, FlowGraphV2, DisplayResult - Add approvers with tooltips, flow arguments section - Add admin self-approval bypass warning - Add "Open run details" link - Fetch full job alongside approval_info for all UI data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: filter _MODULES from args, show 'workflow' for WAC approvals Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove deno template from approval/prompt SuspendDrawer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: approval page form display + hide deno from approval script picker - Fix form schema rendering on new approval page by wrapping flat WAC form schemas in { properties, order } for SchemaForm - Hide deno from the approval step language picker in flow editor Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove deno from canHaveApproval in script_helpers.ts The insert menu uses canHaveApproval() from script_helpers.ts via FlowInputsQuick, not the displayLang function in FlowInputs.svelte. Revert the unnecessary FlowInputs.svelte change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: return form schema and description in approval_info for classic flows The approval_info endpoint was returning None for form_schema on classic flows. Now fetches raw_flow to get suspend.resume_form schema, hide_cancel, and the step's completed result for description. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: inline Login component on approval page instead of redirect Show the Login component directly on the approval page when authentication is required. On successful login, reloads user and approval info without navigating away. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: show resume buttons for all users, not just owners The resume_suspended endpoint handles authorization server-side, so the frontend should always show the buttons. Remove isOwner gate and the "cannot resume" message. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prevent layout shift on resume by removing spinner from cancel button Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prevent resume button expansion by using disabled instead of loading The loading prop adds a Loader2 spinner that expands the button width. Use disabled={loading} instead to prevent layout shift. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: approval page login redirects back with full page reload Set rd to the full URL (starts with http) so Login.redirectUser() uses window.location.href instead of goto(), triggering a full page reload after login. This ensures the approval page re-fetches data as an authenticated user. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: fetch flow definition from flow_version when raw_flow is null Deployed flows don't store raw_flow on the job. Fall back to flow_version table using runnable_id to get suspend settings (form schema, hide_cancel) for the approval_info endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show specific reasons when user cannot approve Display whether denial is due to self-approval being disabled, required group membership, or both. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: support both nested and flat form schema in waitForApproval Users can now pass either: waitForApproval({ form: { schema: { name: { type: "string" } } } }) or: waitForApproval({ form: { name: { type: "string" } } }) Both WorkflowTimeline and approval page handle both formats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: convert sqlx query macros to non-macro for CI offline cache Replace sqlx::query! and sqlx::query_scalar! with sqlx::query and sqlx::query_as to avoid SQLX_OFFLINE cache misses in CI. Also remove unused LogIn import from approval page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: suppress dead code warning + unused isOwner variable - Add #[allow(dead_code)] to without_flow method (CI -D warnings) - Rename isOwner to _isOwner in FlowStatusWaitingForEvents (unused) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: security and robustness fixes from PR review - Add workspace_id verification in resume_suspended to prevent cross-workspace approval (#3) - Fix token leakage: use relative path for login redirect instead of full URL with token (#4) - Handle getJob failure independently from approval_info so the page works for unauthenticated users (#7) - Clear error state on successful data load (#13) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address review feedback — shared token gen, rand resume_id, UX - Move generate_approval_token to windmill-common::variables (shared between windmill-api and windmill-worker, eliminates duplicate HMAC) - Use rand::random::<u32>() for resume_id instead of DefaultHasher - Stop polling after approve/reject on approval page - Add cancelLoading state to WorkflowTimeline Reject button Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1685 lines
48 KiB
TypeScript
1685 lines
48 KiB
TypeScript
import { type Script } from './gen'
|
|
|
|
import type { SupportedLanguage } from './common'
|
|
|
|
import CLAUDE_SANDBOX_INIT_CODE from './templates/claude_sandbox.ts.template?raw'
|
|
import WAC_PYTHON_INIT_CODE from './templates/wac_python.py.template?raw'
|
|
import WAC_TYPESCRIPT_INIT_CODE from './templates/wac_typescript.ts.template?raw'
|
|
|
|
const PYTHON_FAILURE_MODULE_CODE = `import os
|
|
|
|
def main(message: str, name: str, step_id: str):
|
|
flow_id = os.environ.get("WM_ROOT_FLOW_JOB_ID")
|
|
print("message", message)
|
|
print("name", name)
|
|
print("step_id", step_id)
|
|
return { "message": message, "flow_id": flow_id, "step_id": step_id, "recover": False }`
|
|
|
|
const PYTHON_INIT_CODE_CLEAR = `# import wmill
|
|
|
|
|
|
def main(x: str):
|
|
return x`
|
|
|
|
const PYTHON_INIT_CODE_TRIGGER = `import wmill
|
|
|
|
|
|
def main():
|
|
# A common trigger script would follow this pattern:
|
|
# 1. Get the last saved state
|
|
# state = wmill.get_state()
|
|
# 2. Get the actual state from the external service
|
|
# newState = ...
|
|
# 3. Compare the two states and update the internal state
|
|
# wmill.setState(newState)
|
|
# 4. Return the new rows
|
|
# return range from (state to newState)
|
|
#
|
|
# For more complex states, consider using Data Tables:
|
|
# https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables
|
|
return [1, 2, 3]`
|
|
|
|
const PYTHON_INIT_CODE = `import os
|
|
import wmill
|
|
|
|
# You can import any PyPi package.
|
|
# See here for more info: https://www.windmill.dev/docs/advanced/dependencies_in_python
|
|
|
|
# you can use typed resources by doing a type alias to dict
|
|
#postgresql = dict
|
|
|
|
def main(
|
|
no_default: str,
|
|
#db: postgresql,
|
|
name="Nicolas Bourbaki",
|
|
age=42,
|
|
obj: dict = {"even": "dicts"},
|
|
l: list = ["or", "lists!"],
|
|
file_: bytes = bytes(0),
|
|
):
|
|
|
|
print(f"Hello World and a warm welcome especially to {name}")
|
|
print("and its acolytes..", age, obj, l, len(file_))
|
|
|
|
# retrieve variables, resources, states using the wmill client
|
|
try:
|
|
secret = wmill.get_variable("f/examples/secret")
|
|
except:
|
|
secret = "No secret yet at f/examples/secret !"
|
|
print(f"The variable at \`f/examples/secret\`: {secret}")
|
|
|
|
# Get last state of this script execution by the same trigger/user
|
|
last_state = wmill.get_state()
|
|
new_state = {"foo": 42} if last_state is None else last_state
|
|
new_state["foo"] += 1
|
|
wmill.set_state(new_state)
|
|
|
|
# fetch context variables
|
|
user = os.environ.get("WM_USERNAME")
|
|
|
|
# return value is converted to JSON
|
|
return {"splitted": name.split(), "user": user, "state": new_state}`
|
|
|
|
const NATIVETS_INIT_CODE = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
|
|
//import * as wmill from './windmill.ts'
|
|
|
|
export async function main(example_input: number = 3) {
|
|
// "3" is the default value of example_input, it can be overriden with code or using the UI
|
|
const res = await fetch(\`https://jsonplaceholder.typicode.com/todos/\${example_input}\`, {
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
return res.json();
|
|
}
|
|
`
|
|
|
|
const BUNNATIVE_INIT_CODE = `//native
|
|
//you can add proxy support using //proxy http(s)://host:port
|
|
|
|
// native scripts are bun scripts that are executed on native workers and can be parallelized
|
|
// only fetch is allowed, but imports will work as long as they also use only fetch and the standard lib
|
|
|
|
//import * as wmill from "windmill-client"
|
|
|
|
export async function main(example_input: number = 3) {
|
|
// "3" is the default value of example_input, it can be overriden with code or using the UI
|
|
const res = await fetch(\`https://jsonplaceholder.typicode.com/todos/\${example_input}\`, {
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
return res.json();
|
|
}
|
|
`
|
|
|
|
const NATIVETS_INIT_CODE_CLEAR = `// Fetch-only script, no imports allowed (except windmill) but benefits from a dedicated highly efficient runtime
|
|
//import * as wmill from './windmill.ts'
|
|
|
|
export async function main() {
|
|
const res = await fetch("https://jsonplaceholder.typicode.com/todos/1", {
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
return res.json();
|
|
}
|
|
`
|
|
|
|
const DENO_INIT_BLOCK = `// Ctrl/CMD+. to cache dependencies on imports hover.
|
|
|
|
// Deno uses "npm:" prefix to import from npm (https://deno.land/manual@v1.36.3/node/npm_specifiers)
|
|
// import * as wmill from "npm:windmill-client@${__pkg__.version}"
|
|
|
|
// fill the type, or use the +Resource type to get a type-safe reference to a resource
|
|
// type Postgresql = object`
|
|
|
|
const DENO_INIT_CODE =
|
|
DENO_INIT_BLOCK +
|
|
`
|
|
|
|
export async function main(
|
|
a: number,
|
|
b: "my" | "enum",
|
|
//c: Postgresql,
|
|
d = "inferred type string from default arg",
|
|
e = { nested: "object" },
|
|
//e: wmill.Base64
|
|
) {
|
|
// let x = await wmill.getVariable('u/user/foo')
|
|
return { foo: a };
|
|
}
|
|
`
|
|
|
|
const BUN_INIT_BLOCK = `// there are multiple modes to add as header: //nobundling //native //npm //nodejs
|
|
// https://www.windmill.dev/docs/getting_started/scripts_quickstart/typescript#modes
|
|
|
|
// import { toWords } from "number-to-words@1"
|
|
import * as wmill from "windmill-client"
|
|
|
|
// fill the type, or use the +Resource type to get a type-safe reference to a resource
|
|
// type Postgresql = object`
|
|
|
|
const BUN_INIT_CODE =
|
|
BUN_INIT_BLOCK +
|
|
`
|
|
|
|
|
|
export async function main(
|
|
a: number,
|
|
b: "my" | "enum",
|
|
//c: Postgresql,
|
|
//d: wmill.S3Object, // https://www.windmill.dev/docs/core_concepts/persistent_storage/large_data_files
|
|
//d: DynSelect_foo, // https://www.windmill.dev/docs/core_concepts/json_schema_and_parsing#dynamic-select
|
|
e = "inferred type string from default arg",
|
|
f = { nested: "object" },
|
|
g: {
|
|
label: "Variant 1",
|
|
foo: string
|
|
} | {
|
|
label: "Variant 2",
|
|
bar: number
|
|
}
|
|
) {
|
|
// let x = await wmill.getVariable('u/user/foo')
|
|
return { foo: a };
|
|
}
|
|
`
|
|
|
|
const GO_INIT_CODE = `package inner
|
|
|
|
import (
|
|
"fmt"
|
|
"rsc.io/quote"
|
|
// wmill "github.com/windmill-labs/windmill-go-client"
|
|
)
|
|
|
|
// Pin dependencies partially in go.mod with a comment starting with "//require":
|
|
//require rsc.io/quote v1.5.1
|
|
|
|
// the main must return (interface{}, error)
|
|
|
|
func main(x string, nested struct {
|
|
Foo string \`json:"foo"\`
|
|
}) (interface{}, error) {
|
|
fmt.Println("Hello, World")
|
|
fmt.Println(nested.Foo)
|
|
fmt.Println(quote.Opt())
|
|
// v, _ := wmill.GetVariable("f/examples/secret")
|
|
return x, nil
|
|
}
|
|
`
|
|
|
|
const GO_FAILURE_MODULE_CODE = `package inner
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// connect the error parameter to 'previous_result.error'
|
|
|
|
func main(message string, name string) (interface{}, error) {
|
|
fmt.Println(message)
|
|
fmt.Println(name)
|
|
fmt.Println("flow id that failed", os.Getenv("WM_FLOW_JOB_ID"))
|
|
return message, nil
|
|
}
|
|
`
|
|
|
|
const DENO_INIT_CODE_CLEAR = `// import * as wmill from "npm:windmill-client@${__pkg__.version}"
|
|
|
|
export async function main(x: string) {
|
|
return x
|
|
}
|
|
`
|
|
|
|
const BUN_INIT_CODE_CLEAR = `// import * as wmill from "windmill-client"
|
|
|
|
export async function main(x: string) {
|
|
return x
|
|
}
|
|
`
|
|
|
|
const DENO_FAILURE_MODULE_CODE = `
|
|
export async function main(message: string, name: string, step_id: string) {
|
|
const flow_id = Deno.env.get("WM_ROOT_FLOW_JOB_ID")
|
|
console.log("message", message)
|
|
console.log("name",name)
|
|
console.log("step_id", step_id)
|
|
return { message, flow_id, step_id, recover: false }
|
|
}
|
|
`
|
|
|
|
const BUN_FAILURE_MODULE_CODE = `
|
|
export async function main(message: string, name: string, step_id: string) {
|
|
const flow_id = process.env.WM_ROOT_FLOW_JOB_ID
|
|
console.log("message", message)
|
|
console.log("name",name)
|
|
console.log("step_id", step_id)
|
|
return { message, flow_id, step_id, recover: false }
|
|
}
|
|
`
|
|
|
|
const POSTGRES_INIT_CODE = `-- result_collection=last_statement_all_rows
|
|
-- to pin the database use '-- database f/your/path'
|
|
-- to stream a large query result to your workspace storage use '-- s3'
|
|
-- $1 name1 = default arg
|
|
-- $2 name2
|
|
-- $3 name3
|
|
-- $4 name4
|
|
INSERT INTO demo VALUES (\$1::TEXT, \$2::INT, \$3::TEXT[]) RETURNING *;
|
|
UPDATE demo SET col2 = \$4::INT WHERE col2 = \$2::INT;
|
|
`
|
|
|
|
const MYSQL_INIT_CODE = `-- result_collection=last_statement_all_rows
|
|
-- to pin the database use '-- database f/your/path'
|
|
-- to stream a large query result to your workspace storage use '-- s3'
|
|
-- :name1 (text) = default arg
|
|
-- :name2 (int)
|
|
-- :name3 (int)
|
|
INSERT INTO demo VALUES (:name1, :name2);
|
|
UPDATE demo SET col2 = :name3 WHERE col2 = :name2;
|
|
`
|
|
|
|
const BIGQUERY_INIT_CODE = `-- result_collection=last_statement_all_rows
|
|
-- to pin the database use '-- database f/your/path'
|
|
-- to stream a large query result to your workspace storage use '-- s3'
|
|
-- @name1 (string) = default arg
|
|
-- @name2 (integer)
|
|
-- @name3 (string[])
|
|
-- @name4 (integer)
|
|
INSERT INTO \`demodb.demo\` VALUES (@name1, @name2, @name3);
|
|
UPDATE \`demodb.demo\` SET col2 = @name4 WHERE col2 = @name2;
|
|
`
|
|
|
|
const ORACLEDB_INIT_CODE = `-- result_collection=last_statement_all_rows
|
|
-- to pin the database use '-- database f/your/path'
|
|
-- to stream a large query result to your workspace storage use '-- s3'
|
|
-- :name1 (text) = default arg
|
|
-- :name2 (int)
|
|
-- :name3 (int)
|
|
INSERT INTO demo VALUES (:name1, :name2);
|
|
UPDATE demo SET col2 = :name3 WHERE col2 = :name2;
|
|
`
|
|
|
|
const SNOWFLAKE_INIT_CODE = `-- result_collection=last_statement_all_rows
|
|
-- to pin the database use '-- database f/your/path'
|
|
-- to stream a large query result to your workspace storage use '-- s3'
|
|
-- ? name1 (varchar) = default arg
|
|
-- ? name2 (int)
|
|
INSERT INTO demo VALUES (?, ?);
|
|
-- ? name3 (int)
|
|
-- ? name2 (int)
|
|
UPDATE demo SET col2 = ? WHERE col2 = ?;
|
|
`
|
|
|
|
const MSSQL_INIT_CODE = `-- result_collection=last_statement_all_rows
|
|
-- to pin the database use '-- database f/your/path'
|
|
-- to stream a large query result to your workspace storage use '-- s3'
|
|
-- @P1 name1 (varchar) = default arg
|
|
-- @P2 name2 (int)
|
|
-- @P3 name3 (int)
|
|
INSERT INTO demo VALUES (@P1, @P2);
|
|
UPDATE demo SET col2 = @P3 WHERE col2 = @P2;
|
|
`
|
|
|
|
const DUCKDB_INIT_CODE = `-- result_collection=last_statement_all_rows
|
|
-- $name (text) = Ben
|
|
-- $age (text) = 20
|
|
-- -- $friends_csv (s3object)
|
|
|
|
-- Click the +Database button to connect to a database
|
|
-- https://www.windmill.dev/docs/getting_started/scripts_quickstart/sql#duckdb-1
|
|
--
|
|
-- ATTACH '$res:u/demo/amazed_postgresql' AS db (TYPE postgres);
|
|
-- SELECT * FROM db.public.friends;
|
|
|
|
-- Click the +Ducklake button to use a ducklake
|
|
-- https://www.windmill.dev/docs/core_concepts/persistent_storage/ducklake
|
|
--
|
|
-- ATTACH 'ducklake' AS dl;
|
|
-- USE dl;
|
|
-- SELECT * FROM customers;
|
|
|
|
CREATE TABLE friends (
|
|
name text,
|
|
age int
|
|
);
|
|
|
|
INSERT INTO friends VALUES ($name, $age);
|
|
-- INSERT INTO friends
|
|
-- SELECT name, age FROM read_csv($friends_csv);
|
|
|
|
SELECT * FROM friends;
|
|
`
|
|
|
|
const GRAPHQL_INIT_CODE = `query($name4: String, $name2: Int, $name3: [String]) {
|
|
demo(name1: $name1, name2: $name2, name3: $name3) {
|
|
name1,
|
|
name2,
|
|
name3
|
|
}
|
|
}
|
|
`
|
|
|
|
const PHP_INIT_CODE = `<?php
|
|
|
|
// remove the first // of the following lines to specify packages to install using composer
|
|
// // require:
|
|
// // monolog/monolog@3.6.0
|
|
// // stripe/stripe-php
|
|
|
|
function main(
|
|
// Postgresql $a,
|
|
// array $b,
|
|
// object $c,
|
|
int $d = 123,
|
|
string $e = "default value",
|
|
float $f = 3.5,
|
|
bool $g = true,
|
|
) {
|
|
return $d;
|
|
}
|
|
`
|
|
|
|
const RUST_INIT_CODE = `//! Add dependencies in the following partial Cargo.toml manifest
|
|
//!
|
|
//! \`\`\`cargo
|
|
//! [dependencies]
|
|
//! anyhow = "1.0.86"
|
|
//! rand = "0.7.2"
|
|
//! \`\`\`
|
|
//!
|
|
//! Note that serde is used by default with the \`derive\` feature.
|
|
//! You can still reimport it if you need additional features.
|
|
|
|
use anyhow::anyhow;
|
|
use rand::seq::SliceRandom;
|
|
use serde::Serialize;
|
|
|
|
#[derive(Serialize, Debug)]
|
|
struct Ret {
|
|
msg: String,
|
|
number: i8,
|
|
}
|
|
|
|
fn main(who_to_greet: String, numbers: Vec<i8>) -> anyhow::Result<Ret> {
|
|
println!(
|
|
"Person to greet: {} - numbers to choose: {:?}",
|
|
who_to_greet, numbers
|
|
);
|
|
Ok(Ret {
|
|
msg: format!("Greetings {}!", who_to_greet),
|
|
number: *numbers
|
|
.choose(&mut rand::thread_rng())
|
|
.ok_or(anyhow!("There should be some numbers to choose from"))?,
|
|
})
|
|
}
|
|
`
|
|
|
|
const CSHARP_INIT_CODE = `#r "nuget: Humanizer, 2.14.1"
|
|
|
|
using System;
|
|
using System.Linq;
|
|
using Humanizer;
|
|
|
|
|
|
class Script
|
|
{
|
|
public static int Main(string[] extraWords, string word = "clue", int highNumberThreshold = 50)
|
|
{
|
|
Console.WriteLine("Hello, World!");
|
|
|
|
Console.WriteLine("Your chosen words are pluralized here:");
|
|
|
|
string[] newWordArray = extraWords.Concat(new[] { word }).ToArray();
|
|
|
|
foreach (var s in newWordArray)
|
|
{
|
|
Console.WriteLine($" {s.Pluralize()}");
|
|
}
|
|
|
|
var random = new Random();
|
|
int randomNumber = random.Next(1, 101);
|
|
|
|
Console.WriteLine($"Random number: {randomNumber}");
|
|
|
|
string greeting = randomNumber > highNumberThreshold ? "High number!" : "Low number!";
|
|
greeting += " (according to the threshold parameter)";
|
|
Console.WriteLine(greeting);
|
|
// Humanize a timespan
|
|
var timespan = TimeSpan.FromMinutes(90);
|
|
Console.WriteLine($"Timespan: {timespan.Humanize()}");
|
|
|
|
// Humanize numbers into words
|
|
int number = 123;
|
|
Console.WriteLine($"Number: {number.ToWords()}");
|
|
|
|
// Pluralize words
|
|
string singular = "apple";
|
|
|
|
// Humanize date difference
|
|
var date = DateTime.UtcNow.AddDays(-3);
|
|
Console.WriteLine($"Date: {date.Humanize()}");
|
|
return 2;
|
|
}
|
|
}
|
|
`
|
|
|
|
const NU_INIT_CODE = `use std assert
|
|
|
|
# Nushell
|
|
# A new type of shell
|
|
def main [
|
|
no_default: string,
|
|
name = "Nicolas Bourbaki",
|
|
age: int = 42,
|
|
date_of_birth?: datetime,
|
|
obj: record = {"records": "included"},
|
|
l: list<string> = ["or", "lists!"],
|
|
tables?: table,
|
|
enable_kill_mode?: bool = true,
|
|
] {
|
|
# Test
|
|
# https://www.nushell.sh/book/testing.html
|
|
assert ($age == 42)
|
|
|
|
print $"Hello World and a warm welcome especially to ($name)"
|
|
print "and its acolytes.." $age $obj $l
|
|
print $tables
|
|
|
|
let secret = try {
|
|
get_variable f/examples/secret
|
|
} catch {
|
|
'No secret yet at f/examples/secret !'
|
|
};
|
|
|
|
print $"The variable at \`f/examples/secret\`: ($secret)"
|
|
# fetch context variables
|
|
let user = $env.WM_USERNAME
|
|
|
|
# Nu pipelines
|
|
ls | where size > 1kb | sort-by modified | print "ls:" $in
|
|
|
|
# Nu works with existing data
|
|
# Nu speaks JSON, YAML, SQLite, Excel, and more out of the box.
|
|
# It's easy to bring data into a Nu pipeline whether it's in a file, a database, or a web API:
|
|
let nu_license = http get https://api.github.com/repos/nushell/nushell | get license
|
|
|
|
return { splitted: ($name | split words), user: $user, nu_license: $nu_license}
|
|
# Interested in learning more?
|
|
# https://www.nushell.sh/book/getting_started.html
|
|
}
|
|
`
|
|
|
|
const FETCH_INIT_CODE = `export async function main(
|
|
url: string | undefined,
|
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' = 'GET',
|
|
body: Object = {},
|
|
headers: Record<string, string> = {}
|
|
): Promise<Response | null> {
|
|
if (!url) {
|
|
console.error('Error: URL is undefined')
|
|
return null
|
|
}
|
|
|
|
const requestOptions: RequestInit = {
|
|
method: method || 'GET',
|
|
headers: headers || {}
|
|
}
|
|
|
|
if (requestOptions.method !== 'GET' && requestOptions.method !== 'HEAD' && body !== undefined) {
|
|
requestOptions.body = JSON.stringify(body)
|
|
requestOptions.headers = {
|
|
'Content-Type': 'application/json',
|
|
...requestOptions.headers
|
|
}
|
|
}
|
|
|
|
return await fetch(url, requestOptions)
|
|
.then((res) => res.json())
|
|
.catch(() => {
|
|
throw new Error('An error occurred')
|
|
})
|
|
}`
|
|
|
|
const BASH_INIT_CODE = `# shellcheck shell=bash
|
|
# arguments of the form X="$I" are parsed as parameters X of type string
|
|
msg="$1"
|
|
dflt="\${2:-default value}"
|
|
|
|
# the last line of the stdout is the return value
|
|
# unless you write json to './result.json' or a string to './result.out'
|
|
echo "Hello $msg"
|
|
`
|
|
|
|
const DENO_INIT_CODE_TRIGGER = `import * as wmill from "npm:windmill-client@${__pkg__.version}"
|
|
|
|
export async function main() {
|
|
|
|
// A common trigger script would follow this pattern:
|
|
// 1. Get the last saved state
|
|
// const state = await wmill.getState()
|
|
// 2. Get the actual state from the external service
|
|
// const newState = await (await fetch('https://hacker-news.firebaseio.com/v0/topstories.json')).json()
|
|
// 3. Compare the two states and update the internal state
|
|
// await wmill.setState(newState)
|
|
// 4. Return the new rows
|
|
// return range from (state to newState)
|
|
//
|
|
// For more complex states, consider using Data Tables:
|
|
// https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables
|
|
|
|
return [1,2,3]
|
|
|
|
// In subsequent scripts, you may refer to each row/value returned by the trigger script using
|
|
// 'flow_input.iter.value'
|
|
}
|
|
`
|
|
|
|
const BUN_INIT_CODE_TRIGGER = `import * as wmill from "windmill-client"
|
|
|
|
export async function main() {
|
|
|
|
// A common trigger script would follow this pattern:
|
|
// 1. Get the last saved state
|
|
// const state = await wmill.getState()
|
|
// 2. Get the actual state from the external service
|
|
// const newState = await (await fetch('https://hacker-news.firebaseio.com/v0/topstories.json')).json()
|
|
// 3. Compare the two states and update the internal state
|
|
// await wmill.setState(newState)
|
|
// 4. Return the new rows
|
|
// return range from (state to newState)
|
|
//
|
|
// For more complex states, consider using Data Tables:
|
|
// https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables
|
|
|
|
return [1,2,3]
|
|
|
|
// In subsequent scripts, you may refer to each row/value returned by the trigger script using
|
|
// 'flow_input.iter.value'
|
|
}
|
|
`
|
|
|
|
const GO_INIT_CODE_TRIGGER = `package inner
|
|
|
|
import (
|
|
wmill "github.com/windmill-labs/windmill-go-client"
|
|
)
|
|
|
|
func main() (interface{}, error) {
|
|
|
|
// A common trigger script would follow this pattern:
|
|
// 1. Get the last saved state
|
|
state, _ := wmill.GetState()
|
|
// 2. Get the actual state from the external service
|
|
// newState := ...
|
|
// 3. Compare the two states and update the internal state
|
|
wmill.SetState(4)
|
|
// 4. Return the new rows
|
|
//
|
|
// For more complex states, consider using Data Tables:
|
|
// https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables
|
|
|
|
return state, nil
|
|
|
|
// In subsequent scripts, you may refer to each row/value returned by the trigger script using
|
|
// 'flow_input.iter.value'
|
|
}
|
|
`
|
|
|
|
const DENO_INIT_CODE_APPROVAL = `import * as wmill from "npm:windmill-client@^1.158.2"
|
|
|
|
export async function main(approver?: string) {
|
|
const urls = await wmill.getResumeUrls(approver)
|
|
// send the urls to their intended recipients
|
|
|
|
return {
|
|
// if the resumeUrls are part of the response, they will be available to any persons having access
|
|
// to the run page and allowed to be approved from there, even from non owners of the flow
|
|
// self-approval is disableable in the suspend options
|
|
...urls,
|
|
|
|
// to have prompts (self-approvable steps), clude instead the resume url in the returned payload of the step
|
|
// the UX will automatically adapt and show the prompt to the operator when running the flow. e.g:
|
|
// resume: urls['resume'],
|
|
|
|
default_args: {},
|
|
enums: {},
|
|
description: undefined
|
|
// supports all formats from rich display rendering such as simple strings,
|
|
// but also markdown, html, images, tables, maps, render_all, etc...
|
|
// https://www.windmill.dev/docs/core_concepts/rich_display_rendering
|
|
}
|
|
}
|
|
|
|
// add a form in Advanced - Suspend
|
|
// all on approval steps: https://www.windmill.dev/docs/flows/flow_approval`
|
|
|
|
const BUN_INIT_CODE_APPROVAL = `import * as wmill from "windmill-client@^1.158.2"
|
|
|
|
export async function main(approver?: string) {
|
|
const urls = await wmill.getResumeUrls(approver)
|
|
// send the urls to their intended recipients
|
|
|
|
return {
|
|
// if the resumeUrls are part of the response, they will be available to any persons having access
|
|
// to the run page and allowed to be approved from there, even from non owners of the flow
|
|
// self-approval is disableable in the suspend options
|
|
...urls,
|
|
|
|
// to have prompts (self-approvable steps), clude instead the resume url in the returned payload of the step
|
|
// the UX will automatically adapt and show the prompt to the operator when running the flow. e.g:
|
|
// resume: urls['resume'],
|
|
|
|
default_args: {},
|
|
enums: {},
|
|
description: undefined
|
|
// supports all formats from rich display rendering such as simple strings,
|
|
// but also markdown, html, images, tables, maps, render_all, etc...
|
|
// https://www.windmill.dev/docs/core_concepts/rich_display_rendering
|
|
}
|
|
}
|
|
|
|
// add a form in Advanced - Suspend
|
|
// all on approval steps: https://www.windmill.dev/docs/flows/flow_approval`
|
|
|
|
export const TS_PREPROCESSOR_SCRIPT_INTRO = `/**
|
|
* Trigger preprocessor
|
|
*
|
|
* ⚠️ This function runs BEFORE the main function.
|
|
*
|
|
* It processes raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email)
|
|
* before passing it to \`main\`. This separates the trigger logic from the main logic and keeps the auto-generated runnable UI clean.
|
|
*
|
|
* The returned object defines the parameter values passed to \`main()\`.
|
|
* e.g., { b: 1, a: 2 } → Calls \`main(2, 1)\`, assuming \`main\` is defined as \`main(a: number, b: number)\`.
|
|
* Ensure that the parameter names in \`main\` match the keys in the returned object.
|
|
*
|
|
* Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
|
|
*/\n`
|
|
|
|
export const TS_PREPROCESSOR_FLOW_INTRO = `/**
|
|
* Trigger preprocessor
|
|
*
|
|
* It processes raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email)
|
|
* before passing it to the flow. This separates the trigger logic from the flow logic and keeps the auto-generated UI clean.
|
|
*
|
|
* The returned object determines the parameter values passed to the flow.
|
|
* e.g., \`{ b: 1, a: 2 }\` → Calls the flow with \`a = 2\` and \`b = 1\`, assuming the flow has two inputs called \`a\` and \`b\`.
|
|
* Ensure that the input names of the flow match the keys in the returned object.
|
|
*
|
|
* Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
|
|
*/\n`
|
|
|
|
export const TS_PREPROCESSOR_MODULE_CODE = `export async function preprocessor(event: TriggerEvent) {
|
|
return {
|
|
// return the args to be passed to the runnable
|
|
};
|
|
}
|
|
|
|
type TriggerEvent =
|
|
| {
|
|
kind: "webhook";
|
|
body: any;
|
|
raw_string: string | null;
|
|
query: Record<string, string>;
|
|
headers: Record<string, string>;
|
|
}
|
|
| {
|
|
kind: "http";
|
|
trigger_path: string;
|
|
body: any;
|
|
raw_string: string | null;
|
|
route: string;
|
|
path: string;
|
|
method: string;
|
|
params: Record<string, string>;
|
|
query: Record<string, string>;
|
|
headers: Record<string, string>;
|
|
}
|
|
| {
|
|
kind: "email";
|
|
trigger_path: string;
|
|
parsed_email: any;
|
|
raw_email: string;
|
|
email_extra_args?: Record<string, string>;
|
|
}
|
|
| { kind: "websocket"; trigger_path: string; msg: string; url: string }
|
|
| {
|
|
kind: "kafka";
|
|
trigger_path: string;
|
|
payload: string;
|
|
brokers: string[];
|
|
topic: string;
|
|
partition: number;
|
|
offset: number;
|
|
group_id: string;
|
|
}
|
|
| {
|
|
kind: "nats";
|
|
trigger_path: string;
|
|
payload: string;
|
|
servers: string[];
|
|
subject: string;
|
|
headers?: Record<string, string[]>;
|
|
status?: number;
|
|
description?: string;
|
|
length: number;
|
|
}
|
|
| {
|
|
kind: "sqs";
|
|
trigger_path: string;
|
|
msg: string;
|
|
queue_url: string;
|
|
message_id?: string;
|
|
receipt_handle?: string;
|
|
attributes: Record<string, string>;
|
|
message_attributes?: Record<
|
|
string,
|
|
{ string_value?: string; data_type: string }
|
|
>;
|
|
}
|
|
| {
|
|
kind: "mqtt";
|
|
trigger_path: string;
|
|
payload: string;
|
|
topic: string;
|
|
retain: boolean;
|
|
pkid: number;
|
|
qos: number;
|
|
v5?: {
|
|
payload_format_indicator?: number;
|
|
topic_alias?: number;
|
|
response_topic?: string;
|
|
correlation_data?: Array<number>;
|
|
user_properties?: Array<[string, string]>;
|
|
subscription_identifiers?: Array<number>;
|
|
content_type?: string;
|
|
};
|
|
}
|
|
| {
|
|
kind: "gcp";
|
|
trigger_path: string;
|
|
payload: string;
|
|
message_id: string;
|
|
subscription: string;
|
|
ordering_key?: string;
|
|
attributes?: Record<string, string>;
|
|
delivery_type: "push" | "pull";
|
|
headers?: Record<string, string>;
|
|
publish_time?: string;
|
|
ack_id?: string;
|
|
}
|
|
| {
|
|
kind: "postgres";
|
|
trigger_path: string;
|
|
transaction_type: "insert" | "update" | "delete";
|
|
schema_name: string;
|
|
table_name: string;
|
|
old_row?: Record<string, any>;
|
|
row: Record<string, any>;
|
|
};
|
|
`
|
|
|
|
const PYTHON_INIT_CODE_APPROVAL = `import wmill
|
|
|
|
def main():
|
|
urls = wmill.get_resume_urls()
|
|
# send the urls to their intended recipients
|
|
|
|
return {
|
|
# if the get_resume_urls are part of the response, they will be available to any persons having access
|
|
# to the run page and allowed to be approved from there, even from non owners of the flow
|
|
# self-approval is disableable in the suspend options
|
|
**urls,
|
|
|
|
# to have prompts (self-approvable steps), clude instead the resume url in the returned payload of the step
|
|
# the UX will automatically adapt and show the prompt to the operator when running the flow. e.g:
|
|
# "resume": urls["resume"],
|
|
|
|
"default_args": {},
|
|
"enums": {},
|
|
"description": None,
|
|
# supports all formats from rich display rendering such as simple strings,
|
|
# but also markdown, html, images, tables, maps, render_all, etc...
|
|
# https://www.windmill.dev/docs/core_concepts/rich_display_rendering
|
|
}
|
|
|
|
# add a form in Advanced - Suspend
|
|
# all on approval steps: https://www.windmill.dev/docs/flows/flow_approval`
|
|
|
|
export const PYTHON_PREPROCESSOR_SCRIPT_INTRO = `# Trigger preprocessor
|
|
#
|
|
# ⚠️ This function runs BEFORE the main function.
|
|
#
|
|
# It processes raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email)
|
|
# before passing it to \`main\`. This separates the trigger logic from the main logic and keeps the auto-generated UI clean.
|
|
#
|
|
# The returned object defines the parameter values passed to \`main()\`.
|
|
# e.g., { b: 1, a: 2 } → Calls \`main(2, 1)\`, assuming \`main\` is defined as \`main(a: int, b: int)\`.
|
|
# Ensure that the parameter names in \`main\` match the keys in the returned object.
|
|
#
|
|
# Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors\n\n`
|
|
|
|
export const PYTHON_PREPROCESSOR_FLOW_INTRO = `# Trigger preprocessor
|
|
#
|
|
# It processes raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email)
|
|
# before passing it to the flow. This separates the trigger logic from the flow logic and keeps the auto-generated UI clean.
|
|
#
|
|
# The returned object determines the parameter values passed to the flow.
|
|
# e.g., \`{ b: 1, a: 2 }\` → Calls the flow with \`a = 2\` and \`b = 1\`, assuming the flow has two inputs called \`a\` and \`b\`.
|
|
# Ensure that the input names of the flow match the keys in the returned object.
|
|
#
|
|
# Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors\n\n`
|
|
|
|
export const PYTHON_PREPROCESSOR_MODULE_CODE = `from typing import TypedDict, Literal, Optional, Union
|
|
|
|
|
|
class WebhookEvent(TypedDict):
|
|
kind: Literal["webhook"]
|
|
body: dict
|
|
raw_string: Optional[str]
|
|
query: dict[str, str]
|
|
headers: dict[str, str]
|
|
|
|
|
|
class HttpEvent(TypedDict):
|
|
kind: Literal["http"]
|
|
trigger_path: str
|
|
body: dict
|
|
raw_string: Optional[str]
|
|
route: str
|
|
path: str
|
|
method: str
|
|
params: dict[str, str]
|
|
query: dict[str, str]
|
|
headers: dict[str, str]
|
|
|
|
|
|
class EmailEvent(TypedDict):
|
|
kind: Literal["email"]
|
|
trigger_path: str
|
|
parsed_email: dict
|
|
raw_email: str
|
|
email_extra_args: Optional[dict[str, str]]
|
|
|
|
|
|
class WebsocketEvent(TypedDict):
|
|
kind: Literal["websocket"]
|
|
trigger_path: str
|
|
msg: str
|
|
url: str
|
|
|
|
|
|
class KafkaEvent(TypedDict):
|
|
kind: Literal["kafka"]
|
|
trigger_path: str
|
|
payload: str
|
|
brokers: list[str]
|
|
topic: str
|
|
partition: int
|
|
offset: int
|
|
group_id: str
|
|
|
|
|
|
class NatsEvent(TypedDict):
|
|
kind: Literal["nats"]
|
|
trigger_path: str
|
|
payload: str
|
|
servers: list[str]
|
|
subject: str
|
|
headers: Optional[dict[str, list[str]]]
|
|
status: Optional[int]
|
|
description: Optional[str]
|
|
length: int
|
|
|
|
|
|
class MessageAttribute(TypedDict):
|
|
string_value: Optional[str]
|
|
data_type: str
|
|
|
|
|
|
class SqsEvent(TypedDict):
|
|
kind: Literal["sqs"]
|
|
trigger_path: str
|
|
msg: str
|
|
queue_url: str
|
|
message_id: Optional[str]
|
|
receipt_handle: Optional[str]
|
|
attributes: dict[str, str]
|
|
message_attributes: Optional[dict[str, MessageAttribute]]
|
|
|
|
|
|
class MqttV5Properties(TypedDict, total=False):
|
|
payload_format_indicator: Optional[int]
|
|
topic_alias: Optional[int]
|
|
response_topic: Optional[str]
|
|
correlation_data: Optional[list[int]]
|
|
user_properties: Optional[list[tuple[str, str]]]
|
|
subscription_identifiers: Optional[list[int]]
|
|
content_type: Optional[str]
|
|
|
|
|
|
class MqttEvent(TypedDict):
|
|
kind: Literal["mqtt"]
|
|
trigger_path: str
|
|
payload: str
|
|
topic: str
|
|
retain: bool
|
|
pkid: int
|
|
qos: int
|
|
v5: Optional[MqttV5Properties]
|
|
|
|
|
|
class GcpEvent(TypedDict):
|
|
kind: Literal["gcp"]
|
|
trigger_path: str
|
|
payload: str
|
|
message_id: str
|
|
subscription: str
|
|
ordering_key: Optional[str]
|
|
attributes: Optional[dict[str, str]]
|
|
delivery_type: Literal["push", "pull"]
|
|
headers: Optional[dict[str, str]]
|
|
publish_time: Optional[str]
|
|
ack_id: Optional[str]
|
|
|
|
|
|
class PostgresEvent(TypedDict):
|
|
kind: Literal["postgres"]
|
|
trigger_path: str
|
|
transaction_type: Literal["insert", "update", "delete"]
|
|
schema_name: str
|
|
table_name: str
|
|
old_row: Optional[dict[str, any]]
|
|
row: dict[str, any]
|
|
|
|
|
|
Event = Union[
|
|
WebhookEvent,
|
|
HttpEvent,
|
|
EmailEvent,
|
|
WebsocketEvent,
|
|
KafkaEvent,
|
|
NatsEvent,
|
|
SqsEvent,
|
|
MqttEvent,
|
|
GcpEvent,
|
|
PostgresEvent,
|
|
]
|
|
|
|
|
|
def preprocessor(event: Event):
|
|
return {
|
|
# return the args to be passed to the runnable
|
|
}
|
|
`
|
|
|
|
export const PHP_PREPROCESSOR_SCRIPT_INTRO = `<?php
|
|
/**
|
|
* Trigger preprocessor
|
|
*
|
|
* ⚠️ This function runs BEFORE the main function.
|
|
*
|
|
* It processes raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email)
|
|
* before passing it to \`main\`. This separates the trigger logic from the main logic and keeps the auto-generated runnable UI clean.
|
|
*
|
|
* The returned object defines the parameter values passed to \`main()\`.
|
|
* e.g., ['b' => 1, 'a' => 2] → Calls \`main(2, 1)\`, assuming \`main\` is defined as \`main($a, $b)\`.
|
|
* Ensure that the parameter names in \`main\` match the keys in the returned array.
|
|
*
|
|
* Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
|
|
*/
|
|
|
|
`
|
|
|
|
export const PHP_PREPROCESSOR_FLOW_INTRO = `<?php
|
|
/**
|
|
* Trigger preprocessor
|
|
*
|
|
* It processes raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email)
|
|
* before passing it to the flow. This separates the trigger logic from the flow logic and keeps the auto-generated UI clean.
|
|
*
|
|
* The returned object determines the parameter values passed to the flow.
|
|
* e.g., ['b' => 1, 'a' => 2] → Calls the flow with \`a = 2\` and \`b = 1\`, assuming the flow has two inputs called \`a\` and \`b\`.
|
|
* Ensure that the input names of the flow match the keys in the returned array.
|
|
*
|
|
* Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
|
|
*/
|
|
|
|
`
|
|
|
|
export const PHP_PREPROCESSOR_MODULE_CODE = `function preprocessor(object $event) {
|
|
// $event can be one of the following types:
|
|
//
|
|
// All events (except webhook) include 'trigger_path' => '...' (the path of the trigger in Windmill)
|
|
//
|
|
// Webhook event:
|
|
// ['kind' => 'webhook', 'body' => [...], 'raw_string' => '...', 'query' => [...], 'headers' => [...]]
|
|
//
|
|
// HTTP event:
|
|
// ['kind' => 'http', 'trigger_path' => '...', 'body' => [...], 'raw_string' => '...', 'route' => '...', 'path' => '...',
|
|
// 'method' => '...', 'params' => [...], 'query' => [...], 'headers' => [...]]
|
|
//
|
|
// Email event:
|
|
// ['kind' => 'email', 'trigger_path' => '...', 'parsed_email' => [...], 'raw_email' => '...', 'email_extra_args' => [...]]
|
|
//
|
|
// WebSocket event:
|
|
// ['kind' => 'websocket', 'trigger_path' => '...', 'msg' => '...', 'url' => '...']
|
|
//
|
|
// Kafka event:
|
|
// ['kind' => 'kafka', 'trigger_path' => '...', 'payload' => '...', 'brokers' => [...], 'topic' => '...',
|
|
// 'partition' => 0, 'offset' => 0, 'group_id' => '...']
|
|
//
|
|
// NATS event:
|
|
// ['kind' => 'nats', 'trigger_path' => '...', 'payload' => '...', 'servers' => [...], 'subject' => '...',
|
|
// 'headers' => [...], 'status' => 200, 'description' => '...', 'length' => 100]
|
|
//
|
|
// SQS event:
|
|
// ['kind' => 'sqs', 'trigger_path' => '...', 'msg' => '...', 'queue_url' => '...', 'message_id' => '...',
|
|
// 'receipt_handle' => '...', 'attributes' => [...], 'message_attributes' => [...]]
|
|
//
|
|
// MQTT event:
|
|
// ['kind' => 'mqtt', 'trigger_path' => '...', 'payload' => '...', 'topic' => '...', 'retain' => true, 'pkid' => 1,
|
|
// 'qos' => 1, 'v5' => [...]]
|
|
//
|
|
// GCP event:
|
|
// ['kind' => 'gcp', 'trigger_path' => '...', 'payload' => '...', 'message_id' => '...', 'subscription' => '...',
|
|
// 'ordering_key' => '...', 'attributes' => [...], 'delivery_type' => 'push',
|
|
// 'headers' => [...], 'publish_time' => '...', 'ack_id' => '...']
|
|
//
|
|
// Postgres event:
|
|
// ['kind' => 'postgres', 'trigger_path' => '...', 'transaction_type' => 'insert', 'schema_name' => '...',
|
|
// 'table_name' => '...', 'old_row' => [...], 'row' => [...]]
|
|
|
|
return [
|
|
// return the args to be passed to the runnable
|
|
];
|
|
}
|
|
`
|
|
|
|
const DOCKER_INIT_CODE = `# shellcheck shell=bash
|
|
# docker
|
|
# The annotation "docker" above is important, it tells windmill that after
|
|
# the end of the bash script, it should manage the container at id $WM_JOB_ID:
|
|
# pipe logs, monitor memory usage, kill container if job is cancelled.
|
|
|
|
msg="\${1:-world}"
|
|
|
|
IMAGE="alpine:latest"
|
|
COMMAND="/bin/echo Hello $msg"
|
|
|
|
# ensure that the image is up-to-date
|
|
docker pull $IMAGE
|
|
|
|
# if using the 'docker' mode, name it with $WM_JOB_ID for windmill to monitor it
|
|
docker run --name $WM_JOB_ID -it -d $IMAGE $COMMAND
|
|
`
|
|
|
|
const POWERSHELL_INIT_CODE = `param($Msg, [string[]]$Names, [PSCustomObject]$Obj, $Dflt = "default value", [int]$Nb = 3)
|
|
|
|
# Import-Module MyModule
|
|
|
|
# Import-Module WindmillClient
|
|
# Connect-Windmill
|
|
# Get-WindmillVariable -Path 'u/user/foo'
|
|
|
|
# the last line of the stdout is the return value
|
|
Write-Output "Hello $Msg"`
|
|
|
|
const ANSIBLE_PLAYBOOK_INIT_CODE = `---
|
|
inventory:
|
|
- resource_type: ansible_inventory
|
|
# You can pin an inventory to this script by hardcoding the resource path:
|
|
# resource: u/user/your_resource
|
|
# - name: hcloud.yml
|
|
# resource_type: dynamic_inventory
|
|
|
|
options:
|
|
- verbosity: vvv
|
|
|
|
|
|
# File resources will be written in the relative \`target\` location before
|
|
# running the playbook
|
|
# files:
|
|
# - resource: u/user/fabulous_jinja_template
|
|
# target: ./config_template.j2
|
|
# - variable: u/user/ssh_key
|
|
# target: ./ssh_key
|
|
# mode: '0600'
|
|
|
|
# Define the arguments of the windmill script
|
|
extra_vars:
|
|
world_qualifier:
|
|
type: string
|
|
|
|
# If using Ansible Vault:
|
|
# vault_password: u/user/ansible_vault_password
|
|
|
|
dependencies:
|
|
galaxy:
|
|
collections:
|
|
- name: community.general
|
|
- name: community.vmware
|
|
roles:
|
|
python:
|
|
- jmespath
|
|
---
|
|
- name: Echo
|
|
hosts: 127.0.0.1
|
|
connection: local
|
|
vars:
|
|
my_result:
|
|
a: 2
|
|
b: true
|
|
c: "Hello"
|
|
|
|
tasks:
|
|
- name: Print debug message
|
|
debug:
|
|
msg: "Hello, {{world_qualifier}} world!"
|
|
- name: Write variable my_result to result.json
|
|
delegate_to: localhost
|
|
copy:
|
|
content: "{{ my_result | to_json }}"
|
|
dest: result.json
|
|
`
|
|
const JAVA_INIT_CODE = `//requirements:
|
|
//com.google.code.gson:gson:2.8.9
|
|
//com.github.ricksbrown:cowsay:1.1.0
|
|
|
|
import com.google.gson.Gson;
|
|
import com.google.gson.GsonBuilder;
|
|
import com.github.ricksbrown.cowsay.Cowsay;
|
|
import com.github.ricksbrown.cowsay.plugin.CowExecutor;
|
|
|
|
public class Main {
|
|
public static class Person {
|
|
private String name;
|
|
private int age;
|
|
|
|
// Constructor
|
|
public Person(String name, int age) {
|
|
this.name = name;
|
|
this.age = age;
|
|
}
|
|
}
|
|
|
|
public static Object main(
|
|
// Primitive
|
|
int a,
|
|
float b,
|
|
// Objects
|
|
Integer age,
|
|
Float d,
|
|
Object e,
|
|
String name,
|
|
// Lists
|
|
String[] f
|
|
// No trailing commas!
|
|
){
|
|
Gson gson = new Gson();
|
|
|
|
// Get resources
|
|
var theme = Wmill.getResource("f/app_themes/theme_0");
|
|
System.out.println("Theme: " + theme);
|
|
|
|
// Create a Person object
|
|
Person person = new Person( (name == "") ? "Alice" : name, (age == null) ? 30 : age);
|
|
|
|
// Serialize the Person object to JSON
|
|
String json = gson.toJson(person);
|
|
System.out.println("Serialized JSON: " + json);
|
|
|
|
// Use cowsay
|
|
String[] args = new String[]{"-f", "dragon", json };
|
|
String result = Cowsay.say(args);
|
|
return result;
|
|
}
|
|
}
|
|
`
|
|
const RUBY_INIT_CODE = `require 'windmill/inline'
|
|
require 'windmill/mini'
|
|
|
|
# Dependency management: declare gems in gemfile block for automatic installation
|
|
# Windmill uses bundler/inline compatible syntax with automatic requiring
|
|
gemfile do
|
|
source 'https://rubygems.org'
|
|
gem 'amazing_print', '~> 1.6'
|
|
end
|
|
|
|
def main(
|
|
no_default,
|
|
name = "Nicolas Bourbaki",
|
|
age = 42,
|
|
obj = { "even": "hashes" },
|
|
list = ["or", "arrays!"]
|
|
)
|
|
puts "Hello World and a warm welcome especially to #{name}"
|
|
puts "and its acolytes.. #{age} #{obj} #{list}"
|
|
|
|
# Retrieve variables using the Windmill mini client
|
|
begin
|
|
secret = get_variable("f/examples/secret")
|
|
rescue => e
|
|
secret = "No secret yet at f/examples/secret!"
|
|
end
|
|
puts "The variable at 'f/examples/secret': #{secret}"
|
|
|
|
# Get typed resources using the mini client
|
|
# database = get_resource("u/user/my_postgresql")
|
|
|
|
# Access environment variables provided by Windmill
|
|
user = ENV['WM_USERNAME']
|
|
|
|
# Pretty print results using amazing_print (automatically required from gemfile)
|
|
result = {
|
|
"splitted" => name.split,
|
|
"user" => user,
|
|
"age" => age,
|
|
"obj" => obj,
|
|
"list" => list
|
|
}
|
|
|
|
ap result
|
|
|
|
# Return value is automatically converted to JSON
|
|
return result
|
|
end
|
|
`
|
|
// for related places search: ADD_NEW_LANG
|
|
export const INITIAL_CODE = {
|
|
bun: {
|
|
scriptInitCodeBlock: BUN_INIT_BLOCK,
|
|
script: BUN_INIT_CODE,
|
|
trigger: BUN_INIT_CODE_TRIGGER,
|
|
approval: BUN_INIT_CODE_APPROVAL,
|
|
failure: BUN_FAILURE_MODULE_CODE,
|
|
preprocessor: TS_PREPROCESSOR_FLOW_INTRO + TS_PREPROCESSOR_MODULE_CODE,
|
|
clear: BUN_INIT_CODE_CLEAR
|
|
},
|
|
python3: {
|
|
script: PYTHON_INIT_CODE,
|
|
trigger: PYTHON_INIT_CODE_TRIGGER,
|
|
approval: PYTHON_INIT_CODE_APPROVAL,
|
|
failure: PYTHON_FAILURE_MODULE_CODE,
|
|
preprocessor: PYTHON_PREPROCESSOR_FLOW_INTRO + PYTHON_PREPROCESSOR_MODULE_CODE,
|
|
clear: PYTHON_INIT_CODE_CLEAR
|
|
},
|
|
deno: {
|
|
scriptInitCodeBlock: DENO_INIT_BLOCK,
|
|
script: DENO_INIT_CODE,
|
|
trigger: DENO_INIT_CODE_TRIGGER,
|
|
approval: DENO_INIT_CODE_APPROVAL,
|
|
failure: DENO_FAILURE_MODULE_CODE,
|
|
preprocessor: TS_PREPROCESSOR_FLOW_INTRO + TS_PREPROCESSOR_MODULE_CODE,
|
|
fetch: FETCH_INIT_CODE,
|
|
clear: DENO_INIT_CODE_CLEAR
|
|
},
|
|
go: {
|
|
script: GO_INIT_CODE,
|
|
trigger: GO_INIT_CODE_TRIGGER,
|
|
failure: GO_FAILURE_MODULE_CODE
|
|
},
|
|
bash: {
|
|
script: BASH_INIT_CODE
|
|
},
|
|
powershell: {
|
|
script: POWERSHELL_INIT_CODE
|
|
},
|
|
nativets: {
|
|
script: NATIVETS_INIT_CODE
|
|
},
|
|
postgresql: {
|
|
script: POSTGRES_INIT_CODE
|
|
},
|
|
mysql: {
|
|
script: MYSQL_INIT_CODE
|
|
},
|
|
bigquery: {
|
|
script: BIGQUERY_INIT_CODE
|
|
},
|
|
snowflake: {
|
|
script: SNOWFLAKE_INIT_CODE
|
|
},
|
|
mssql: {
|
|
script: MSSQL_INIT_CODE
|
|
},
|
|
duckdb: {
|
|
script: DUCKDB_INIT_CODE
|
|
},
|
|
graphql: {
|
|
script: GRAPHQL_INIT_CODE
|
|
},
|
|
oracledb: {
|
|
script: ORACLEDB_INIT_CODE
|
|
},
|
|
php: {
|
|
script: PHP_INIT_CODE,
|
|
preprocessor: PHP_PREPROCESSOR_FLOW_INTRO + PHP_PREPROCESSOR_MODULE_CODE
|
|
},
|
|
rust: {
|
|
script: RUST_INIT_CODE
|
|
},
|
|
ansible: {
|
|
script: ANSIBLE_PLAYBOOK_INIT_CODE
|
|
},
|
|
csharp: {
|
|
script: CSHARP_INIT_CODE
|
|
},
|
|
nu: {
|
|
script: NU_INIT_CODE
|
|
},
|
|
docker: {
|
|
script: DOCKER_INIT_CODE
|
|
},
|
|
bunnative: {
|
|
script: BUNNATIVE_INIT_CODE
|
|
},
|
|
java: {
|
|
script: JAVA_INIT_CODE
|
|
},
|
|
ruby: {
|
|
script: RUBY_INIT_CODE
|
|
},
|
|
claudesandbox: {
|
|
script: CLAUDE_SANDBOX_INIT_CODE
|
|
},
|
|
wac_python: {
|
|
script: WAC_PYTHON_INIT_CODE
|
|
},
|
|
wac_typescript: {
|
|
script: WAC_TYPESCRIPT_INIT_CODE
|
|
}
|
|
// for related places search: ADD_NEW_LANG
|
|
}
|
|
|
|
export function isInitialCode(content: string): boolean {
|
|
for (const lang of Object.values(INITIAL_CODE)) {
|
|
for (const code of Object.values(lang)) {
|
|
if (content === code) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function initialCode(
|
|
language: SupportedLanguage | 'bunnative' | undefined,
|
|
kind: Script['kind'] | undefined,
|
|
subkind:
|
|
| 'pgsql'
|
|
| 'mysql'
|
|
| 'flow'
|
|
| 'script'
|
|
| 'fetch'
|
|
| 'docker'
|
|
| 'powershell'
|
|
| 'bunnative'
|
|
| 'claudesandbox'
|
|
| 'wac_python'
|
|
| 'wac_typescript'
|
|
| undefined,
|
|
templateScript?: boolean
|
|
): string {
|
|
if (!kind) {
|
|
kind = 'script'
|
|
}
|
|
if (language === 'deno') {
|
|
if (kind === 'trigger') {
|
|
return INITIAL_CODE.deno.trigger
|
|
} else if (kind === 'script') {
|
|
if (subkind === 'flow') {
|
|
return INITIAL_CODE.deno.clear
|
|
} else if (subkind === 'pgsql') {
|
|
return INITIAL_CODE.postgresql.script
|
|
} else if (subkind === 'mysql') {
|
|
return INITIAL_CODE.mysql.script
|
|
} else if (subkind === 'fetch') {
|
|
return INITIAL_CODE.deno.fetch
|
|
} else {
|
|
return INITIAL_CODE.deno.script
|
|
}
|
|
} else if (kind === 'failure') {
|
|
return INITIAL_CODE.deno.failure
|
|
} else if (kind === 'approval') {
|
|
return INITIAL_CODE.deno.approval
|
|
} else if (kind === 'preprocessor') {
|
|
return INITIAL_CODE.deno.preprocessor
|
|
} else {
|
|
return INITIAL_CODE.deno.script
|
|
}
|
|
} else if (subkind === 'wac_python') {
|
|
return INITIAL_CODE.wac_python.script
|
|
} else if (subkind === 'wac_typescript') {
|
|
return INITIAL_CODE.wac_typescript.script
|
|
} else if (language === 'python3') {
|
|
if (kind === 'trigger') {
|
|
return INITIAL_CODE.python3.trigger
|
|
} else if (kind === 'approval') {
|
|
return INITIAL_CODE.python3.approval
|
|
} else if (kind === 'failure') {
|
|
return INITIAL_CODE.python3.failure
|
|
} else if (kind === 'preprocessor') {
|
|
return INITIAL_CODE.python3.preprocessor
|
|
} else if (subkind === 'flow') {
|
|
return INITIAL_CODE.python3.clear
|
|
} else {
|
|
return INITIAL_CODE.python3.script
|
|
}
|
|
} else if (language == 'bash') {
|
|
if (subkind === 'docker') {
|
|
return INITIAL_CODE.docker.script
|
|
} else {
|
|
return INITIAL_CODE.bash.script
|
|
}
|
|
} else if (language == 'powershell') {
|
|
return INITIAL_CODE.powershell.script
|
|
} else if (language == 'nativets') {
|
|
return INITIAL_CODE.nativets.script
|
|
} else if (language == 'postgresql') {
|
|
return INITIAL_CODE.postgresql.script
|
|
} else if (language == 'mysql') {
|
|
return INITIAL_CODE.mysql.script
|
|
} else if (language == 'bigquery') {
|
|
return INITIAL_CODE.bigquery.script
|
|
} else if (language == 'oracledb') {
|
|
return INITIAL_CODE.oracledb.script
|
|
} else if (language == 'snowflake') {
|
|
return INITIAL_CODE.snowflake.script
|
|
} else if (language == 'mssql') {
|
|
return INITIAL_CODE.mssql.script
|
|
} else if (language == 'graphql') {
|
|
return INITIAL_CODE.graphql.script
|
|
} else if (language == 'duckdb') {
|
|
return INITIAL_CODE.duckdb.script
|
|
} else if (language == 'php') {
|
|
if (kind == 'preprocessor') {
|
|
return INITIAL_CODE.php.preprocessor
|
|
}
|
|
return INITIAL_CODE.php.script
|
|
} else if (language == 'rust') {
|
|
return INITIAL_CODE.rust.script
|
|
} else if (language == 'ansible') {
|
|
return INITIAL_CODE.ansible.script
|
|
} else if (language == 'csharp') {
|
|
return INITIAL_CODE.csharp.script
|
|
} else if (language == 'nu') {
|
|
return INITIAL_CODE.nu.script
|
|
} else if (language == 'java') {
|
|
return INITIAL_CODE.java.script
|
|
} else if (language == 'ruby') {
|
|
return INITIAL_CODE.ruby.script
|
|
// for related places search: ADD_NEW_LANG
|
|
} else if (language == 'bun' || language == 'bunnative') {
|
|
if (subkind === 'claudesandbox') {
|
|
return INITIAL_CODE.claudesandbox.script
|
|
} else if (kind == 'trigger') {
|
|
return INITIAL_CODE.bun.trigger
|
|
} else if (language == 'bunnative' || subkind === 'bunnative') {
|
|
return INITIAL_CODE.bunnative.script
|
|
} else if (kind === 'approval') {
|
|
return INITIAL_CODE.bun.approval
|
|
} else if (kind === 'failure') {
|
|
return INITIAL_CODE.bun.failure
|
|
} else if (kind === 'preprocessor') {
|
|
return INITIAL_CODE.bun.preprocessor
|
|
} else if (templateScript == true) {
|
|
return INITIAL_CODE.bun.scriptInitCodeBlock
|
|
} else if (subkind === 'flow') {
|
|
return INITIAL_CODE.bun.clear
|
|
}
|
|
|
|
return INITIAL_CODE.bun.script
|
|
} else {
|
|
if (kind === 'failure') {
|
|
return INITIAL_CODE.go.failure
|
|
} else if (kind === 'trigger') {
|
|
return INITIAL_CODE.go.trigger
|
|
} else {
|
|
return INITIAL_CODE.go.script
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getResetCode(
|
|
language: SupportedLanguage | 'bunnative' | undefined,
|
|
kind: Script['kind'] | undefined,
|
|
subkind:
|
|
| 'pgsql'
|
|
| 'mysql'
|
|
| 'flow'
|
|
| 'script'
|
|
| 'fetch'
|
|
| 'docker'
|
|
| 'powershell'
|
|
| 'bunnative'
|
|
| 'claudesandbox'
|
|
| 'wac_python'
|
|
| 'wac_typescript'
|
|
| undefined
|
|
) {
|
|
if (language === 'deno') {
|
|
return DENO_INIT_CODE_CLEAR
|
|
} else if (language === 'python3') {
|
|
return PYTHON_INIT_CODE_CLEAR
|
|
} else if (language === 'nativets') {
|
|
return NATIVETS_INIT_CODE_CLEAR
|
|
} else if (language === 'bun') {
|
|
return BUN_INIT_CODE_CLEAR
|
|
} else if (language === 'bunnative') {
|
|
return BUNNATIVE_INIT_CODE
|
|
} else {
|
|
return initialCode(language, kind, subkind)
|
|
}
|
|
}
|
|
|
|
export const PREPROCESSOR_SUPPORTED_LANGUAGES = [
|
|
'typescript',
|
|
'python',
|
|
'python3',
|
|
'deno',
|
|
'bun',
|
|
'php'
|
|
] as const
|
|
|
|
export function canHavePreprocessor(language: string | undefined): boolean {
|
|
if (!language) {
|
|
return false
|
|
}
|
|
|
|
return PREPROCESSOR_SUPPORTED_LANGUAGES.includes(language as any)
|
|
}
|
|
|
|
export function canHaveTrigger(language: SupportedLanguage | undefined): boolean {
|
|
if (!language) {
|
|
return false
|
|
}
|
|
|
|
return ['python3', 'bun', 'deno', 'go'].includes(language)
|
|
}
|
|
|
|
export function canHaveApproval(language: SupportedLanguage | undefined): boolean {
|
|
if (!language) {
|
|
return false
|
|
}
|
|
|
|
return ['python3', 'bun'].includes(language)
|
|
}
|
|
|
|
export function canHaveFailure(language: SupportedLanguage | undefined): boolean {
|
|
if (!language) {
|
|
return false
|
|
}
|
|
|
|
return ['python3', 'bun', 'deno', 'go'].includes(language)
|
|
}
|
|
|
|
export function getPreprocessorIntro(
|
|
language: SupportedLanguage | 'docker' | 'bunnative' | undefined,
|
|
isFlow: boolean = false
|
|
): string {
|
|
if (!language || !PREPROCESSOR_SUPPORTED_LANGUAGES.includes(language as any)) {
|
|
return ''
|
|
}
|
|
|
|
switch (language) {
|
|
case 'python3':
|
|
return isFlow ? PYTHON_PREPROCESSOR_FLOW_INTRO : PYTHON_PREPROCESSOR_SCRIPT_INTRO
|
|
case 'deno':
|
|
case 'bun':
|
|
return isFlow ? TS_PREPROCESSOR_FLOW_INTRO : TS_PREPROCESSOR_SCRIPT_INTRO
|
|
case 'php':
|
|
return isFlow ? PHP_PREPROCESSOR_FLOW_INTRO : PHP_PREPROCESSOR_SCRIPT_INTRO
|
|
default:
|
|
return ''
|
|
}
|
|
}
|
|
|
|
export function getPreprocessorModuleCode(
|
|
language: SupportedLanguage | 'docker' | 'bunnative' | undefined
|
|
): string {
|
|
if (!language || !PREPROCESSOR_SUPPORTED_LANGUAGES.includes(language as any)) {
|
|
return ''
|
|
}
|
|
|
|
switch (language) {
|
|
case 'python3':
|
|
return PYTHON_PREPROCESSOR_MODULE_CODE
|
|
case 'deno':
|
|
case 'bun':
|
|
return TS_PREPROCESSOR_MODULE_CODE
|
|
case 'php':
|
|
return PHP_PREPROCESSOR_MODULE_CODE
|
|
default:
|
|
return ''
|
|
}
|
|
}
|
|
|
|
export function getPreprocessorFullCode(
|
|
language: SupportedLanguage | 'docker' | 'bunnative' | undefined,
|
|
isFlow: boolean = false
|
|
): string {
|
|
const intro = getPreprocessorIntro(language, isFlow)
|
|
const moduleCode = getPreprocessorModuleCode(language)
|
|
return intro + moduleCode
|
|
}
|
|
|
|
export function getMainFunctionPattern(
|
|
language: SupportedLanguage | 'docker' | 'bunnative' | undefined
|
|
): string {
|
|
if (!language) {
|
|
return ''
|
|
}
|
|
|
|
switch (language) {
|
|
case 'python3':
|
|
return 'def main'
|
|
case 'deno':
|
|
case 'bun':
|
|
case 'nativets':
|
|
return 'export async function main'
|
|
case 'php':
|
|
return 'function main'
|
|
default:
|
|
return 'main'
|
|
}
|
|
}
|