@objectstack/formula 9.11.0 → 10.2.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/formula@9.11.0 build /home/runner/work/framework/framework/packages/formula
2
+ > @objectstack/formula@10.2.0 build /home/runner/work/framework/framework/packages/formula
3
3
  > tsup --config ../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- ESM dist/index.mjs 29.97 KB
14
- ESM dist/index.mjs.map 83.86 KB
15
- ESM ⚡️ Build success in 126ms
16
- CJS dist/index.js 31.74 KB
17
- CJS dist/index.js.map 85.24 KB
18
- CJS ⚡️ Build success in 134ms
13
+ ESM dist/index.mjs 43.08 KB
14
+ ESM dist/index.mjs.map 118.46 KB
15
+ ESM ⚡️ Build success in 103ms
16
+ CJS dist/index.js 45.02 KB
17
+ CJS dist/index.js.map 120.34 KB
18
+ CJS ⚡️ Build success in 101ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 4238ms
21
- DTS dist/index.d.mts 16.79 KB
22
- DTS dist/index.d.ts 16.79 KB
20
+ DTS ⚡️ Build success in 5359ms
21
+ DTS dist/index.d.mts 20.19 KB
22
+ DTS dist/index.d.ts 20.19 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,96 @@
1
1
  # @objectstack/formula
2
2
 
3
+ ## 10.2.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [b496498]
8
+ - @objectstack/spec@10.2.0
9
+
10
+ ## 10.1.0
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [49da36e]
15
+ - Updated dependencies [ac79f16]
16
+ - @objectstack/spec@10.1.0
17
+
18
+ ## 10.0.0
19
+
20
+ ### Minor Changes
21
+
22
+ - cfd86ce: ADR-0058 — expression & predicate surface unification. Adds the canonical
23
+ CEL→FilterCondition pushdown compiler in `@objectstack/formula`
24
+ (`compileCelToFilter`, `isPushdownableCel`, `lowerCelAst`) plus an in-memory
25
+ `matchesFilterCondition` backend (one AST, three backends). `plugin-security`
26
+ (RLS `using`, via a SQL bridge) and `plugin-sharing` (`celToFilter`) cut over to
27
+ it, retiring the bespoke regex/field-equality front-ends. Compound sharing
28
+ conditions now compile and enforce end-to-end (closes #1887). The RLS `check`
29
+ clause is now enforced on the write post-image (insert/by-id update), fail-closed.
30
+ Non-pushdownable predicates (arithmetic, functions, subqueries, cross-object) are
31
+ an authoring compile error, never silently dropped (ADR-0049/0055).
32
+
33
+ ### Patch Changes
34
+
35
+ - 48a307a: build: validate UI action `visible` / `disabled` predicates at compile time
36
+
37
+ Extends the ADR-0032 build-time expression check to cover action `visible` and
38
+ `disabled` predicates (stack-level and object-attached), evaluated record-scoped
39
+ like validation rules. A record-header / row action's `visible` is evaluated by
40
+ `ActionEngine` against `{ record, recordId, objectName, user, … }` with
41
+ fail-closed semantics, so a **bare** field reference (`!done` instead of
42
+ `!record.done`) throws at runtime and the action is **silently hidden on every
43
+ record** — the trap behind the #2183 "Mark Done never hides" debugging hunt.
44
+ `os build` now reports it as an error with the corrective `record.<field>`
45
+ message instead of letting it ship.
46
+
47
+ `@objectstack/formula`: `ctx` and `features` are added to the record-scope
48
+ namespace roots (alongside the existing `user`, `data`, `context`, …) so the
49
+ ambient globals real action predicates use (`record.id == ctx.user.id`,
50
+ `features.multiOrgEnabled`) are not false-positives. Verified against the full
51
+ monorepo build (every example + platform bundle still compiles clean).
52
+
53
+ - 25fc0e4: build: extend ADR-0032 predicate validation to all flat record-scoped sites
54
+
55
+ Builds on the action-predicate guard. `os build` now also validates these
56
+ record-scoped predicates for bare field references (`status` instead of
57
+ `record.status`), which otherwise evaluate to nothing at runtime and silently
58
+ mis-behave:
59
+
60
+ - **field conditional rules** — `requiredWhen`, `readonlyWhen`,
61
+ `conditionalRequired`, `visibleWhen` (server-enforced; a broken one is
62
+ fail-open — the required/readonly rule just never fires);
63
+ - **sharing-rule `condition`** (security-critical — decides which rows a
64
+ principal sees);
65
+ - **lifecycle hook `condition`** (skips the handler when false);
66
+ - **nested `when`** on `conditional` validation rules (previously only the
67
+ top-level rule predicate was checked).
68
+
69
+ `@objectstack/formula`: adds `parent` to the record-scope namespace roots —
70
+ master-detail inline grids inject the header record as `parent` for a child
71
+ field's `readonlyWhen`/`requiredWhen` (ADR-0036, #1581), so `parent.status` is
72
+ legitimate, not a bare ref. Verified against the full monorepo build (76 tasks
73
+ clean).
74
+
75
+ Not yet covered (separate follow-up — needs a recursive view/page tree walker
76
+ and per-node scope classification): deeply-nested UI visibility predicates
77
+ (`view` element/section `visibleOn`/`condition`, `page` component `visibility`),
78
+ object field-group `visibleOn`, and app-nav `visible` (user/feature-scoped, not
79
+ record-scoped).
80
+
81
+ - Updated dependencies [d7ff626]
82
+ - Updated dependencies [2a1b16b]
83
+ - Updated dependencies [e16f2a8]
84
+ - Updated dependencies [e411a82]
85
+ - Updated dependencies [a581385]
86
+ - Updated dependencies [220ce5b]
87
+ - Updated dependencies [3efe334]
88
+ - Updated dependencies [feead7e]
89
+ - Updated dependencies [6ca20b3]
90
+ - Updated dependencies [5f875fe]
91
+ - Updated dependencies [b469950]
92
+ - @objectstack/spec@10.0.0
93
+
3
94
  ## 9.11.0
4
95
 
5
96
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Expression, ExpressionInput } from '@objectstack/spec';
2
- import { Environment } from '@marcbachmann/cel-js';
2
+ import { Environment, ASTNode } from '@marcbachmann/cel-js';
3
+ import { FilterCondition } from '@objectstack/spec/data';
3
4
 
