@objectstack/formula 9.6.0 → 9.8.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 +83 -0
- package/dist/index.d.mts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +141 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +141 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/cel-engine.test.ts +132 -0
- package/src/cel-engine.ts +86 -2
- package/src/stdlib.ts +91 -0
- package/src/validate.test.ts +73 -0
- package/src/validate.ts +80 -11
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/formula@9.
|
|
2
|
+
> @objectstack/formula@9.8.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
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[
|
|
13
|
+
[32mCJS[39m [1mdist/index.js [22m[32m29.77 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m78.89 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 123ms
|
|
16
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m28.04 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m77.53 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 124ms
|
|
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 4371ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m15.71 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m15.71 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# @objectstack/formula
|
|
2
2
|
|
|
3
|
+
## 9.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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)
|
|
8
|
+
|
|
9
|
+
`introspectScope` / `CEL_STDLIB_FUNCTIONS` advertised 25 functions to authors
|
|
10
|
+
(incl. AI), but only 8 were registered — 14 faulted at runtime (`daysBetween`,
|
|
11
|
+
`abs`, `round`, `min`, `max`, `upper`, `lower`, `len`, `isEmpty`, `contains`,
|
|
12
|
+
`startsWith`, `endsWith`, `matches`, plus `date`/`datetime`). Authors were told
|
|
13
|
+
to call functions that don't exist (e.g. `daysBetween` for "days remaining").
|
|
14
|
+
|
|
15
|
+
Register the genuinely-useful set in `registerStdLib` with dyn-lenient signatures
|
|
16
|
+
(so a `Field.date` arriving as a string still works) and internal coercion, and
|
|
17
|
+
reconcile the catalog so every advertised entry resolves — guarded by a test that
|
|
18
|
+
evaluates every `CEL_STDLIB_FUNCTIONS` entry. Pure additions; no behavior change
|
|
19
|
+
to existing expressions.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [97c55b3]
|
|
24
|
+
- Updated dependencies [1b1f490]
|
|
25
|
+
- @objectstack/spec@9.8.0
|
|
26
|
+
|
|
27
|
+
## 9.7.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- ff0a87a: feat(validate): flag bare field references in record-scoped CEL sites at build time
|
|
32
|
+
|
|
33
|
+
> **Heads-up for downstream:** this adds a NEW build-time error. A `Field.formula`
|
|
34
|
+
> or validation predicate that references a field bare (`amount` instead of
|
|
35
|
+
> `record.amount`) now fails `objectstack compile`. These expressions were already
|
|
36
|
+
> silently broken at runtime (they evaluated to `null` / never fired), so this is a
|
|
37
|
+
> fix that surfaces a latent bug — but a stack carrying one will go from
|
|
38
|
+
> "builds, silently wrong" to "fails the build" on upgrade. The error message
|
|
39
|
+
> states the exact correction (`write record.<field>`).
|
|
40
|
+
|
|
41
|
+
A `Field.formula` and an object validation predicate evaluate against the
|
|
42
|
+
`record` namespace only — there is no field flattening — so a bare top-level
|
|
43
|
+
identifier (`amount`, `status`) resolves to nothing and the expression silently
|
|
44
|
+
evaluates to `null` / never fires. This is the silent-at-runtime class behind
|
|
45
|
+
the broken example-crm formulas (#1927) and is exactly what AI authors get wrong.
|
|
46
|
+
|
|
47
|
+
`validateExpression` now takes an evaluation `scope` and, for `scope: 'record'`,
|
|
48
|
+
reports a bare reference with the corrective form (`write record.<field>`). The
|
|
49
|
+
check is schema-free and acts only on cel-js's `Unknown variable` fault, so it
|
|
50
|
+
cannot false-positive on arithmetic/comparison/null-guard type overloads. Flow
|
|
51
|
+
and automation conditions keep the default `scope: 'flattened'` — the record's
|
|
52
|
+
fields ARE spread to top-level there (alongside flow variables), so bare refs
|
|
53
|
+
are correct and are NOT flagged. `objectstack compile` wires `record` scope for
|
|
54
|
+
field formulas and validation predicates; flow conditions stay flattened.
|
|
55
|
+
|
|
56
|
+
### Patch Changes
|
|
57
|
+
|
|
58
|
+
- 82c7438: fix(formula): register mixed `double <op> int` arithmetic overloads so number-field formulas compute
|
|
59
|
+
|
|
60
|
+
cel-js types a record field number as `double` and a bare integer literal as
|
|
61
|
+
`int`, and ships overloads only for matching numeric pairs. So an everyday
|
|
62
|
+
formula like `record.amount / 100` or `record.price * 2` faulted at runtime
|
|
63
|
+
(`no such overload: dyn<double> / int`); the engine caught the fault and the
|
|
64
|
+
formula silently evaluated to `null` — passing build, empty at runtime (#1928).
|
|
65
|
+
|
|
66
|
+
The CEL engine now registers the missing `double <op> int` / `int <op> double`
|
|
67
|
+
overloads for `+ - * / %`, computing the result as a `double` (CEL's mixed-numeric
|
|
68
|
+
promotion). Pure `int op int` is untouched, so integer division (`7 / 2 == 3`)
|
|
69
|
+
keeps its semantics — the overloads fire only when the operands are genuinely a
|
|
70
|
+
`double` and an `int`. Authors no longer need the `/ 100.0` float-literal workaround.
|
|
71
|
+
|
|
72
|
+
- 417b6ac: feat(validate): advisory did-you-mean warnings for likely field typos in flow conditions
|
|
73
|
+
|
|
74
|
+
Adds a non-blocking warning channel to build-time expression validation (#1928
|
|
75
|
+
tier 3). Flow / automation conditions flatten the record's fields to top-level,
|
|
76
|
+
so a bare `status` is correct — but a bare NON-field identifier is either a flow
|
|
77
|
+
variable or a typo. When it is a near-miss of a known field (edit distance), the
|
|
78
|
+
build now emits a `did you mean \`status\`?`warning instead of staying silent,
|
|
79
|
+
WITHOUT failing the build (a genuine flow variable won't be close to a field
|
|
80
|
+
name, so it stays quiet).`ExprValidationResult`gains a`warnings`array and`ExprIssue`a`severity`; `objectstack compile` prints warnings and only fails on
|
|
81
|
+
errors. This closes the silent-skip gap for misspelled trigger-condition fields
|
|
82
|
+
(the #1877 family) without the false-positive risk of a hard gate.
|
|
83
|
+
|
|
84
|
+
- @objectstack/spec@9.7.0
|
|
85
|
+
|
|
3
86
|
## 9.6.0
|
|
4
87
|
|
|
5
88
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -310,6 +310,23 @@ interface ExprSchemaHint {
|
|
|
310
310
|
objectName?: string;
|
|
311
311
|
/** Known top-level field names, so `record.<field>` can be checked. */
|
|
312
312
|
fields?: readonly string[];
|
|
313
|
+
/**
|
|
314
|
+
* Evaluation scope of the authoring site — determines whether a bare top-level
|
|
315
|
+
* identifier is legal (#1928):
|
|
316
|
+
* - `'record'` → the record is bound only as the `record` namespace, with
|
|
317
|
+
* no field flattening (`Field.formula`, object validation
|
|
318
|
+
* predicates). A bare `amount` resolves to nothing and the
|
|
319
|
+
* expression silently evaluates to `null` / never fires, so
|
|
320
|
+
* it MUST be written `record.amount`. We flag bare refs.
|
|
321
|
+
* - `'flattened'` → the record's own fields are spread to top-level alongside
|
|
322
|
+
* flow variables (flow / automation conditions), so bare
|
|
323
|
+
* `status` is correct and is NOT an error. Flow variables
|
|
324
|
+
* are not schema-knowable, so a non-field bare identifier
|
|
325
|
+
* can't be soundly told apart from a typo — but when one is
|
|
326
|
+
* a near-miss of a known field we emit a non-blocking
|
|
327
|
+
* did-you-mean *warning*. (Default.)
|
|
328
|
+
*/
|
|
329
|
+
scope?: 'record' | 'flattened';
|
|
313
330
|
}
|
|
314
331
|
interface ExprValidationError {
|
|
315
332
|
/** Self-correcting message: what is wrong + the correct form. */
|
|
@@ -320,6 +337,13 @@ interface ExprValidationError {
|
|
|
320
337
|
interface ExprValidationResult {
|
|
321
338
|
ok: boolean;
|
|
322
339
|
errors: ExprValidationError[];
|
|
340
|
+
/**
|
|
341
|
+
* Non-blocking advisories (#1928 tier 3): a likely-typo'd field reference in a
|
|
342
|
+
* flattened flow condition. Never affects `ok` — callers surface these without
|
|
343
|
+
* failing the build, since a bare identifier there may legitimately be a flow
|
|
344
|
+
* variable.
|
|
345
|
+
*/
|
|
346
|
+
warnings: ExprValidationError[];
|
|
323
347
|
}
|
|
324
348
|
/** The dialect a field role expects (Decision 2). */
|
|
325
349
|
declare function expectedDialect(role: FieldRole): 'cel' | 'template';
|
|
@@ -340,7 +364,12 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
|
|
|
340
364
|
roots: string[];
|
|
341
365
|
functions: string[];
|
|
342
366
|
};
|
|
343
|
-
/**
|
|
367
|
+
/**
|
|
368
|
+
* Public catalog of CEL functions available in expressions — what `introspectScope`
|
|
369
|
+
* advertises to authors (incl. AI). Every entry MUST actually resolve at runtime:
|
|
370
|
+
* either registered in `registerStdLib` or a verified cel-js built-in. Drifting this
|
|
371
|
+
* list ahead of the runtime tells the author to call functions that fault (#1928).
|
|
372
|
+
*/
|
|
344
373
|
declare const CEL_STDLIB_FUNCTIONS: string[];
|
|
345
374
|
|
|
346
375
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -310,6 +310,23 @@ interface ExprSchemaHint {
|
|
|
310
310
|
objectName?: string;
|
|
311
311
|
/** Known top-level field names, so `record.<field>` can be checked. */
|
|
312
312
|
fields?: readonly string[];
|
|
313
|
+
/**
|
|
314
|
+
* Evaluation scope of the authoring site — determines whether a bare top-level
|
|
315
|
+
* identifier is legal (#1928):
|
|
316
|
+
* - `'record'` → the record is bound only as the `record` namespace, with
|
|
317
|
+
* no field flattening (`Field.formula`, object validation
|
|
318
|
+
* predicates). A bare `amount` resolves to nothing and the
|
|
319
|
+
* expression silently evaluates to `null` / never fires, so
|
|
320
|
+
* it MUST be written `record.amount`. We flag bare refs.
|
|
321
|
+
* - `'flattened'` → the record's own fields are spread to top-level alongside
|
|
322
|
+
* flow variables (flow / automation conditions), so bare
|
|
323
|
+
* `status` is correct and is NOT an error. Flow variables
|
|
324
|
+
* are not schema-knowable, so a non-field bare identifier
|
|
325
|
+
* can't be soundly told apart from a typo — but when one is
|
|
326
|
+
* a near-miss of a known field we emit a non-blocking
|
|
327
|
+
* did-you-mean *warning*. (Default.)
|
|
328
|
+
*/
|
|
329
|
+
scope?: 'record' | 'flattened';
|
|
313
330
|
}
|
|
314
331
|
interface ExprValidationError {
|
|
315
332
|
/** Self-correcting message: what is wrong + the correct form. */
|
|
@@ -320,6 +337,13 @@ interface ExprValidationError {
|
|
|
320
337
|
interface ExprValidationResult {
|
|
321
338
|
ok: boolean;
|
|
322
339
|
errors: ExprValidationError[];
|
|
340
|
+
/**
|
|
341
|
+
* Non-blocking advisories (#1928 tier 3): a likely-typo'd field reference in a
|
|
342
|
+
* flattened flow condition. Never affects `ok` — callers surface these without
|
|
343
|
+
* failing the build, since a bare identifier there may legitimately be a flow
|
|
344
|
+
* variable.
|
|
345
|
+
*/
|
|
346
|
+
warnings: ExprValidationError[];
|
|
323
347
|
}
|
|
324
348
|
/** The dialect a field role expects (Decision 2). */
|
|
325
349
|
declare function expectedDialect(role: FieldRole): 'cel' | 'template';
|
|
@@ -340,7 +364,12 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
|
|
|
340
364
|
roots: string[];
|
|
341
365
|
functions: string[];
|
|
342
366
|
};
|
|
343
|
-
/**
|
|
367
|
+
/**
|
|
368
|
+
* Public catalog of CEL functions available in expressions — what `introspectScope`
|
|
369
|
+
* advertises to authors (incl. AI). Every entry MUST actually resolve at runtime:
|
|
370
|
+
* either registered in `registerStdLib` or a verified cel-js built-in. Drifting this
|
|
371
|
+
* list ahead of the runtime tells the author to call functions that fault (#1928).
|
|
372
|
+
*/
|
|
344
373
|
declare const CEL_STDLIB_FUNCTIONS: string[];
|
|
345
374
|
|
|
346
375
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,12 @@ function startOfDayUtc(d) {
|
|
|
51
51
|
out.setUTCHours(0, 0, 0, 0);
|
|
52
52
|
return out;
|
|
53
53
|
}
|
|
54
|
+
function toDate(v) {
|
|
55
|
+
if (v instanceof Date) return v;
|
|
56
|
+
if (typeof v === "number" || typeof v === "bigint") return new Date(Number(v));
|
|
57
|
+
return new Date(String(v));
|
|
58
|
+
}
|
|
59
|
+
var MS_PER_DAY = 864e5;
|
|
54
60
|
function addDaysUtc(d, n) {
|
|
55
61
|
const out = new Date(d.getTime());
|
|
56
62
|
out.setUTCDate(out.getUTCDate() + n);
|
|
@@ -96,8 +102,35 @@ function registerStdLib(env, now) {
|
|
|
96
102
|
}
|
|
97
103
|
return parts.join(separator);
|
|
98
104
|
}
|
|
105
|
+
).registerFunction(
|
|
106
|
+
"daysBetween(dyn, dyn): int",
|
|
107
|
+
(a, b) => BigInt(Math.round((toDate(b).getTime() - toDate(a).getTime()) / MS_PER_DAY))
|
|
108
|
+
).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(
|
|
109
|
+
"isEmpty(dyn): bool",
|
|
110
|
+
(v) => v === null || v === void 0 || lengthOf(v) === 0
|
|
99
111
|
);
|
|
100
112
|
}
|
|
113
|
+
function lengthOf(v) {
|
|
114
|
+
if (v === null || v === void 0) return 0;
|
|
115
|
+
if (typeof v === "string" || Array.isArray(v)) return v.length;
|
|
116
|
+
if (typeof v === "object") return Object.keys(v).length;
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
function registerNumericCoercions(env) {
|
|
120
|
+
const ops = {
|
|
121
|
+
"+": (a, b) => a + b,
|
|
122
|
+
"-": (a, b) => a - b,
|
|
123
|
+
"*": (a, b) => a * b,
|
|
124
|
+
"/": (a, b) => a / b,
|
|
125
|
+
"%": (a, b) => a % b
|
|
126
|
+
};
|
|
127
|
+
for (const [op, fn] of Object.entries(ops)) {
|
|
128
|
+
const impl = (a, b) => fn(Number(a), Number(b));
|
|
129
|
+
env.registerOperator(`double ${op} int`, impl);
|
|
130
|
+
env.registerOperator(`int ${op} double`, impl);
|
|
131
|
+
}
|
|
132
|
+
return env;
|
|
133
|
+
}
|
|
101
134
|
function buildScope(ctx) {
|
|
102
135
|
const scope = {};
|
|
103
136
|
if (ctx.record !== void 0) scope.record = ctx.record;
|
|
@@ -126,7 +159,66 @@ function buildEnv(now) {
|
|
|
126
159
|
enableOptionalTypes: true,
|
|
127
160
|
limits: DEFAULT_LIMITS
|
|
128
161
|
});
|
|
129
|
-
return registerStdLib(env, now);
|
|
162
|
+
return registerNumericCoercions(registerStdLib(env, now));
|
|
163
|
+
}
|
|
164
|
+
var SCOPE_ROOTS = [
|
|
165
|
+
"record",
|
|
166
|
+
"previous",
|
|
167
|
+
"input",
|
|
168
|
+
"output",
|
|
169
|
+
"os",
|
|
170
|
+
"vars",
|
|
171
|
+
"variables",
|
|
172
|
+
"automation",
|
|
173
|
+
"context",
|
|
174
|
+
"args",
|
|
175
|
+
"item",
|
|
176
|
+
"env",
|
|
177
|
+
"user",
|
|
178
|
+
"step",
|
|
179
|
+
"result",
|
|
180
|
+
"trigger",
|
|
181
|
+
"event",
|
|
182
|
+
"payload",
|
|
183
|
+
"data",
|
|
184
|
+
"params",
|
|
185
|
+
"config",
|
|
186
|
+
"settings"
|
|
187
|
+
];
|
|
188
|
+
function buildScopedEnv(knownFields) {
|
|
189
|
+
const env = new import_cel_js.Environment({
|
|
190
|
+
unlistedVariablesAreDyn: false,
|
|
191
|
+
enableOptionalTypes: true,
|
|
192
|
+
limits: DEFAULT_LIMITS
|
|
193
|
+
});
|
|
194
|
+
registerStdLib(env, () => /* @__PURE__ */ new Date(0));
|
|
195
|
+
for (const root of SCOPE_ROOTS) {
|
|
196
|
+
try {
|
|
197
|
+
env.registerVariable(root, "map");
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const field of knownFields) {
|
|
202
|
+
try {
|
|
203
|
+
env.registerVariable(field, "dyn");
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return env;
|
|
208
|
+
}
|
|
209
|
+
var recordScopeEnv;
|
|
210
|
+
function firstUndeclaredReference(source, knownFields = []) {
|
|
211
|
+
if (typeof source !== "string" || !source.trim()) return null;
|
|
212
|
+
try {
|
|
213
|
+
const env = knownFields.length === 0 ? recordScopeEnv ?? (recordScopeEnv = buildScopedEnv([])) : buildScopedEnv(knownFields);
|
|
214
|
+
const result = env.parse(source).check?.();
|
|
215
|
+
if (result && result.valid === false) {
|
|
216
|
+
const m = /Unknown variable:\s*([A-Za-z_$][\w$]*)/.exec(result.error?.message ?? "");
|
|
217
|
+
if (m) return m[1];
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
130
222
|
}
|
|
131
223
|
function coerce(value) {
|
|
132
224
|
if (typeof value === "bigint") {
|
|
@@ -715,11 +807,12 @@ function levenshtein(a, b) {
|
|
|
715
807
|
function validateExpression(role, input, schema) {
|
|
716
808
|
const { dialect, source } = toSource(input);
|
|
717
809
|
const errors = [];
|
|
718
|
-
|
|
810
|
+
const warnings = [];
|
|
811
|
+
if (!source.trim()) return { ok: true, errors, warnings };
|
|
719
812
|
if (role === "template") {
|
|
720
813
|
if (dialect && dialect !== "template") {
|
|
721
814
|
errors.push({ source, message: `expected a text template but got a \`${dialect}\` expression.` });
|
|
722
|
-
return { ok: false, errors };
|
|
815
|
+
return { ok: false, errors, warnings };
|
|
723
816
|
}
|
|
724
817
|
const compiled2 = templateEngine.compile(source);
|
|
725
818
|
if (!compiled2.ok) {
|
|
@@ -727,11 +820,11 @@ function validateExpression(role, input, schema) {
|
|
|
727
820
|
}
|
|
728
821
|
const hint = SINGLE_BRACE_RE.test(source) ? bracesHintForTemplate(source) : null;
|
|
729
822
|
if (hint) errors.push({ source, message: hint });
|
|
730
|
-
return { ok: errors.length === 0, errors };
|
|
823
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
731
824
|
}
|
|
732
825
|
if (dialect && dialect !== "cel") {
|
|
733
826
|
errors.push({ source, message: `expected a CEL expression but got a \`${dialect}\` dialect.` });
|
|
734
|
-
return { ok: false, errors };
|
|
827
|
+
return { ok: false, errors, warnings };
|
|
735
828
|
}
|
|
736
829
|
const compiled = celEngine.compile(source);
|
|
737
830
|
if (!compiled.ok) {
|
|
@@ -742,8 +835,28 @@ function validateExpression(role, input, schema) {
|
|
|
742
835
|
});
|
|
743
836
|
} else {
|
|
744
837
|
checkFieldExistence(source, schema, errors);
|
|
838
|
+
if (schema?.scope === "record") {
|
|
839
|
+
const bare = firstUndeclaredReference(source);
|
|
840
|
+
if (bare) {
|
|
841
|
+
errors.push({
|
|
842
|
+
source,
|
|
843
|
+
message: `bare reference \`${bare}\` \u2014 a formula/validation expression binds the record as the \`record\` namespace, not at top level, so \`${bare}\` resolves to nothing and the expression silently evaluates to null. Write \`record.${bare}\`.`
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
} else if (schema?.fields && schema.fields.length > 0) {
|
|
847
|
+
const unknown = firstUndeclaredReference(source, schema.fields);
|
|
848
|
+
if (unknown) {
|
|
849
|
+
const suggestion = nearest(unknown, schema.fields);
|
|
850
|
+
if (suggestion) {
|
|
851
|
+
warnings.push({
|
|
852
|
+
source,
|
|
853
|
+
message: `\`${unknown}\` is not a field of \`${schema.objectName ?? "the trigger object"}\` \u2014 did you mean \`${suggestion}\`? (flow conditions reference fields bare, e.g. \`${suggestion} == \u2026\`). If \`${unknown}\` is a flow variable this is safe to ignore.`
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
745
858
|
}
|
|
746
|
-
return { ok: errors.length === 0, errors };
|
|
859
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
747
860
|
}
|
|
748
861
|
function bracesHintForTemplate(source) {
|
|
749
862
|
const m = SINGLE_BRACE_RE.exec(source);
|
|
@@ -759,22 +872,20 @@ function introspectScope(role, schema) {
|
|
|
759
872
|
};
|
|
760
873
|
}
|
|
761
874
|
var CEL_STDLIB_FUNCTIONS = [
|
|
875
|
+
// Dates (registered stdlib)
|
|
762
876
|
"now",
|
|
763
877
|
"today",
|
|
764
878
|
"daysFromNow",
|
|
879
|
+
"daysAgo",
|
|
765
880
|
"daysBetween",
|
|
766
881
|
"date",
|
|
767
882
|
"datetime",
|
|
768
|
-
|
|
769
|
-
"
|
|
770
|
-
"
|
|
771
|
-
"
|
|
772
|
-
"
|
|
773
|
-
|
|
774
|
-
"int",
|
|
775
|
-
"float",
|
|
776
|
-
"string",
|
|
777
|
-
"bool",
|
|
883
|
+
// Numbers (registered stdlib)
|
|
884
|
+
"abs",
|
|
885
|
+
"round",
|
|
886
|
+
"min",
|
|
887
|
+
"max",
|
|
888
|
+
// Strings (registered stdlib)
|
|
778
889
|
"upper",
|
|
779
890
|
"lower",
|
|
780
891
|
"trim",
|
|
@@ -782,11 +893,21 @@ var CEL_STDLIB_FUNCTIONS = [
|
|
|
782
893
|
"startsWith",
|
|
783
894
|
"endsWith",
|
|
784
895
|
"matches",
|
|
896
|
+
"joinNonEmpty",
|
|
897
|
+
// Collections / null-ish (registered stdlib)
|
|
898
|
+
"isBlank",
|
|
899
|
+
"isEmpty",
|
|
900
|
+
"coalesce",
|
|
901
|
+
"len",
|
|
902
|
+
// cel-js built-ins (verified to resolve)
|
|
903
|
+
"size",
|
|
785
904
|
"has",
|
|
786
|
-
"
|
|
787
|
-
"
|
|
788
|
-
"
|
|
789
|
-
"
|
|
905
|
+
"int",
|
|
906
|
+
"string",
|
|
907
|
+
"bool",
|
|
908
|
+
"double",
|
|
909
|
+
"timestamp",
|
|
910
|
+
"duration"
|
|
790
911
|
];
|
|
791
912
|
// Annotate the CommonJS export names for ESM import in node:
|
|
792
913
|
0 && (module.exports = {
|