@telorun/templating 0.4.1 → 0.5.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 +7 -14
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +7 -1
- package/dist/cel/compile.d.ts +15 -1
- package/dist/cel/compile.d.ts.map +1 -1
- package/dist/cel/compile.js +37 -0
- package/dist/engines/cel.d.ts +7 -1
- package/dist/engines/cel.d.ts.map +1 -1
- package/dist/engines/cel.js +38 -30
- package/dist/engines/sql.d.ts +11 -0
- package/dist/engines/sql.d.ts.map +1 -0
- package/dist/engines/sql.js +38 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/package.json +2 -2
- package/src/builtins.ts +7 -1
- package/src/cel/compile.ts +39 -1
- package/src/engines/cel.ts +44 -35
- package/src/engines/sql.ts +44 -0
- package/src/index.ts +2 -0
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:
|
package/dist/builtins.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"
|
|
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 = [
|
|
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) {
|
package/dist/cel/compile.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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,
|
|
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"}
|
package/dist/cel/compile.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/engines/cel.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type { TemplatingEngine } from "../engine.js";
|
|
1
|
+
import type { AnalyzeEnv, EngineDiagnostic, TemplatingEngine } from "../engine.js";
|
|
2
|
+
/** Statically analyze one CEL expression against the effective context schema:
|
|
3
|
+
* parse → extract member-access chains → validate each chain → flag nullable
|
|
4
|
+
* access. Single source of truth shared by the `!cel` engine (one expression)
|
|
5
|
+
* and the `!sql` engine (one per `${{ }}` interpolation), so diagnostic wording
|
|
6
|
+
* can't drift between them. */
|
|
7
|
+
export declare function analyzeCelExpression(source: string, env: AnalyzeEnv): EngineDiagnostic[];
|
|
2
8
|
/** The `!cel` engine. Treats the entire tagged scalar as a single CEL
|
|
3
9
|
* expression — no `${{ }}` wrapping. Analysis runs the same chain validator
|
|
4
10
|
* as the untagged path: parse → extract member-access chains → validate each
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cel.d.ts","sourceRoot":"","sources":["../../src/engines/cel.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"cel.d.ts","sourceRoot":"","sources":["../../src/engines/cel.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEnF;;;;gCAIgC;AAChC,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,gBAAgB,EAAE,CAmCxF;AAED;;;kDAGkD;AAClD,eAAO,MAAM,SAAS,EAAE,gBAWvB,CAAC"}
|
package/dist/engines/cel.js
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
import { extractAccessChains, findNullableAccessIssues, validateChainAgainstSchema, } from "../cel/analyze.js";
|
|
2
2
|
import { compileExpression } from "../cel/compile.js";
|
|
3
|
+
/** Statically analyze one CEL expression against the effective context schema:
|
|
4
|
+
* parse → extract member-access chains → validate each chain → flag nullable
|
|
5
|
+
* access. Single source of truth shared by the `!cel` engine (one expression)
|
|
6
|
+
* and the `!sql` engine (one per `${{ }}` interpolation), so diagnostic wording
|
|
7
|
+
* can't drift between them. */
|
|
8
|
+
export function analyzeCelExpression(source, env) {
|
|
9
|
+
const out = [];
|
|
10
|
+
let parsed;
|
|
11
|
+
try {
|
|
12
|
+
parsed = env.celEnv.parse(source);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
out.push({
|
|
16
|
+
code: "CEL_SYNTAX_ERROR",
|
|
17
|
+
message: e instanceof Error ? e.message : String(e),
|
|
18
|
+
});
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
if (!env.contextSchema)
|
|
22
|
+
return out;
|
|
23
|
+
const chains = extractAccessChains(parsed.ast);
|
|
24
|
+
for (const chain of chains) {
|
|
25
|
+
const err = validateChainAgainstSchema(chain, env.contextSchema);
|
|
26
|
+
if (err)
|
|
27
|
+
out.push({ code: "CEL_UNKNOWN_FIELD", message: err });
|
|
28
|
+
}
|
|
29
|
+
for (const issue of findNullableAccessIssues(parsed.ast, env.contextSchema)) {
|
|
30
|
+
// Index access (member "[index]") attaches without a dot; a named field
|
|
31
|
+
// attaches with one — so the suggested CEL stays valid either way.
|
|
32
|
+
const access = issue.member === "[index]" ? issue.member : `.${issue.member}`;
|
|
33
|
+
out.push({
|
|
34
|
+
code: "CEL_NULLABLE_ACCESS",
|
|
35
|
+
message: `'${issue.path}' may be null — guard it (e.g. '${issue.path} != null && …' or '${issue.path} == null ? … : ${issue.path}${access}') before accessing '${access}'`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
3
40
|
/** The `!cel` engine. Treats the entire tagged scalar as a single CEL
|
|
4
41
|
* expression — no `${{ }}` wrapping. Analysis runs the same chain validator
|
|
5
42
|
* as the untagged path: parse → extract member-access chains → validate each
|
|
@@ -11,35 +48,6 @@ export const celEngine = {
|
|
|
11
48
|
return compileExpression(source, env.celEnv);
|
|
12
49
|
},
|
|
13
50
|
analyze(source, env) {
|
|
14
|
-
|
|
15
|
-
let parsed;
|
|
16
|
-
try {
|
|
17
|
-
parsed = env.celEnv.parse(source);
|
|
18
|
-
}
|
|
19
|
-
catch (e) {
|
|
20
|
-
out.push({
|
|
21
|
-
code: "CEL_SYNTAX_ERROR",
|
|
22
|
-
message: e instanceof Error ? e.message : String(e),
|
|
23
|
-
});
|
|
24
|
-
return out;
|
|
25
|
-
}
|
|
26
|
-
if (!env.contextSchema)
|
|
27
|
-
return out;
|
|
28
|
-
const chains = extractAccessChains(parsed.ast);
|
|
29
|
-
for (const chain of chains) {
|
|
30
|
-
const err = validateChainAgainstSchema(chain, env.contextSchema);
|
|
31
|
-
if (err)
|
|
32
|
-
out.push({ code: "CEL_UNKNOWN_FIELD", message: err });
|
|
33
|
-
}
|
|
34
|
-
for (const issue of findNullableAccessIssues(parsed.ast, env.contextSchema)) {
|
|
35
|
-
// Index access (member "[index]") attaches without a dot; a named field
|
|
36
|
-
// attaches with one — so the suggested CEL stays valid either way.
|
|
37
|
-
const access = issue.member === "[index]" ? issue.member : `.${issue.member}`;
|
|
38
|
-
out.push({
|
|
39
|
-
code: "CEL_NULLABLE_ACCESS",
|
|
40
|
-
message: `'${issue.path}' may be null — guard it (e.g. '${issue.path} != null && …' or '${issue.path} == null ? … : ${issue.path}${access}') before accessing '${access}'`,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
return out;
|
|
51
|
+
return analyzeCelExpression(source, env);
|
|
44
52
|
},
|
|
45
53
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isParameterizedSql, type ParameterizedSql } from "@telorun/sdk";
|
|
2
|
+
import type { TemplatingEngine } from "../engine.js";
|
|
3
|
+
export { isParameterizedSql, type ParameterizedSql };
|
|
4
|
+
/** The `!sql` engine. Treats the tagged scalar as a SQL string with `${{ }}`
|
|
5
|
+
* interpolations whose values are *bound*, not spliced. Unlike `!cel` (one bare
|
|
6
|
+
* expression) it keeps the literal text and each interpolation separate: at
|
|
7
|
+
* runtime `call()` returns a {@link ParameterizedSql} the consumer turns into a
|
|
8
|
+
* parameterized query. Generic expansion passes that object through untouched
|
|
9
|
+
* (it is the `call()` result), so it survives the step-level input expansion. */
|
|
10
|
+
export declare const sqlEngine: TemplatingEngine;
|
|
11
|
+
//# sourceMappingURL=sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/engines/sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAsB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG7F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,CAAC;AAErD;;;;;kFAKkF;AAClF,eAAO,MAAM,SAAS,EAAE,gBAqBvB,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isParameterizedSql } from "@telorun/sdk";
|
|
2
|
+
import { analyzeCelExpression } from "./cel.js";
|
|
3
|
+
import { compileString, toParameterized, TEMPLATE_REGEX } from "../cel/compile.js";
|
|
4
|
+
export { isParameterizedSql };
|
|
5
|
+
/** The `!sql` engine. Treats the tagged scalar as a SQL string with `${{ }}`
|
|
6
|
+
* interpolations whose values are *bound*, not spliced. Unlike `!cel` (one bare
|
|
7
|
+
* expression) it keeps the literal text and each interpolation separate: at
|
|
8
|
+
* runtime `call()` returns a {@link ParameterizedSql} the consumer turns into a
|
|
9
|
+
* parameterized query. Generic expansion passes that object through untouched
|
|
10
|
+
* (it is the `call()` result), so it survives the step-level input expansion. */
|
|
11
|
+
export const sqlEngine = {
|
|
12
|
+
name: "sql",
|
|
13
|
+
language: "sql",
|
|
14
|
+
compile(source, env) {
|
|
15
|
+
const inner = compileString(source, env.celEnv);
|
|
16
|
+
return {
|
|
17
|
+
__compiled: true,
|
|
18
|
+
source,
|
|
19
|
+
call: (ctx) => {
|
|
20
|
+
const { fragments, values } = toParameterized(inner, ctx);
|
|
21
|
+
return { __teloParameterized: true, fragments, values };
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
analyze(source, env) {
|
|
26
|
+
// Each `${{ }}` interpolation is its own CEL expression; reuse the shared
|
|
27
|
+
// per-expression analyzer so diagnostics match the `!cel` engine exactly.
|
|
28
|
+
return expressionsOf(source).flatMap((expr) => analyzeCelExpression(expr, env));
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
/** Extract each `${{ expr }}` body from a `!sql` template source. */
|
|
32
|
+
function expressionsOf(source) {
|
|
33
|
+
const exprs = [];
|
|
34
|
+
for (const m of source.matchAll(TEMPLATE_REGEX)) {
|
|
35
|
+
exprs.push(m[1].trim());
|
|
36
|
+
}
|
|
37
|
+
return exprs;
|
|
38
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { buildCelEnvironment, type CelHandlers } from "./cel/environment.js";
|
|
2
|
-
export { compileExpression, compileString, TEMPLATE_REGEX, EXACT_TEMPLATE_REGEX, } from "./cel/compile.js";
|
|
2
|
+
export { compileExpression, compileString, toParameterized, TEMPLATE_REGEX, EXACT_TEMPLATE_REGEX, } from "./cel/compile.js";
|
|
3
3
|
export { extractAccessChains, findNullableAccessIssues, INDEX_SEGMENT, validateChainAgainstSchema, } from "./cel/analyze.js";
|
|
4
4
|
export { walkCelExpressions } from "./cel/walk.js";
|
|
5
5
|
export { celEngine } from "./engines/cel.js";
|
|
6
6
|
export { literalEngine } from "./engines/literal.js";
|
|
7
7
|
export { refEngine } from "./engines/ref.js";
|
|
8
|
+
export { sqlEngine, isParameterizedSql, type ParameterizedSql } from "./engines/sql.js";
|
|
8
9
|
export { TemplatingEngineRegistry } from "./registry.js";
|
|
9
10
|
export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";
|
|
10
11
|
export type { AnalyzeEnv, CompileEnv, EngineDiagnostic, TemplatingEngine, } from "./engine.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,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;
|
|
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"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { buildCelEnvironment } from "./cel/environment.js";
|
|
2
|
-
export { compileExpression, compileString, TEMPLATE_REGEX, EXACT_TEMPLATE_REGEX, } from "./cel/compile.js";
|
|
2
|
+
export { compileExpression, compileString, toParameterized, TEMPLATE_REGEX, EXACT_TEMPLATE_REGEX, } from "./cel/compile.js";
|
|
3
3
|
export { extractAccessChains, findNullableAccessIssues, INDEX_SEGMENT, validateChainAgainstSchema, } from "./cel/analyze.js";
|
|
4
4
|
export { walkCelExpressions } from "./cel/walk.js";
|
|
5
5
|
export { celEngine } from "./engines/cel.js";
|
|
6
6
|
export { literalEngine } from "./engines/literal.js";
|
|
7
7
|
export { refEngine } from "./engines/ref.js";
|
|
8
|
+
export { sqlEngine, isParameterizedSql } from "./engines/sql.js";
|
|
8
9
|
export { TemplatingEngineRegistry } from "./registry.js";
|
|
9
10
|
export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";
|
|
10
11
|
export { isRefSentinel, isTaggedSentinel, makeTaggedSentinel } from "./sentinel.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/templating",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Telo Templating - Engine registry and shared CEL core for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@types/node": "^20.0.0",
|
|
43
43
|
"typescript": "^5.0.0",
|
|
44
44
|
"vitest": "^2.1.8",
|
|
45
|
-
"@telorun/sdk": "0.
|
|
45
|
+
"@telorun/sdk": "0.21.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@telorun/sdk": "*"
|
package/src/builtins.ts
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
|
import type { TemplatingEngine } from "./engine.js";
|
|
6
7
|
|
|
@@ -10,7 +11,12 @@ import type { TemplatingEngine } from "./engine.js";
|
|
|
10
11
|
* agree on which engines exist. Per-host à-la-carte registration would let
|
|
11
12
|
* a manifest validate clean in one host (e.g. `cel` only) and crash in
|
|
12
13
|
* another (e.g. `cel + literal`); always ship the same set. */
|
|
13
|
-
export const builtinEngines: readonly TemplatingEngine[] = [
|
|
14
|
+
export const builtinEngines: readonly TemplatingEngine[] = [
|
|
15
|
+
celEngine,
|
|
16
|
+
literalEngine,
|
|
17
|
+
refEngine,
|
|
18
|
+
sqlEngine,
|
|
19
|
+
];
|
|
14
20
|
|
|
15
21
|
export function createDefaultRegistry(): TemplatingEngineRegistry {
|
|
16
22
|
const registry = new TemplatingEngineRegistry();
|
package/src/cel/compile.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { isCompiledValue, type CompiledValue } from "@telorun/sdk";
|
|
2
2
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
3
3
|
|
|
4
4
|
export const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
@@ -41,7 +41,45 @@ export function compileString(s: string, env: Environment): unknown {
|
|
|
41
41
|
return {
|
|
42
42
|
__compiled: true,
|
|
43
43
|
source: s,
|
|
44
|
+
parts,
|
|
44
45
|
call: (ctx: Record<string, unknown>) =>
|
|
45
46
|
parts.map((p) => (typeof p === "string" ? p : String(p.call(ctx) ?? ""))).join(""),
|
|
46
47
|
} satisfies CompiledValue;
|
|
47
48
|
}
|
|
49
|
+
|
|
50
|
+
/** Split an interpolated value into literal fragments and the evaluated values
|
|
51
|
+
* of its embedded expressions, instead of joining them into one string. Lets a
|
|
52
|
+
* consumer emit its own placeholders between fragments and bind the values
|
|
53
|
+
* separately (e.g. parameterized SQL). The invariant
|
|
54
|
+
* `fragments.length === values.length + 1` always holds.
|
|
55
|
+
*
|
|
56
|
+
* - plain string (no `${{ }}`) → `{ fragments: [s], values: [] }`
|
|
57
|
+
* - bare single expression `${{ x }}` → `{ fragments: ["", ""], values: [x] }`
|
|
58
|
+
* - interpolated `"a ${{ x }} b"` → `{ fragments: ["a ", " b"], values: [x] }`
|
|
59
|
+
*/
|
|
60
|
+
export function toParameterized(
|
|
61
|
+
value: unknown,
|
|
62
|
+
ctx: Record<string, unknown>,
|
|
63
|
+
): { fragments: string[]; values: unknown[] } {
|
|
64
|
+
if (typeof value === "string") return { fragments: [value], values: [] };
|
|
65
|
+
if (!isCompiledValue(value)) {
|
|
66
|
+
throw new Error("toParameterized expects a string or CompiledValue");
|
|
67
|
+
}
|
|
68
|
+
if (!value.parts) {
|
|
69
|
+
return { fragments: ["", ""], values: [value.call(ctx)] };
|
|
70
|
+
}
|
|
71
|
+
const fragments: string[] = [];
|
|
72
|
+
const values: unknown[] = [];
|
|
73
|
+
let current = "";
|
|
74
|
+
for (const p of value.parts) {
|
|
75
|
+
if (typeof p === "string") {
|
|
76
|
+
current += p;
|
|
77
|
+
} else {
|
|
78
|
+
fragments.push(current);
|
|
79
|
+
current = "";
|
|
80
|
+
values.push(p.call(ctx));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
fragments.push(current);
|
|
84
|
+
return { fragments, values };
|
|
85
|
+
}
|
package/src/engines/cel.ts
CHANGED
|
@@ -4,7 +4,49 @@ import {
|
|
|
4
4
|
validateChainAgainstSchema,
|
|
5
5
|
} from "../cel/analyze.js";
|
|
6
6
|
import { compileExpression } from "../cel/compile.js";
|
|
7
|
-
import type { EngineDiagnostic, TemplatingEngine } from "../engine.js";
|
|
7
|
+
import type { AnalyzeEnv, EngineDiagnostic, TemplatingEngine } from "../engine.js";
|
|
8
|
+
|
|
9
|
+
/** Statically analyze one CEL expression against the effective context schema:
|
|
10
|
+
* parse → extract member-access chains → validate each chain → flag nullable
|
|
11
|
+
* access. Single source of truth shared by the `!cel` engine (one expression)
|
|
12
|
+
* and the `!sql` engine (one per `${{ }}` interpolation), so diagnostic wording
|
|
13
|
+
* can't drift between them. */
|
|
14
|
+
export function analyzeCelExpression(source: string, env: AnalyzeEnv): EngineDiagnostic[] {
|
|
15
|
+
const out: EngineDiagnostic[] = [];
|
|
16
|
+
|
|
17
|
+
let parsed: ReturnType<typeof env.celEnv.parse>;
|
|
18
|
+
try {
|
|
19
|
+
parsed = env.celEnv.parse(source);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
out.push({
|
|
22
|
+
code: "CEL_SYNTAX_ERROR",
|
|
23
|
+
message: e instanceof Error ? e.message : String(e),
|
|
24
|
+
});
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!env.contextSchema) return out;
|
|
29
|
+
|
|
30
|
+
const chains = extractAccessChains(parsed.ast);
|
|
31
|
+
for (const chain of chains) {
|
|
32
|
+
const err = validateChainAgainstSchema(chain, env.contextSchema as Record<string, any>);
|
|
33
|
+
if (err) out.push({ code: "CEL_UNKNOWN_FIELD", message: err });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const issue of findNullableAccessIssues(
|
|
37
|
+
parsed.ast,
|
|
38
|
+
env.contextSchema as Record<string, any>,
|
|
39
|
+
)) {
|
|
40
|
+
// Index access (member "[index]") attaches without a dot; a named field
|
|
41
|
+
// attaches with one — so the suggested CEL stays valid either way.
|
|
42
|
+
const access = issue.member === "[index]" ? issue.member : `.${issue.member}`;
|
|
43
|
+
out.push({
|
|
44
|
+
code: "CEL_NULLABLE_ACCESS",
|
|
45
|
+
message: `'${issue.path}' may be null — guard it (e.g. '${issue.path} != null && …' or '${issue.path} == null ? … : ${issue.path}${access}') before accessing '${access}'`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
8
50
|
|
|
9
51
|
/** The `!cel` engine. Treats the entire tagged scalar as a single CEL
|
|
10
52
|
* expression — no `${{ }}` wrapping. Analysis runs the same chain validator
|
|
@@ -19,39 +61,6 @@ export const celEngine: TemplatingEngine = {
|
|
|
19
61
|
},
|
|
20
62
|
|
|
21
63
|
analyze(source, env) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let parsed: ReturnType<typeof env.celEnv.parse>;
|
|
25
|
-
try {
|
|
26
|
-
parsed = env.celEnv.parse(source);
|
|
27
|
-
} catch (e) {
|
|
28
|
-
out.push({
|
|
29
|
-
code: "CEL_SYNTAX_ERROR",
|
|
30
|
-
message: e instanceof Error ? e.message : String(e),
|
|
31
|
-
});
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (!env.contextSchema) return out;
|
|
36
|
-
|
|
37
|
-
const chains = extractAccessChains(parsed.ast);
|
|
38
|
-
for (const chain of chains) {
|
|
39
|
-
const err = validateChainAgainstSchema(chain, env.contextSchema as Record<string, any>);
|
|
40
|
-
if (err) out.push({ code: "CEL_UNKNOWN_FIELD", message: err });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
for (const issue of findNullableAccessIssues(
|
|
44
|
-
parsed.ast,
|
|
45
|
-
env.contextSchema as Record<string, any>,
|
|
46
|
-
)) {
|
|
47
|
-
// Index access (member "[index]") attaches without a dot; a named field
|
|
48
|
-
// attaches with one — so the suggested CEL stays valid either way.
|
|
49
|
-
const access = issue.member === "[index]" ? issue.member : `.${issue.member}`;
|
|
50
|
-
out.push({
|
|
51
|
-
code: "CEL_NULLABLE_ACCESS",
|
|
52
|
-
message: `'${issue.path}' may be null — guard it (e.g. '${issue.path} != null && …' or '${issue.path} == null ? … : ${issue.path}${access}') before accessing '${access}'`,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
64
|
+
return analyzeCelExpression(source, env);
|
|
56
65
|
},
|
|
57
66
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isParameterizedSql, type CompiledValue, type ParameterizedSql } from "@telorun/sdk";
|
|
2
|
+
import { analyzeCelExpression } from "./cel.js";
|
|
3
|
+
import { compileString, toParameterized, TEMPLATE_REGEX } from "../cel/compile.js";
|
|
4
|
+
import type { TemplatingEngine } from "../engine.js";
|
|
5
|
+
|
|
6
|
+
export { isParameterizedSql, type ParameterizedSql };
|
|
7
|
+
|
|
8
|
+
/** The `!sql` engine. Treats the tagged scalar as a SQL string with `${{ }}`
|
|
9
|
+
* interpolations whose values are *bound*, not spliced. Unlike `!cel` (one bare
|
|
10
|
+
* expression) it keeps the literal text and each interpolation separate: at
|
|
11
|
+
* runtime `call()` returns a {@link ParameterizedSql} the consumer turns into a
|
|
12
|
+
* parameterized query. Generic expansion passes that object through untouched
|
|
13
|
+
* (it is the `call()` result), so it survives the step-level input expansion. */
|
|
14
|
+
export const sqlEngine: TemplatingEngine = {
|
|
15
|
+
name: "sql",
|
|
16
|
+
language: "sql",
|
|
17
|
+
|
|
18
|
+
compile(source, env) {
|
|
19
|
+
const inner = compileString(source, env.celEnv);
|
|
20
|
+
return {
|
|
21
|
+
__compiled: true,
|
|
22
|
+
source,
|
|
23
|
+
call: (ctx: Record<string, unknown>): ParameterizedSql => {
|
|
24
|
+
const { fragments, values } = toParameterized(inner, ctx);
|
|
25
|
+
return { __teloParameterized: true, fragments, values };
|
|
26
|
+
},
|
|
27
|
+
} satisfies CompiledValue;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
analyze(source, env) {
|
|
31
|
+
// Each `${{ }}` interpolation is its own CEL expression; reuse the shared
|
|
32
|
+
// per-expression analyzer so diagnostics match the `!cel` engine exactly.
|
|
33
|
+
return expressionsOf(source).flatMap((expr) => analyzeCelExpression(expr, env));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Extract each `${{ expr }}` body from a `!sql` template source. */
|
|
38
|
+
function expressionsOf(source: string): string[] {
|
|
39
|
+
const exprs: string[] = [];
|
|
40
|
+
for (const m of source.matchAll(TEMPLATE_REGEX)) {
|
|
41
|
+
exprs.push(m[1]!.trim());
|
|
42
|
+
}
|
|
43
|
+
return exprs;
|
|
44
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { buildCelEnvironment, type CelHandlers } from "./cel/environment.js";
|
|
|
2
2
|
export {
|
|
3
3
|
compileExpression,
|
|
4
4
|
compileString,
|
|
5
|
+
toParameterized,
|
|
5
6
|
TEMPLATE_REGEX,
|
|
6
7
|
EXACT_TEMPLATE_REGEX,
|
|
7
8
|
} from "./cel/compile.js";
|
|
@@ -16,6 +17,7 @@ export { walkCelExpressions } from "./cel/walk.js";
|
|
|
16
17
|
export { celEngine } from "./engines/cel.js";
|
|
17
18
|
export { literalEngine } from "./engines/literal.js";
|
|
18
19
|
export { refEngine } from "./engines/ref.js";
|
|
20
|
+
export { sqlEngine, isParameterizedSql, type ParameterizedSql } from "./engines/sql.js";
|
|
19
21
|
|
|
20
22
|
export { TemplatingEngineRegistry } from "./registry.js";
|
|
21
23
|
export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";
|