4
5
  /**
5
6
  * @objectstack/formula — public types
@@ -296,6 +297,81 @@ declare function normalizeExpressionTree(root: unknown, path?: string[]): {
296
297
  error: EvalError;
297
298
  } | null;
298
299
 
300
+ type CelFilterFailReason =
301
+ /** CEL did not parse (syntax error). */
302
+ 'parse-error'
303
+ /** Shape is not pushdown-able (arithmetic, function call, relation traversal, …). */
304
+ | 'unsupported'
305
+ /** A required `variableRoot` reference was undefined/null in `variables`. */
306
+ | 'unresolved-variable';
307
+ type CelFilterCompileResult = {
308
+ ok: true;
309
+ filter: FilterCondition;
310
+ } | {
311
+ ok: false;
312
+ reason: CelFilterFailReason;
313
+ detail: string;
314
+ };
315
+ interface CelFilterCompileOptions {
316
+ /** Member-access roots that denote a record FIELD path. Default `['record']`. */
317
+ fieldRoots?: readonly string[];
318
+ /** Roots resolved as VALUES against {@link variables}. Default `['current_user']`. */
319
+ variableRoots?: readonly string[];
320
+ /**
321
+ * Value-resolution context, keyed by variable root. e.g.
322
+ * `{ current_user: { id, organization_id, org_user_ids } }`. A `record.*`
323
+ * (field) reference is NEVER resolved here — only `variableRoot` leaves are.
324
+ */
325
+ variables?: Record<string, unknown>;
326
+ }
327
+ /**
328
+ * Compile a CEL predicate into a {@link FilterCondition}, resolving `variableRoot`
329
+ * leaves against `opts.variables`. Returns a discriminated result — never throws
330
+ * for an authoring-level fault; a `false` result with a reason is the caller's
331
+ * cue to fail closed (deny) or surface a compile error.
332
+ */
333
+ declare function compileCelToFilter(input: string | {
334
+ source?: string;
335
+ }, opts?: CelFilterCompileOptions): CelFilterCompileResult;
336
+ /**
337
+ * Shape-only check: is this CEL predicate pushdown-able at all? Used by the
338
+ * authoring gate (ADR-0056 D4) to REJECT a predicate the runtime could only
339
+ * silently drop. Does not resolve `variables`.
340
+ */
341
+ declare function isPushdownableCel(input: string | {
342
+ source?: string;
343
+ }, opts?: Pick<CelFilterCompileOptions, 'fieldRoots' | 'variableRoots'>): {
344
+ ok: true;
345
+ } | {
346
+ ok: false;
347
+ reason: CelFilterFailReason;
348
+ detail: string;
349
+ };
350
+ /**
351
+ * Lower a pre-parsed cel-js AST node — the variant that lets the interpreter and
352
+ * the compiler share ONE parse (ADR-0058 D6, "one AST, two backends").
353
+ */
354
+ declare function lowerCelAst(ast: ASTNode, opts?: CelFilterCompileOptions, mode?: 'value' | 'shape'): CelFilterCompileResult;
355
+
356
+ /**
357
+ * matchesFilterCondition — evaluate a Mongo-style {@link FilterCondition} against
358
+ * ONE in-memory record (ADR-0058 D4/D6).
359
+ *
360
+ * This is the third backend for the canonical filter shape, completing the
361
+ * round-trip: `compileCelToFilter` lowers CEL → FilterCondition; the engine runs
362
+ * it as a `where`; `read-scope-sql` lowers it to SQL; and THIS evaluates it
363
+ * against a single record for write-side validation — the RLS `check` clause
364
+ * (post-image of an insert/update), where there is no query to push down to.
365
+ *
366
+ * Security posture: **fail closed.** Anything it cannot evaluate — a malformed
367
+ * node, an unknown operator, a nested relation object a flat record can't
368
+ * satisfy — returns `false` (the write is denied), never `true`. The operator
369
+ * vocabulary mirrors `read-scope-sql.ts` so the in-memory and SQL backends agree.
370
+ */
371
+
372
+ /** True iff `record` satisfies `filter`. A null/empty filter matches everything. */
373
+ declare function matchesFilterCondition(record: Record<string, unknown>, filter: FilterCondition | null | undefined): boolean;
374
+
299
375
  /**
300
376
  * Shared expression validator (ADR-0032 §Decision 1/5).
301
377
  *
@@ -393,4 +469,4 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
393
469
  */
