@telorun/templating 0.4.1 → 0.6.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/README.md CHANGED
@@ -54,20 +54,13 @@ metadata:
54
54
  description: |
55
55
  A complete feedback collection REST API — no code, pure YAML.
56
56
  Persists entries to SQLite and serves them over HTTP.
57
+ imports:
58
+ Http: std/http-server@0.8.0
59
+ Sql: std/sql@0.5.1
57
60
  targets:
58
61
  - Migrations
59
62
  - Server
60
63
  ---
61
- kind: Telo.Import
62
- metadata:
63
- name: Http
64
- source: std/http-server@0.5.0
65
- ---
66
- kind: Telo.Import
67
- metadata:
68
- name: Sql
69
- source: std/sql@0.3.0
70
- ---
71
64
  # SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
72
65
  kind: Sql.Connection
73
66
  metadata:
@@ -127,7 +120,7 @@ routes:
127
120
  minLength: 1
128
121
  source:
129
122
  type: string
130
- required: [text]
123
+ required: [ text ]
131
124
  handler:
132
125
  kind: Sql.Exec
133
126
  connection:
@@ -157,7 +150,7 @@ routes:
157
150
  kind: Sql.Connection
158
151
  name: Db
159
152
  from: feedback
160
- columns: [id, text, source, score, created_at]
153
+ columns: [ id, text, source, score, created_at ]
161
154
  orderBy:
162
155
  - { column: created_at, direction: desc }
163
156
  response:
@@ -176,14 +169,14 @@ routes:
176
169
  properties:
177
170
  id:
178
171
  type: integer
179
- required: [id]
172
+ required: [ id ]
180
173
  handler:
181
174
  kind: Sql.Select
182
175
  connection:
183
176
  kind: Sql.Connection
184
177
  name: Db
185
178
  from: feedback
186
- columns: [id, text, source, score, created_at]
179
+ columns: [ id, text, source, score, created_at ]
187
180
  where:
188
181
  - { column: id, op: "=", value: "${{ request.params.id }}" }
189
182
  response:
