@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.
@@ -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
- const out: EngineDiagnostic[] = [];
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
@@ -1,7 +1,15 @@
1
1
  export { buildCelEnvironment, type CelHandlers } from "./cel/environment.js";
2
+ export {
3
+ celFunctionCatalog,
4
+ CEL_FUNCTIONS,
5
+ type CelFunctionInfo,
6
+ type CelFunctionDoc,
7
+ type CelFunctionCategory,
8
+ } from "./cel/catalog.js";
2
9
  export {
3
10
  compileExpression,
4
11
  compileString,
12
+ toParameterized,
5
13
  TEMPLATE_REGEX,
6
14
  EXACT_TEMPLATE_REGEX,
7
15
  } from "./cel/compile.js";
@@ -16,6 +24,7 @@ export { walkCelExpressions } from "./cel/walk.js";
16
24
  export { celEngine } from "./engines/cel.js";
17
25
  export { literalEngine } from "./engines/literal.js";
18
26
  export { refEngine } from "./engines/ref.js";
27
+ export { sqlEngine, isParameterizedSql, type ParameterizedSql } from "./engines/sql.js";
19
28
 
20
29
  export { TemplatingEngineRegistry } from "./registry.js";
21
30
  export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";