windmill-cli 1.700.1 → 1.701.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/main.js +188 -126
- package/package.json +1 -1
package/esm/main.js
CHANGED
|
@@ -16752,7 +16752,7 @@ var init_OpenAPI = __esm(() => {
|
|
|
16752
16752
|
PASSWORD: undefined,
|
|
16753
16753
|
TOKEN: getEnv3("WM_TOKEN"),
|
|
16754
16754
|
USERNAME: undefined,
|
|
16755
|
-
VERSION: "1.
|
|
16755
|
+
VERSION: "1.701.0",
|
|
16756
16756
|
WITH_CREDENTIALS: true,
|
|
16757
16757
|
interceptors: {
|
|
16758
16758
|
request: new Interceptors,
|
|
@@ -44417,7 +44417,7 @@ var require_zipEntries = __commonJS((exports, module) => {
|
|
|
44417
44417
|
if (this.centralDirRecords !== this.files.length) {
|
|
44418
44418
|
if (this.centralDirRecords !== 0 && this.files.length === 0) {
|
|
44419
44419
|
throw new Error("Corrupted zip or bug: expected " + this.centralDirRecords + " records in central dir, got " + this.files.length);
|
|
44420
|
-
}
|
|
44420
|
+
}
|
|
44421
44421
|
}
|
|
44422
44422
|
},
|
|
44423
44423
|
readEndOfCentral: function() {
|
|
@@ -55347,7 +55347,7 @@ var require_loader = __commonJS((exports, module) => {
|
|
|
55347
55347
|
var error2 = generateError(state, message);
|
|
55348
55348
|
if (state.onWarning) {
|
|
55349
55349
|
state.onWarning.call(null, error2);
|
|
55350
|
-
}
|
|
55350
|
+
}
|
|
55351
55351
|
}
|
|
55352
55352
|
var directiveHandlers = {
|
|
55353
55353
|
YAML: function handleYamlDirective(state, name, args) {
|
|
@@ -55885,7 +55885,7 @@ var require_loader = __commonJS((exports, module) => {
|
|
|
55885
55885
|
} else if (detectedIndent) {
|
|
55886
55886
|
sc.value += common3.repeat(`
|
|
55887
55887
|
`, emptyLines + 1);
|
|
55888
|
-
}
|
|
55888
|
+
}
|
|
55889
55889
|
detectedIndent = true;
|
|
55890
55890
|
emptyLines = 0;
|
|
55891
55891
|
captureStart = state.position;
|
|
@@ -64370,12 +64370,16 @@ var init_path_assigner = __esm(() => {
|
|
|
64370
64370
|
});
|
|
64371
64371
|
|
|
64372
64372
|
// windmill-utils-internal/src/inline-scripts/extractor.ts
|
|
64373
|
-
function extractRawscriptInline(id, summary, rawscript, mapping, separator, assigner) {
|
|
64373
|
+
function extractRawscriptInline(id, summary, rawscript, mapping, separator, assigner, failOnInlineDirective) {
|
|
64374
64374
|
const [basePath, ext2] = assigner.assignPath(summary ?? id, rawscript.language);
|
|
64375
64375
|
const mappedPath = mapping[id];
|
|
64376
64376
|
const path10 = mappedPath ?? basePath + ext2;
|
|
64377
64377
|
const language = rawscript.language;
|
|
64378
64378
|
const content = rawscript.content;
|
|
64379
|
+
if (failOnInlineDirective && typeof content === "string" && content.startsWith("!inline ")) {
|
|
64380
|
+
throw new Error(`Refusing to extract corrupted inline script for module '${id}': ` + `rawscript.content is the literal string \`${content.split(`
|
|
64381
|
+
`)[0]}\` ` + `instead of script source. The backend's flow_version.value is corrupt — ` + `re-push from a known-good local copy to repair it.`);
|
|
64382
|
+
}
|
|
64379
64383
|
const r = [{ path: path10, content, language, is_lock: false }];
|
|
64380
64384
|
rawscript.content = "!inline " + path10.replaceAll(separator, "/");
|
|
64381
64385
|
const lock = rawscript.lock;
|
|
@@ -64390,19 +64394,20 @@ function extractRawscriptInline(id, summary, rawscript, mapping, separator, assi
|
|
|
64390
64394
|
}
|
|
64391
64395
|
function extractInlineScripts(modules, mapping = {}, separator = "/", defaultTs, pathAssigner, options) {
|
|
64392
64396
|
const assigner = pathAssigner ?? newPathAssigner(defaultTs ?? "bun", { skipInlineScriptSuffix: options?.skipInlineScriptSuffix });
|
|
64397
|
+
const failOnInlineDirective = options?.failOnInlineDirective ?? false;
|
|
64393
64398
|
return modules.flatMap((m) => {
|
|
64394
64399
|
if (m.value.type == "rawscript") {
|
|
64395
|
-
return extractRawscriptInline(m.id, m.summary, m.value, mapping, separator, assigner);
|
|
64400
|
+
return extractRawscriptInline(m.id, m.summary, m.value, mapping, separator, assigner, failOnInlineDirective);
|
|
64396
64401
|
} else if (m.value.type == "forloopflow") {
|
|
64397
|
-
return extractInlineScripts(m.value.modules, mapping, separator, defaultTs, assigner);
|
|
64402
|
+
return extractInlineScripts(m.value.modules, mapping, separator, defaultTs, assigner, options);
|
|
64398
64403
|
} else if (m.value.type == "branchall") {
|
|
64399
|
-
return m.value.branches.flatMap((b) => extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner));
|
|
64404
|
+
return m.value.branches.flatMap((b) => extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner, options));
|
|
64400
64405
|
} else if (m.value.type == "whileloopflow") {
|
|
64401
|
-
return extractInlineScripts(m.value.modules, mapping, separator, defaultTs, assigner);
|
|
64406
|
+
return extractInlineScripts(m.value.modules, mapping, separator, defaultTs, assigner, options);
|
|
64402
64407
|
} else if (m.value.type == "branchone") {
|
|
64403
64408
|
return [
|
|
64404
|
-
...m.value.branches.flatMap((b) => extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner)),
|
|
64405
|
-
...extractInlineScripts(m.value.default, mapping, separator, defaultTs, assigner)
|
|
64409
|
+
...m.value.branches.flatMap((b) => extractInlineScripts(b.modules, mapping, separator, defaultTs, assigner, options)),
|
|
64410
|
+
...extractInlineScripts(m.value.default, mapping, separator, defaultTs, assigner, options)
|
|
64406
64411
|
];
|
|
64407
64412
|
} else if (m.value.type == "aiagent") {
|
|
64408
64413
|
return (m.value.tools ?? []).flatMap((tool) => {
|
|
@@ -64410,7 +64415,7 @@ function extractInlineScripts(modules, mapping = {}, separator = "/", defaultTs,
|
|
|
64410
64415
|
if (!toolValue || toolValue.tool_type !== "flowmodule" || toolValue.type !== "rawscript") {
|
|
64411
64416
|
return [];
|
|
64412
64417
|
}
|
|
64413
|
-
return extractRawscriptInline(tool.id, tool.summary, toolValue, mapping, separator, assigner);
|
|
64418
|
+
return extractRawscriptInline(tool.id, tool.summary, toolValue, mapping, separator, assigner, failOnInlineDirective);
|
|
64414
64419
|
});
|
|
64415
64420
|
} else {
|
|
64416
64421
|
return [];
|
|
@@ -64786,12 +64791,16 @@ async function generateFlowLockInternal(folder, dryRun, workspace, opts, justUpd
|
|
|
64786
64791
|
const treePath = fileToTreePath.get(k) ?? folderNormalized + "/" + path10.basename(k, path10.extname(k));
|
|
64787
64792
|
return tree.isStale(treePath);
|
|
64788
64793
|
}) : changedScripts;
|
|
64789
|
-
|
|
64794
|
+
const missingFiles = [];
|
|
64795
|
+
await replaceInlineScripts(flowValue.value.modules, fileReader, exports_log, folder + SEP7, SEP7, locksToRemove, missingFiles);
|
|
64790
64796
|
if (flowValue.value.failure_module) {
|
|
64791
|
-
await replaceInlineScripts([flowValue.value.failure_module], fileReader, exports_log, folder + SEP7, SEP7, locksToRemove);
|
|
64797
|
+
await replaceInlineScripts([flowValue.value.failure_module], fileReader, exports_log, folder + SEP7, SEP7, locksToRemove, missingFiles);
|
|
64792
64798
|
}
|
|
64793
64799
|
if (flowValue.value.preprocessor_module) {
|
|
64794
|
-
await replaceInlineScripts([flowValue.value.preprocessor_module], fileReader, exports_log, folder + SEP7, SEP7, locksToRemove);
|
|
64800
|
+
await replaceInlineScripts([flowValue.value.preprocessor_module], fileReader, exports_log, folder + SEP7, SEP7, locksToRemove, missingFiles);
|
|
64801
|
+
}
|
|
64802
|
+
if (missingFiles.length > 0) {
|
|
64803
|
+
throw new Error(`Cannot regenerate lock for flow ${remote_path}: missing inline script file(s): ${missingFiles.join(", ")}. ` + `Either restore the file(s) or remove the !inline reference(s) from flow.yaml before retrying.`);
|
|
64795
64804
|
}
|
|
64796
64805
|
const tempScriptRefs = tree?.getTempScriptRefs(folderNormalized);
|
|
64797
64806
|
const savedNotes = flowValue.value.notes;
|
|
@@ -64804,12 +64813,13 @@ async function generateFlowLockInternal(folder, dryRun, workspace, opts, justUpd
|
|
|
64804
64813
|
const lockAssigner = newPathAssigner(opts.defaultTs ?? "bun", {
|
|
64805
64814
|
skipInlineScriptSuffix: getNonDottedPaths()
|
|
64806
64815
|
});
|
|
64807
|
-
const
|
|
64816
|
+
const extractOpts = { skipInlineScriptSuffix: getNonDottedPaths(), failOnInlineDirective: true };
|
|
64817
|
+
const inlineScripts = extractInlineScripts(flowValue.value.modules, currentMapping, SEP7, opts.defaultTs, lockAssigner, extractOpts);
|
|
64808
64818
|
if (flowValue.value.failure_module) {
|
|
64809
|
-
inlineScripts.push(...extractInlineScripts([flowValue.value.failure_module], currentMapping, SEP7, opts.defaultTs, lockAssigner));
|
|
64819
|
+
inlineScripts.push(...extractInlineScripts([flowValue.value.failure_module], currentMapping, SEP7, opts.defaultTs, lockAssigner, extractOpts));
|
|
64810
64820
|
}
|
|
64811
64821
|
if (flowValue.value.preprocessor_module) {
|
|
64812
|
-
inlineScripts.push(...extractInlineScripts([flowValue.value.preprocessor_module], currentMapping, SEP7, opts.defaultTs, lockAssigner));
|
|
64822
|
+
inlineScripts.push(...extractInlineScripts([flowValue.value.preprocessor_module], currentMapping, SEP7, opts.defaultTs, lockAssigner, extractOpts));
|
|
64813
64823
|
}
|
|
64814
64824
|
inlineScripts.forEach((s) => {
|
|
64815
64825
|
writeIfChanged(process.cwd() + SEP7 + folder + SEP7 + s.path, s.content);
|
|
@@ -65931,12 +65941,12 @@ function ZipFSElement(zip, useYaml, defaultTs, resourceTypeToFormatExtension, re
|
|
|
65931
65941
|
try {
|
|
65932
65942
|
const assigner = newPathAssigner(defaultTs, { skipInlineScriptSuffix: getNonDottedPaths() });
|
|
65933
65943
|
const inlineMapping = extractCurrentMapping(flow.value.modules, {}, flow.value.failure_module, flow.value.preprocessor_module);
|
|
65934
|
-
inlineScripts = extractInlineScripts(flow.value.modules, inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths() });
|
|
65944
|
+
inlineScripts = extractInlineScripts(flow.value.modules, inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths(), failOnInlineDirective: true });
|
|
65935
65945
|
if (flow.value.failure_module) {
|
|
65936
|
-
inlineScripts.push(...extractInlineScripts([flow.value.failure_module], inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths() }));
|
|
65946
|
+
inlineScripts.push(...extractInlineScripts([flow.value.failure_module], inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths(), failOnInlineDirective: true }));
|
|
65937
65947
|
}
|
|
65938
65948
|
if (flow.value.preprocessor_module) {
|
|
65939
|
-
inlineScripts.push(...extractInlineScripts([flow.value.preprocessor_module], inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths() }));
|
|
65949
|
+
inlineScripts.push(...extractInlineScripts([flow.value.preprocessor_module], inlineMapping, SEP9, defaultTs, assigner, { skipInlineScriptSuffix: getNonDottedPaths(), failOnInlineDirective: true }));
|
|
65940
65950
|
}
|
|
65941
65951
|
} catch (error2) {
|
|
65942
65952
|
error(`Failed to extract inline scripts for flow at path: ${p}`);
|
|
@@ -75684,7 +75694,7 @@ async function pushFlow(workspace, remotePath, localPath, message, permissionedA
|
|
|
75684
75694
|
await replaceInlineScripts([localFlow.value.preprocessor_module], fileReader, exports_log, localPath, SEP20, undefined, missingFiles);
|
|
75685
75695
|
}
|
|
75686
75696
|
if (missingFiles.length > 0) {
|
|
75687
|
-
|
|
75697
|
+
throw new Error(`Cannot push flow ${remotePath}: missing inline script file(s): ${missingFiles.join(", ")}. ` + `Either restore the file(s) or remove the !inline reference(s) from flow.yaml before pushing.`);
|
|
75688
75698
|
}
|
|
75689
75699
|
const hasOnBehalfOf = localFlow.has_on_behalf_of ?? !!localFlow.on_behalf_of_email;
|
|
75690
75700
|
delete localFlow.has_on_behalf_of;
|
|
@@ -84945,9 +84955,9 @@ name: raw-app
|
|
|
84945
84955
|
description: MUST use when creating raw apps.
|
|
84946
84956
|
---
|
|
84947
84957
|
|
|
84948
|
-
# Windmill Raw Apps
|
|
84958
|
+
# Windmill Raw Apps — CLI workflow
|
|
84949
84959
|
|
|
84950
|
-
|
|
84960
|
+
This guide covers raw apps from the terminal: scaffolding via \`wmill app new\`, the on-disk layout, and the file-based conventions the CLI uses to represent backend runnables and data table configuration. The platform shape (how a raw app behaves at runtime — frontend bundling, runnable types, datatable SDK calls) is covered in the companion authoring guide.
|
|
84951
84961
|
|
|
84952
84962
|
## Creating a Raw App
|
|
84953
84963
|
|
|
@@ -85016,7 +85026,7 @@ wmill app new
|
|
|
85016
85026
|
|
|
85017
85027
|
This is the wizard. It only works when run by a human in a real terminal. Don't call it this way from an agent.
|
|
85018
85028
|
|
|
85019
|
-
##
|
|
85029
|
+
## On-disk app layout
|
|
85020
85030
|
|
|
85021
85031
|
\`\`\`
|
|
85022
85032
|
my_app{{RAW_APP_SUFFIX}}/
|
|
@@ -85036,11 +85046,7 @@ my_app{{RAW_APP_SUFFIX}}/
|
|
|
85036
85046
|
└── *.sql # SQL files to apply via dev server
|
|
85037
85047
|
\`\`\`
|
|
85038
85048
|
|
|
85039
|
-
## Backend
|
|
85040
|
-
|
|
85041
|
-
Backend runnables are server-side scripts that your frontend can call. They live in the \`backend/\` folder.
|
|
85042
|
-
|
|
85043
|
-
### Creating a Backend Runnable
|
|
85049
|
+
## Backend runnables on disk
|
|
85044
85050
|
|
|
85045
85051
|
Add a code file to the \`backend/\` folder:
|
|
85046
85052
|
|
|
@@ -85050,7 +85056,7 @@ backend/<id>.<ext>
|
|
|
85050
85056
|
|
|
85051
85057
|
The runnable ID is the filename without extension. For example, \`get_user.ts\` creates a runnable with ID \`get_user\`.
|
|
85052
85058
|
|
|
85053
|
-
### Supported
|
|
85059
|
+
### Supported languages (extension-driven)
|
|
85054
85060
|
|
|
85055
85061
|
| Language | Extension | Example |
|
|
85056
85062
|
|------------------|--------------|------------------|
|
|
@@ -85072,27 +85078,14 @@ The runnable ID is the filename without extension. For example, \`get_user.ts\`
|
|
|
85072
85078
|
| C# | \`.cs\` | \`myFunc.cs\` |
|
|
85073
85079
|
| Java | \`.java\` | \`myFunc.java\` |
|
|
85074
85080
|
|
|
85075
|
-
|
|
85076
|
-
|
|
85077
|
-
**backend/get_user.ts:**
|
|
85078
|
-
\`\`\`typescript
|
|
85079
|
-
import * as wmill from 'windmill-client';
|
|
85080
|
-
|
|
85081
|
-
export async function main(user_id: string) {
|
|
85082
|
-
const sql = wmill.datatable();
|
|
85083
|
-
const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
|
|
85084
|
-
return user;
|
|
85085
|
-
}
|
|
85086
|
-
\`\`\`
|
|
85087
|
-
|
|
85088
|
-
After creating, tell the user they can generate lock files by running:
|
|
85081
|
+
After creating a runnable, tell the user they can generate lock files by running:
|
|
85089
85082
|
\`\`\`bash
|
|
85090
85083
|
wmill generate-metadata
|
|
85091
85084
|
\`\`\`
|
|
85092
85085
|
|
|
85093
|
-
### Optional YAML
|
|
85086
|
+
### Optional YAML configuration
|
|
85094
85087
|
|
|
85095
|
-
Add a \`<id>.yaml\` file to configure fields or static values:
|
|
85088
|
+
Add a \`<id>.yaml\` file alongside the code to configure fields or static values:
|
|
85096
85089
|
|
|
85097
85090
|
**backend/get_user.yaml:**
|
|
85098
85091
|
\`\`\`yaml
|
|
@@ -85103,7 +85096,7 @@ fields:
|
|
|
85103
85096
|
value: "default_user"
|
|
85104
85097
|
\`\`\`
|
|
85105
85098
|
|
|
85106
|
-
### Referencing
|
|
85099
|
+
### Referencing existing scripts
|
|
85107
85100
|
|
|
85108
85101
|
To use an existing Windmill script instead of inline code:
|
|
85109
85102
|
|
|
@@ -85119,32 +85112,9 @@ type: flow
|
|
|
85119
85112
|
path: f/my_folder/my_flow
|
|
85120
85113
|
\`\`\`
|
|
85121
85114
|
|
|
85122
|
-
|
|
85123
|
-
|
|
85124
|
-
Import from the auto-generated \`wmill.ts\`:
|
|
85125
|
-
|
|
85126
|
-
\`\`\`typescript
|
|
85127
|
-
import { backend } from './wmill';
|
|
85128
|
-
|
|
85129
|
-
// Call a backend runnable
|
|
85130
|
-
const user = await backend.get_user({ user_id: '123' });
|
|
85131
|
-
\`\`\`
|
|
85132
|
-
|
|
85133
|
-
The \`wmill.ts\` file provides type-safe access to all backend runnables.
|
|
85134
|
-
|
|
85135
|
-
## Data Tables
|
|
85136
|
-
|
|
85137
|
-
Raw apps can query Windmill datatables (PostgreSQL databases managed by Windmill).
|
|
85138
|
-
|
|
85139
|
-
### Critical Rules
|
|
85115
|
+
## Data tables — \`raw_app.yaml\` config
|
|
85140
85116
|
|
|
85141
|
-
|
|
85142
|
-
|
|
85143
|
-
2. **ADD TABLES BEFORE USING**: To use a new table, first add it to \`data.tables\` in \`raw_app.yaml\`.
|
|
85144
|
-
|
|
85145
|
-
3. **USE CONFIGURED DATATABLE/SCHEMA**: Check the app's \`raw_app.yaml\` for the default datatable and schema.
|
|
85146
|
-
|
|
85147
|
-
### Configuration in raw_app.yaml
|
|
85117
|
+
The \`data\` block in \`raw_app.yaml\` controls which tables the app can query.
|
|
85148
85118
|
|
|
85149
85119
|
\`\`\`yaml
|
|
85150
85120
|
data:
|
|
@@ -85156,48 +85126,9 @@ data:
|
|
|
85156
85126
|
\`\`\`
|
|
85157
85127
|
|
|
85158
85128
|
**Table reference formats:**
|
|
85159
|
-
- \`<datatable>\`
|
|
85160
|
-
- \`<datatable>/<table>\`
|
|
85161
|
-
- \`<datatable>/<schema>:<table>\`
|
|
85162
|
-
|
|
85163
|
-
### Querying in TypeScript (Bun/Deno)
|
|
85164
|
-
|
|
85165
|
-
\`\`\`typescript
|
|
85166
|
-
import * as wmill from 'windmill-client';
|
|
85167
|
-
|
|
85168
|
-
export async function main(user_id: string) {
|
|
85169
|
-
const sql = wmill.datatable(); // Or: wmill.datatable('other_datatable')
|
|
85170
|
-
|
|
85171
|
-
// Parameterized queries (safe from SQL injection)
|
|
85172
|
-
const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
|
|
85173
|
-
const users = await sql\`SELECT * FROM users WHERE active = \${true}\`.fetch();
|
|
85174
|
-
|
|
85175
|
-
// Insert/Update
|
|
85176
|
-
await sql\`INSERT INTO users (name, email) VALUES (\${name}, \${email})\`;
|
|
85177
|
-
await sql\`UPDATE users SET name = \${newName} WHERE id = \${user_id}\`;
|
|
85178
|
-
|
|
85179
|
-
return user;
|
|
85180
|
-
}
|
|
85181
|
-
\`\`\`
|
|
85182
|
-
|
|
85183
|
-
### Querying in Python
|
|
85184
|
-
|
|
85185
|
-
\`\`\`python
|
|
85186
|
-
import wmill
|
|
85187
|
-
|
|
85188
|
-
def main(user_id: str):
|
|
85189
|
-
db = wmill.datatable() # Or: wmill.datatable('other_datatable')
|
|
85190
|
-
|
|
85191
|
-
# Use $1, $2, etc. for parameters
|
|
85192
|
-
user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
|
|
85193
|
-
users = db.query('SELECT * FROM users WHERE active = $1', True).fetch()
|
|
85194
|
-
|
|
85195
|
-
# Insert/Update
|
|
85196
|
-
db.query('INSERT INTO users (name, email) VALUES ($1, $2)', name, email)
|
|
85197
|
-
db.query('UPDATE users SET name = $1 WHERE id = $2', new_name, user_id)
|
|
85198
|
-
|
|
85199
|
-
return user
|
|
85200
|
-
\`\`\`
|
|
85129
|
+
- \`<datatable>\` — All tables in the datatable
|
|
85130
|
+
- \`<datatable>/<table>\` — Specific table in public schema
|
|
85131
|
+
- \`<datatable>/<schema>:<table>\` — Table in specific schema
|
|
85201
85132
|
|
|
85202
85133
|
## SQL Migrations (sql_to_apply/)
|
|
85203
85134
|
|
|
@@ -85206,11 +85137,11 @@ The \`sql_to_apply/\` folder is for creating/modifying database tables during de
|
|
|
85206
85137
|
### Workflow
|
|
85207
85138
|
|
|
85208
85139
|
1. Create \`.sql\` files in \`sql_to_apply/\`
|
|
85209
|
-
2. Run \`wmill app dev\`
|
|
85140
|
+
2. Run \`wmill app dev\` — the dev server watches this folder
|
|
85210
85141
|
3. When SQL files change, a modal appears in the browser to confirm execution
|
|
85211
85142
|
4. After creating tables, **add them to \`data.tables\`** in \`raw_app.yaml\`
|
|
85212
85143
|
|
|
85213
|
-
### Example
|
|
85144
|
+
### Example migration
|
|
85214
85145
|
|
|
85215
85146
|
**sql_to_apply/001_create_users.sql:**
|
|
85216
85147
|
\`\`\`sql
|
|
@@ -85229,12 +85160,12 @@ data:
|
|
|
85229
85160
|
- main/users
|
|
85230
85161
|
\`\`\`
|
|
85231
85162
|
|
|
85232
|
-
### Migration
|
|
85163
|
+
### Migration best practices
|
|
85233
85164
|
|
|
85234
85165
|
- **Use idempotent SQL**: \`CREATE TABLE IF NOT EXISTS\`, etc.
|
|
85235
85166
|
- **Number files**: \`001_\`, \`002_\` for ordering
|
|
85236
85167
|
- **Always whitelist tables** after creation
|
|
85237
|
-
- This folder is NOT synced
|
|
85168
|
+
- This folder is NOT synced — it's for local development only
|
|
85238
85169
|
|
|
85239
85170
|
## CLI Commands
|
|
85240
85171
|
|
|
@@ -85250,14 +85181,145 @@ For everything else, tell the user which command fits their intent and let them
|
|
|
85250
85181
|
| \`wmill sync push\` | Deploy app to Windmill |
|
|
85251
85182
|
| \`wmill sync pull\` | Pull latest from Windmill |
|
|
85252
85183
|
|
|
85184
|
+
|
|
85185
|
+
|
|
85186
|
+
# Windmill Raw Apps
|
|
85187
|
+
|
|
85188
|
+
Raw apps let you build custom frontends with React, Svelte, or Vue that connect to Windmill backend runnables and datatables.
|
|
85189
|
+
|
|
85190
|
+
## App shape
|
|
85191
|
+
|
|
85192
|
+
A raw app has three logical parts:
|
|
85193
|
+
|
|
85194
|
+
- **Frontend** — bundled with esbuild from \`index.tsx\` as the entrypoint. Files include the entrypoint, components (\`App.tsx\`), styles, etc.
|
|
85195
|
+
- **Backend runnables** — server-side scripts the frontend calls, each addressed by a unique key.
|
|
85196
|
+
- **Data** — optional whitelisted datatables (managed PostgreSQL) that the backend runnables can query. The frontend never queries the database directly; backend runnables are the only bridge.
|
|
85197
|
+
|
|
85198
|
+
## Frontend
|
|
85199
|
+
|
|
85200
|
+
### Entrypoint
|
|
85201
|
+
|
|
85202
|
+
\`index.tsx\` is the bundling entrypoint. It typically renders a top-level \`App\` component. The bundler is esbuild.
|
|
85203
|
+
|
|
85204
|
+
### Generated bindings (\`wmill.d.ts\` / \`wmill.ts\`)
|
|
85205
|
+
|
|
85206
|
+
The frontend imports a generated module that mirrors the backend runnables. **Never write to it directly** — it gets regenerated whenever backend runnables change. Modifying it by hand will be overwritten.
|
|
85207
|
+
|
|
85208
|
+
### Calling backend runnables
|
|
85209
|
+
|
|
85210
|
+
Import the generated bindings and call the runnable like a function:
|
|
85211
|
+
|
|
85212
|
+
\`\`\`typescript
|
|
85213
|
+
import { backend } from './wmill';
|
|
85214
|
+
|
|
85215
|
+
// Call a backend runnable
|
|
85216
|
+
const user = await backend.get_user({ user_id: '123' });
|
|
85217
|
+
\`\`\`
|
|
85218
|
+
|
|
85219
|
+
The frontend cannot reach datatables, workspace items, or external services on its own — it goes through \`backend.<key>(args)\` for everything server-side.
|
|
85220
|
+
|
|
85221
|
+
## Backend runnables
|
|
85222
|
+
|
|
85223
|
+
Each runnable has a unique key (used to call it from the frontend) and one of four types:
|
|
85224
|
+
|
|
85225
|
+
| Type | What it is |
|
|
85226
|
+
|---|---|
|
|
85227
|
+
| \`inline\` | Custom code stored on the app itself. Most common for app-specific logic. |
|
|
85228
|
+
| \`script\` | Reference to an existing workspace script by path. |
|
|
85229
|
+
| \`flow\` | Reference to an existing workspace flow by path. |
|
|
85230
|
+
| \`hubscript\` | Reference to a hub script by path. |
|
|
85231
|
+
|
|
85232
|
+
### Inline runnables
|
|
85233
|
+
|
|
85234
|
+
Inline runnables carry their own source code. For file-based raw apps, the runnable language is determined by the backend file extension. The script must expose a \`main\` function as its entrypoint.
|
|
85235
|
+
|
|
85236
|
+
**TypeScript example** (\`backend/get_user.ts\`):
|
|
85237
|
+
|
|
85238
|
+
\`\`\`typescript
|
|
85239
|
+
import * as wmill from 'windmill-client';
|
|
85240
|
+
|
|
85241
|
+
export async function main(user_id: string) {
|
|
85242
|
+
const sql = wmill.datatable();
|
|
85243
|
+
const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
|
|
85244
|
+
return user;
|
|
85245
|
+
}
|
|
85246
|
+
\`\`\`
|
|
85247
|
+
|
|
85248
|
+
**Python example** (\`backend/get_user.py\`):
|
|
85249
|
+
|
|
85250
|
+
\`\`\`python
|
|
85251
|
+
import wmill
|
|
85252
|
+
|
|
85253
|
+
def main(user_id: str):
|
|
85254
|
+
db = wmill.datatable()
|
|
85255
|
+
user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
|
|
85256
|
+
return user
|
|
85257
|
+
\`\`\`
|
|
85258
|
+
|
|
85259
|
+
### Path runnables (script / flow / hubscript)
|
|
85260
|
+
|
|
85261
|
+
When \`type\` is \`script\`, \`flow\`, or \`hubscript\`, the runnable just stores a \`path\` to an existing workspace or hub item — no inline code. The referenced item's input/output schema becomes the runnable's surface.
|
|
85262
|
+
|
|
85263
|
+
### Static inputs
|
|
85264
|
+
|
|
85265
|
+
\`staticInputs\` is an optional \`Record<string, any>\` for arguments not overridable from the frontend. Useful with path runnables to pre-fill some args while leaving the rest to the frontend caller.
|
|
85266
|
+
|
|
85267
|
+
## Data Tables
|
|
85268
|
+
|
|
85269
|
+
Data tables are PostgreSQL databases managed by Windmill. Backend runnables query them via the \`wmill\` client; the frontend never queries them directly.
|
|
85270
|
+
|
|
85271
|
+
### Critical rules
|
|
85272
|
+
|
|
85273
|
+
1. **Whitelisted tables only**: a runnable can only query tables listed in the app's \`data.tables\` config. Tables not in this list are not accessible.
|
|
85274
|
+
2. **Add tables before using**: queries against unlisted tables fail at runtime. When you introduce a new table, register it in \`data.tables\` first.
|
|
85275
|
+
3. **Use the configured datatable/schema**: the app's \`data\` config sets the default datatable and schema; reference them consistently across runnables.
|
|
85276
|
+
|
|
85277
|
+
### Querying in TypeScript (Bun/Deno)
|
|
85278
|
+
|
|
85279
|
+
\`\`\`typescript
|
|
85280
|
+
import * as wmill from 'windmill-client';
|
|
85281
|
+
|
|
85282
|
+
export async function main(user_id: string) {
|
|
85283
|
+
const sql = wmill.datatable(); // Or: wmill.datatable('other_datatable')
|
|
85284
|
+
|
|
85285
|
+
// Parameterized queries (safe from SQL injection)
|
|
85286
|
+
const user = await sql\`SELECT * FROM users WHERE id = \${user_id}\`.fetchOne();
|
|
85287
|
+
const users = await sql\`SELECT * FROM users WHERE active = \${true}\`.fetch();
|
|
85288
|
+
|
|
85289
|
+
// Insert/Update
|
|
85290
|
+
await sql\`INSERT INTO users (name, email) VALUES (\${name}, \${email})\`;
|
|
85291
|
+
await sql\`UPDATE users SET name = \${newName} WHERE id = \${user_id}\`;
|
|
85292
|
+
|
|
85293
|
+
return user;
|
|
85294
|
+
}
|
|
85295
|
+
\`\`\`
|
|
85296
|
+
|
|
85297
|
+
### Querying in Python
|
|
85298
|
+
|
|
85299
|
+
\`\`\`python
|
|
85300
|
+
import wmill
|
|
85301
|
+
|
|
85302
|
+
def main(user_id: str):
|
|
85303
|
+
db = wmill.datatable() # Or: wmill.datatable('other_datatable')
|
|
85304
|
+
|
|
85305
|
+
# Use $1, $2, etc. for parameters
|
|
85306
|
+
user = db.query('SELECT * FROM users WHERE id = $1', user_id).fetch_one()
|
|
85307
|
+
users = db.query('SELECT * FROM users WHERE active = $1', True).fetch()
|
|
85308
|
+
|
|
85309
|
+
# Insert/Update
|
|
85310
|
+
db.query('INSERT INTO users (name, email) VALUES ($1, $2)', name, email)
|
|
85311
|
+
db.query('UPDATE users SET name = $1 WHERE id = $2', new_name, user_id)
|
|
85312
|
+
|
|
85313
|
+
return user
|
|
85314
|
+
\`\`\`
|
|
85315
|
+
|
|
85253
85316
|
## Best Practices
|
|
85254
85317
|
|
|
85255
|
-
1. **Check
|
|
85256
|
-
2. **Use parameterized queries**
|
|
85257
|
-
3. **Keep runnables focused**
|
|
85258
|
-
4. **Use descriptive
|
|
85259
|
-
5. **Always whitelist tables**
|
|
85260
|
-
6. **Generate locks** - tell the user to run \`wmill generate-metadata\` after adding/modifying backend runnables
|
|
85318
|
+
1. **Check existing tables** before creating new ones — reuse beats schema growth.
|
|
85319
|
+
2. **Use parameterized queries** — never concatenate user input into SQL.
|
|
85320
|
+
3. **Keep runnables focused** — one function per runnable; small surface area.
|
|
85321
|
+
4. **Use descriptive keys** — \`get_user\`, not \`a\`.
|
|
85322
|
+
5. **Always whitelist tables** — adding a runnable that queries a new table requires the table to be in \`data.tables\` first.
|
|
85261
85323
|
`,
|
|
85262
85324
|
triggers: `---
|
|
85263
85325
|
name: triggers
|
|
@@ -89624,7 +89686,7 @@ var config_default = command35;
|
|
|
89624
89686
|
|
|
89625
89687
|
// src/main.ts
|
|
89626
89688
|
await init_context();
|
|
89627
|
-
var VERSION = "1.
|
|
89689
|
+
var VERSION = "1.701.0";
|
|
89628
89690
|
async function checkVersionSafe(cmd) {
|
|
89629
89691
|
const mainCommand = cmd.getMainCommand();
|
|
89630
89692
|
const upgradeCommand = mainCommand.getCommand("upgrade");
|