@telorun/templating 0.5.0 → 0.7.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 +2 -2
- package/dist/cel/catalog.d.ts +47 -0
- package/dist/cel/catalog.d.ts.map +1 -0
- package/dist/cel/catalog.js +473 -0
- package/dist/cel/environment.d.ts +17 -9
- package/dist/cel/environment.d.ts.map +1 -1
- package/dist/cel/environment.js +63 -20
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +5 -3
- package/src/cel/catalog.ts +540 -0
- package/src/cel/environment.ts +62 -24
- package/src/index.ts +7 -0
package/dist/cel/environment.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { Environment } from "@marcbachmann/cel-js";
|
|
2
2
|
import { Stream } from "@telorun/sdk";
|
|
3
|
+
import { CEL_FUNCTIONS } from "./catalog.js";
|
|
3
4
|
const stub = (name) => () => {
|
|
4
5
|
throw new Error(`${name}() is not available in this environment. ` +
|
|
5
6
|
`Construct StaticAnalyzer or Loader with celHandlers to enable it.`);
|
|
6
7
|
};
|
|
7
8
|
const STUB_HANDLERS = {
|
|
8
9
|
sha256: stub("sha256"),
|
|
10
|
+
md5: stub("md5"),
|
|
11
|
+
sha1: stub("sha1"),
|
|
12
|
+
sha512: stub("sha512"),
|
|
13
|
+
hmac: stub("hmac"),
|
|
14
|
+
base64Encode: stub("base64Encode"),
|
|
15
|
+
base64Decode: stub("base64Decode"),
|
|
9
16
|
json: stub("json"),
|
|
10
17
|
};
|
|
11
|
-
/** Build a CEL `Environment` with Telo's stdlib
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
18
|
+
/** Build a CEL `Environment` with Telo's stdlib. Every function comes from the
|
|
19
|
+
* single-source catalog (`CEL_FUNCTIONS`), so registration and the documented
|
|
20
|
+
* surface (`telo cel functions`) can never drift. Always registers the same
|
|
21
|
+
* signatures (so `env.check()` succeeds for type-inference); the host-injected
|
|
22
|
+
* handlers govern what `hostBacked` functions do at runtime. Analyzer-only
|
|
23
|
+
* callers can omit handlers (the stubs throw if such a function is evaluated);
|
|
24
|
+
* runtime callers (kernel) supply real ones.
|
|
15
25
|
*
|
|
16
26
|
* Also registers the `Stream` object type, backed by the `Stream` class from
|
|
17
27
|
* `@telorun/sdk`. CEL's type-checker rejects values whose constructor isn't
|
|
@@ -21,20 +31,53 @@ const STUB_HANDLERS = {
|
|
|
21
31
|
* no fields, so terminal access (passing the value through CEL) succeeds but
|
|
22
32
|
* member access raises a CEL error at runtime — matching the analyzer's
|
|
23
33
|
* static check on `x-telo-stream`-marked properties. */
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
/** Expand a documented signature that may contain `type?`-marked optional
|
|
35
|
+
* parameters into one cel-js registration signature per arity. For example,
|
|
36
|
+
* `"nowIso(string?): string"` produces `["nowIso(): string",
|
|
37
|
+
* "nowIso(string): string"]`. Required parameters must precede optional ones.
|
|
38
|
+
* Returns `[signature]` unchanged when no `?` is present or the signature
|
|
39
|
+
* cannot be parsed. */
|
|
40
|
+
export function deriveSignatures(signature) {
|
|
41
|
+
const m = signature.match(/^(\w+)\((.*?)\):\s*(.+)$/);
|
|
42
|
+
if (!m)
|
|
43
|
+
return [signature];
|
|
44
|
+
const name = m[1];
|
|
45
|
+
const paramsStr = m[2].trim();
|
|
46
|
+
const returnType = m[3].trim();
|
|
47
|
+
if (!paramsStr.includes("?"))
|
|
48
|
+
return [signature];
|
|
49
|
+
const params = paramsStr.split(",").map((p) => p.trim());
|
|
50
|
+
const required = [];
|
|
51
|
+
const optional = [];
|
|
52
|
+
for (const p of params) {
|
|
53
|
+
if (p.endsWith("?")) {
|
|
54
|
+
optional.push(p.slice(0, -1));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
if (optional.length > 0)
|
|
58
|
+
return [signature];
|
|
59
|
+
required.push(p);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (optional.length === 0)
|
|
63
|
+
return [signature];
|
|
64
|
+
return Array.from({ length: optional.length + 1 }, (_, i) => {
|
|
65
|
+
const allParams = [...required, ...optional.slice(0, i)];
|
|
66
|
+
return `${name}(${allParams.join(", ")}): ${returnType}`;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export function buildCelEnvironment(handlers = {}) {
|
|
70
|
+
const h = { ...STUB_HANDLERS, ...handlers };
|
|
71
|
+
let env = new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
|
|
72
|
+
for (const fn of CEL_FUNCTIONS) {
|
|
73
|
+
const impl = fn.build(h);
|
|
74
|
+
// `register` lists one cel-js signature per arity (overloaded functions).
|
|
75
|
+
// When absent, `deriveSignatures` expands `type?` optional-param notation
|
|
76
|
+
// into one registration per arity — so `nowIso(string?): string` registers
|
|
77
|
+
// both `nowIso(): string` and `nowIso(string): string` automatically.
|
|
78
|
+
for (const sig of fn.register ?? deriveSignatures(fn.signature)) {
|
|
79
|
+
env = env.registerFunction(sig, impl);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return env.registerType("Stream", Stream);
|
|
40
83
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildCelEnvironment, type CelHandlers } from "./cel/environment.js";
|
|
2
|
+
export { celFunctionCatalog, CEL_FUNCTIONS, type CelFunctionInfo, type CelFunctionDoc, type CelFunctionCategory, } from "./cel/catalog.js";
|
|
2
3
|
export { compileExpression, compileString, toParameterized, TEMPLATE_REGEX, EXACT_TEMPLATE_REGEX, } from "./cel/compile.js";
|
|
3
4
|
export { extractAccessChains, findNullableAccessIssues, INDEX_SEGMENT, validateChainAgainstSchema, } from "./cel/analyze.js";
|
|
4
5
|
export { walkCelExpressions } from "./cel/walk.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,cAAc,EACd,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAExF,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACvF,YAAY,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,cAAc,EACd,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAExF,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACvF,YAAY,EACV,UAAU,EACV,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { buildCelEnvironment } from "./cel/environment.js";
|
|
2
|
+
export { celFunctionCatalog, CEL_FUNCTIONS, } from "./cel/catalog.js";
|
|
2
3
|
export { compileExpression, compileString, toParameterized, TEMPLATE_REGEX, EXACT_TEMPLATE_REGEX, } from "./cel/compile.js";
|
|
3
4
|
export { extractAccessChains, findNullableAccessIssues, INDEX_SEGMENT, validateChainAgainstSchema, } from "./cel/analyze.js";
|
|
4
5
|
export { walkCelExpressions } from "./cel/walk.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/templating",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Telo Templating - Engine registry and shared CEL core for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -35,14 +35,16 @@
|
|
|
35
35
|
"src/**"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@marcbachmann/cel-js": "^7.
|
|
38
|
+
"@marcbachmann/cel-js": "^7.6.1",
|
|
39
|
+
"uuid": "^10.0.0",
|
|
39
40
|
"yaml": "^2.8.3"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/node": "^20.0.0",
|
|
44
|
+
"@types/uuid": "^10.0.0",
|
|
43
45
|
"typescript": "^5.0.0",
|
|
44
46
|
"vitest": "^2.1.8",
|
|
45
|
-
"@telorun/sdk": "0.
|
|
47
|
+
"@telorun/sdk": "0.23.0"
|
|
46
48
|
},
|
|
47
49
|
"peerDependencies": {
|
|
48
50
|
"@telorun/sdk": "*"
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { v1, v3, v4, v5, v6, v7, validate as uuidValidate, version as uuidVersion } from "uuid";
|
|
2
|
+
|
|
3
|
+
/** Host-injected functions that need platform APIs the templating package must
|
|
4
|
+
* not import directly (Node `crypto` / `Buffer`), keeping it browser-safe. The
|
|
5
|
+
* kernel supplies real implementations; the analyzer omits them (the stubs
|
|
6
|
+
* throw, since static analysis never executes these). */
|
|
7
|
+
export interface CelHandlers {
|
|
8
|
+
sha256: (s: string) => string;
|
|
9
|
+
md5: (s: string) => string;
|
|
10
|
+
sha1: (s: string) => string;
|
|
11
|
+
sha512: (s: string) => string;
|
|
12
|
+
hmac: (algorithm: string, key: string, message: string) => string;
|
|
13
|
+
base64Encode: (s: string) => string;
|
|
14
|
+
base64Decode: (s: string) => string;
|
|
15
|
+
json: (value: unknown) => string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type CelFunctionCategory =
|
|
19
|
+
| "conversion"
|
|
20
|
+
| "time"
|
|
21
|
+
| "uuid"
|
|
22
|
+
| "string"
|
|
23
|
+
| "math"
|
|
24
|
+
| "collection"
|
|
25
|
+
| "json"
|
|
26
|
+
| "encoding"
|
|
27
|
+
| "hashing"
|
|
28
|
+
| "null";
|
|
29
|
+
|
|
30
|
+
/** One entry in the CEL standard library — the single source of truth that both
|
|
31
|
+
* registers the function (`build`) and documents it (everything else). `telo
|
|
32
|
+
* cel functions` and `celFunctionCatalog()` read the metadata; `buildCelEnvironment`
|
|
33
|
+
* calls `build`. */
|
|
34
|
+
export interface CelFunctionDoc {
|
|
35
|
+
/** Bare function name (`nowIso`, `uuidv4`). */
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/** Human-facing signature for docs (`nowIso(tz?): string`). May use `?` for
|
|
38
|
+
* optional args even though cel-js itself has no optional syntax. */
|
|
39
|
+
readonly signature: string;
|
|
40
|
+
/** Actual cel-js signatures to register — one per arity for an overloaded
|
|
41
|
+
* function. When omitted, `deriveSignatures(signature)` is used: if the
|
|
42
|
+
* signature contains `type?`-marked optional params (e.g. `fn(string?): T`),
|
|
43
|
+
* it auto-expands to one registration per arity. Set `register` explicitly
|
|
44
|
+
* only when the auto-derivation is insufficient. */
|
|
45
|
+
readonly register?: readonly string[];
|
|
46
|
+
readonly category: CelFunctionCategory;
|
|
47
|
+
readonly summary: string;
|
|
48
|
+
/** False → re-evaluates per call; in an `x-telo-eval: compile` field it bakes
|
|
49
|
+
* once at load. */
|
|
50
|
+
readonly deterministic: boolean;
|
|
51
|
+
/** Needs a `CelHandlers` implementation (Node `crypto` / `Buffer`); the
|
|
52
|
+
* analyzer's stub throws if such a function is actually evaluated. */
|
|
53
|
+
readonly hostBacked: boolean;
|
|
54
|
+
readonly build: (h: CelHandlers) => (...args: any[]) => unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Public, build-free view of a catalog entry (for `--json` / docs). */
|
|
58
|
+
export type CelFunctionInfo = Omit<CelFunctionDoc, "build">;
|
|
59
|
+
|
|
60
|
+
const num = (x: unknown): number => Number(x);
|
|
61
|
+
|
|
62
|
+
const minMax = (list: unknown[], isMin: boolean): unknown => {
|
|
63
|
+
if (!Array.isArray(list) || list.length === 0) return null;
|
|
64
|
+
let best = list[0];
|
|
65
|
+
let bestN = num(best);
|
|
66
|
+
for (const x of list) {
|
|
67
|
+
const n = num(x);
|
|
68
|
+
if (isMin ? n < bestN : n > bestN) {
|
|
69
|
+
best = x;
|
|
70
|
+
bestN = n;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return best;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const sortList = (list: unknown[]): unknown[] =>
|
|
77
|
+
[...list].sort((a, b) => {
|
|
78
|
+
if (typeof a === "number" || typeof a === "bigint") {
|
|
79
|
+
const d = num(a) - num(b);
|
|
80
|
+
return d < 0 ? -1 : d > 0 ? 1 : 0;
|
|
81
|
+
}
|
|
82
|
+
return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/** `Intl.DateTimeFormat` is an ECMA-402 global in Node (full ICU) and browsers,
|
|
86
|
+
* so timezone handling needs no Node-only API and stays browser-safe. */
|
|
87
|
+
const zoneParts = (date: Date, tz: string, opts: Intl.DateTimeFormatOptions): Record<string, string> => {
|
|
88
|
+
const parts: Record<string, string> = {};
|
|
89
|
+
for (const p of new Intl.DateTimeFormat("en-US", { timeZone: tz, ...opts }).formatToParts(date)) {
|
|
90
|
+
parts[p.type] = p.value;
|
|
91
|
+
}
|
|
92
|
+
return parts;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Current instant as ISO-8601 in `tz`: UTC `…Z` for "UTC", else the zone's
|
|
96
|
+
* offset (e.g. `2026-06-06T18:30:00.000-05:00`). Uses only standard Intl
|
|
97
|
+
* fields and derives the offset arithmetically, so it needs no newer Intl
|
|
98
|
+
* type-lib features and stays portable. */
|
|
99
|
+
const isoInZone = (tz: string): string => {
|
|
100
|
+
const now = new Date();
|
|
101
|
+
if (tz === "UTC" || tz === "Z") return now.toISOString();
|
|
102
|
+
const p = zoneParts(now, tz, {
|
|
103
|
+
hourCycle: "h23",
|
|
104
|
+
year: "numeric",
|
|
105
|
+
month: "2-digit",
|
|
106
|
+
day: "2-digit",
|
|
107
|
+
hour: "2-digit",
|
|
108
|
+
minute: "2-digit",
|
|
109
|
+
second: "2-digit",
|
|
110
|
+
});
|
|
111
|
+
// Sub-second is timezone-independent; read it off the instant directly.
|
|
112
|
+
const ms = String(now.getUTCMilliseconds()).padStart(3, "0");
|
|
113
|
+
// Offset = the zone's wall-clock read as UTC, minus the real instant.
|
|
114
|
+
const asUtc = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second);
|
|
115
|
+
const offsetMin = Math.round((asUtc - now.getTime()) / 60000);
|
|
116
|
+
const offset =
|
|
117
|
+
offsetMin === 0
|
|
118
|
+
? "Z"
|
|
119
|
+
: `${offsetMin > 0 ? "+" : "-"}${String(Math.floor(Math.abs(offsetMin) / 60)).padStart(2, "0")}:${String(Math.abs(offsetMin) % 60).padStart(2, "0")}`;
|
|
120
|
+
return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}.${ms}${offset}`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Current calendar date (`YYYY-MM-DD`) in `tz`. */
|
|
124
|
+
const dateInZone = (tz: string): string => {
|
|
125
|
+
const now = new Date();
|
|
126
|
+
if (tz === "UTC" || tz === "Z") return now.toISOString().slice(0, 10);
|
|
127
|
+
const p = zoneParts(now, tz, { year: "numeric", month: "2-digit", day: "2-digit" });
|
|
128
|
+
return `${p.year}-${p.month}-${p.day}`;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
|
|
132
|
+
// Collections
|
|
133
|
+
{
|
|
134
|
+
name: "join",
|
|
135
|
+
signature: "join(list, string): string",
|
|
136
|
+
category: "collection",
|
|
137
|
+
summary: "Join list elements into a string with a separator.",
|
|
138
|
+
deterministic: true,
|
|
139
|
+
hostBacked: false,
|
|
140
|
+
build: () => (list: unknown[], sep: string) => list.map(String).join(sep),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "keys",
|
|
144
|
+
signature: "keys(map): list",
|
|
145
|
+
category: "collection",
|
|
146
|
+
summary: "List a map's keys.",
|
|
147
|
+
deterministic: true,
|
|
148
|
+
hostBacked: false,
|
|
149
|
+
build: () => (map: unknown) =>
|
|
150
|
+
map instanceof Map ? [...map.keys()] : Object.keys(map as Record<string, unknown>),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "values",
|
|
154
|
+
signature: "values(map): list",
|
|
155
|
+
category: "collection",
|
|
156
|
+
summary: "List a map's values.",
|
|
157
|
+
deterministic: true,
|
|
158
|
+
hostBacked: false,
|
|
159
|
+
build: () => (map: unknown) =>
|
|
160
|
+
map instanceof Map ? [...map.values()] : Object.values(map as Record<string, unknown>),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "distinct",
|
|
164
|
+
signature: "distinct(list): list",
|
|
165
|
+
category: "collection",
|
|
166
|
+
summary: "Remove duplicate elements, preserving order.",
|
|
167
|
+
deterministic: true,
|
|
168
|
+
hostBacked: false,
|
|
169
|
+
build: () => (list: unknown[]) => [...new Set(list)],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "reverse",
|
|
173
|
+
signature: "reverse(list): list",
|
|
174
|
+
category: "collection",
|
|
175
|
+
summary: "Reverse a list (copy; never mutates the input).",
|
|
176
|
+
deterministic: true,
|
|
177
|
+
hostBacked: false,
|
|
178
|
+
build: () => (list: unknown[]) => [...list].reverse(),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "flatten",
|
|
182
|
+
signature: "flatten(list): list",
|
|
183
|
+
category: "collection",
|
|
184
|
+
summary: "Flatten one level of nested lists.",
|
|
185
|
+
deterministic: true,
|
|
186
|
+
hostBacked: false,
|
|
187
|
+
build: () => (list: unknown[]) => list.flat(),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "sort",
|
|
191
|
+
signature: "sort(list): list",
|
|
192
|
+
category: "collection",
|
|
193
|
+
summary: "Sort a list numerically (numbers) or lexicographically; copy.",
|
|
194
|
+
deterministic: true,
|
|
195
|
+
hostBacked: false,
|
|
196
|
+
build: () => (list: unknown[]) => sortList(list),
|
|
197
|
+
},
|
|
198
|
+
// Strings
|
|
199
|
+
{
|
|
200
|
+
name: "lower",
|
|
201
|
+
signature: "lower(string): string",
|
|
202
|
+
category: "string",
|
|
203
|
+
summary: "Lowercase a string.",
|
|
204
|
+
deterministic: true,
|
|
205
|
+
hostBacked: false,
|
|
206
|
+
build: () => (s: string) => s.toLowerCase(),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "upper",
|
|
210
|
+
signature: "upper(string): string",
|
|
211
|
+
category: "string",
|
|
212
|
+
summary: "Uppercase a string.",
|
|
213
|
+
deterministic: true,
|
|
214
|
+
hostBacked: false,
|
|
215
|
+
build: () => (s: string) => s.toUpperCase(),
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "trim",
|
|
219
|
+
signature: "trim(string): string",
|
|
220
|
+
category: "string",
|
|
221
|
+
summary: "Strip leading/trailing whitespace.",
|
|
222
|
+
deterministic: true,
|
|
223
|
+
hostBacked: false,
|
|
224
|
+
build: () => (s: string) => s.trim(),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "replace",
|
|
228
|
+
signature: "replace(string, string, string): string",
|
|
229
|
+
category: "string",
|
|
230
|
+
summary: "Replace all occurrences of a substring.",
|
|
231
|
+
deterministic: true,
|
|
232
|
+
hostBacked: false,
|
|
233
|
+
build: () => (s: string, a: string, b: string) => s.split(a).join(b),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "split",
|
|
237
|
+
signature: "split(string, string): list",
|
|
238
|
+
category: "string",
|
|
239
|
+
summary: "Split a string on a separator into a list.",
|
|
240
|
+
deterministic: true,
|
|
241
|
+
hostBacked: false,
|
|
242
|
+
build: () => (s: string, sep: string) => s.split(sep),
|
|
243
|
+
},
|
|
244
|
+
// Math
|
|
245
|
+
{
|
|
246
|
+
name: "abs",
|
|
247
|
+
signature: "abs(dyn): double",
|
|
248
|
+
category: "math",
|
|
249
|
+
summary: "Absolute value.",
|
|
250
|
+
deterministic: true,
|
|
251
|
+
hostBacked: false,
|
|
252
|
+
build: () => (x: unknown) => Math.abs(num(x)),
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "floor",
|
|
256
|
+
signature: "floor(dyn): double",
|
|
257
|
+
category: "math",
|
|
258
|
+
summary: "Round down to an integer.",
|
|
259
|
+
deterministic: true,
|
|
260
|
+
hostBacked: false,
|
|
261
|
+
build: () => (x: unknown) => Math.floor(num(x)),
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "ceil",
|
|
265
|
+
signature: "ceil(dyn): double",
|
|
266
|
+
category: "math",
|
|
267
|
+
summary: "Round up to an integer.",
|
|
268
|
+
deterministic: true,
|
|
269
|
+
hostBacked: false,
|
|
270
|
+
build: () => (x: unknown) => Math.ceil(num(x)),
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
name: "round",
|
|
274
|
+
signature: "round(dyn): double",
|
|
275
|
+
category: "math",
|
|
276
|
+
summary: "Round to the nearest integer.",
|
|
277
|
+
deterministic: true,
|
|
278
|
+
hostBacked: false,
|
|
279
|
+
build: () => (x: unknown) => Math.round(num(x)),
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "min",
|
|
283
|
+
signature: "min(list): dyn",
|
|
284
|
+
category: "math",
|
|
285
|
+
summary: "Smallest element (by numeric value); null for an empty list.",
|
|
286
|
+
deterministic: true,
|
|
287
|
+
hostBacked: false,
|
|
288
|
+
build: () => (list: unknown[]) => minMax(list, true),
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "max",
|
|
292
|
+
signature: "max(list): dyn",
|
|
293
|
+
category: "math",
|
|
294
|
+
summary: "Largest element (by numeric value); null for an empty list.",
|
|
295
|
+
deterministic: true,
|
|
296
|
+
hostBacked: false,
|
|
297
|
+
build: () => (list: unknown[]) => minMax(list, false),
|
|
298
|
+
},
|
|
299
|
+
// JSON
|
|
300
|
+
{
|
|
301
|
+
name: "json",
|
|
302
|
+
signature: "json(dyn): string",
|
|
303
|
+
category: "json",
|
|
304
|
+
summary: "Serialize any value to a JSON string.",
|
|
305
|
+
deterministic: true,
|
|
306
|
+
hostBacked: true,
|
|
307
|
+
build: (h) => (value: unknown) => h.json(value),
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "parseJson",
|
|
311
|
+
signature: "parseJson(string): dyn",
|
|
312
|
+
category: "json",
|
|
313
|
+
summary: "Parse a JSON string into a value (numbers come back as doubles).",
|
|
314
|
+
deterministic: true,
|
|
315
|
+
hostBacked: false,
|
|
316
|
+
build: () => (s: string) => JSON.parse(s),
|
|
317
|
+
},
|
|
318
|
+
// Encoding
|
|
319
|
+
{
|
|
320
|
+
name: "base64Encode",
|
|
321
|
+
signature: "base64Encode(string): string",
|
|
322
|
+
category: "encoding",
|
|
323
|
+
summary: "Encode a UTF-8 string as base64.",
|
|
324
|
+
deterministic: true,
|
|
325
|
+
hostBacked: true,
|
|
326
|
+
build: (h) => (s: string) => h.base64Encode(s),
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "base64Decode",
|
|
330
|
+
signature: "base64Decode(string): string",
|
|
331
|
+
category: "encoding",
|
|
332
|
+
summary: "Decode a base64 string to UTF-8.",
|
|
333
|
+
deterministic: true,
|
|
334
|
+
hostBacked: true,
|
|
335
|
+
build: (h) => (s: string) => h.base64Decode(s),
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "urlEncode",
|
|
339
|
+
signature: "urlEncode(string): string",
|
|
340
|
+
category: "encoding",
|
|
341
|
+
summary: "Percent-encode a URI component.",
|
|
342
|
+
deterministic: true,
|
|
343
|
+
hostBacked: false,
|
|
344
|
+
build: () => (s: string) => encodeURIComponent(s),
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "urlDecode",
|
|
348
|
+
signature: "urlDecode(string): string",
|
|
349
|
+
category: "encoding",
|
|
350
|
+
summary: "Decode a percent-encoded URI component.",
|
|
351
|
+
deterministic: true,
|
|
352
|
+
hostBacked: false,
|
|
353
|
+
build: () => (s: string) => decodeURIComponent(s),
|
|
354
|
+
},
|
|
355
|
+
// Hashing
|
|
356
|
+
{
|
|
357
|
+
name: "sha256",
|
|
358
|
+
signature: "sha256(string): string",
|
|
359
|
+
category: "hashing",
|
|
360
|
+
summary: "SHA-256 hash, hex-encoded.",
|
|
361
|
+
deterministic: true,
|
|
362
|
+
hostBacked: true,
|
|
363
|
+
build: (h) => (s: string) => h.sha256(s),
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: "md5",
|
|
367
|
+
signature: "md5(string): string",
|
|
368
|
+
category: "hashing",
|
|
369
|
+
summary: "MD5 hash, hex-encoded.",
|
|
370
|
+
deterministic: true,
|
|
371
|
+
hostBacked: true,
|
|
372
|
+
build: (h) => (s: string) => h.md5(s),
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "sha1",
|
|
376
|
+
signature: "sha1(string): string",
|
|
377
|
+
category: "hashing",
|
|
378
|
+
summary: "SHA-1 hash, hex-encoded.",
|
|
379
|
+
deterministic: true,
|
|
380
|
+
hostBacked: true,
|
|
381
|
+
build: (h) => (s: string) => h.sha1(s),
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "sha512",
|
|
385
|
+
signature: "sha512(string): string",
|
|
386
|
+
category: "hashing",
|
|
387
|
+
summary: "SHA-512 hash, hex-encoded.",
|
|
388
|
+
deterministic: true,
|
|
389
|
+
hostBacked: true,
|
|
390
|
+
build: (h) => (s: string) => h.sha512(s),
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "hmac",
|
|
394
|
+
signature: "hmac(string, string, string): string",
|
|
395
|
+
category: "hashing",
|
|
396
|
+
summary: "HMAC of message under key for an algorithm (e.g. 'sha256'), hex.",
|
|
397
|
+
deterministic: true,
|
|
398
|
+
hostBacked: true,
|
|
399
|
+
build: (h) => (algo: string, key: string, msg: string) => h.hmac(algo, key, msg),
|
|
400
|
+
},
|
|
401
|
+
// Null handling
|
|
402
|
+
{
|
|
403
|
+
name: "default",
|
|
404
|
+
signature: "default(dyn, dyn): dyn",
|
|
405
|
+
category: "null",
|
|
406
|
+
summary: "Return the value, or the fallback when it is null.",
|
|
407
|
+
deterministic: true,
|
|
408
|
+
hostBacked: false,
|
|
409
|
+
build: () => (v: unknown, fallback: unknown) =>
|
|
410
|
+
v === null || v === undefined ? fallback : v,
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
name: "coalesce",
|
|
414
|
+
signature: "coalesce(list): dyn",
|
|
415
|
+
category: "null",
|
|
416
|
+
summary: "First non-null element of a list, or null.",
|
|
417
|
+
deterministic: true,
|
|
418
|
+
hostBacked: false,
|
|
419
|
+
build: () => (list: unknown[]) => {
|
|
420
|
+
const found = list.find((x) => x !== null && x !== undefined);
|
|
421
|
+
return found === undefined ? null : found;
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
// Time (non-deterministic). `nowIso`/`today` take an optional IANA timezone
|
|
425
|
+
// (default "UTC"); epoch values are absolute and take none.
|
|
426
|
+
{
|
|
427
|
+
name: "nowIso",
|
|
428
|
+
signature: "nowIso(string?): string",
|
|
429
|
+
category: "time",
|
|
430
|
+
summary: "Current time as ISO-8601; UTC by default, or in the given IANA timezone.",
|
|
431
|
+
deterministic: false,
|
|
432
|
+
hostBacked: false,
|
|
433
|
+
build: () => (tz?: string) => isoInZone(tz ?? "UTC"),
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: "today",
|
|
437
|
+
signature: "today(string?): string",
|
|
438
|
+
category: "time",
|
|
439
|
+
summary: "Current calendar date (YYYY-MM-DD); UTC by default, or in the given IANA timezone.",
|
|
440
|
+
deterministic: false,
|
|
441
|
+
hostBacked: false,
|
|
442
|
+
build: () => (tz?: string) => dateInZone(tz ?? "UTC"),
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: "nowMillis",
|
|
446
|
+
signature: "nowMillis(): int",
|
|
447
|
+
category: "time",
|
|
448
|
+
summary: "Current time as epoch milliseconds (absolute; timezone-independent).",
|
|
449
|
+
deterministic: false,
|
|
450
|
+
hostBacked: false,
|
|
451
|
+
build: () => () => BigInt(Date.now()),
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: "nowSeconds",
|
|
455
|
+
signature: "nowSeconds(): int",
|
|
456
|
+
category: "time",
|
|
457
|
+
summary: "Current time as epoch seconds (absolute; timezone-independent).",
|
|
458
|
+
deterministic: false,
|
|
459
|
+
hostBacked: false,
|
|
460
|
+
build: () => () => BigInt(Math.floor(Date.now() / 1000)),
|
|
461
|
+
},
|
|
462
|
+
// UUID
|
|
463
|
+
{
|
|
464
|
+
name: "uuidv1",
|
|
465
|
+
signature: "uuidv1(): string",
|
|
466
|
+
category: "uuid",
|
|
467
|
+
summary: "Time-based UUID (v1).",
|
|
468
|
+
deterministic: false,
|
|
469
|
+
hostBacked: false,
|
|
470
|
+
build: () => () => v1(),
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "uuidv4",
|
|
474
|
+
signature: "uuidv4(): string",
|
|
475
|
+
category: "uuid",
|
|
476
|
+
summary: "Random UUID (v4).",
|
|
477
|
+
deterministic: false,
|
|
478
|
+
hostBacked: false,
|
|
479
|
+
build: () => () => v4(),
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "uuidv6",
|
|
483
|
+
signature: "uuidv6(): string",
|
|
484
|
+
category: "uuid",
|
|
485
|
+
summary: "Time-ordered UUID (v6).",
|
|
486
|
+
deterministic: false,
|
|
487
|
+
hostBacked: false,
|
|
488
|
+
build: () => () => v6(),
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: "uuidv7",
|
|
492
|
+
signature: "uuidv7(): string",
|
|
493
|
+
category: "uuid",
|
|
494
|
+
summary: "Time-ordered UUID (v7).",
|
|
495
|
+
deterministic: false,
|
|
496
|
+
hostBacked: false,
|
|
497
|
+
build: () => () => v7(),
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: "uuidv3",
|
|
501
|
+
signature: "uuidv3(string, string): string",
|
|
502
|
+
category: "uuid",
|
|
503
|
+
summary: "Name-based UUID (v3, MD5) under a namespace UUID.",
|
|
504
|
+
deterministic: true,
|
|
505
|
+
hostBacked: false,
|
|
506
|
+
build: () => (name: string, ns: string) => v3(name, ns),
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: "uuidv5",
|
|
510
|
+
signature: "uuidv5(string, string): string",
|
|
511
|
+
category: "uuid",
|
|
512
|
+
summary: "Name-based UUID (v5, SHA-1) under a namespace UUID.",
|
|
513
|
+
deterministic: true,
|
|
514
|
+
hostBacked: false,
|
|
515
|
+
build: () => (name: string, ns: string) => v5(name, ns),
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
name: "uuidValidate",
|
|
519
|
+
signature: "uuidValidate(string): bool",
|
|
520
|
+
category: "uuid",
|
|
521
|
+
summary: "True if the string is a valid UUID.",
|
|
522
|
+
deterministic: true,
|
|
523
|
+
hostBacked: false,
|
|
524
|
+
build: () => (s: string) => uuidValidate(s),
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "uuidVersion",
|
|
528
|
+
signature: "uuidVersion(string): int",
|
|
529
|
+
category: "uuid",
|
|
530
|
+
summary: "The version number of a UUID.",
|
|
531
|
+
deterministic: true,
|
|
532
|
+
hostBacked: false,
|
|
533
|
+
build: () => (s: string) => BigInt(uuidVersion(s)),
|
|
534
|
+
},
|
|
535
|
+
];
|
|
536
|
+
|
|
537
|
+
/** Build-free catalog for the CLI / docs (`telo cel functions`). */
|
|
538
|
+
export function celFunctionCatalog(): CelFunctionInfo[] {
|
|
539
|
+
return CEL_FUNCTIONS.map(({ build: _build, ...info }) => info);
|
|
540
|
+
}
|