@objectstack/formula 9.7.0 → 9.9.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +58 -0
- package/dist/index.d.mts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.js +85 -26
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +84 -26
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/cel-engine.test.ts +93 -2
- package/src/cel-engine.ts +3 -3
- package/src/index.ts +1 -1
- package/src/seed-eval.test.ts +6 -2
- package/src/skill-catalog-sync.test.ts +32 -0
- package/src/stdlib.ts +101 -3
- package/src/template-engine.ts +39 -5
- package/src/template-formatters.test.ts +36 -3
- package/src/types.ts +7 -0
- package/src/validate.ts +16 -5
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/formula@9.
|
|
2
|
+
> @objectstack/formula@9.9.0 build /home/runner/work/framework/framework/packages/formula
|
|
3
3
|
> tsup --config ../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
14
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
15
|
-
[32mCJS[39m ⚡️ Build success in
|
|
16
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
17
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
18
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mCJS[39m [1mdist/index.js [22m[32m31.13 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m83.10 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 67ms
|
|
16
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m29.37 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m81.72 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 84ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 4303ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m16.79 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m16.79 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# @objectstack/formula
|
|
2
2
|
|
|
3
|
+
## 9.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d99a75a: feat(formula): timezone-aware `today()` / `daysFromNow()` / `daysAgo()` (ADR-0053 Phase 2)
|
|
8
|
+
|
|
9
|
+
These are now **calendar-day** functions resolved in a reference timezone, threaded from `ExecutionContext.timezone` (#1978) through `EvalContext.timezone` into the CEL stdlib. Each returns the reference-tz calendar day expressed as a **UTC-midnight `Date`** (ADR-0053 decision D1) — the one representation consistent with how `Field.date` strings hydrate, how the SQL driver normalizes date filters, and how Phase 1 stores dates. So `record.close_date == daysFromNow(30)` now matches in-memory too, not just at the storage boundary. The timezone calculation uses `Intl.DateTimeFormat` (DST-safe; no hand-rolled offset math).
|
|
10
|
+
|
|
11
|
+
**⚠️ Behavior change:** `daysFromNow(n)` / `daysAgo(n)` previously kept the wall-clock time of `now` (e.g. `daysFromNow(30)` at `10:00Z` → `…T10:00:00Z`). They now drop the time and return the calendar day at **midnight** (`…T00:00:00Z`) — the ADR-0053 "defect #3" fix. `today()` is unchanged at UTC (it already truncated to start-of-day). For a genuine sub-day offset use the documented escape hatch `now() + duration("Nh")`.
|
|
12
|
+
|
|
13
|
+
With no reference timezone configured the zone resolves to `UTC`, so `today()` is byte-for-byte unchanged; only the `daysFromNow`/`daysAgo` midnight-truncation differs from before. `objectql` threads `execCtx.timezone` into read-time formula evaluation (`applyFormulaPlan`) and default-value expressions (`applyFieldDefaults`).
|
|
14
|
+
|
|
15
|
+
Part of #1980. (Consuming a non-UTC reference timezone end-to-end also needs the `localization` settings manifest noted in #1978.)
|
|
16
|
+
|
|
17
|
+
- 575448d: feat(formula,email): render `datetime` in a reference timezone (ADR-0053 Phase 2)
|
|
18
|
+
|
|
19
|
+
`datetime` template holes now render in a reference timezone's wall-clock when one is supplied, at the presentation boundary — storage stays UTC.
|
|
20
|
+
|
|
21
|
+
- **Formula template engine** — the `datetime` formatter takes the reference timezone from `EvalContext.timezone` (threaded in #1980) and passes it to `Intl.DateTimeFormat`. `{{ ts | datetime }}` renders in that zone; `{{ ts | datetime:iso }}` stays UTC (machine-readable). Calendar-day `date` rendering is intentionally **unchanged** (tz-naive — a `Field.date` has no zone). New exported `formatValue(name, value, arg, { locale, timeZone })` makes the whitelisted formatters reusable outside the full CEL template engine.
|
|
22
|
+
- **Email pipeline** — `plugin-email`'s renderer previously bypassed the formatter pipeline (`String()` only), so a datetime went out as raw ISO. Email holes now accept the shared formula formatters — `{{ order.total | currency }}`, `{{ ts | datetime }}` — reusing `formatValue` (single source of truth), while keeping the engine's HTML-escaping and `{{{ }}}` raw-output semantics. `SendTemplateInput.timezone` (mirroring the existing `locale`) flows into rendering so an email's datetime shows the recipient's wall-clock.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [84249a4]
|
|
27
|
+
- Updated dependencies [11af299]
|
|
28
|
+
- Updated dependencies [d5774b5]
|
|
29
|
+
- Updated dependencies [134043a]
|
|
30
|
+
- Updated dependencies [90108e0]
|
|
31
|
+
- Updated dependencies [9afeb2d]
|
|
32
|
+
- Updated dependencies [6bec07e]
|
|
33
|
+
- Updated dependencies [601cc11]
|
|
34
|
+
- Updated dependencies [575448d]
|
|
35
|
+
- @objectstack/spec@9.9.0
|
|
36
|
+
|
|
37
|
+
## 9.8.0
|
|
38
|
+
|
|
39
|
+
### Minor Changes
|
|
40
|
+
|
|
41
|
+
- c17d2c8: feat(formula): register the CEL functions the authoring catalog advertises (daysBetween, abs, round, min, max, upper, lower, contains, startsWith, endsWith, matches, len, isEmpty, date, datetime)
|
|
42
|
+
|
|
43
|
+
`introspectScope` / `CEL_STDLIB_FUNCTIONS` advertised 25 functions to authors
|
|
44
|
+
(incl. AI), but only 8 were registered — 14 faulted at runtime (`daysBetween`,
|
|
45
|
+
`abs`, `round`, `min`, `max`, `upper`, `lower`, `len`, `isEmpty`, `contains`,
|
|
46
|
+
`startsWith`, `endsWith`, `matches`, plus `date`/`datetime`). Authors were told
|
|
47
|
+
to call functions that don't exist (e.g. `daysBetween` for "days remaining").
|
|
48
|
+
|
|
49
|
+
Register the genuinely-useful set in `registerStdLib` with dyn-lenient signatures
|
|
50
|
+
(so a `Field.date` arriving as a string still works) and internal coercion, and
|
|
51
|
+
reconcile the catalog so every advertised entry resolves — guarded by a test that
|
|
52
|
+
evaluates every `CEL_STDLIB_FUNCTIONS` entry. Pure additions; no behavior change
|
|
53
|
+
to existing expressions.
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- Updated dependencies [97c55b3]
|
|
58
|
+
- Updated dependencies [1b1f490]
|
|
59
|
+
- @objectstack/spec@9.8.0
|
|
60
|
+
|
|
3
61
|
## 9.7.0
|
|
4
62
|
|
|
5
63
|
### Minor Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -24,6 +24,13 @@ import { Environment } from '@marcbachmann/cel-js';
|
|
|
24
24
|
interface EvalContext {
|
|
25
25
|
/** Logical "now" snapshot — pinned per evaluation run for determinism. */
|
|
26
26
|
now?: Date;
|
|
27
|
+
/**
|
|
28
|
+
* Reference timezone (IANA name, e.g. `America/New_York`) for calendar-day
|
|
29
|
+
* functions `today()` / `daysFromNow()` / `daysAgo()` and for rendering
|
|
30
|
+
* `datetime` template holes in that zone's wall-clock (ADR-0053 Phase 2).
|
|
31
|
+
* Defaults to `UTC` when unset. Calendar-day `date` rendering stays tz-naive.
|
|
32
|
+
*/
|
|
33
|
+
timezone?: string;
|
|
27
34
|
/** Current authenticated subject (hook / action / view contexts). */
|
|
28
35
|
user?: {
|
|
29
36
|
id: string;
|
|
@@ -198,6 +205,20 @@ declare const cronEngine: DialectEngine;
|
|
|
198
205
|
|
|
199
206
|
/** Public list of whitelisted template formatters (for introspection/docs). */
|
|
200
207
|
declare const TEMPLATE_FORMATTERS: string[];
|
|
208
|
+
/**
|
|
209
|
+
* Apply a whitelisted formatter to a value, the single source of truth for
|
|
210
|
+
* value→string semantics across dialects. Returns `undefined` for an unknown
|
|
211
|
+
* formatter name so callers can decide how to handle it (the template engine
|
|
212
|
+
* rejects at compile time; other consumers may pass the raw value through).
|
|
213
|
+
*
|
|
214
|
+
* Exported so renderers that don't run the full CEL template engine — notably
|
|
215
|
+
* the email pipeline (ADR-0053 Phase 2 slice 4) — format dates, money, etc.
|
|
216
|
+
* identically to in-app templates, including reference-timezone `datetime`.
|
|
217
|
+
*/
|
|
218
|
+
declare function formatValue(name: string, value: unknown, arg: string | undefined, opts?: {
|
|
219
|
+
locale?: string;
|
|
220
|
+
timeZone?: string;
|
|
221
|
+
}): string | undefined;
|
|
201
222
|
declare const templateEngine: DialectEngine;
|
|
202
223
|
|
|
203
224
|
/**
|
|
@@ -221,7 +242,7 @@ declare const templateEngine: DialectEngine;
|
|
|
221
242
|
* and dependency-free — they're the contract surface for AI authors and must
|
|
222
243
|
* stay legible.
|
|
223
244
|
*/
|
|
224
|
-
declare function registerStdLib(env: Environment, now: () => Date): Environment;
|
|
245
|
+
declare function registerStdLib(env: Environment, now: () => Date, timezone?: string): Environment;
|
|
225
246
|
/**
|
|
226
247
|
* Build the variable scope for a single evaluation. Absent fields are simply
|
|
227
248
|
* not bound — CEL macros (`has(record.foo)`) handle missing-key safely.
|
|
@@ -364,7 +385,12 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
|
|
|
364
385
|
roots: string[];
|
|
365
386
|
functions: string[];
|
|
366
387
|
};
|
|
367
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* Public catalog of CEL functions available in expressions — what `introspectScope`
|
|
390
|
+
* advertises to authors (incl. AI). Every entry MUST actually resolve at runtime:
|
|
391
|
+
* either registered in `registerStdLib` or a verified cel-js built-in. Drifting this
|
|
392
|
+
* list ahead of the runtime tells the author to call functions that fault (#1928).
|
|
393
|
+
*/
|
|
368
394
|
declare const CEL_STDLIB_FUNCTIONS: string[];
|
|
369
395
|
|
|
370
|
-
export { CEL_STDLIB_FUNCTIONS, DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, type ExprSchemaHint, type ExprValidationError, type ExprValidationResult, ExpressionEngine, type FieldRole, type SeedPrimitive, type SeedValue, TEMPLATE_FORMATTERS, buildScope, celEngine, cronEngine, expectedDialect, getEngine, hasDialect, introspectScope, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };
|
|
396
|
+
export { CEL_STDLIB_FUNCTIONS, DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, type ExprSchemaHint, type ExprValidationError, type ExprValidationResult, ExpressionEngine, type FieldRole, type SeedPrimitive, type SeedValue, TEMPLATE_FORMATTERS, buildScope, celEngine, cronEngine, expectedDialect, formatValue, getEngine, hasDialect, introspectScope, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,13 @@ import { Environment } from '@marcbachmann/cel-js';
|
|
|
24
24
|
interface EvalContext {
|
|
25
25
|
/** Logical "now" snapshot — pinned per evaluation run for determinism. */
|
|
26
26
|
now?: Date;
|
|
27
|
+
/**
|
|
28
|
+
* Reference timezone (IANA name, e.g. `America/New_York`) for calendar-day
|
|
29
|
+
* functions `today()` / `daysFromNow()` / `daysAgo()` and for rendering
|
|
30
|
+
* `datetime` template holes in that zone's wall-clock (ADR-0053 Phase 2).
|
|
31
|
+
* Defaults to `UTC` when unset. Calendar-day `date` rendering stays tz-naive.
|
|
32
|
+
*/
|
|
33
|
+
timezone?: string;
|
|
27
34
|
/** Current authenticated subject (hook / action / view contexts). */
|
|
28
35
|
user?: {
|
|
29
36
|
id: string;
|
|
@@ -198,6 +205,20 @@ declare const cronEngine: DialectEngine;
|
|
|
198
205
|
|
|
199
206
|
/** Public list of whitelisted template formatters (for introspection/docs). */
|
|
200
207
|
declare const TEMPLATE_FORMATTERS: string[];
|
|
208
|
+
/**
|
|
209
|
+
* Apply a whitelisted formatter to a value, the single source of truth for
|
|
210
|
+
* value→string semantics across dialects. Returns `undefined` for an unknown
|
|
211
|
+
* formatter name so callers can decide how to handle it (the template engine
|
|
212
|
+
* rejects at compile time; other consumers may pass the raw value through).
|
|
213
|
+
*
|
|
214
|
+
* Exported so renderers that don't run the full CEL template engine — notably
|
|
215
|
+
* the email pipeline (ADR-0053 Phase 2 slice 4) — format dates, money, etc.
|
|
216
|
+
* identically to in-app templates, including reference-timezone `datetime`.
|
|
217
|
+
*/
|
|
218
|
+
declare function formatValue(name: string, value: unknown, arg: string | undefined, opts?: {
|
|
219
|
+
locale?: string;
|
|
220
|
+
timeZone?: string;
|
|
221
|
+
}): string | undefined;
|
|
201
222
|
declare const templateEngine: DialectEngine;
|
|
202
223
|
|
|
203
224
|
/**
|
|
@@ -221,7 +242,7 @@ declare const templateEngine: DialectEngine;
|
|
|
221
242
|
* and dependency-free — they're the contract surface for AI authors and must
|
|
222
243
|
* stay legible.
|
|
223
244
|
*/
|
|
224
|
-
declare function registerStdLib(env: Environment, now: () => Date): Environment;
|
|
245
|
+
declare function registerStdLib(env: Environment, now: () => Date, timezone?: string): Environment;
|
|
225
246
|
/**
|
|
226
247
|
* Build the variable scope for a single evaluation. Absent fields are simply
|
|
227
248
|
* not bound — CEL macros (`has(record.foo)`) handle missing-key safely.
|
|
@@ -364,7 +385,12 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
|
|
|
364
385
|
roots: string[];
|
|
365
386
|
functions: string[];
|
|
366
387
|
};
|
|
367
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* Public catalog of CEL functions available in expressions — what `introspectScope`
|
|
390
|
+
* advertises to authors (incl. AI). Every entry MUST actually resolve at runtime:
|
|
391
|
+
* either registered in `registerStdLib` or a verified cel-js built-in. Drifting this
|
|
392
|
+
* list ahead of the runtime tells the author to call functions that fault (#1928).
|
|
393
|
+
*/
|
|
368
394
|
declare const CEL_STDLIB_FUNCTIONS: string[];
|
|
369
395
|
|
|
370
|
-
export { CEL_STDLIB_FUNCTIONS, DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, type ExprSchemaHint, type ExprValidationError, type ExprValidationResult, ExpressionEngine, type FieldRole, type SeedPrimitive, type SeedValue, TEMPLATE_FORMATTERS, buildScope, celEngine, cronEngine, expectedDialect, getEngine, hasDialect, introspectScope, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };
|
|
396
|
+
export { CEL_STDLIB_FUNCTIONS, DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, type ExprSchemaHint, type ExprValidationError, type ExprValidationResult, ExpressionEngine, type FieldRole, type SeedPrimitive, type SeedValue, TEMPLATE_FORMATTERS, buildScope, celEngine, cronEngine, expectedDialect, formatValue, getEngine, hasDialect, introspectScope, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
celEngine: () => celEngine,
|
|
29
29
|
cronEngine: () => cronEngine,
|
|
30
30
|
expectedDialect: () => expectedDialect,
|
|
31
|
+
formatValue: () => formatValue,
|
|
31
32
|
getEngine: () => getEngine,
|
|
32
33
|
hasDialect: () => hasDialect,
|
|
33
34
|
introspectScope: () => introspectScope,
|
|
@@ -46,26 +47,52 @@ module.exports = __toCommonJS(index_exports);
|
|
|
46
47
|
var import_cel_js = require("@marcbachmann/cel-js");
|
|
47
48
|
|
|
48
49
|
// src/stdlib.ts
|
|
50
|
+
function partsInTz(d, tz) {
|
|
51
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
52
|
+
timeZone: tz,
|
|
53
|
+
year: "numeric",
|
|
54
|
+
month: "2-digit",
|
|
55
|
+
day: "2-digit"
|
|
56
|
+
}).formatToParts(d);
|
|
57
|
+
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
58
|
+
return { y: get("year"), m: get("month"), day: get("day") };
|
|
59
|
+
}
|
|
60
|
+
function calendarDayUtc(d, tz) {
|
|
61
|
+
if (tz && tz !== "UTC") {
|
|
62
|
+
try {
|
|
63
|
+
const { y, m, day } = partsInTz(d, tz);
|
|
64
|
+
return new Date(Date.UTC(y, m - 1, day));
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return startOfDayUtc(d);
|
|
69
|
+
}
|
|
49
70
|
function startOfDayUtc(d) {
|
|
50
71
|
const out = new Date(d.getTime());
|
|
51
72
|
out.setUTCHours(0, 0, 0, 0);
|
|
52
73
|
return out;
|
|
53
74
|
}
|
|
75
|
+
function toDate(v) {
|
|
76
|
+
if (v instanceof Date) return v;
|
|
77
|
+
if (typeof v === "number" || typeof v === "bigint") return new Date(Number(v));
|
|
78
|
+
return new Date(String(v));
|
|
79
|
+
}
|
|
80
|
+
var MS_PER_DAY = 864e5;
|
|
54
81
|
function addDaysUtc(d, n) {
|
|
55
82
|
const out = new Date(d.getTime());
|
|
56
83
|
out.setUTCDate(out.getUTCDate() + n);
|
|
57
84
|
return out;
|
|
58
85
|
}
|
|
59
|
-
function registerStdLib(env, now) {
|
|
86
|
+
function registerStdLib(env, now, timezone = "UTC") {
|
|
60
87
|
return env.registerFunction("now(): google.protobuf.Timestamp", () => now()).registerFunction(
|
|
61
88
|
"today(): google.protobuf.Timestamp",
|
|
62
|
-
() =>
|
|
89
|
+
() => calendarDayUtc(now(), timezone)
|
|
63
90
|
).registerFunction(
|
|
64
91
|
"daysFromNow(int): google.protobuf.Timestamp",
|
|
65
|
-
(n) => addDaysUtc(now(), Number(n))
|
|
92
|
+
(n) => addDaysUtc(calendarDayUtc(now(), timezone), Number(n))
|
|
66
93
|
).registerFunction(
|
|
67
94
|
"daysAgo(int): google.protobuf.Timestamp",
|
|
68
|
-
(n) => addDaysUtc(now(), -Number(n))
|
|
95
|
+
(n) => addDaysUtc(calendarDayUtc(now(), timezone), -Number(n))
|
|
69
96
|
).registerFunction(
|
|
70
97
|
"isBlank(dyn): bool",
|
|
71
98
|
(value) => {
|
|
@@ -96,8 +123,20 @@ function registerStdLib(env, now) {
|
|
|
96
123
|
}
|
|
97
124
|
return parts.join(separator);
|
|
98
125
|
}
|
|
126
|
+
).registerFunction(
|
|
127
|
+
"daysBetween(dyn, dyn): int",
|
|
128
|
+
(a, b) => BigInt(Math.round((toDate(b).getTime() - toDate(a).getTime()) / MS_PER_DAY))
|
|
129
|
+
).registerFunction("date(dyn): google.protobuf.Timestamp", (s) => toDate(s)).registerFunction("datetime(dyn): google.protobuf.Timestamp", (s) => toDate(s)).registerFunction("abs(dyn): double", (x) => Math.abs(Number(x))).registerFunction("round(dyn): int", (x) => BigInt(Math.round(Number(x)))).registerFunction("min(dyn, dyn): dyn", (a, b) => Number(a) <= Number(b) ? a : b).registerFunction("max(dyn, dyn): dyn", (a, b) => Number(a) >= Number(b) ? a : b).registerFunction("upper(dyn): string", (s) => String(s ?? "").toUpperCase()).registerFunction("lower(dyn): string", (s) => String(s ?? "").toLowerCase()).registerFunction("contains(dyn, dyn): bool", (s, sub) => String(s ?? "").includes(String(sub ?? ""))).registerFunction("startsWith(dyn, dyn): bool", (s, p) => String(s ?? "").startsWith(String(p ?? ""))).registerFunction("endsWith(dyn, dyn): bool", (s, p) => String(s ?? "").endsWith(String(p ?? ""))).registerFunction("matches(dyn, dyn): bool", (s, re) => new RegExp(String(re ?? "")).test(String(s ?? ""))).registerFunction("len(dyn): int", (v) => BigInt(lengthOf(v))).registerFunction(
|
|
130
|
+
"isEmpty(dyn): bool",
|
|
131
|
+
(v) => v === null || v === void 0 || lengthOf(v) === 0
|
|
99
132
|
);
|
|
100
133
|
}
|
|
134
|
+
function lengthOf(v) {
|
|
135
|
+
if (v === null || v === void 0) return 0;
|
|
136
|
+
if (typeof v === "string" || Array.isArray(v)) return v.length;
|
|
137
|
+
if (typeof v === "object") return Object.keys(v).length;
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
101
140
|
function registerNumericCoercions(env) {
|
|
102
141
|
const ops = {
|
|
103
142
|
"+": (a, b) => a + b,
|
|
@@ -135,13 +174,13 @@ var DEFAULT_LIMITS = {
|
|
|
135
174
|
maxMapEntries: 64,
|
|
136
175
|
maxCallArguments: 16
|
|
137
176
|
};
|
|
138
|
-
function buildEnv(now) {
|
|
177
|
+
function buildEnv(now, timezone = "UTC") {
|
|
139
178
|
const env = new import_cel_js.Environment({
|
|
140
179
|
unlistedVariablesAreDyn: true,
|
|
141
180
|
enableOptionalTypes: true,
|
|
142
181
|
limits: DEFAULT_LIMITS
|
|
143
182
|
});
|
|
144
|
-
return registerNumericCoercions(registerStdLib(env, now));
|
|
183
|
+
return registerNumericCoercions(registerStdLib(env, now, timezone));
|
|
145
184
|
}
|
|
146
185
|
var SCOPE_ROOTS = [
|
|
147
186
|
"record",
|
|
@@ -287,7 +326,7 @@ var celEngine = {
|
|
|
287
326
|
}
|
|
288
327
|
const now = () => ctx.now ?? /* @__PURE__ */ new Date();
|
|
289
328
|
try {
|
|
290
|
-
const env = buildEnv(now);
|
|
329
|
+
const env = buildEnv(now, ctx.timezone ?? "UTC");
|
|
291
330
|
const scope = buildScope(ctx);
|
|
292
331
|
try {
|
|
293
332
|
const raw = env.evaluate(source, scope);
|
|
@@ -427,7 +466,9 @@ var FORMATTERS = {
|
|
|
427
466
|
maximumFractionDigits: Number.isNaN(digits) ? 0 : digits
|
|
428
467
|
}).format(n);
|
|
429
468
|
},
|
|
430
|
-
// date | date:long | date:iso → date-only
|
|
469
|
+
// date | date:long | date:iso → date-only. Intentionally tz-naive
|
|
470
|
+
// (ADR-0053): a `Field.date` is a calendar day with no zone, so rendering
|
|
471
|
+
// never applies a reference timezone — that would shift the day.
|
|
431
472
|
date: (v, arg, locale) => {
|
|
432
473
|
const d = asDate(v);
|
|
433
474
|
if (!d) return baseString(v);
|
|
@@ -435,15 +476,18 @@ var FORMATTERS = {
|
|
|
435
476
|
const style = arg === "long" ? "long" : arg === "medium" ? "medium" : "short";
|
|
436
477
|
return new Intl.DateTimeFormat(locale, { dateStyle: style }).format(d);
|
|
437
478
|
},
|
|
438
|
-
// datetime | datetime:long | datetime:iso
|
|
439
|
-
|
|
479
|
+
// datetime | datetime:long | datetime:iso. A `datetime` is a UTC instant;
|
|
480
|
+
// when a reference `timeZone` is supplied (ADR-0053 Phase 2) the wall-clock
|
|
481
|
+
// styles render in that zone. `iso` stays UTC (machine-readable, unambiguous).
|
|
482
|
+
datetime: (v, arg, locale, timeZone) => {
|
|
440
483
|
const d = asDate(v);
|
|
441
484
|
if (!d) return baseString(v);
|
|
442
485
|
if (arg === "iso") return d.toISOString();
|
|
443
486
|
const style = arg === "long" ? "long" : arg === "medium" ? "medium" : "short";
|
|
444
487
|
return new Intl.DateTimeFormat(locale, {
|
|
445
488
|
dateStyle: style,
|
|
446
|
-
timeStyle: style
|
|
489
|
+
timeStyle: style,
|
|
490
|
+
...timeZone ? { timeZone } : {}
|
|
447
491
|
}).format(d);
|
|
448
492
|
},
|
|
449
493
|
// truncate:80 → cut with an ellipsis
|
|
@@ -467,6 +511,11 @@ var FORMATTERS = {
|
|
|
467
511
|
}
|
|
468
512
|
};
|
|
469
513
|
var TEMPLATE_FORMATTERS = Object.keys(FORMATTERS);
|
|
514
|
+
function formatValue(name, value, arg, opts = {}) {
|
|
515
|
+
const fmt = FORMATTERS[name];
|
|
516
|
+
if (!fmt) return void 0;
|
|
517
|
+
return fmt(value, arg, opts.locale ?? "en-US", opts.timeZone);
|
|
518
|
+
}
|
|
470
519
|
function baseString(value) {
|
|
471
520
|
if (value === null || value === void 0) return "";
|
|
472
521
|
if (value instanceof Date) return value.toISOString();
|
|
@@ -552,12 +601,13 @@ var templateEngine = {
|
|
|
552
601
|
if (!check.ok) return check;
|
|
553
602
|
const scope = buildScope(ctx);
|
|
554
603
|
const locale = ctx.extra && typeof ctx.extra.locale === "string" && ctx.extra.locale || typeof ctx.locale === "string" && ctx.locale || "en-US";
|
|
604
|
+
const timeZone = typeof ctx.timezone === "string" ? ctx.timezone : void 0;
|
|
555
605
|
const out = expr.source.replace(HOLE_RE, (_match, inner) => {
|
|
556
606
|
const parsed = parseHole(String(inner));
|
|
557
607
|
if (!parsed) return _match;
|
|
558
608
|
const value = resolvePath(scope, parsed.path);
|
|
559
609
|
if (parsed.filter) {
|
|
560
|
-
return FORMATTERS[parsed.filter.name](value, parsed.filter.arg, locale);
|
|
610
|
+
return FORMATTERS[parsed.filter.name](value, parsed.filter.arg, locale, timeZone);
|
|
561
611
|
}
|
|
562
612
|
return baseString(value);
|
|
563
613
|
});
|
|
@@ -854,22 +904,20 @@ function introspectScope(role, schema) {
|
|
|
854
904
|
};
|
|
855
905
|
}
|
|
856
906
|
var CEL_STDLIB_FUNCTIONS = [
|
|
907
|
+
// Dates (registered stdlib)
|
|
857
908
|
"now",
|
|
858
909
|
"today",
|
|
859
910
|
"daysFromNow",
|
|
911
|
+
"daysAgo",
|
|
860
912
|
"daysBetween",
|
|
861
913
|
"date",
|
|
862
914
|
"datetime",
|
|
863
|
-
|
|
864
|
-
"
|
|
865
|
-
"
|
|
866
|
-
"
|
|
867
|
-
"
|
|
868
|
-
|
|
869
|
-
"int",
|
|
870
|
-
"float",
|
|
871
|
-
"string",
|
|
872
|
-
"bool",
|
|
915
|
+
// Numbers (registered stdlib)
|
|
916
|
+
"abs",
|
|
917
|
+
"round",
|
|
918
|
+
"min",
|
|
919
|
+
"max",
|
|
920
|
+
// Strings (registered stdlib)
|
|
873
921
|
"upper",
|
|
874
922
|
"lower",
|
|
875
923
|
"trim",
|
|
@@ -877,11 +925,21 @@ var CEL_STDLIB_FUNCTIONS = [
|
|
|
877
925
|
"startsWith",
|
|
878
926
|
"endsWith",
|
|
879
927
|
"matches",
|
|
928
|
+
"joinNonEmpty",
|
|
929
|
+
// Collections / null-ish (registered stdlib)
|
|
930
|
+
"isBlank",
|
|
931
|
+
"isEmpty",
|
|
932
|
+
"coalesce",
|
|
933
|
+
"len",
|
|
934
|
+
// cel-js built-ins (verified to resolve)
|
|
935
|
+
"size",
|
|
880
936
|
"has",
|
|
881
|
-
"
|
|
882
|
-
"
|
|
883
|
-
"
|
|
884
|
-
"
|
|
937
|
+
"int",
|
|
938
|
+
"string",
|
|
939
|
+
"bool",
|
|
940
|
+
"double",
|
|
941
|
+
"timestamp",
|
|
942
|
+
"duration"
|
|
885
943
|
];
|
|
886
944
|
// Annotate the CommonJS export names for ESM import in node:
|
|
887
945
|
0 && (module.exports = {
|
|
@@ -893,6 +951,7 @@ var CEL_STDLIB_FUNCTIONS = [
|
|
|
893
951
|
celEngine,
|
|
894
952
|
cronEngine,
|
|
895
953
|
expectedDialect,
|
|
954
|
+
formatValue,
|
|
896
955
|
getEngine,
|
|
897
956
|
hasDialect,
|
|
898
957
|
introspectScope,
|