@objectstack/formula 4.0.4
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/.turbo/turbo-build.log +22 -0
- package/LICENSE +202 -0
- package/README.md +9 -0
- package/dist/index.d.mts +240 -0
- package/dist/index.d.ts +240 -0
- package/dist/index.js +335 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +300 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
- package/src/cel-engine.test.ts +85 -0
- package/src/cel-engine.ts +123 -0
- package/src/index.ts +18 -0
- package/src/normalize.test.ts +99 -0
- package/src/normalize.ts +103 -0
- package/src/registry.test.ts +47 -0
- package/src/registry.ts +94 -0
- package/src/seed-eval.test.ts +83 -0
- package/src/seed-eval.ts +81 -0
- package/src/stdlib.ts +81 -0
- package/src/types.ts +91 -0
- package/tsconfig.json +9 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Expression, ExpressionInput } from '@objectstack/spec';
|
|
2
|
+
import { Environment } from '@marcbachmann/cel-js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @objectstack/formula — public types
|
|
6
|
+
*
|
|
7
|
+
* The expression engine surface is intentionally minimal:
|
|
8
|
+
*
|
|
9
|
+
* - {@link EvalContext}: input passed by call sites (hooks, seed loader, views).
|
|
10
|
+
* - {@link EvalResult}: discriminated union — never throws to the caller.
|
|
11
|
+
* - {@link DialectEngine}: contract any dialect (cel, js, cron) implements.
|
|
12
|
+
*
|
|
13
|
+
* The shape is shared across `cel`, `js` and `cron` so the kernel can route
|
|
14
|
+
* any persisted `Expression` to the correct engine without conditional logic.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runtime context for evaluating an expression.
|
|
19
|
+
*
|
|
20
|
+
* Every field is optional — call sites populate only what they have. The CEL
|
|
21
|
+
* engine binds `record`, `previous`, `input`, `os` directly as top-level
|
|
22
|
+
* variables when present.
|
|
23
|
+
*/
|
|
24
|
+
interface EvalContext {
|
|
25
|
+
/** Logical "now" snapshot — pinned per evaluation run for determinism. */
|
|
26
|
+
now?: Date;
|
|
27
|
+
/** Current authenticated subject (hook / action / view contexts). */
|
|
28
|
+
user?: {
|
|
29
|
+
id: string;
|
|
30
|
+
role?: string;
|
|
31
|
+
email?: string;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
/** Current organization (multi-tenant context). */
|
|
35
|
+
org?: {
|
|
36
|
+
id: string;
|
|
37
|
+
tier?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
/** Deployment environment marker. */
|
|
41
|
+
env?: 'prod' | 'dev' | 'test' | string;
|
|
42
|
+
/** Record-shaped data: target row, hook record, view row, etc. */
|
|
43
|
+
record?: Record<string, unknown>;
|
|
44
|
+
/** Previous record state for update hooks. */
|
|
45
|
+
previous?: Record<string, unknown>;
|
|
46
|
+
/** Action / flow input payload. */
|
|
47
|
+
input?: Record<string, unknown>;
|
|
48
|
+
/**
|
|
49
|
+
* Optional kernel API for `os.exists / os.count / os.lookup`.
|
|
50
|
+
* Implemented opportunistically by call sites that have a query engine.
|
|
51
|
+
*/
|
|
52
|
+
api?: {
|
|
53
|
+
exists?: (object: string, predicate: Expression) => boolean;
|
|
54
|
+
count?: (object: string, predicate: Expression) => number;
|
|
55
|
+
lookup?: (object: string, id: string) => Record<string, unknown> | null;
|
|
56
|
+
};
|
|
57
|
+
/** Free-form bag for niche call sites; merged onto the variable scope. */
|
|
58
|
+
extra?: Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
/** Result of a single evaluation. Never throws; callers branch on `ok`. */
|
|
61
|
+
type EvalResult<T = unknown> = {
|
|
62
|
+
ok: true;
|
|
63
|
+
value: T;
|
|
64
|
+
} | {
|
|
65
|
+
ok: false;
|
|
66
|
+
error: EvalError;
|
|
67
|
+
};
|
|
68
|
+
/** Structured error so AI callers can self-correct. */
|
|
69
|
+
interface EvalError {
|
|
70
|
+
/**
|
|
71
|
+
* - `parse` source string failed to parse to AST
|
|
72
|
+
* - `type` static type-check failed
|
|
73
|
+
* - `runtime` evaluation threw (division by zero, missing field, …)
|
|
74
|
+
* - `bounds` exceeded execution limits (AST size, depth, …)
|
|
75
|
+
* - `dialect` no engine registered for `expression.dialect`
|
|
76
|
+
*/
|
|
77
|
+
kind: 'parse' | 'type' | 'runtime' | 'bounds' | 'dialect';
|
|
78
|
+
message: string;
|
|
79
|
+
/** Source position when known. */
|
|
80
|
+
pos?: {
|
|
81
|
+
start: number;
|
|
82
|
+
end: number;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Contract every dialect engine implements. */
|
|
86
|
+
interface DialectEngine {
|
|
87
|
+
/** Dialect identifier — must match `Expression.dialect`. */
|
|
88
|
+
readonly dialect: string;
|
|
89
|
+
/**
|
|
90
|
+
* Parse + type-check + emit AST. Source-only — `expression.ast` is what
|
|
91
|
+
* actually gets persisted in `objectstack.json`.
|
|
92
|
+
*/
|
|
93
|
+
compile(source: string): EvalResult<unknown>;
|
|
94
|
+
/** Evaluate a fully-resolved expression in the given context. */
|
|
95
|
+
evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Dialect-pluggable Expression engine registry.
|
|
100
|
+
*
|
|
101
|
+
* Replaces the per-call-site `compileFormula` / `evaluateFormula` direct
|
|
102
|
+
* imports of the deleted custom engine. Call sites now ask the registry to
|
|
103
|
+
* dispatch by `expression.dialect`.
|
|
104
|
+
*
|
|
105
|
+
* Stub dialects (`js`, `cron`) are registered at module load with explicit
|
|
106
|
+
* `dialect`-error responses so call sites get a clear message instead of
|
|
107
|
+
* silent `undefined` (the old engine's anti-pattern).
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/** Register or replace a dialect engine. */
|
|
111
|
+
declare function register(engine: DialectEngine): void;
|
|
112
|
+
/** Look up a dialect engine without dispatching. */
|
|
113
|
+
declare function getEngine(dialect: string): DialectEngine | undefined;
|
|
114
|
+
/** Whether a dialect has a real (non-stub) implementation registered. */
|
|
115
|
+
declare function hasDialect(dialect: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* The unified evaluation entry point. Replaces the old direct calls to
|
|
118
|
+
* `evaluateFormula` from the deleted custom engine.
|
|
119
|
+
*/
|
|
120
|
+
declare const ExpressionEngine: {
|
|
121
|
+
register: typeof register;
|
|
122
|
+
getEngine: typeof getEngine;
|
|
123
|
+
hasDialect: typeof hasDialect;
|
|
124
|
+
/**
|
|
125
|
+
* Compile-only — parse + type-check, returning the engine-native AST. Used
|
|
126
|
+
* by `objectstack compile` to normalize source into AST in artifacts.
|
|
127
|
+
*/
|
|
128
|
+
compile(expr: Expression): EvalResult<unknown>;
|
|
129
|
+
/**
|
|
130
|
+
* Evaluate an expression in the given context. Never throws — branch on
|
|
131
|
+
* `result.ok`. Errors carry a `kind` for caller-side classification.
|
|
132
|
+
*/
|
|
133
|
+
evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* CEL dialect engine — wraps `@marcbachmann/cel-js` with the ObjectStack
|
|
138
|
+
* stdlib, bounded execution limits, and result coercion.
|
|
139
|
+
*
|
|
140
|
+
* Why a thin wrapper:
|
|
141
|
+
*
|
|
142
|
+
* - cel-js returns `BigInt` for ints. The kernel and CRM expect plain
|
|
143
|
+
* numbers, so we coerce at the boundary.
|
|
144
|
+
* - cel-js parses dotted names as receiver-typed methods; we register
|
|
145
|
+
* `now()`, `today()`, `daysFromNow()` as bare functions and let `os.*`
|
|
146
|
+
* refer to context data only (see {@link buildScope}).
|
|
147
|
+
* - Bounds (`maxAstNodes`, `maxDepth`, …) are enforced spec-wide so
|
|
148
|
+
* third-party plugins can't ship runaway predicates.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Default execution bounds. Picked conservatively — every metadata-authored
|
|
153
|
+
* expression we've seen is well under these. If you hit them, the expression
|
|
154
|
+
* is too complex for ObjectStack and should be moved to a hook (`dialect: js`).
|
|
155
|
+
*/
|
|
156
|
+
declare const DEFAULT_LIMITS: {
|
|
157
|
+
readonly maxAstNodes: 256;
|
|
158
|
+
readonly maxDepth: 32;
|
|
159
|
+
readonly maxListElements: 64;
|
|
160
|
+
readonly maxMapEntries: 64;
|
|
161
|
+
readonly maxCallArguments: 16;
|
|
162
|
+
};
|
|
163
|
+
declare const celEngine: DialectEngine;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* ObjectStack standard CEL function library.
|
|
167
|
+
*
|
|
168
|
+
* Registered into the per-evaluation `Environment` by the CEL engine. All
|
|
169
|
+
* functions are pure given a pinned `now` — that determinism is what makes
|
|
170
|
+
* `objectstack build` artifacts byte-stable across runs.
|
|
171
|
+
*
|
|
172
|
+
* Function naming intentionally avoids the `os.` prefix because cel-js binds
|
|
173
|
+
* dotted names to receiver types. Instead, the `os` namespace in CEL holds
|
|
174
|
+
* *data* (`os.user`, `os.org`, `os.env`) supplied by the caller's
|
|
175
|
+
* {@link EvalContext}.
|
|
176
|
+
*/
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Register the ObjectStack standard library into a CEL environment.
|
|
180
|
+
*
|
|
181
|
+
* The `now` resolver is closed over so each call uses the pinned
|
|
182
|
+
* `EvalContext.now` (or wall-clock fallback). Implementations are kept tiny
|
|
183
|
+
* and dependency-free — they're the contract surface for AI authors and must
|
|
184
|
+
* stay legible.
|
|
185
|
+
*/
|
|
186
|
+
declare function registerStdLib(env: Environment, now: () => Date): Environment;
|
|
187
|
+
/**
|
|
188
|
+
* Build the variable scope for a single evaluation. Absent fields are simply
|
|
189
|
+
* not bound — CEL macros (`has(record.foo)`) handle missing-key safely.
|
|
190
|
+
*/
|
|
191
|
+
declare function buildScope(ctx: EvalContext): Record<string, unknown>;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Seed-value resolver.
|
|
195
|
+
*
|
|
196
|
+
* `Dataset.records` accepts {@link SeedValue} = primitive | Expression | array
|
|
197
|
+
* | object — install-time resolution walks the tree and replaces any
|
|
198
|
+
* Expression node with its evaluated result. This is what makes
|
|
199
|
+
* `close_date: cel\`now() + duration("P30D")\`` resolve to *the customer's*
|
|
200
|
+
* "today + 30 days" instead of the developer's compile-time clock.
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
type SeedPrimitive = string | number | boolean | null | Date;
|
|
204
|
+
type SeedValue = SeedPrimitive | Expression | SeedValue[] | {
|
|
205
|
+
[key: string]: SeedValue;
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Recursively resolve a SeedValue. Records that contain Expression leaves are
|
|
209
|
+
* evaluated with `ctx`; other values are passed through unchanged.
|
|
210
|
+
*
|
|
211
|
+
* Returns the first failure encountered. Callers (seed loader) typically
|
|
212
|
+
* abort the whole record on failure rather than silently writing partial data.
|
|
213
|
+
*/
|
|
214
|
+
declare function resolveSeed(value: SeedValue, ctx: EvalContext): EvalResult<unknown>;
|
|
215
|
+
/**
|
|
216
|
+
* Resolve a single record (object of fields), pinning `ctx.now` so all
|
|
217
|
+
* expressions within see one logical clock.
|
|
218
|
+
*/
|
|
219
|
+
declare function resolveSeedRecord(record: Record<string, SeedValue>, ctx: EvalContext): EvalResult<Record<string, unknown>>;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Normalize an {@link ExpressionInput} (string shorthand OR full envelope) into
|
|
223
|
+
* a fully-resolved {@link Expression} carrying both `source` and `ast`.
|
|
224
|
+
*
|
|
225
|
+
* Returns an EvalResult so the caller can render a structured compile error
|
|
226
|
+
* pointing at the offending metadata path.
|
|
227
|
+
*/
|
|
228
|
+
declare function normalizeExpression(input: ExpressionInput): EvalResult<Expression>;
|
|
229
|
+
/**
|
|
230
|
+
* Walk an arbitrary JSON tree and normalize every embedded Expression in
|
|
231
|
+
* place. Used by the build pipeline to traverse the assembled metadata
|
|
232
|
+
* artifact. Returns the first error encountered (paired with the dotted path
|
|
233
|
+
* for diagnostics) or `null` when fully clean.
|
|
234
|
+
*/
|
|
235
|
+
declare function normalizeExpressionTree(root: unknown, path?: string[]): {
|
|
236
|
+
path: string;
|
|
237
|
+
error: EvalError;
|
|
238
|
+
} | null;
|
|
239
|
+
|
|
240
|
+
export { DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, ExpressionEngine, type SeedPrimitive, type SeedValue, buildScope, celEngine, getEngine, hasDialect, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_LIMITS: () => DEFAULT_LIMITS,
|
|
24
|
+
ExpressionEngine: () => ExpressionEngine,
|
|
25
|
+
buildScope: () => buildScope,
|
|
26
|
+
celEngine: () => celEngine,
|
|
27
|
+
getEngine: () => getEngine,
|
|
28
|
+
hasDialect: () => hasDialect,
|
|
29
|
+
normalizeExpression: () => normalizeExpression,
|
|
30
|
+
normalizeExpressionTree: () => normalizeExpressionTree,
|
|
31
|
+
register: () => register,
|
|
32
|
+
registerStdLib: () => registerStdLib,
|
|
33
|
+
resolveSeed: () => resolveSeed,
|
|
34
|
+
resolveSeedRecord: () => resolveSeedRecord
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/cel-engine.ts
|
|
39
|
+
var import_cel_js = require("@marcbachmann/cel-js");
|
|
40
|
+
|
|
41
|
+
// src/stdlib.ts
|
|
42
|
+
function startOfDayUtc(d) {
|
|
43
|
+
const out = new Date(d.getTime());
|
|
44
|
+
out.setUTCHours(0, 0, 0, 0);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function addDaysUtc(d, n) {
|
|
48
|
+
const out = new Date(d.getTime());
|
|
49
|
+
out.setUTCDate(out.getUTCDate() + n);
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function registerStdLib(env, now) {
|
|
53
|
+
return env.registerFunction("now(): google.protobuf.Timestamp", () => now()).registerFunction(
|
|
54
|
+
"today(): google.protobuf.Timestamp",
|
|
55
|
+
() => startOfDayUtc(now())
|
|
56
|
+
).registerFunction(
|
|
57
|
+
"daysFromNow(int): google.protobuf.Timestamp",
|
|
58
|
+
(n) => addDaysUtc(now(), Number(n))
|
|
59
|
+
).registerFunction(
|
|
60
|
+
"daysAgo(int): google.protobuf.Timestamp",
|
|
61
|
+
(n) => addDaysUtc(now(), -Number(n))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
function buildScope(ctx) {
|
|
65
|
+
const scope = {};
|
|
66
|
+
if (ctx.record !== void 0) scope.record = ctx.record;
|
|
67
|
+
if (ctx.previous !== void 0) scope.previous = ctx.previous;
|
|
68
|
+
if (ctx.input !== void 0) scope.input = ctx.input;
|
|
69
|
+
const os = {};
|
|
70
|
+
if (ctx.user !== void 0) os.user = ctx.user;
|
|
71
|
+
if (ctx.org !== void 0) os.org = ctx.org;
|
|
72
|
+
if (ctx.env !== void 0) os.env = ctx.env;
|
|
73
|
+
if (Object.keys(os).length > 0) scope.os = os;
|
|
74
|
+
if (ctx.extra !== void 0) Object.assign(scope, ctx.extra);
|
|
75
|
+
return scope;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/cel-engine.ts
|
|
79
|
+
var DEFAULT_LIMITS = {
|
|
80
|
+
maxAstNodes: 256,
|
|
81
|
+
maxDepth: 32,
|
|
82
|
+
maxListElements: 64,
|
|
83
|
+
maxMapEntries: 64,
|
|
84
|
+
maxCallArguments: 16
|
|
85
|
+
};
|
|
86
|
+
function buildEnv(now) {
|
|
87
|
+
const env = new import_cel_js.Environment({
|
|
88
|
+
unlistedVariablesAreDyn: true,
|
|
89
|
+
enableOptionalTypes: true,
|
|
90
|
+
limits: DEFAULT_LIMITS
|
|
91
|
+
});
|
|
92
|
+
return registerStdLib(env, now);
|
|
93
|
+
}
|
|
94
|
+
function coerce(value) {
|
|
95
|
+
if (typeof value === "bigint") {
|
|
96
|
+
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
97
|
+
return Number(value);
|
|
98
|
+
}
|
|
99
|
+
return value.toString();
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(value)) return value.map(coerce);
|
|
102
|
+
if (value && typeof value === "object" && !(value instanceof Date)) {
|
|
103
|
+
const out = {};
|
|
104
|
+
for (const [k, v] of Object.entries(value)) out[k] = coerce(v);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function classifyError(err) {
|
|
110
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
111
|
+
let kind = "runtime";
|
|
112
|
+
if (/Exceeded max/i.test(message)) kind = "bounds";
|
|
113
|
+
else if (/parse|unexpected|syntax/i.test(message)) kind = "parse";
|
|
114
|
+
else if (/type|unknown variable|undeclared/i.test(message)) kind = "type";
|
|
115
|
+
return { ok: false, error: { kind, message } };
|
|
116
|
+
}
|
|
117
|
+
var celEngine = {
|
|
118
|
+
dialect: "cel",
|
|
119
|
+
compile(source) {
|
|
120
|
+
try {
|
|
121
|
+
const env = buildEnv(() => /* @__PURE__ */ new Date(0));
|
|
122
|
+
const compiled = env.parse(source);
|
|
123
|
+
const checkErrors = compiled.check?.();
|
|
124
|
+
if (checkErrors && Array.isArray(checkErrors) && checkErrors.length > 0) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: { kind: "type", message: checkErrors.join("; ") }
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return { ok: true, value: compiled.ast };
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return classifyError(err);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
evaluate(expr, ctx) {
|
|
136
|
+
if (expr.dialect !== "cel") {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: { kind: "dialect", message: `celEngine cannot evaluate dialect '${expr.dialect}'` }
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const source = expr.source;
|
|
143
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
error: { kind: "parse", message: "AST-only evaluation not yet supported; persist `source`" }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const now = () => ctx.now ?? /* @__PURE__ */ new Date();
|
|
150
|
+
try {
|
|
151
|
+
const env = buildEnv(now);
|
|
152
|
+
const scope = buildScope(ctx);
|
|
153
|
+
const raw = env.evaluate(source, scope);
|
|
154
|
+
return { ok: true, value: coerce(raw) };
|
|
155
|
+
} catch (err) {
|
|
156
|
+
return classifyError(err);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/registry.ts
|
|
162
|
+
var registry = /* @__PURE__ */ new Map();
|
|
163
|
+
function register(engine) {
|
|
164
|
+
registry.set(engine.dialect, engine);
|
|
165
|
+
}
|
|
166
|
+
function getEngine(dialect) {
|
|
167
|
+
return registry.get(dialect);
|
|
168
|
+
}
|
|
169
|
+
function hasDialect(dialect) {
|
|
170
|
+
return registry.has(dialect) && !registry.get(dialect).dialect.startsWith("stub:");
|
|
171
|
+
}
|
|
172
|
+
function makeStub(dialect, reason) {
|
|
173
|
+
return {
|
|
174
|
+
dialect,
|
|
175
|
+
compile: () => ({ ok: false, error: { kind: "dialect", message: reason } }),
|
|
176
|
+
evaluate: () => ({ ok: false, error: { kind: "dialect", message: reason } })
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
register(celEngine);
|
|
180
|
+
register(makeStub("js", "dialect 'js' not registered. Install @objectstack/plugin-js-vm"));
|
|
181
|
+
register(makeStub("cron", "dialect 'cron' not registered. Install @objectstack/plugin-cron"));
|
|
182
|
+
var ExpressionEngine = {
|
|
183
|
+
register,
|
|
184
|
+
getEngine,
|
|
185
|
+
hasDialect,
|
|
186
|
+
/**
|
|
187
|
+
* Compile-only — parse + type-check, returning the engine-native AST. Used
|
|
188
|
+
* by `objectstack compile` to normalize source into AST in artifacts.
|
|
189
|
+
*/
|
|
190
|
+
compile(expr) {
|
|
191
|
+
const engine = registry.get(expr.dialect);
|
|
192
|
+
if (!engine) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
error: { kind: "dialect", message: `No engine registered for dialect '${expr.dialect}'` }
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (typeof expr.source !== "string") {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
error: { kind: "parse", message: "Expression.source required for compile()" }
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return engine.compile(expr.source);
|
|
205
|
+
},
|
|
206
|
+
/**
|
|
207
|
+
* Evaluate an expression in the given context. Never throws — branch on
|
|
208
|
+
* `result.ok`. Errors carry a `kind` for caller-side classification.
|
|
209
|
+
*/
|
|
210
|
+
evaluate(expr, ctx) {
|
|
211
|
+
const engine = registry.get(expr.dialect);
|
|
212
|
+
if (!engine) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
error: { kind: "dialect", message: `No engine registered for dialect '${expr.dialect}'` }
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return engine.evaluate(expr, ctx);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/seed-eval.ts
|
|
223
|
+
var import_spec = require("@objectstack/spec");
|
|
224
|
+
function isExpressionLike(value) {
|
|
225
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
226
|
+
const v = value;
|
|
227
|
+
if (typeof v.dialect !== "string") return false;
|
|
228
|
+
return import_spec.ExpressionSchema.safeParse(v).success;
|
|
229
|
+
}
|
|
230
|
+
function resolveSeed(value, ctx) {
|
|
231
|
+
if (value === null || value === void 0) {
|
|
232
|
+
return { ok: true, value };
|
|
233
|
+
}
|
|
234
|
+
const t = typeof value;
|
|
235
|
+
if (t === "string" || t === "number" || t === "boolean") {
|
|
236
|
+
return { ok: true, value };
|
|
237
|
+
}
|
|
238
|
+
if (value instanceof Date) {
|
|
239
|
+
return { ok: true, value };
|
|
240
|
+
}
|
|
241
|
+
if (Array.isArray(value)) {
|
|
242
|
+
const out2 = [];
|
|
243
|
+
for (const item of value) {
|
|
244
|
+
const r = resolveSeed(item, ctx);
|
|
245
|
+
if (!r.ok) return r;
|
|
246
|
+
out2.push(r.value);
|
|
247
|
+
}
|
|
248
|
+
return { ok: true, value: out2 };
|
|
249
|
+
}
|
|
250
|
+
if (isExpressionLike(value)) {
|
|
251
|
+
return ExpressionEngine.evaluate(value, ctx);
|
|
252
|
+
}
|
|
253
|
+
const out = {};
|
|
254
|
+
for (const [k, v] of Object.entries(value)) {
|
|
255
|
+
const r = resolveSeed(v, ctx);
|
|
256
|
+
if (!r.ok) return r;
|
|
257
|
+
out[k] = r.value;
|
|
258
|
+
}
|
|
259
|
+
return { ok: true, value: out };
|
|
260
|
+
}
|
|
261
|
+
function resolveSeedRecord(record, ctx) {
|
|
262
|
+
const pinnedCtx = { ...ctx, now: ctx.now ?? /* @__PURE__ */ new Date() };
|
|
263
|
+
const result = resolveSeed(record, pinnedCtx);
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/normalize.ts
|
|
268
|
+
var import_spec2 = require("@objectstack/spec");
|
|
269
|
+
function normalizeExpression(input) {
|
|
270
|
+
const parsed = import_spec2.ExpressionInputSchema.safeParse(input);
|
|
271
|
+
if (!parsed.success) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
error: { kind: "parse", message: parsed.error.message }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const expr = parsed.data;
|
|
278
|
+
if (expr.ast !== void 0 && expr.source === void 0) {
|
|
279
|
+
return { ok: true, value: expr };
|
|
280
|
+
}
|
|
281
|
+
const compiled = ExpressionEngine.compile(expr);
|
|
282
|
+
if (!compiled.ok) {
|
|
283
|
+
return compiled;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
ok: true,
|
|
287
|
+
value: {
|
|
288
|
+
...expr,
|
|
289
|
+
ast: compiled.value
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function normalizeExpressionTree(root, path = []) {
|
|
294
|
+
if (root === null || typeof root !== "object") return null;
|
|
295
|
+
if (looksLikeExpression(root)) {
|
|
296
|
+
const r = normalizeExpression(root);
|
|
297
|
+
if (!r.ok) return { path: path.join("."), error: r.error };
|
|
298
|
+
Object.assign(root, r.value);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
if (Array.isArray(root)) {
|
|
302
|
+
for (let i = 0; i < root.length; i++) {
|
|
303
|
+
const r = normalizeExpressionTree(root[i], [...path, String(i)]);
|
|
304
|
+
if (r) return r;
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
for (const [k, v] of Object.entries(root)) {
|
|
309
|
+
const r = normalizeExpressionTree(v, [...path, k]);
|
|
310
|
+
if (r) return r;
|
|
311
|
+
}
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function looksLikeExpression(value) {
|
|
315
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
316
|
+
const v = value;
|
|
317
|
+
if (typeof v.dialect !== "string") return false;
|
|
318
|
+
return import_spec2.ExpressionSchema.safeParse(v).success;
|
|
319
|
+
}
|
|
320
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
321
|
+
0 && (module.exports = {
|
|
322
|
+
DEFAULT_LIMITS,
|
|
323
|
+
ExpressionEngine,
|
|
324
|
+
buildScope,
|
|
325
|
+
celEngine,
|
|
326
|
+
getEngine,
|
|
327
|
+
hasDialect,
|
|
328
|
+
normalizeExpression,
|
|
329
|
+
normalizeExpressionTree,
|
|
330
|
+
register,
|
|
331
|
+
registerStdLib,
|
|
332
|
+
resolveSeed,
|
|
333
|
+
resolveSeedRecord
|
|
334
|
+
});
|
|
335
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/cel-engine.ts","../src/stdlib.ts","../src/registry.ts","../src/seed-eval.ts","../src/normalize.ts"],"sourcesContent":["/**\n * @objectstack/formula\n *\n * Canonical expression engine for ObjectStack. CEL (Common Expression\n * Language) is the default dialect; `js` and `cron` are dispatched to\n * dedicated plugin engines.\n *\n * @see content/docs/concepts/north-star.mdx §8 \"No private expression DSL\"\n * @see ROADMAP.md M9 \"Expression Unification\"\n */\n\nexport { ExpressionEngine, getEngine, hasDialect, register } from './registry';\nexport { celEngine, DEFAULT_LIMITS } from './cel-engine';\nexport { registerStdLib, buildScope } from './stdlib';\nexport { resolveSeed, resolveSeedRecord } from './seed-eval';\nexport { normalizeExpression, normalizeExpressionTree } from './normalize';\nexport type { SeedValue, SeedPrimitive } from './seed-eval';\nexport type { DialectEngine, EvalContext, EvalResult, EvalError } from './types';\n","/**\n * CEL dialect engine — wraps `@marcbachmann/cel-js` with the ObjectStack\n * stdlib, bounded execution limits, and result coercion.\n *\n * Why a thin wrapper:\n *\n * - cel-js returns `BigInt` for ints. The kernel and CRM expect plain\n * numbers, so we coerce at the boundary.\n * - cel-js parses dotted names as receiver-typed methods; we register\n * `now()`, `today()`, `daysFromNow()` as bare functions and let `os.*`\n * refer to context data only (see {@link buildScope}).\n * - Bounds (`maxAstNodes`, `maxDepth`, …) are enforced spec-wide so\n * third-party plugins can't ship runaway predicates.\n */\n\nimport { Environment } from '@marcbachmann/cel-js';\nimport type { Expression } from '@objectstack/spec';\n\nimport { buildScope, registerStdLib } from './stdlib';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\n/**\n * Default execution bounds. Picked conservatively — every metadata-authored\n * expression we've seen is well under these. If you hit them, the expression\n * is too complex for ObjectStack and should be moved to a hook (`dialect: js`).\n */\nexport const DEFAULT_LIMITS = {\n maxAstNodes: 256,\n maxDepth: 32,\n maxListElements: 64,\n maxMapEntries: 64,\n maxCallArguments: 16,\n} as const;\n\nfunction buildEnv(now: () => Date): Environment {\n const env = new Environment({\n unlistedVariablesAreDyn: true,\n enableOptionalTypes: true,\n limits: DEFAULT_LIMITS,\n });\n return registerStdLib(env, now);\n}\n\n/** Coerce cel-js's BigInt-flavored return into spec-friendly JS values. */\nfunction coerce(value: unknown): unknown {\n if (typeof value === 'bigint') {\n // BigInt → number when safe, else string to avoid silent truncation.\n if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {\n return Number(value);\n }\n return value.toString();\n }\n if (Array.isArray(value)) return value.map(coerce);\n if (value && typeof value === 'object' && !(value instanceof Date)) {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = coerce(v);\n return out;\n }\n return value;\n}\n\nfunction classifyError(err: unknown): EvalResult<never> {\n const message = err instanceof Error ? err.message : String(err);\n let kind: 'parse' | 'type' | 'runtime' | 'bounds' = 'runtime';\n if (/Exceeded max/i.test(message)) kind = 'bounds';\n else if (/parse|unexpected|syntax/i.test(message)) kind = 'parse';\n else if (/type|unknown variable|undeclared/i.test(message)) kind = 'type';\n return { ok: false, error: { kind, message } };\n}\n\nexport const celEngine: DialectEngine = {\n dialect: 'cel',\n\n compile(source: string): EvalResult<unknown> {\n try {\n // We use a wall-clock now() here purely for parse-time stdlib\n // type-checking; the function is never actually called.\n const env = buildEnv(() => new Date(0));\n const compiled = env.parse(source);\n // Surface check errors eagerly.\n const checkErrors = compiled.check?.();\n if (checkErrors && Array.isArray(checkErrors) && checkErrors.length > 0) {\n return {\n ok: false,\n error: { kind: 'type', message: checkErrors.join('; ') },\n };\n }\n return { ok: true, value: compiled.ast };\n } catch (err) {\n return classifyError(err);\n }\n },\n\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n if (expr.dialect !== 'cel') {\n return {\n ok: false,\n error: { kind: 'dialect', message: `celEngine cannot evaluate dialect '${expr.dialect}'` },\n };\n }\n const source = expr.source;\n if (typeof source !== 'string' || source.length === 0) {\n // AST-only inputs: cel-js does not currently expose a public API to\n // re-execute a parsed AST without re-serializing. We persist `source`\n // as the canonical form during M9.1 and revisit AST-only execution in\n // M9.7 when we cut the spec persistence over.\n return {\n ok: false,\n error: { kind: 'parse', message: 'AST-only evaluation not yet supported; persist `source`' },\n };\n }\n\n const now = () => ctx.now ?? new Date();\n try {\n const env = buildEnv(now);\n const scope = buildScope(ctx);\n const raw = env.evaluate(source, scope);\n return { ok: true, value: coerce(raw) as T };\n } catch (err) {\n return classifyError(err);\n }\n },\n};\n","/**\n * ObjectStack standard CEL function library.\n *\n * Registered into the per-evaluation `Environment` by the CEL engine. All\n * functions are pure given a pinned `now` — that determinism is what makes\n * `objectstack build` artifacts byte-stable across runs.\n *\n * Function naming intentionally avoids the `os.` prefix because cel-js binds\n * dotted names to receiver types. Instead, the `os` namespace in CEL holds\n * *data* (`os.user`, `os.org`, `os.env`) supplied by the caller's\n * {@link EvalContext}.\n */\n\nimport type { Environment } from '@marcbachmann/cel-js';\n\nimport type { EvalContext } from './types';\n\n/** Truncate a Date to start-of-day in UTC. */\nfunction startOfDayUtc(d: Date): Date {\n const out = new Date(d.getTime());\n out.setUTCHours(0, 0, 0, 0);\n return out;\n}\n\n/** Add `n` days to a Date in UTC; returns a new Date. */\nfunction addDaysUtc(d: Date, n: number): Date {\n const out = new Date(d.getTime());\n out.setUTCDate(out.getUTCDate() + n);\n return out;\n}\n\n/**\n * Register the ObjectStack standard library into a CEL environment.\n *\n * The `now` resolver is closed over so each call uses the pinned\n * `EvalContext.now` (or wall-clock fallback). Implementations are kept tiny\n * and dependency-free — they're the contract surface for AI authors and must\n * stay legible.\n */\nexport function registerStdLib(\n env: Environment,\n now: () => Date,\n): Environment {\n return env\n .registerFunction('now(): google.protobuf.Timestamp', () => now())\n .registerFunction(\n 'today(): google.protobuf.Timestamp',\n () => startOfDayUtc(now()),\n )\n .registerFunction(\n 'daysFromNow(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), Number(n)),\n )\n .registerFunction(\n 'daysAgo(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), -Number(n)),\n );\n}\n\n/**\n * Build the variable scope for a single evaluation. Absent fields are simply\n * not bound — CEL macros (`has(record.foo)`) handle missing-key safely.\n */\nexport function buildScope(ctx: EvalContext): Record<string, unknown> {\n const scope: Record<string, unknown> = {};\n\n if (ctx.record !== undefined) scope.record = ctx.record;\n if (ctx.previous !== undefined) scope.previous = ctx.previous;\n if (ctx.input !== undefined) scope.input = ctx.input;\n\n // Namespaced data — written as `os.user.id`, `os.env`, etc. in CEL.\n const os: Record<string, unknown> = {};\n if (ctx.user !== undefined) os.user = ctx.user;\n if (ctx.org !== undefined) os.org = ctx.org;\n if (ctx.env !== undefined) os.env = ctx.env;\n if (Object.keys(os).length > 0) scope.os = os;\n\n if (ctx.extra !== undefined) Object.assign(scope, ctx.extra);\n\n return scope;\n}\n","/**\n * Dialect-pluggable Expression engine registry.\n *\n * Replaces the per-call-site `compileFormula` / `evaluateFormula` direct\n * imports of the deleted custom engine. Call sites now ask the registry to\n * dispatch by `expression.dialect`.\n *\n * Stub dialects (`js`, `cron`) are registered at module load with explicit\n * `dialect`-error responses so call sites get a clear message instead of\n * silent `undefined` (the old engine's anti-pattern).\n */\n\nimport type { Expression } from '@objectstack/spec';\n\nimport { celEngine } from './cel-engine';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\nconst registry = new Map<string, DialectEngine>();\n\n/** Register or replace a dialect engine. */\nexport function register(engine: DialectEngine): void {\n registry.set(engine.dialect, engine);\n}\n\n/** Look up a dialect engine without dispatching. */\nexport function getEngine(dialect: string): DialectEngine | undefined {\n return registry.get(dialect);\n}\n\n/** Whether a dialect has a real (non-stub) implementation registered. */\nexport function hasDialect(dialect: string): boolean {\n return registry.has(dialect) && !registry.get(dialect)!.dialect.startsWith('stub:');\n}\n\nfunction makeStub(dialect: string, reason: string): DialectEngine {\n return {\n dialect,\n compile: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n evaluate: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n };\n}\n\n// Real engines.\nregister(celEngine);\n\n// Stubs — phased in by later milestones (M9.5+ for `js`, M9.6 for `cron`).\nregister(makeStub('js', \"dialect 'js' not registered. Install @objectstack/plugin-js-vm\"));\nregister(makeStub('cron', \"dialect 'cron' not registered. Install @objectstack/plugin-cron\"));\n\n/**\n * The unified evaluation entry point. Replaces the old direct calls to\n * `evaluateFormula` from the deleted custom engine.\n */\nexport const ExpressionEngine = {\n register,\n getEngine,\n hasDialect,\n\n /**\n * Compile-only — parse + type-check, returning the engine-native AST. Used\n * by `objectstack compile` to normalize source into AST in artifacts.\n */\n compile(expr: Expression): EvalResult<unknown> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n if (typeof expr.source !== 'string') {\n return {\n ok: false,\n error: { kind: 'parse', message: 'Expression.source required for compile()' },\n };\n }\n return engine.compile(expr.source);\n },\n\n /**\n * Evaluate an expression in the given context. Never throws — branch on\n * `result.ok`. Errors carry a `kind` for caller-side classification.\n */\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n return engine.evaluate<T>(expr, ctx);\n },\n};\n","/**\n * Seed-value resolver.\n *\n * `Dataset.records` accepts {@link SeedValue} = primitive | Expression | array\n * | object — install-time resolution walks the tree and replaces any\n * Expression node with its evaluated result. This is what makes\n * `close_date: cel\\`now() + duration(\"P30D\")\\`` resolve to *the customer's*\n * \"today + 30 days\" instead of the developer's compile-time clock.\n */\n\nimport { ExpressionSchema, type Expression } from '@objectstack/spec';\n\nimport type { EvalContext, EvalResult } from './types';\nimport { ExpressionEngine } from './registry';\n\nexport type SeedPrimitive = string | number | boolean | null | Date;\nexport type SeedValue = SeedPrimitive | Expression | SeedValue[] | { [key: string]: SeedValue };\n\n/** Detect an Expression-shaped object without throwing on unrelated shapes. */\nfunction isExpressionLike(value: unknown): value is Expression {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n\n/**\n * Recursively resolve a SeedValue. Records that contain Expression leaves are\n * evaluated with `ctx`; other values are passed through unchanged.\n *\n * Returns the first failure encountered. Callers (seed loader) typically\n * abort the whole record on failure rather than silently writing partial data.\n */\nexport function resolveSeed(\n value: SeedValue,\n ctx: EvalContext,\n): EvalResult<unknown> {\n if (value === null || value === undefined) {\n return { ok: true, value };\n }\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') {\n return { ok: true, value };\n }\n if (value instanceof Date) {\n return { ok: true, value };\n }\n if (Array.isArray(value)) {\n const out: unknown[] = [];\n for (const item of value) {\n const r = resolveSeed(item, ctx);\n if (!r.ok) return r;\n out.push(r.value);\n }\n return { ok: true, value: out };\n }\n if (isExpressionLike(value)) {\n return ExpressionEngine.evaluate(value, ctx);\n }\n // Plain object — recurse field-by-field.\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, SeedValue>)) {\n const r = resolveSeed(v, ctx);\n if (!r.ok) return r;\n out[k] = r.value;\n }\n return { ok: true, value: out };\n}\n\n/**\n * Resolve a single record (object of fields), pinning `ctx.now` so all\n * expressions within see one logical clock.\n */\nexport function resolveSeedRecord(\n record: Record<string, SeedValue>,\n ctx: EvalContext,\n): EvalResult<Record<string, unknown>> {\n const pinnedCtx: EvalContext = { ...ctx, now: ctx.now ?? new Date() };\n const result = resolveSeed(record, pinnedCtx) as EvalResult<Record<string, unknown>>;\n return result;\n}\n","/**\n * Build-time normalization helpers.\n *\n * The CLI `objectstack compile` step walks the assembled `objectstack.json`\n * artifact and rewrites every Expression so that:\n *\n * 1. String shorthand input is replaced by `{ dialect: 'cel', source }`.\n * 2. The persisted envelope carries an `ast` field produced by the dialect\n * engine (M9.2 deliverable). Source is retained for round-trip / debug.\n *\n * Spec layer cannot do step 2 because it must remain dependency-free; this\n * package owns the engine import and therefore the AST step.\n */\n\nimport {\n ExpressionInputSchema,\n ExpressionSchema,\n type Expression,\n type ExpressionInput,\n} from '@objectstack/spec';\n\nimport { ExpressionEngine } from './registry';\nimport type { EvalResult } from './types';\n\n/**\n * Normalize an {@link ExpressionInput} (string shorthand OR full envelope) into\n * a fully-resolved {@link Expression} carrying both `source` and `ast`.\n *\n * Returns an EvalResult so the caller can render a structured compile error\n * pointing at the offending metadata path.\n */\nexport function normalizeExpression(input: ExpressionInput): EvalResult<Expression> {\n const parsed = ExpressionInputSchema.safeParse(input);\n if (!parsed.success) {\n return {\n ok: false,\n error: { kind: 'parse', message: parsed.error.message },\n };\n }\n\n const expr = parsed.data as Expression;\n\n // Already AST-only — accept as-is.\n if (expr.ast !== undefined && expr.source === undefined) {\n return { ok: true, value: expr };\n }\n\n // Source-bearing: ask the dialect engine to compile. Failures surface here\n // as part of the build (no silent skip).\n const compiled = ExpressionEngine.compile(expr);\n if (!compiled.ok) {\n return compiled;\n }\n\n return {\n ok: true,\n value: {\n ...expr,\n ast: compiled.value,\n },\n };\n}\n\n/**\n * Walk an arbitrary JSON tree and normalize every embedded Expression in\n * place. Used by the build pipeline to traverse the assembled metadata\n * artifact. Returns the first error encountered (paired with the dotted path\n * for diagnostics) or `null` when fully clean.\n */\nexport function normalizeExpressionTree(\n root: unknown,\n path: string[] = [],\n): { path: string; error: import('./types').EvalError } | null {\n if (root === null || typeof root !== 'object') return null;\n\n if (looksLikeExpression(root)) {\n const r = normalizeExpression(root as ExpressionInput);\n if (!r.ok) return { path: path.join('.'), error: r.error };\n Object.assign(root as Record<string, unknown>, r.value);\n return null;\n }\n\n if (Array.isArray(root)) {\n for (let i = 0; i < root.length; i++) {\n const r = normalizeExpressionTree(root[i], [...path, String(i)]);\n if (r) return r;\n }\n return null;\n }\n\n for (const [k, v] of Object.entries(root as Record<string, unknown>)) {\n const r = normalizeExpressionTree(v, [...path, k]);\n if (r) return r;\n }\n return null;\n}\n\nfunction looksLikeExpression(value: unknown): boolean {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n"],"mappings":"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","names":["out","import_spec"]}
|