@objectstack/formula 9.5.1 → 9.7.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 +73 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +104 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +104 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/cel-engine.test.ts +74 -0
- package/src/cel-engine.ts +97 -6
- package/src/stdlib.ts +32 -0
- package/src/validate.test.ts +94 -0
- package/src/validate.ts +64 -6
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/formula@9.
|
|
2
|
+
> @objectstack/formula@9.7.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[32m27.85 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m73.20 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 103ms
|
|
16
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m26.12 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m71.83 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 104ms
|
|
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 5303ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m15.44 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m15.44 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# @objectstack/formula
|
|
2
2
|
|
|
3
|
+
## 9.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ff0a87a: feat(validate): flag bare field references in record-scoped CEL sites at build time
|
|
8
|
+
|
|
9
|
+
> **Heads-up for downstream:** this adds a NEW build-time error. A `Field.formula`
|
|
10
|
+
> or validation predicate that references a field bare (`amount` instead of
|
|
11
|
+
> `record.amount`) now fails `objectstack compile`. These expressions were already
|
|
12
|
+
> silently broken at runtime (they evaluated to `null` / never fired), so this is a
|
|
13
|
+
> fix that surfaces a latent bug — but a stack carrying one will go from
|
|
14
|
+
> "builds, silently wrong" to "fails the build" on upgrade. The error message
|
|
15
|
+
> states the exact correction (`write record.<field>`).
|
|
16
|
+
|
|
17
|
+
A `Field.formula` and an object validation predicate evaluate against the
|
|
18
|
+
`record` namespace only — there is no field flattening — so a bare top-level
|
|
19
|
+
identifier (`amount`, `status`) resolves to nothing and the expression silently
|
|
20
|
+
evaluates to `null` / never fires. This is the silent-at-runtime class behind
|
|
21
|
+
the broken example-crm formulas (#1927) and is exactly what AI authors get wrong.
|
|
22
|
+
|
|
23
|
+
`validateExpression` now takes an evaluation `scope` and, for `scope: 'record'`,
|
|
24
|
+
reports a bare reference with the corrective form (`write record.<field>`). The
|
|
25
|
+
check is schema-free and acts only on cel-js's `Unknown variable` fault, so it
|
|
26
|
+
cannot false-positive on arithmetic/comparison/null-guard type overloads. Flow
|
|
27
|
+
and automation conditions keep the default `scope: 'flattened'` — the record's
|
|
28
|
+
fields ARE spread to top-level there (alongside flow variables), so bare refs
|
|
29
|
+
are correct and are NOT flagged. `objectstack compile` wires `record` scope for
|
|
30
|
+
field formulas and validation predicates; flow conditions stay flattened.
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 82c7438: fix(formula): register mixed `double <op> int` arithmetic overloads so number-field formulas compute
|
|
35
|
+
|
|
36
|
+
cel-js types a record field number as `double` and a bare integer literal as
|
|
37
|
+
`int`, and ships overloads only for matching numeric pairs. So an everyday
|
|
38
|
+
formula like `record.amount / 100` or `record.price * 2` faulted at runtime
|
|
39
|
+
(`no such overload: dyn<double> / int`); the engine caught the fault and the
|
|
40
|
+
formula silently evaluated to `null` — passing build, empty at runtime (#1928).
|
|
41
|
+
|
|
42
|
+
The CEL engine now registers the missing `double <op> int` / `int <op> double`
|
|
43
|
+
overloads for `+ - * / %`, computing the result as a `double` (CEL's mixed-numeric
|
|
44
|
+
promotion). Pure `int op int` is untouched, so integer division (`7 / 2 == 3`)
|
|
45
|
+
keeps its semantics — the overloads fire only when the operands are genuinely a
|
|
46
|
+
`double` and an `int`. Authors no longer need the `/ 100.0` float-literal workaround.
|
|
47
|
+
|
|
48
|
+
- 417b6ac: feat(validate): advisory did-you-mean warnings for likely field typos in flow conditions
|
|
49
|
+
|
|
50
|
+
Adds a non-blocking warning channel to build-time expression validation (#1928
|
|
51
|
+
tier 3). Flow / automation conditions flatten the record's fields to top-level,
|
|
52
|
+
so a bare `status` is correct — but a bare NON-field identifier is either a flow
|
|
53
|
+
variable or a typo. When it is a near-miss of a known field (edit distance), the
|
|
54
|
+
build now emits a `did you mean \`status\`?`warning instead of staying silent,
|
|
55
|
+
WITHOUT failing the build (a genuine flow variable won't be close to a field
|
|
56
|
+
name, so it stays quiet).`ExprValidationResult`gains a`warnings`array and`ExprIssue`a`severity`; `objectstack compile` prints warnings and only fails on
|
|
57
|
+
errors. This closes the silent-skip gap for misspelled trigger-condition fields
|
|
58
|
+
(the #1877 family) without the false-positive risk of a hard gate.
|
|
59
|
+
|
|
60
|
+
- @objectstack/spec@9.7.0
|
|
61
|
+
|
|
62
|
+
## 9.6.0
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- bb00a50: fix(formula): catch unknown functions in CEL conditions at build (#1877)
|
|
67
|
+
|
|
68
|
+
`compile()` discarded cel-js's type-check verdict because `check()` returns a `TypeCheckResult` object (`{ valid, error }`), not an array — so the `Array.isArray(checkErrors)` guard never matched. A condition calling an unknown function (`PRIOR(status)`, a typo'd `isBlnk(...)`) type-checks as `found no matching overload`, but that result never surfaced, so `objectstack compile`, `registerFlow`, and the `validate_expression` tool all accepted the predicate, which then silently no-op'd the flow at runtime. Now reads the documented `{ valid, error }` shape, closing the gap for flow conditions, validation rules, and field formulas at once.
|
|
69
|
+
|
|
70
|
+
- Updated dependencies [d1e930a]
|
|
71
|
+
- Updated dependencies [71578f2]
|
|
72
|
+
- Updated dependencies [5e3a301]
|
|
73
|
+
- Updated dependencies [5db2742]
|
|
74
|
+
- @objectstack/spec@9.6.0
|
|
75
|
+
|
|
3
76
|
## 9.5.1
|
|
4
77
|
|
|
5
78
|
### 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';
|
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';
|
package/dist/index.js
CHANGED
|
@@ -98,6 +98,21 @@ function registerStdLib(env, now) {
|
|
|
98
98
|
}
|
|
99
99
|
);
|
|
100
100
|
}
|
|
101
|
+
function registerNumericCoercions(env) {
|
|
102
|
+
const ops = {
|
|
103
|
+
"+": (a, b) => a + b,
|
|
104
|
+
"-": (a, b) => a - b,
|
|
105
|
+
"*": (a, b) => a * b,
|
|
106
|
+
"/": (a, b) => a / b,
|
|
107
|
+
"%": (a, b) => a % b
|
|
108
|
+
};
|
|
109
|
+
for (const [op, fn] of Object.entries(ops)) {
|
|
110
|
+
const impl = (a, b) => fn(Number(a), Number(b));
|
|
111
|
+
env.registerOperator(`double ${op} int`, impl);
|
|
112
|
+
env.registerOperator(`int ${op} double`, impl);
|
|
113
|
+
}
|
|
114
|
+
return env;
|
|
115
|
+
}
|
|
101
116
|
function buildScope(ctx) {
|
|
102
117
|
const scope = {};
|
|
103
118
|
if (ctx.record !== void 0) scope.record = ctx.record;
|
|
@@ -126,7 +141,66 @@ function buildEnv(now) {
|
|
|
126
141
|
enableOptionalTypes: true,
|
|
127
142
|
limits: DEFAULT_LIMITS
|
|
128
143
|
});
|
|
129
|
-
return registerStdLib(env, now);
|
|
144
|
+
return registerNumericCoercions(registerStdLib(env, now));
|
|
145
|
+
}
|
|
146
|
+
var SCOPE_ROOTS = [
|
|
147
|
+
"record",
|
|
148
|
+
"previous",
|
|
149
|
+
"input",
|
|
150
|
+
"output",
|
|
151
|
+
"os",
|
|
152
|
+
"vars",
|
|
153
|
+
"variables",
|
|
154
|
+
"automation",
|
|
155
|
+
"context",
|
|
156
|
+
"args",
|
|
157
|
+
"item",
|
|
158
|
+
"env",
|
|
159
|
+
"user",
|
|
160
|
+
"step",
|
|
161
|
+
"result",
|
|
162
|
+
"trigger",
|
|
163
|
+
"event",
|
|
164
|
+
"payload",
|
|
165
|
+
"data",
|
|
166
|
+
"params",
|
|
167
|
+
"config",
|
|
168
|
+
"settings"
|
|
169
|
+
];
|
|
170
|
+
function buildScopedEnv(knownFields) {
|
|
171
|
+
const env = new import_cel_js.Environment({
|
|
172
|
+
unlistedVariablesAreDyn: false,
|
|
173
|
+
enableOptionalTypes: true,
|
|
174
|
+
limits: DEFAULT_LIMITS
|
|
175
|
+
});
|
|
176
|
+
registerStdLib(env, () => /* @__PURE__ */ new Date(0));
|
|
177
|
+
for (const root of SCOPE_ROOTS) {
|
|
178
|
+
try {
|
|
179
|
+
env.registerVariable(root, "map");
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const field of knownFields) {
|
|
184
|
+
try {
|
|
185
|
+
env.registerVariable(field, "dyn");
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return env;
|
|
190
|
+
}
|
|
191
|
+
var recordScopeEnv;
|
|
192
|
+
function firstUndeclaredReference(source, knownFields = []) {
|
|
193
|
+
if (typeof source !== "string" || !source.trim()) return null;
|
|
194
|
+
try {
|
|
195
|
+
const env = knownFields.length === 0 ? recordScopeEnv ?? (recordScopeEnv = buildScopedEnv([])) : buildScopedEnv(knownFields);
|
|
196
|
+
const result = env.parse(source).check?.();
|
|
197
|
+
if (result && result.valid === false) {
|
|
198
|
+
const m = /Unknown variable:\s*([A-Za-z_$][\w$]*)/.exec(result.error?.message ?? "");
|
|
199
|
+
if (m) return m[1];
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
130
204
|
}
|
|
131
205
|
function coerce(value) {
|
|
132
206
|
if (typeof value === "bigint") {
|
|
@@ -185,11 +259,11 @@ var celEngine = {
|
|
|
185
259
|
try {
|
|
186
260
|
const env = buildEnv(() => /* @__PURE__ */ new Date(0));
|
|
187
261
|
const compiled = env.parse(source);
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
262
|
+
const checkResult = compiled.check?.();
|
|
263
|
+
if (checkResult && checkResult.valid === false) {
|
|
190
264
|
return {
|
|
191
265
|
ok: false,
|
|
192
|
-
error: { kind: "type", message:
|
|
266
|
+
error: { kind: "type", message: checkResult.error?.message ?? "expression failed type checking" }
|
|
193
267
|
};
|
|
194
268
|
}
|
|
195
269
|
return { ok: true, value: compiled.ast };
|
|
@@ -715,11 +789,12 @@ function levenshtein(a, b) {
|
|
|
715
789
|
function validateExpression(role, input, schema) {
|
|
716
790
|
const { dialect, source } = toSource(input);
|
|
717
791
|
const errors = [];
|
|
718
|
-
|
|
792
|
+
const warnings = [];
|
|
793
|
+
if (!source.trim()) return { ok: true, errors, warnings };
|
|
719
794
|
if (role === "template") {
|
|
720
795
|
if (dialect && dialect !== "template") {
|
|
721
796
|
errors.push({ source, message: `expected a text template but got a \`${dialect}\` expression.` });
|
|
722
|
-
return { ok: false, errors };
|
|
797
|
+
return { ok: false, errors, warnings };
|
|
723
798
|
}
|
|
724
799
|
const compiled2 = templateEngine.compile(source);
|
|
725
800
|
if (!compiled2.ok) {
|
|
@@ -727,11 +802,11 @@ function validateExpression(role, input, schema) {
|
|
|
727
802
|
}
|
|
728
803
|
const hint = SINGLE_BRACE_RE.test(source) ? bracesHintForTemplate(source) : null;
|
|
729
804
|
if (hint) errors.push({ source, message: hint });
|
|
730
|
-
return { ok: errors.length === 0, errors };
|
|
805
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
731
806
|
}
|
|
732
807
|
if (dialect && dialect !== "cel") {
|
|
733
808
|
errors.push({ source, message: `expected a CEL expression but got a \`${dialect}\` dialect.` });
|
|
734
|
-
return { ok: false, errors };
|
|
809
|
+
return { ok: false, errors, warnings };
|
|
735
810
|
}
|
|
736
811
|
const compiled = celEngine.compile(source);
|
|
737
812
|
if (!compiled.ok) {
|
|
@@ -742,8 +817,28 @@ function validateExpression(role, input, schema) {
|
|
|
742
817
|
});
|
|
743
818
|
} else {
|
|
744
819
|
checkFieldExistence(source, schema, errors);
|
|
820
|
+
if (schema?.scope === "record") {
|
|
821
|
+
const bare = firstUndeclaredReference(source);
|
|
822
|
+
if (bare) {
|
|
823
|
+
errors.push({
|
|
824
|
+
source,
|
|
825
|
+
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}\`.`
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
} else if (schema?.fields && schema.fields.length > 0) {
|
|
829
|
+
const unknown = firstUndeclaredReference(source, schema.fields);
|
|
830
|
+
if (unknown) {
|
|
831
|
+
const suggestion = nearest(unknown, schema.fields);
|
|
832
|
+
if (suggestion) {
|
|
833
|
+
warnings.push({
|
|
834
|
+
source,
|
|
835
|
+
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.`
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
745
840
|
}
|
|
746
|
-
return { ok: errors.length === 0, errors };
|
|
841
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
747
842
|
}
|
|
748
843
|
function bracesHintForTemplate(source) {
|
|
749
844
|
const m = SINGLE_BRACE_RE.exec(source);
|