Files
windmill/cli/windmill-utils-internal/src/parse/parse-schema.ts
T
Ruben Fiszel 4fedfdfd11 feat(cli): add consistent get/list/new subcommands for all item types (#8047)
* feat(cli): add consistent get/list/new subcommands for all item types

Make the CLI consistent so every item type (script, flow, app, resource,
resource-type, variable, schedule, folder, trigger) supports get/list/new
subcommands, enabling the CLI to be used as a full API client in bash
scripts with jq piping.

- Add --json flag to all list commands for machine-readable output
- Register explicit "list" subcommand alongside default action
- Add "get <path> [--json]" subcommand to fetch single items from API
- Rename "bootstrap" to "new" for script/flow, keep "bootstrap" as alias
- Add "new" subcommand for resource, resource-type, variable, schedule,
  folder, and trigger to create local template YAML files
- Update cli-commands skill documentation for wmill init
- Add integration tests for all new commands

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

* all

* feat: install wmill CLI in Docker images and use it for bash variable/resource access

- Install windmill-cli via bun in all Dockerfiles that include bun
- DockerfileCli: switch from node:slim to oven/bun:slim
- CLI: auto-configure from WM_WORKSPACE/WM_TOKEN/BASE_INTERNAL_URL env vars
  as last-resort fallback when no workspace is configured
- Frontend: replace curl-based bash snippets with wmill variable/resource get
- Add backend integration tests for wmill CLI in bash scripts

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

* fix(ci): install windmill-cli in backend test workflow

Ensures wmill is available on PATH for bash integration tests
that use `wmill variable get` and `wmill resource get`.

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

* refactor(cli): replace @std/* Deno dependencies with Node.js equivalents

Replace @std/log with a lightweight custom logger (core/log.ts),
@std/path with node:path, and @std/yaml with the yaml npm package.
Also fix process hang on exit, add --node option to install_dev.sh,
and add missing hasRequiredPermissions to NpmProvider.

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

* all

* all

* all

* refactor(cli): replace @ayonli/jsext and @std/encoding with lightweight alternatives

Replace @ayonli/jsext (8.4MB) with tar-stream (32kB) for tar creation,
replace @std/encoding with Node.js Buffer.toString("hex"), and fix
@windmill-labs/shared-utils to use direct npm instead of JSR mirror.
Also resolve merge conflicts in sync.ts and fix pre-existing type errors.

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

* fix(cli): use singleQuote YAML output and pass yamlOptions in gitsync pull

The yaml library defaults to double quotes, but the codebase (and tests)
expect single-quoted strings. Add singleQuote: true to yamlOptions and
pass yamlOptions to gitsync-settings pull writeFile calls.

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

* all

* all

* fix(cli): address code review feedback

- Install CLI from source in backend tests instead of npm
- Fix script bootstrap catch block to re-throw "File already exists"
- Add type-safe local variable after trigger kind validation
- Use created_by instead of policy.on_behalf_of for app get output
- Note --kind is recommended for faster trigger lookup in help text
- Document node symlink purpose in Dockerfiles

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

* fix(ci): use /usr/bin for wmill wrapper to ensure it's in PATH

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

* fix(ci): install wmill to ~/.local/bin to avoid permission issues

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

* ci(backend): switch to Blacksmith runner and add cargo caching

- Switch from ubicloud-standard-16 to blacksmith-16vcpu-ubuntu-2404 for faster NVMe-backed builds
- Add stickydisk for cargo target directory (persistent NVMe cache across runs)
- Add cache for cargo registry and git dependencies
- Upgrade DuckDB FFI cache from actions/cache@v3 to useblacksmith/cache@v1
- Enable CARGO_INCREMENTAL=1 to benefit from persistent target cache

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

* fix ci

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 07:53:28 +00:00

312 lines
9.1 KiB
TypeScript

/**
* Type alias for enum values - can be an array of strings or undefined
*/
export type EnumType =
| string[]
| { label: string; value: string }[]
| undefined;
/**
* Represents a property in a JSON schema with various validation and display options
*/
export interface SchemaProperty {
type: string | undefined;
description?: string;
pattern?: string;
default?: any;
enum?: EnumType;
contentEncoding?: "base64" | "binary";
format?: string;
items?: {
type?: "string" | "number" | "bytes" | "object" | "resource";
contentEncoding?: "base64";
enum?: EnumType;
resourceType?: string;
properties?: { [name: string]: SchemaProperty };
};
min?: number;
max?: number;
currency?: string;
currencyLocale?: string;
multiselect?: boolean;
customErrorMessage?: string;
properties?: { [name: string]: SchemaProperty };
required?: string[];
showExpr?: string;
password?: boolean;
order?: string[];
nullable?: boolean;
dateFormat?: string;
title?: string;
placeholder?: string;
oneOf?: SchemaProperty[];
originalType?: string;
}
const ITEMS_PRESERVED_FIELDS = [
"properties",
"required",
"additionalProperties",
"enum",
"resourceType",
"contentEncoding",
"description",
] as (keyof SchemaProperty["items"])[];
/**
* Converts argument signature types to JSON schema properties.
* This function handles various Windmill-specific types and converts them
* to standard JSON schema format while preserving existing property metadata.
*
* @param t - The argument signature type definition (can be string or complex object types)
* @param oldS - Existing schema property to update with new type information
*/
export function argSigToJsonSchemaType(
t:
| string
| { resource: string | null }
| {
list:
| (string | { name?: string; props?: { key: string; typ: any }[] })
| { str: any }
| { object: { name?: string; props?: { key: string; typ: any }[] } }
| null;
}
| { dynselect: string }
| { dynmultiselect: string }
| { str: string[] | null }
| { object: { name?: string; props?: { key: string; typ: any }[] } }
| {
oneof: {
label: string;
properties: { key: string; typ: any }[];
}[];
},
oldS: SchemaProperty
): void {
const newS: SchemaProperty = { type: "" };
if (t === "int") {
newS.type = "integer";
} else if (t === "float") {
newS.type = "number";
} else if (t === "bool") {
newS.type = "boolean";
} else if (t === "email") {
newS.type = "string";
newS.format = "email";
} else if (t === "sql") {
newS.type = "string";
newS.format = "sql";
} else if (t === "yaml") {
newS.type = "string";
newS.format = "yaml";
} else if (t === "bytes") {
newS.type = "string";
newS.contentEncoding = "base64";
newS.originalType = "bytes";
} else if (t === "datetime") {
newS.type = "string";
newS.format = "date-time";
} else if (t === "date") {
newS.type = "string";
newS.format = "date";
} else if (typeof t !== "string" && "oneof" in t) {
newS.type = "object";
if (t.oneof) {
newS.oneOf = t.oneof.map((obj) => {
const oldObjS =
oldS.oneOf?.find((o) => o?.title === obj.label) ?? undefined;
const properties: Record<string, any> = {};
for (const prop of obj.properties) {
if (oldObjS?.properties && prop.key in oldObjS?.properties) {
properties[prop.key] = oldObjS?.properties[prop.key];
} else {
properties[prop.key] = { description: "", type: "" };
}
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
}
return {
type: "object",
title: obj.label,
properties,
order: oldObjS?.order ?? undefined,
};
});
}
} else if (typeof t !== "string" && `object` in t) {
newS.type = "object";
if (t.object.name) {
newS.format = `resource-${t.object.name}`;
}
if (t.object.props) {
const properties: Record<string, any> = {};
for (const prop of t.object.props) {
if (oldS.properties && prop.key in oldS.properties) {
properties[prop.key] = oldS.properties[prop.key];
} else {
properties[prop.key] = { description: "", type: "" };
}
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
}
newS.properties = properties;
}
} else if (typeof t !== "string" && `str` in t) {
newS.type = "string";
if (t.str) {
newS.originalType = "enum";
newS.enum = t.str;
} else if (oldS.originalType == "string" && oldS.enum) {
newS.originalType = "string";
newS.enum = oldS.enum;
} else {
newS.originalType = "string";
newS.enum = undefined;
}
} else if (typeof t !== "string" && `resource` in t) {
newS.type = "object";
newS.format = `resource-${t.resource}`;
} else if (typeof t !== "string" && `dynselect` in t) {
newS.type = "object";
newS.format = `dynselect-${t.dynselect}`;
} else if (typeof t !== "string" && `dynmultiselect` in t) {
newS.type = "object";
newS.format = `dynmultiselect-${t.dynmultiselect}`;
} else if (typeof t !== "string" && `list` in t) {
newS.type = "array";
if (t.list === "int" || t.list === "float") {
newS.items = { type: "number" };
newS.originalType = "number[]";
} else if (t.list === "bytes") {
newS.items = { type: "string", contentEncoding: "base64" };
newS.originalType = "bytes[]";
} else if (
t.list &&
typeof t.list == "object" &&
"str" in t.list &&
t.list.str
) {
newS.items = { type: "string", enum: t.list.str };
newS.originalType = "enum[]";
} else if (
t.list == "string" ||
(t.list && typeof t.list == "object" && "str" in t.list)
) {
newS.items = { type: "string", enum: oldS.items?.enum };
newS.originalType = "string[]";
} else if (
t.list &&
typeof t.list == "object" &&
"resource" in t.list &&
t.list.resource
) {
newS.items = {
type: "resource",
resourceType: t.list.resource as string,
};
newS.originalType = "resource[]";
} else if (
t.list &&
typeof t.list == "object" &&
"object" in t.list &&
t.list.object
) {
if (t.list.object.name) {
newS.format = `resource-${t.list.object.name}`;
}
if (t.list.object.props && t.list.object.props.length > 0) {
const properties: Record<string, any> = {};
for (const prop of t.list.object.props) {
properties[prop.key] = { description: "", type: "" };
argSigToJsonSchemaType(prop.typ, properties[prop.key]);
}
newS.items = { type: "object", properties: properties };
} else {
// Preserve ALL user-defined fields when parser cannot infer structure
newS.items = { type: oldS.items?.type || "object" };
if (oldS.items && typeof oldS.items === "object") {
ITEMS_PRESERVED_FIELDS.forEach((field) => {
if (oldS.items && oldS.items[field] !== undefined) {
(newS.items as any)[field] = oldS.items[field];
}
});
}
}
newS.originalType = "record[]";
} else {
// Preserve ALL user-defined fields for untyped lists (same as record[] branch)
newS.items = { type: oldS.items?.type || "object" };
if (oldS.items && typeof oldS.items === "object") {
ITEMS_PRESERVED_FIELDS.forEach((field) => {
if (oldS.items && oldS.items[field] !== undefined) {
(newS.items as any)[field] = oldS.items[field];
}
});
}
newS.originalType = "object[]";
}
} else {
// Preserve existing type when inference fails, default to "object" for undefined/null
newS.type = oldS.type ?? "object";
}
const preservedFields = [
"description",
"pattern",
"min",
"max",
"currency",
"currencyLocale",
"multiselect",
"customErrorMessage",
"required",
"showExpr",
"password",
"order",
"dateFormat",
"title",
"placeholder",
];
preservedFields.forEach((field) => {
// @ts-ignore
if (oldS[field] !== undefined) {
// @ts-ignore
newS[field] = oldS[field];
}
});
if (oldS.type != newS.type) {
for (const prop of Object.getOwnPropertyNames(newS)) {
if (prop != "description") {
// @ts-ignore
delete oldS[prop];
}
}
} else if (
(oldS.format == "date" || oldS.format === "date-time") &&
newS.format == "string"
) {
newS.format = oldS.format;
} else if (newS.format == "date-time" && oldS.format == "date") {
newS.format = "date";
} else if (newS.format == "date" || newS.format == "date-time") {
newS.format = oldS.format;
} else if (oldS.items?.type != newS.items?.type) {
delete oldS.items;
} else if (oldS.type == "string" && oldS.format != undefined) {
newS.format = oldS.format;
}
if (
(oldS.type != newS.type || newS.type != "string") &&
newS.format == undefined
) {
oldS.format = undefined;
}
Object.assign(oldS, newS);
}