@@ -1 +1 @@
1
- {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;gEAKgE;AAChE,eAAO,MAAM,cAAc,EAAE,SAAS,gBAAgB,EAA0C,CAAC;AAEjG,wBAAgB,qBAAqB,IAAI,wBAAwB,CAMhE;AAID;;;oBAGoB;AACpB,wBAAgB,eAAe,IAAI,wBAAwB,CAK1D"}
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;gEAKgE;AAChE,eAAO,MAAM,cAAc,EAAE,SAAS,gBAAgB,EAKrD,CAAC;AAEF,wBAAgB,qBAAqB,IAAI,wBAAwB,CAMhE;AAID;;;oBAGoB;AACpB,wBAAgB,eAAe,IAAI,wBAAwB,CAK1D"}
package/dist/builtins.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { celEngine } from "./engines/cel.js";
2
2
  import { literalEngine } from "./engines/literal.js";
3
3
  import { refEngine } from "./engines/ref.js";
4
+ import { sqlEngine } from "./engines/sql.js";
4
5
  import { TemplatingEngineRegistry } from "./registry.js";
5
6
  /** Single source of truth for the built-in templating engines. Every host
6
7
  * (kernel, analyzer, editor, vscode extension) calls `createDefaultRegistry`
@@ -8,7 +9,12 @@ import { TemplatingEngineRegistry } from "./registry.js";
8
9
  * agree on which engines exist. Per-host à-la-carte registration would let
9
10
  * a manifest validate clean in one host (e.g. `cel` only) and crash in
10
11
  * another (e.g. `cel + literal`); always ship the same set. */
11
- export const builtinEngines = [celEngine, literalEngine, refEngine];
12
+ export const builtinEngines = [
13
+ celEngine,
14
+ literalEngine,
15
+ refEngine,
16
+ sqlEngine,
17
+ ];
12
18
  export function createDefaultRegistry() {
13
19
  const registry = new TemplatingEngineRegistry();
14
20
  for (const engine of builtinEngines) {
@@ -0,0 +1,45 @@
1
+ /** Host-injected functions that need platform APIs the templating package must
2
+ * not import directly (Node `crypto` / `Buffer`), keeping it browser-safe. The
3
+ * kernel supplies real implementations; the analyzer omits them (the stubs
4
+ * throw, since static analysis never executes these). */
5
+ export interface CelHandlers {
6
+ sha256: (s: string) => string;
7
+ md5: (s: string) => string;
8
+ sha1: (s: string) => string;
9
+ sha512: (s: string) => string;
10
+ hmac: (algorithm: string, key: string, message: string) => string;
11
+ base64Encode: (s: string) => string;
12
+ base64Decode: (s: string) => string;
13
+ json: (value: unknown) => string;
14
+ }
15
+ export type CelFunctionCategory = "conversion" | "time" | "uuid" | "string" | "math" | "collection" | "json" | "encoding" | "hashing" | "null";
16
+ /** One entry in the CEL standard library — the single source of truth that both
17
+ * registers the function (`build`) and documents it (everything else). `telo
18
+ * cel functions` and `celFunctionCatalog()` read the metadata; `buildCelEnvironment`
19
+ * calls `build`. */
20
+ export interface CelFunctionDoc {
21
+ /** Bare function name (`nowIso`, `uuidv4`). */
22
+ readonly name: string;
23
+ /** Human-facing signature for docs (`nowIso(tz?): string`). May use `?` for
24
+ * optional args even though cel-js itself has no optional syntax. */
25
+ readonly signature: string;
26
+ /** Actual cel-js signatures to register — one per arity for an overloaded
27
+ * function. Defaults to `[signature]` when omitted (the common single-arity
28
+ * case, where `signature` is itself a valid cel-js signature). */
29
+ readonly register?: readonly string[];
30
+ readonly category: CelFunctionCategory;
31
+ readonly summary: string;
32
+ /** False → re-evaluates per call; in an `x-telo-eval: compile` field it bakes
33
+ * once at load. */
34
+ readonly deterministic: boolean;
35
+ /** Needs a `CelHandlers` implementation (Node `crypto` / `Buffer`); the
36
+ * analyzer's stub throws if such a function is actually evaluated. */
37
+ readonly hostBacked: boolean;
38
+ readonly build: (h: CelHandlers) => (...args: any[]) => unknown;
39
+ }
40
+ /** Public, build-free view of a catalog entry (for `--json` / docs). */
41
+ export type CelFunctionInfo = Omit<CelFunctionDoc, "build">;
42
+ export declare const CEL_FUNCTIONS: readonly CelFunctionDoc[];
43
+ /** Build-free catalog for the CLI / docs (`telo cel functions`). */
44
+ export declare function celFunctionCatalog(): CelFunctionInfo[];
45
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"AAEA;;;0DAG0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AAED,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,MAAM,GACN,MAAM,GACN,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,MAAM,GACN,UAAU,GACV,SAAS,GACT,MAAM,CAAC;AAEX;;;qBAGqB;AACrB,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;0EACsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;uEAEmE;IACnE,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;wBACoB;IACpB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;2EACuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;CACjE;AAED,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAyE5D,eAAO,MAAM,aAAa,EAAE,SAAS,cAAc,EAsZlD,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,kBAAkB,IAAI,eAAe,EAAE,CAEtD"}
@@ -0,0 +1,475 @@
1
+ import { v1, v3, v4, v5, v6, v7, validate as uuidValidate, version as uuidVersion } from "uuid";
2
+ const num = (x) => Number(x);
3
+ const minMax = (list, isMin) => {
4
+ if (!Array.isArray(list) || list.length === 0)
5
+ return null;
6
+ let best = list[0];
7
+ let bestN = num(best);
8
+ for (const x of list) {
9
+ const n = num(x);
10
+ if (isMin ? n < bestN : n > bestN) {
11
+ best = x;
12
+ bestN = n;
13
+ }
14
+ }
15
+ return best;
16
+ };
17
+ const sortList = (list) => [...list].sort((a, b) => {
18
+ if (typeof a === "number" || typeof a === "bigint") {
19
+ const d = num(a) - num(b);
20
+ return d < 0 ? -1 : d > 0 ? 1 : 0;
21
+ }
22
+ return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
23
+ });
24
+ /** `Intl.DateTimeFormat` is an ECMA-402 global in Node (full ICU) and browsers,
25
+ * so timezone handling needs no Node-only API and stays browser-safe. */
26
+ const zoneParts = (date, tz, opts) => {
27
+ const parts = {};
28
+ for (const p of new Intl.DateTimeFormat("en-US", { timeZone: tz, ...opts }).formatToParts(date)) {
29
+ parts[p.type] = p.value;
30
+ }
31
+ return parts;
32
+ };
33
+ /** Current instant as ISO-8601 in `tz`: UTC `…Z` for "UTC", else the zone's
34
+ * offset (e.g. `2026-06-06T18:30:00.000-05:00`). Uses only standard Intl
35
+ * fields and derives the offset arithmetically, so it needs no newer Intl
36
+ * type-lib features and stays portable. */
37
+ const isoInZone = (tz) => {
38
+ const now = new Date();
39
+ if (tz === "UTC" || tz === "Z")
40
+ return now.toISOString();
41
+ const p = zoneParts(now, tz, {
42
+ hourCycle: "h23",
43
+ year: "numeric",
44
+ month: "2-digit",
45
+ day: "2-digit",
46
+ hour: "2-digit",
47
+ minute: "2-digit",
48
+ second: "2-digit",
49
+ });
50
+ // Sub-second is timezone-independent; read it off the instant directly.
51
+ const ms = String(now.getUTCMilliseconds()).padStart(3, "0");
52
+ // Offset = the zone's wall-clock read as UTC, minus the real instant.
53
+ const asUtc = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second);
54
+ const offsetMin = Math.round((asUtc - now.getTime()) / 60000);
55
+ const offset = offsetMin === 0
56
+ ? "Z"
57
+ : `${offsetMin > 0 ? "+" : "-"}${String(Math.floor(Math.abs(offsetMin) / 60)).padStart(2, "0")}:${String(Math.abs(offsetMin) % 60).padStart(2, "0")}`;
58
+ return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}.${ms}${offset}`;
59
+ };
60
+ /** Current calendar date (`YYYY-MM-DD`) in `tz`. */
61
+ const dateInZone = (tz) => {
62
+ const now = new Date();
63
+ if (tz === "UTC" || tz === "Z")
64
+ return now.toISOString().slice(0, 10);
65
+ const p = zoneParts(now, tz, { year: "numeric", month: "2-digit", day: "2-digit" });
66
+ return `${p.year}-${p.month}-${p.day}`;
67
+ };
68
+ export const CEL_FUNCTIONS = [
69
+ // Collections
70
+ {
71
+ name: "join",
72
+ signature: "join(list, string): string",
73
+ category: "collection",
74
+ summary: "Join list elements into a string with a separator.",
75
+ deterministic: true,
76
+ hostBacked: false,
77
+ build: () => (list, sep) => list.map(String).join(sep),
78
+ },
79
+ {
80
+ name: "keys",
81
+ signature: "keys(map): list",
82
+ category: "collection",
83
+ summary: "List a map's keys.",
84
+ deterministic: true,
85
+ hostBacked: false,
86
+ build: () => (map) => map instanceof Map ? [...map.keys()] : Object.keys(map),
87
+ },
88
+ {
89
+ name: "values",
90
+ signature: "values(map): list",
91
+ category: "collection",
92
+ summary: "List a map's values.",
93
+ deterministic: true,
94
+ hostBacked: false,
95
+ build: () => (map) => map instanceof Map ? [...map.values()] : Object.values(map),
96
+ },
97
+ {
98
+ name: "distinct",
99
+ signature: "distinct(list): list",
100
+ category: "collection",
101
+ summary: "Remove duplicate elements, preserving order.",
102
+ deterministic: true,
103
+ hostBacked: false,
104
+ build: () => (list) => [...new Set(list)],
105
+ },
106
+ {
107
+ name: "reverse",
108
+ signature: "reverse(list): list",
109
+ category: "collection",
110
+ summary: "Reverse a list (copy; never mutates the input).",
111
+ deterministic: true,
112
+ hostBacked: false,
113
+ build: () => (list) => [...list].reverse(),
114
+ },
115
+ {
116
+ name: "flatten",
117
+ signature: "flatten(list): list",
118
+ category: "collection",
119
+ summary: "Flatten one level of nested lists.",
120
+ deterministic: true,
121
+ hostBacked: false,
122
+ build: () => (list) => list.flat(),
123
+ },
124
+ {
125
+ name: "sort",
126
+ signature: "sort(list): list",
127
+ category: "collection",
128
+ summary: "Sort a list numerically (numbers) or lexicographically; copy.",
129
+ deterministic: true,
130
+ hostBacked: false,
131
+ build: () => (list) => sortList(list),
132
+ },
133
+ // Strings
134
+ {
135
+ name: "lower",
136
+ signature: "lower(string): string",
137
+ category: "string",
138
+ summary: "Lowercase a string.",
139
+ deterministic: true,
140
+ hostBacked: false,
141
+ build: () => (s) => s.toLowerCase(),
142
+ },
143
+ {
144
+ name: "upper",
145
+ signature: "upper(string): string",
146
+ category: "string",
147
+ summary: "Uppercase a string.",
148
+ deterministic: true,
149
+ hostBacked: false,
150
+ build: () => (s) => s.toUpperCase(),
151
+ },
152
+ {
153
+ name: "trim",
154
+ signature: "trim(string): string",
155
+ category: "string",
156
+ summary: "Strip leading/trailing whitespace.",
157
+ deterministic: true,
158
+ hostBacked: false,
159
+ build: () => (s) => s.trim(),
160
+ },
161
+ {
162
+ name: "replace",
163
+ signature: "replace(string, string, string): string",
164
+ category: "string",
165
+ summary: "Replace all occurrences of a substring.",
166
+ deterministic: true,
167
+ hostBacked: false,
168
+ build: () => (s, a, b) => s.split(a).join(b),
169
+ },
170
+ {
171
+ name: "split",
172
+ signature: "split(string, string): list",
173
+ category: "string",
174
+ summary: "Split a string on a separator into a list.",
175
+ deterministic: true,
176
+ hostBacked: false,
177
+ build: () => (s, sep) => s.split(sep),
178
+ },
179
+ // Math
180
+ {
181
+ name: "abs",
182
+ signature: "abs(dyn): double",
183
+ category: "math",
184
+ summary: "Absolute value.",
185
+ deterministic: true,
186
+ hostBacked: false,
187
+ build: () => (x) => Math.abs(num(x)),
188
+ },
189
+ {
190
+ name: "floor",
191
+ signature: "floor(dyn): double",
192
+ category: "math",
193
+ summary: "Round down to an integer.",
194
+ deterministic: true,
195
+ hostBacked: false,
196
+ build: () => (x) => Math.floor(num(x)),
197
+ },
198
+ {
199
+ name: "ceil",
200
+ signature: "ceil(dyn): double",
201
+ category: "math",
202
+ summary: "Round up to an integer.",
203
+ deterministic: true,
204
+ hostBacked: false,
205
+ build: () => (x) => Math.ceil(num(x)),
206
+ },
207
+ {
208
+ name: "round",
209
+ signature: "round(dyn): double",
210
+ category: "math",
211
+ summary: "Round to the nearest integer.",
212
+ deterministic: true,
213
+ hostBacked: false,
214
+ build: () => (x) => Math.round(num(x)),
215
+ },
216
+ {
217
+ name: "min",
218
+ signature: "min(list): dyn",
219
+ category: "math",
220
+ summary: "Smallest element (by numeric value); null for an empty list.",
221
+ deterministic: true,
222
+ hostBacked: false,
223
+ build: () => (list) => minMax(list, true),
224
+ },
225
+ {
226
+ name: "max",
227
+ signature: "max(list): dyn",
228
+ category: "math",
229
+ summary: "Largest element (by numeric value); null for an empty list.",
230
+ deterministic: true,
231
+ hostBacked: false,
232
+ build: () => (list) => minMax(list, false),
233
+ },
234
+ // JSON
235
+ {
236
+ name: "json",
237
+ signature: "json(dyn): string",
238
+ category: "json",
239
+ summary: "Serialize any value to a JSON string.",
240
+ deterministic: true,
241
+ hostBacked: true,
242
+ build: (h) => (value) => h.json(value),
243
+ },
244
+ {
245
+ name: "parseJson",
246
+ signature: "parseJson(string): dyn",
247
+ category: "json",
248
+ summary: "Parse a JSON string into a value (numbers come back as doubles).",
249
+ deterministic: true,
250
+ hostBacked: false,
251
+ build: () => (s) => JSON.parse(s),
252
+ },
253
+ // Encoding
254
+ {
255
+ name: "base64Encode",
256
+ signature: "base64Encode(string): string",
257
+ category: "encoding",
258
+ summary: "Encode a UTF-8 string as base64.",
259
+ deterministic: true,
260
+ hostBacked: true,
261
+ build: (h) => (s) => h.base64Encode(s),
262
+ },
263
+ {
264
+ name: "base64Decode",
265
+ signature: "base64Decode(string): string",
266
+ category: "encoding",
267
+ summary: "Decode a base64 string to UTF-8.",
268
+ deterministic: true,
269
+ hostBacked: true,
270
+ build: (h) => (s) => h.base64Decode(s),
271
+ },
272
+ {
273
+ name: "urlEncode",
274
+ signature: "urlEncode(string): string",
275
+ category: "encoding",
276
+ summary: "Percent-encode a URI component.",
277
+ deterministic: true,
278
+ hostBacked: false,
279
+ build: () => (s) => encodeURIComponent(s),
280
+ },
281
+ {
282
+ name: "urlDecode",
283
+ signature: "urlDecode(string): string",
284
+ category: "encoding",
285
+ summary: "Decode a percent-encoded URI component.",
286
+ deterministic: true,
287
+ hostBacked: false,
288
+ build: () => (s) => decodeURIComponent(s),
289
+ },
290
+ // Hashing
291
+ {
292
+ name: "sha256",
293
+ signature: "sha256(string): string",
294
+ category: "hashing",
295
+ summary: "SHA-256 hash, hex-encoded.",
296
+ deterministic: true,
297
+ hostBacked: true,
298
+ build: (h) => (s) => h.sha256(s),
299
+ },
300
+ {
301
+ name: "md5",
302
+ signature: "md5(string): string",
303
+ category: "hashing",
304
+ summary: "MD5 hash, hex-encoded.",
305
+ deterministic: true,
306
+ hostBacked: true,
307
+ build: (h) => (s) => h.md5(s),
308
+ },
309
+ {
310
+ name: "sha1",
311
+ signature: "sha1(string): string",
312
+ category: "hashing",
313
+ summary: "SHA-1 hash, hex-encoded.",
314
+ deterministic: true,
315
+ hostBacked: true,
316
+ build: (h) => (s) => h.sha1(s),
317
+ },
318
+ {
319
+ name: "sha512",
320
+ signature: "sha512(string): string",
321
+ category: "hashing",
322
+ summary: "SHA-512 hash, hex-encoded.",
323
+ deterministic: true,
324
+ hostBacked: true,
325
+ build: (h) => (s) => h.sha512(s),
326
+ },
327
+ {
328
+ name: "hmac",
329
+ signature: "hmac(string, string, string): string",
330
+ category: "hashing",
331
+ summary: "HMAC of message under key for an algorithm (e.g. 'sha256'), hex.",
332
+ deterministic: true,
333
+ hostBacked: true,
334
+ build: (h) => (algo, key, msg) => h.hmac(algo, key, msg),
335
+ },
336
+ // Null handling
337
+ {
338
+ name: "default",
339
+ signature: "default(dyn, dyn): dyn",
340
+ category: "null",
341
+ summary: "Return the value, or the fallback when it is null.",
342
+ deterministic: true,
343
+ hostBacked: false,
344
+ build: () => (v, fallback) => v === null || v === undefined ? fallback : v,
345
+ },
346
+ {
347
+ name: "coalesce",
348
+ signature: "coalesce(list): dyn",
349
+ category: "null",
350
+ summary: "First non-null element of a list, or null.",
351
+ deterministic: true,
352
+ hostBacked: false,
353
+ build: () => (list) => {
354
+ const found = list.find((x) => x !== null && x !== undefined);
355
+ return found === undefined ? null : found;
356
+ },
357
+ },
358
+ // Time (non-deterministic). `nowIso`/`today` take an optional IANA timezone
359
+ // (default "UTC"); epoch values are absolute and take none.
360
+ {
361
+ name: "nowIso",
362
+ signature: "nowIso(tz?): string",
363
+ register: ["nowIso(): string", "nowIso(string): string"],
364
+ category: "time",
365
+ summary: "Current time as ISO-8601; UTC by default, or in the given IANA timezone.",
366
+ deterministic: false,
367
+ hostBacked: false,
368
+ build: () => (tz) => isoInZone(tz ?? "UTC"),
369
+ },
370
+ {
371
+ name: "today",
372
+ signature: "today(tz?): string",
373
+ register: ["today(): string", "today(string): string"],
374
+ category: "time",
375
+ summary: "Current calendar date (YYYY-MM-DD); UTC by default, or in the given IANA timezone.",
376
+ deterministic: false,
377
+ hostBacked: false,
378
+ build: () => (tz) => dateInZone(tz ?? "UTC"),
379
+ },
380
+ {
381
+ name: "nowMillis",
382
+ signature: "nowMillis(): int",
383
+ category: "time",
384
+ summary: "Current time as epoch milliseconds (absolute; timezone-independent).",
385
+ deterministic: false,
386
+ hostBacked: false,
387
+ build: () => () => BigInt(Date.now()),
388
+ },
389
+ {
390
+ name: "nowSeconds",
391
+ signature: "nowSeconds(): int",
392
+ category: "time",
393
+ summary: "Current time as epoch seconds (absolute; timezone-independent).",
394
+ deterministic: false,
395
+ hostBacked: false,
396
+ build: () => () => BigInt(Math.floor(Date.now() / 1000)),
397
+ },
398
+ // UUID
399
+ {
400
+ name: "uuidv1",
401
+ signature: "uuidv1(): string",
402
+ category: "uuid",
403
+ summary: "Time-based UUID (v1).",
404
+ deterministic: false,
405
+ hostBacked: false,
406
+ build: () => () => v1(),
407
+ },
408
+ {
409
+ name: "uuidv4",
410
+ signature: "uuidv4(): string",
411
+ category: "uuid",
412
+ summary: "Random UUID (v4).",
413
+ deterministic: false,
414
+ hostBacked: false,
415
+ build: () => () => v4(),
416
+ },
417
+ {
418
+ name: "uuidv6",
419
+ signature: "uuidv6(): string",
420
+ category: "uuid",
421
+ summary: "Time-ordered UUID (v6).",
422
+ deterministic: false,
423
+ hostBacked: false,
424
+ build: () => () => v6(),
425
+ },
426
+ {
427
+ name: "uuidv7",
428
+ signature: "uuidv7(): string",
429
+ category: "uuid",
430
+ summary: "Time-ordered UUID (v7).",
431
+ deterministic: false,
432
+ hostBacked: false,
433
+ build: () => () => v7(),
434
+ },
435
+ {
436
+ name: "uuidv3",
437
+ signature: "uuidv3(string, string): string",
438
+ category: "uuid",
439
+ summary: "Name-based UUID (v3, MD5) under a namespace UUID.",
440
+ deterministic: true,
441
+ hostBacked: false,
442
+ build: () => (name, ns) => v3(name, ns),
443
+ },
444
+ {
445
+ name: "uuidv5",
446
+ signature: "uuidv5(string, string): string",
447
+ category: "uuid",
448
+ summary: "Name-based UUID (v5, SHA-1) under a namespace UUID.",
449
+ deterministic: true,
450
+ hostBacked: false,
451
+ build: () => (name, ns) => v5(name, ns),
452
+ },
453
+ {
454
+ name: "uuidValidate",
455
+ signature: "uuidValidate(string): bool",
456
+ category: "uuid",
457
+ summary: "True if the string is a valid UUID.",
458
+ deterministic: true,
459
+ hostBacked: false,
460
+ build: () => (s) => uuidValidate(s),
461
+ },
462
+ {
463
+ name: "uuidVersion",
464
+ signature: "uuidVersion(string): int",
465
+ category: "uuid",
466
+ summary: "The version number of a UUID.",
467
+ deterministic: true,
468
+ hostBacked: false,
469
+ build: () => (s) => BigInt(uuidVersion(s)),
470
+ },
471
+ ];
472
+ /** Build-free catalog for the CLI / docs (`telo cel functions`). */
473
+ export function celFunctionCatalog() {
474
+ return CEL_FUNCTIONS.map(({ build: _build, ...info }) => info);
475
+ }
@@ -1,4 +1,4 @@
1
- import type { CompiledValue } from "@telorun/sdk";
1
+ import { type CompiledValue } from "@telorun/sdk";
2
2
  import type { Environment } from "@marcbachmann/cel-js";
3
3
  export declare const TEMPLATE_REGEX: RegExp;
4
4
  export declare const EXACT_TEMPLATE_REGEX: RegExp;
@@ -12,4 +12,18 @@ export declare function compileExpression(expr: string, env: Environment): Compi
12
12
  * with stringified expression results. If no expressions are present, returns
13
13
  * the input string unchanged. Throws on CEL syntax errors. */
14
14
  export declare function compileString(s: string, env: Environment): unknown;
15
+ /** Split an interpolated value into literal fragments and the evaluated values
16
+ * of its embedded expressions, instead of joining them into one string. Lets a
17
+ * consumer emit its own placeholders between fragments and bind the values
18
+ * separately (e.g. parameterized SQL). The invariant
19
+ * `fragments.length === values.length + 1` always holds.
20
+ *
21
+ * - plain string (no `${{ }}`) → `{ fragments: [s], values: [] }`
22
+ * - bare single expression `${{ x }}` → `{ fragments: ["", ""], values: [x] }`
23
+ * - interpolated `"a ${{ x }} b"` → `{ fragments: ["a ", " b"], values: [x] }`
24
+ */
25
+ export declare function toParameterized(value: unknown, ctx: Record<string, unknown>): {
26
+ fragments: string[];
27
+ values: unknown[];
28
+ };
15
29
  //# sourceMappingURL=compile.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/cel/compile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,eAAO,MAAM,cAAc,QAA8B,CAAC;AAC1D,eAAO,MAAM,oBAAoB,QAAqC,CAAC;AAEvE;;2CAE2C;AAC3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAO/E;AAED;;;;+DAI+D;AAC/D,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAuBlE"}
1
+ {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/cel/compile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,eAAO,MAAM,cAAc,QAA8B,CAAC;AAC1D,eAAO,MAAM,oBAAoB,QAAqC,CAAC;AAEvE;;2CAE2C;AAC3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,aAAa,CAO/E;AAED;;;;+DAI+D;AAC/D,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAwBlE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,OAAO,EAAE,CAAA;CAAE,CAsB5C"}
@@ -1,3 +1,4 @@
1
+ import { isCompiledValue } from "@telorun/sdk";
1
2
  export const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
2
3
  export const EXACT_TEMPLATE_REGEX = /^\s*\$\{\{\s*([^}]+?)\s*\}\}\s*$/;
3
4
  /** Compile a single CEL expression (no `${{ }}` wrapping) into a CompiledValue.
@@ -36,6 +37,42 @@ export function compileString(s, env) {
36
37
  return {
37
38
  __compiled: true,
38
39
  source: s,
40
+ parts,
39
41
  call: (ctx) => parts.map((p) => (typeof p === "string" ? p : String(p.call(ctx) ?? ""))).join(""),
40
42
  };
41
43
  }
44
+ /** Split an interpolated value into literal fragments and the evaluated values
45
+ * of its embedded expressions, instead of joining them into one string. Lets a
46
+ * consumer emit its own placeholders between fragments and bind the values
47
+ * separately (e.g. parameterized SQL). The invariant
48
+ * `fragments.length === values.length + 1` always holds.
49
+ *
50
+ * - plain string (no `${{ }}`) → `{ fragments: [s], values: [] }`
51
+ * - bare single expression `${{ x }}` → `{ fragments: ["", ""], values: [x] }`
52
+ * - interpolated `"a ${{ x }} b"` → `{ fragments: ["a ", " b"], values: [x] }`
53
+ */
54
+ export function toParameterized(value, ctx) {
55
+ if (typeof value === "string")
56
+ return { fragments: [value], values: [] };
57
+ if (!isCompiledValue(value)) {
58
+ throw new Error("toParameterized expects a string or CompiledValue");
59
+ }
60
+ if (!value.parts) {
61
+ return { fragments: ["", ""], values: [value.call(ctx)] };
62
+ }
63
+ const fragments = [];
64
+ const values = [];
65
+ let current = "";
66
+ for (const p of value.parts) {
67
+ if (typeof p === "string") {
68
+ current += p;
69
+ }
70
+ else {
71
+ fragments.push(current);
72
+ current = "";
73
+ values.push(p.call(ctx));
74
+ }
75
+ }
76
+ fragments.push(current);
77
+ return { fragments, values };
78
+ }