394
470
  declare const CEL_STDLIB_FUNCTIONS: string[];
395
471
 
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 };
472
+ export { CEL_STDLIB_FUNCTIONS, type CelFilterCompileOptions, type CelFilterCompileResult, type CelFilterFailReason, 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, compileCelToFilter, cronEngine, expectedDialect, formatValue, getEngine, hasDialect, introspectScope, isPushdownableCel, lowerCelAst, matchesFilterCondition, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Expression, ExpressionInput } from '@objectstack/spec';
2
- import { Environment } from '@marcbachmann/cel-js';
2
+ import { Environment, ASTNode } from '@marcbachmann/cel-js';
3
+ import { FilterCondition } from '@objectstack/spec/data';
3
4
 
4
5
  /**
5
6
  * @objectstack/formula — public types
@@ -296,6 +297,81 @@ declare function normalizeExpressionTree(root: unknown, path?: string[]): {
296
297
  error: EvalError;
297
298
  } | null;
298
299
 
300
+ type CelFilterFailReason =
301
+ /** CEL did not parse (syntax error). */
302
+ 'parse-error'
303
+ /** Shape is not pushdown-able (arithmetic, function call, relation traversal, …). */
304
+ | 'unsupported'
305
+ /** A required `variableRoot` reference was undefined/null in `variables`. */
306
+ | 'unresolved-variable';
307
+ type CelFilterCompileResult = {
308
+ ok: true;
309
+ filter: FilterCondition;
310
+ } | {
311
+ ok: false;
312
+ reason: CelFilterFailReason;
313
+ detail: string;
314
+ };
315
+ interface CelFilterCompileOptions {
316
+ /** Member-access roots that denote a record FIELD path. Default `['record']`. */
317
+ fieldRoots?: readonly string[];
318
+ /** Roots resolved as VALUES against {@link variables}. Default `['current_user']`. */
319
+ variableRoots?: readonly string[];
320
+ /**
321
+ * Value-resolution context, keyed by variable root. e.g.
322
+ * `{ current_user: { id, organization_id, org_user_ids } }`. A `record.*`
323
+ * (field) reference is NEVER resolved here — only `variableRoot` leaves are.
324
+ */
325
+ variables?: Record<string, unknown>;
326
+ }
327
+ /**
328
+ * Compile a CEL predicate into a {@link FilterCondition}, resolving `variableRoot`
329
+ * leaves against `opts.variables`. Returns a discriminated result — never throws
330
+ * for an authoring-level fault; a `false` result with a reason is the caller's
331
+ * cue to fail closed (deny) or surface a compile error.
332
+ */
333
+ declare function compileCelToFilter(input: string | {
334
+ source?: string;
335
+ }, opts?: CelFilterCompileOptions): CelFilterCompileResult;
336
+ /**
337
+ * Shape-only check: is this CEL predicate pushdown-able at all? Used by the
338
+ * authoring gate (ADR-0056 D4) to REJECT a predicate the runtime could only
339
+ * silently drop. Does not resolve `variables`.
340
+ */
341
+ declare function isPushdownableCel(input: string | {
342
+ source?: string;
343
+ }, opts?: Pick<CelFilterCompileOptions, 'fieldRoots' | 'variableRoots'>): {
344
+ ok: true;
345
+ } | {
346
+ ok: false;
347
+ reason: CelFilterFailReason;
348
+ detail: string;
349
+ };
350
+ /**
351
+ * Lower a pre-parsed cel-js AST node — the variant that lets the interpreter and
352
+ * the compiler share ONE parse (ADR-0058 D6, "one AST, two backends").
353
+ */
354
+ declare function lowerCelAst(ast: ASTNode, opts?: CelFilterCompileOptions, mode?: 'value' | 'shape'): CelFilterCompileResult;
355
+
356
+ /**
357
+ * matchesFilterCondition — evaluate a Mongo-style {@link FilterCondition} against
358
+ * ONE in-memory record (ADR-0058 D4/D6).
359
+ *
360
+ * This is the third backend for the canonical filter shape, completing the
361
+ * round-trip: `compileCelToFilter` lowers CEL → FilterCondition; the engine runs
362
+ * it as a `where`; `read-scope-sql` lowers it to SQL; and THIS evaluates it
363
+ * against a single record for write-side validation — the RLS `check` clause
364
+ * (post-image of an insert/update), where there is no query to push down to.
365
+ *
366
+ * Security posture: **fail closed.** Anything it cannot evaluate — a malformed
367
+ * node, an unknown operator, a nested relation object a flat record can't
368
+ * satisfy — returns `false` (the write is denied), never `true`. The operator
369
+ * vocabulary mirrors `read-scope-sql.ts` so the in-memory and SQL backends agree.
370
+ */
371
+
372
+ /** True iff `record` satisfies `filter`. A null/empty filter matches everything. */
373
+ declare function matchesFilterCondition(record: Record<string, unknown>, filter: FilterCondition | null | undefined): boolean;
374
+
299
375
  /**
300
376
  * Shared expression validator (ADR-0032 §Decision 1/5).
301
377
  *
@@ -393,4 +469,4 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
393
469
  */
394
470
  declare const CEL_STDLIB_FUNCTIONS: string[];
395
471
 
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 };
472
+ export { CEL_STDLIB_FUNCTIONS, type CelFilterCompileOptions, type CelFilterCompileResult, type CelFilterFailReason, 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, compileCelToFilter, cronEngine, expectedDialect, formatValue, getEngine, hasDialect, introspectScope, isPushdownableCel, lowerCelAst, matchesFilterCondition, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };