diff --git a/BACKEND_DOCUMENTATION.MD b/BACKEND_DOCUMENTATION.MD index 69c845b..484f17a 100644 --- a/BACKEND_DOCUMENTATION.MD +++ b/BACKEND_DOCUMENTATION.MD @@ -686,7 +686,12 @@ Node-graph automations (React Flow) run against the connection. A workflow's `{{input.path}}` (dotted paths supported, e.g. `{{input.row.id}}`); values are rendered as SQL literals (strings quoted with `''` doubling, numbers bare, booleans `TRUE`/`FALSE`, missing → `NULL`; a non-scalar value fails the node). - · `http`: `{ method, url, headers, body }` · `js`: `{ code }` + Add an `:ident` modifier — `{{input.field:ident}}` — to render the value as a + quoted SQL **identifier** instead (double-quoted with `""` doubling), for the + few statements that take a bare name and can't be parameterized, e.g. + `VACUUM (ANALYZE) {{input.relname:ident}}`. The value must be a non-empty + string; NULL/number/boolean fail the node so an identifier slot never silently + vanishes or injects. · `http`: `{ method, url, headers, body }` · `js`: `{ code }` - `js`: `{ code }` — the body receives `input`, returns the node output. Besides `input`/`console`, a curated **`crypto`** helper is in scope (no `require`): `crypto.hmac(algo, key, data, enc="hex")`, `crypto.hash(algo, data, enc="hex")`, diff --git a/server.js b/server.js index 2073843..2e8642e 100644 --- a/server.js +++ b/server.js @@ -1361,10 +1361,23 @@ function runUserJs(code, input) { // Dialect-agnostic on purpose: single-quoted strings with '' doubling, bare // numeric literals, and NULL are valid in every roadmap dialect. Non-scalar // values are an error, not silently stringified. +// +// An optional `:ident` modifier — `{{input.field:ident}}` — renders the value +// as a quoted SQL *identifier* (double-quoted with "" doubling) instead of a +// literal, for the few statements that take a bare name and can't be +// parameterized (e.g. VACUUM/ANALYZE/REINDEX ). Double-quoted identifiers +// are standard SQL, so this stays dialect-agnostic. The value must be a +// non-empty string; NULL/number/boolean are rejected so an identifier slot can +// never silently vanish or inject. function substituteWorkflowInput(sql, input) { - return sql.replace(/\{\{\s*input((?:\.[A-Za-z_][A-Za-z0-9_]*)+)\s*\}\}/g, (_, path) => { + return sql.replace(/\{\{\s*input((?:\.[A-Za-z_][A-Za-z0-9_]*)+)\s*(?::\s*(ident))?\s*\}\}/g, (_, path, mod) => { let v = input for (const key of path.slice(1).split('.')) v = v?.[key] + if (mod === 'ident') { + if (typeof v !== 'string' || v === '') + throw new Error(`{{input${path}:ident}} must be a non-empty string identifier (got ${v === null || v === undefined ? 'null' : typeof v})`) + return `"${v.replace(/"/g, '""')}"` + } if (v === null || v === undefined) return 'NULL' if (typeof v === 'number' && Number.isFinite(v)) return String(v) if (typeof v === 'boolean') return v ? 'TRUE' : 'FALSE'