@objectstack/formula 10.3.0 → 11.1.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 +47 -0
- package/dist/index.d.mts +42 -2
- package/dist/index.d.ts +42 -2
- package/dist/index.js +92 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +86 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/cel-engine.ts +36 -0
- package/src/index.ts +2 -2
- package/src/stdlib.ts +42 -1
- package/src/types.ts +14 -1
- package/src/validate.test.ts +43 -1
- package/src/validate.ts +101 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/formula@
|
|
2
|
+
> @objectstack/formula@11.1.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[32m48.18 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m130.43 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 138ms
|
|
16
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m46.17 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m128.47 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 139ms
|
|
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 5265ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m22.39 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m22.39 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @objectstack/formula
|
|
2
2
|
|
|
3
|
+
## 11.1.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [ecf193f]
|
|
8
|
+
- Updated dependencies [51bec81]
|
|
9
|
+
- Updated dependencies [3e593a7]
|
|
10
|
+
- Updated dependencies [63d5403]
|
|
11
|
+
- @objectstack/spec@11.1.0
|
|
12
|
+
|
|
13
|
+
## 11.0.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- ef3ed67: Formula field typing: `inferExpressionType()` + a declared `returnType`.
|
|
18
|
+
|
|
19
|
+
- `@objectstack/formula`: new `inferExpressionType()` (and lower-level `inferCelType()`) surfaces the cel-js type-checker's result for a CEL value/formula expression, mapped to `number | text | boolean | date | unknown`. Conservative — two `dyn` operands stay `unknown`; typed literals/stdlib returns pin a concrete type.
|
|
20
|
+
- `@objectstack/spec`: `FieldSchema` gains an optional `returnType` (`number|text|boolean|date`) so a formula field can carry its declared value type (the way Salesforce/Airtable do), letting consumers (dataset measures, formatting, validation) read a declared type instead of re-parsing the expression.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [ab5718a]
|
|
25
|
+
- Updated dependencies [4845c12]
|
|
26
|
+
- Updated dependencies [c1a754a]
|
|
27
|
+
- Updated dependencies [6fbe91f]
|
|
28
|
+
- Updated dependencies [715d667]
|
|
29
|
+
- Updated dependencies [5eef4cf]
|
|
30
|
+
- Updated dependencies [72759e1]
|
|
31
|
+
- Updated dependencies [6c4fbd9]
|
|
32
|
+
- Updated dependencies [ef3ed67]
|
|
33
|
+
- Updated dependencies [cd51229]
|
|
34
|
+
- Updated dependencies [7697a0e]
|
|
35
|
+
- Updated dependencies [e7e04f1]
|
|
36
|
+
- Updated dependencies [cfd5ac4]
|
|
37
|
+
- Updated dependencies [2be5c1f]
|
|
38
|
+
- Updated dependencies [ad143ce]
|
|
39
|
+
- Updated dependencies [5c4a8c8]
|
|
40
|
+
- Updated dependencies [3afaeed]
|
|
41
|
+
- Updated dependencies [8801c02]
|
|
42
|
+
- Updated dependencies [3d04e06]
|
|
43
|
+
- Updated dependencies [4a84c98]
|
|
44
|
+
- Updated dependencies [d980f0d]
|
|
45
|
+
- Updated dependencies [a658523]
|
|
46
|
+
- Updated dependencies [82ff91c]
|
|
47
|
+
- Updated dependencies [638f472]
|
|
48
|
+
- @objectstack/spec@11.0.0
|
|
49
|
+
|
|
3
50
|
## 10.3.0
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -32,9 +32,22 @@ interface EvalContext {
|
|
|
32
32
|
* Defaults to `UTC` when unset. Calendar-day `date` rendering stays tz-naive.
|
|
33
33
|
*/
|
|
34
34
|
timezone?: string;
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Current authenticated subject (hook / action / view contexts).
|
|
37
|
+
*
|
|
38
|
+
* ADR-0068: the canonical user contract is {@link EvalUser} from
|
|
39
|
+
* `@objectstack/spec`, surfaced to predicates as `current_user` (aliases
|
|
40
|
+
* `user`, `ctx.user`). `roles: string[]` is the only canonical role field;
|
|
41
|
+
* the singular `role` is deprecated (its "overwritten to 'admin' on
|
|
42
|
+
* promotion" behavior is the footgun ADR-0068 eliminates).
|
|
43
|
+
*/
|
|
36
44
|
user?: {
|
|
37
45
|
id: string;
|
|
46
|
+
/** CANONICAL (ADR-0068). Scope-resolved role names assigned to the user. */
|
|
47
|
+
roles?: string[];
|
|
48
|
+
/** Active organization ID (null = platform / unscoped). */
|
|
49
|
+
organizationId?: string | null;
|
|
50
|
+
/** @deprecated ADR-0068 — use {@link roles}. Retained for back-compat only. */
|
|
38
51
|
role?: string;
|
|
39
52
|
email?: string;
|
|
40
53
|
[key: string]: unknown;
|
|
@@ -424,6 +437,14 @@ interface ExprSchemaHint {
|
|
|
424
437
|
* did-you-mean *warning*. (Default.)
|
|
425
438
|
*/
|
|
426
439
|
scope?: 'record' | 'flattened';
|
|
440
|
+
/**
|
|
441
|
+
* ADR-0068 D4 — the closed catalog of valid role names (built-in + declared).
|
|
442
|
+
* When supplied, a role-membership predicate testing a role NOT in this set
|
|
443
|
+
* (e.g. `'org_admni' in current_user.roles`) is flagged as an error. Closes
|
|
444
|
+
* the AI-hallucination hole where a model invents a plausible-but-nonexistent
|
|
445
|
+
* role that then silently never matches. Absent => role checks are skipped.
|
|
446
|
+
*/
|
|
447
|
+
roleCatalog?: readonly string[];
|
|
427
448
|
}
|
|
428
449
|
interface ExprValidationError {
|
|
429
450
|
/** Self-correcting message: what is wrong + the correct form. */
|
|
@@ -459,8 +480,27 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
|
|
|
459
480
|
dialect: 'cel' | 'template';
|
|
460
481
|
fields: string[];
|
|
461
482
|
roots: string[];
|
|
483
|
+
roles: string[];
|
|
462
484
|
functions: string[];
|
|
463
485
|
};
|
|
486
|
+
/**
|
|
487
|
+
* Coarse value categories a `value`/formula expression can compute. `'unknown'`
|
|
488
|
+
* means cel-js could not prove a concrete type — either a `dyn` result (an
|
|
489
|
+
* ambiguous expression over untyped operands) or one that does not type-check.
|
|
490
|
+
*/
|
|
491
|
+
type InferredValueType = 'number' | 'text' | 'boolean' | 'date' | 'unknown';
|
|
492
|
+
/**
|
|
493
|
+
* Infer the coarse value type a `value`/formula expression computes — `'number'`,
|
|
494
|
+
* `'text'`, `'boolean'`, `'date'`, or `'unknown'` when cel-js cannot prove a
|
|
495
|
+
* concrete type. `schema.fields` (the host object's field names) are declared so
|
|
496
|
+
* a bare `<field>` reference resolves the same as `record.<field>`.
|
|
497
|
+
*
|
|
498
|
+
* The motivating use is measure-eligibility: a dataset derives a SUM measure for
|
|
499
|
+
* a `formula` field ONLY when this returns `'number'`, so an ambiguous or
|
|
500
|
+
* non-numeric formula never yields an incoherent measure. Conservative by
|
|
501
|
+
* construction — see {@link inferCelType}.
|
|
502
|
+
*/
|
|
503
|
+
declare function inferExpressionType(input: ExprInput, schema?: ExprSchemaHint): InferredValueType;
|
|
464
504
|
/**
|
|
465
505
|
* Public catalog of CEL functions available in expressions — what `introspectScope`
|
|
466
506
|
* advertises to authors (incl. AI). Every entry MUST actually resolve at runtime:
|
|
@@ -469,4 +509,4 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
|
|
|
469
509
|
*/
|
|
470
510
|
declare const CEL_STDLIB_FUNCTIONS: string[];
|
|
471
511
|
|
|
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 };
|
|
512
|
+
export { CEL_STDLIB_FUNCTIONS, type CelFilterCompileOptions, type CelFilterCompileResult, type CelFilterFailReason, DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, type ExprInput, type ExprSchemaHint, type ExprValidationError, type ExprValidationResult, ExpressionEngine, type FieldRole, type InferredValueType, type SeedPrimitive, type SeedValue, TEMPLATE_FORMATTERS, buildScope, celEngine, compileCelToFilter, cronEngine, expectedDialect, formatValue, getEngine, hasDialect, inferExpressionType, introspectScope, isPushdownableCel, lowerCelAst, matchesFilterCondition, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };
|
package/dist/index.d.ts
CHANGED
|
@@ -32,9 +32,22 @@ interface EvalContext {
|
|
|
32
32
|
* Defaults to `UTC` when unset. Calendar-day `date` rendering stays tz-naive.
|
|
33
33
|
*/
|
|
34
34
|
timezone?: string;
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Current authenticated subject (hook / action / view contexts).
|
|
37
|
+
*
|
|
38
|
+
* ADR-0068: the canonical user contract is {@link EvalUser} from
|
|
39
|
+
* `@objectstack/spec`, surfaced to predicates as `current_user` (aliases
|
|
40
|
+
* `user`, `ctx.user`). `roles: string[]` is the only canonical role field;
|
|
41
|
+
* the singular `role` is deprecated (its "overwritten to 'admin' on
|
|
42
|
+
* promotion" behavior is the footgun ADR-0068 eliminates).
|
|
43
|
+
*/
|
|
36
44
|
user?: {
|
|
37
45
|
id: string;
|
|
46
|
+
/** CANONICAL (ADR-0068). Scope-resolved role names assigned to the user. */
|
|
47
|
+
roles?: string[];
|
|
48
|
+
/** Active organization ID (null = platform / unscoped). */
|
|
49
|
+
organizationId?: string | null;
|
|
50
|
+
/** @deprecated ADR-0068 — use {@link roles}. Retained for back-compat only. */
|
|
38
51
|
role?: string;
|
|
39
52
|
email?: string;
|
|
40
53
|
[key: string]: unknown;
|
|
@@ -424,6 +437,14 @@ interface ExprSchemaHint {
|
|
|
424
437
|
* did-you-mean *warning*. (Default.)
|
|
425
438
|
*/
|
|
426
439
|
scope?: 'record' | 'flattened';
|
|
440
|
+
/**
|
|
441
|
+
* ADR-0068 D4 — the closed catalog of valid role names (built-in + declared).
|
|
442
|
+
* When supplied, a role-membership predicate testing a role NOT in this set
|
|
443
|
+
* (e.g. `'org_admni' in current_user.roles`) is flagged as an error. Closes
|
|
444
|
+
* the AI-hallucination hole where a model invents a plausible-but-nonexistent
|
|
445
|
+
* role that then silently never matches. Absent => role checks are skipped.
|
|
446
|
+
*/
|
|
447
|
+
roleCatalog?: readonly string[];
|
|
427
448
|
}
|
|
428
449
|
interface ExprValidationError {
|
|
429
450
|
/** Self-correcting message: what is wrong + the correct form. */
|
|
@@ -459,8 +480,27 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
|
|
|
459
480
|
dialect: 'cel' | 'template';
|
|
460
481
|
fields: string[];
|
|
461
482
|
roots: string[];
|
|
483
|
+
roles: string[];
|
|
462
484
|
functions: string[];
|
|
463
485
|
};
|
|
486
|
+
/**
|
|
487
|
+
* Coarse value categories a `value`/formula expression can compute. `'unknown'`
|
|
488
|
+
* means cel-js could not prove a concrete type — either a `dyn` result (an
|
|
489
|
+
* ambiguous expression over untyped operands) or one that does not type-check.
|
|
490
|
+
*/
|
|
491
|
+
type InferredValueType = 'number' | 'text' | 'boolean' | 'date' | 'unknown';
|
|
492
|
+
/**
|
|
493
|
+
* Infer the coarse value type a `value`/formula expression computes — `'number'`,
|
|
494
|
+
* `'text'`, `'boolean'`, `'date'`, or `'unknown'` when cel-js cannot prove a
|
|
495
|
+
* concrete type. `schema.fields` (the host object's field names) are declared so
|
|
496
|
+
* a bare `<field>` reference resolves the same as `record.<field>`.
|
|
497
|
+
*
|
|
498
|
+
* The motivating use is measure-eligibility: a dataset derives a SUM measure for
|
|
499
|
+
* a `formula` field ONLY when this returns `'number'`, so an ambiguous or
|
|
500
|
+
* non-numeric formula never yields an incoherent measure. Conservative by
|
|
501
|
+
* construction — see {@link inferCelType}.
|
|
502
|
+
*/
|
|
503
|
+
declare function inferExpressionType(input: ExprInput, schema?: ExprSchemaHint): InferredValueType;
|
|
464
504
|
/**
|
|
465
505
|
* Public catalog of CEL functions available in expressions — what `introspectScope`
|
|
466
506
|
* advertises to authors (incl. AI). Every entry MUST actually resolve at runtime:
|
|
@@ -469,4 +509,4 @@ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
|
|
|
469
509
|
*/
|
|
470
510
|
declare const CEL_STDLIB_FUNCTIONS: string[];
|
|
471
511
|
|
|
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 };
|
|
512
|
+
export { CEL_STDLIB_FUNCTIONS, type CelFilterCompileOptions, type CelFilterCompileResult, type CelFilterFailReason, DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, type ExprInput, type ExprSchemaHint, type ExprValidationError, type ExprValidationResult, ExpressionEngine, type FieldRole, type InferredValueType, type SeedPrimitive, type SeedValue, TEMPLATE_FORMATTERS, buildScope, celEngine, compileCelToFilter, cronEngine, expectedDialect, formatValue, getEngine, hasDialect, inferExpressionType, introspectScope, isPushdownableCel, lowerCelAst, matchesFilterCondition, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ __export(index_exports, {
|
|
|
32
32
|
formatValue: () => formatValue,
|
|
33
33
|
getEngine: () => getEngine,
|
|
34
34
|
hasDialect: () => hasDialect,
|
|
35
|
+
inferExpressionType: () => inferExpressionType,
|
|
35
36
|
introspectScope: () => introspectScope,
|
|
36
37
|
isPushdownableCel: () => isPushdownableCel,
|
|
37
38
|
lowerCelAst: () => lowerCelAst,
|
|
@@ -51,6 +52,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
51
52
|
var import_cel_js = require("@marcbachmann/cel-js");
|
|
52
53
|
|
|
53
54
|
// src/stdlib.ts
|
|
55
|
+
var import_spec = require("@objectstack/spec");
|
|
54
56
|
function partsInTz(d, tz) {
|
|
55
57
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
56
58
|
timeZone: tz,
|
|
@@ -171,13 +173,37 @@ function registerNumericCoercions(env) {
|
|
|
171
173
|
}
|
|
172
174
|
return env;
|
|
173
175
|
}
|
|
176
|
+
function toEvalUser(u) {
|
|
177
|
+
const legacyRole = typeof u.role === "string" && u.role ? [u.role] : [];
|
|
178
|
+
const roles = Array.isArray(u.roles) ? u.roles : [];
|
|
179
|
+
const canonical = (0, import_spec.createEvalUser)({
|
|
180
|
+
id: u.id,
|
|
181
|
+
name: typeof u.name === "string" ? u.name : void 0,
|
|
182
|
+
email: typeof u.email === "string" ? u.email : void 0,
|
|
183
|
+
roles: [...roles, ...legacyRole],
|
|
184
|
+
organizationId: typeof u.organizationId === "string" || u.organizationId === null ? u.organizationId : void 0
|
|
185
|
+
});
|
|
186
|
+
if (typeof u.role === "string" && u.role) {
|
|
187
|
+
canonical.role = u.role;
|
|
188
|
+
}
|
|
189
|
+
return canonical;
|
|
190
|
+
}
|
|
174
191
|
function buildScope(ctx) {
|
|
175
192
|
const scope = {};
|
|
176
193
|
if (ctx.record !== void 0) scope.record = ctx.record;
|
|
177
194
|
if (ctx.previous !== void 0) scope.previous = ctx.previous;
|
|
178
195
|
if (ctx.input !== void 0) scope.input = ctx.input;
|
|
179
196
|
const os = {};
|
|
180
|
-
if (ctx.user !== void 0)
|
|
197
|
+
if (ctx.user !== void 0) {
|
|
198
|
+
const currentUser = toEvalUser(ctx.user);
|
|
199
|
+
scope.current_user = currentUser;
|
|
200
|
+
scope.user = currentUser;
|
|
201
|
+
scope.ctx = {
|
|
202
|
+
...typeof scope.ctx === "object" && scope.ctx !== null ? scope.ctx : {},
|
|
203
|
+
user: currentUser
|
|
204
|
+
};
|
|
205
|
+
os.user = currentUser;
|
|
206
|
+
}
|
|
181
207
|
if (ctx.org !== void 0) os.org = ctx.org;
|
|
182
208
|
if (ctx.env !== void 0) os.env = ctx.env;
|
|
183
209
|
if (Object.keys(os).length > 0) scope.os = os;
|
|
@@ -267,6 +293,17 @@ function firstUndeclaredReference(source, knownFields = []) {
|
|
|
267
293
|
}
|
|
268
294
|
return null;
|
|
269
295
|
}
|
|
296
|
+
function inferCelType(source, knownFields = []) {
|
|
297
|
+
if (typeof source !== "string" || !source.trim()) return null;
|
|
298
|
+
try {
|
|
299
|
+
const env = knownFields.length === 0 ? recordScopeEnv ?? (recordScopeEnv = buildScopedEnv([])) : buildScopedEnv(knownFields);
|
|
300
|
+
const result = env.parse(source).check?.();
|
|
301
|
+
if (!result || result.valid === false) return null;
|
|
302
|
+
return typeof result.type === "string" ? result.type : null;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
270
307
|
function coerce(value) {
|
|
271
308
|
if (typeof value === "bigint") {
|
|
272
309
|
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
@@ -704,12 +741,12 @@ var ExpressionEngine = {
|
|
|
704
741
|
};
|
|
705
742
|
|
|
706
743
|
// src/seed-eval.ts
|
|
707
|
-
var
|
|
744
|
+
var import_spec2 = require("@objectstack/spec");
|
|
708
745
|
function isExpressionLike(value) {
|
|
709
746
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
710
747
|
const v = value;
|
|
711
748
|
if (typeof v.dialect !== "string") return false;
|
|
712
|
-
return
|
|
749
|
+
return import_spec2.ExpressionSchema.safeParse(v).success;
|
|
713
750
|
}
|
|
714
751
|
function resolveSeed(value, ctx) {
|
|
715
752
|
if (value === null || value === void 0) {
|
|
@@ -749,9 +786,9 @@ function resolveSeedRecord(record, ctx) {
|
|
|
749
786
|
}
|
|
750
787
|
|
|
751
788
|
// src/normalize.ts
|
|
752
|
-
var
|
|
789
|
+
var import_spec3 = require("@objectstack/spec");
|
|
753
790
|
function normalizeExpression(input) {
|
|
754
|
-
const parsed =
|
|
791
|
+
const parsed = import_spec3.ExpressionInputSchema.safeParse(input);
|
|
755
792
|
if (!parsed.success) {
|
|
756
793
|
return {
|
|
757
794
|
ok: false,
|
|
@@ -799,7 +836,7 @@ function looksLikeExpression(value) {
|
|
|
799
836
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
800
837
|
const v = value;
|
|
801
838
|
if (typeof v.dialect !== "string") return false;
|
|
802
|
-
return
|
|
839
|
+
return import_spec3.ExpressionSchema.safeParse(v).success;
|
|
803
840
|
}
|
|
804
841
|
|
|
805
842
|
// src/cel-to-filter.ts
|
|
@@ -1213,6 +1250,30 @@ function levenshtein(a, b) {
|
|
|
1213
1250
|
}
|
|
1214
1251
|
return dp[m];
|
|
1215
1252
|
}
|
|
1253
|
+
var ROLE_IN_RE = /(['"])([a-z0-9_]+)\1\s+in\s+(?:current_user|user|ctx\.user)\.roles\b/g;
|
|
1254
|
+
var ROLE_CONTAINS_RE = /(?:current_user|user|ctx\.user)\.roles\s*\.\s*contains\(\s*(['"])([a-z0-9_]+)\1\s*\)/g;
|
|
1255
|
+
var ROLE_EXISTS_RE = /(?:current_user|user|ctx\.user)\.roles\s*\.\s*exists\s*\([^,)]{0,64},[^)=]{0,128}==\s*(['"])([a-z0-9_]+)\1/g;
|
|
1256
|
+
var ROLE_EQ_RE = /(?:current_user|user|ctx\.user)\.role\s*==\s*(['"])([a-z0-9_]+)\1/g;
|
|
1257
|
+
function checkRoleCatalog(source, schema, errors) {
|
|
1258
|
+
const catalog = schema?.roleCatalog;
|
|
1259
|
+
if (!catalog || catalog.length === 0) return;
|
|
1260
|
+
const known = new Set(catalog);
|
|
1261
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1262
|
+
for (const re of [ROLE_IN_RE, ROLE_CONTAINS_RE, ROLE_EXISTS_RE, ROLE_EQ_RE]) {
|
|
1263
|
+
re.lastIndex = 0;
|
|
1264
|
+
let m;
|
|
1265
|
+
while ((m = re.exec(source)) !== null) {
|
|
1266
|
+
const name = m[2];
|
|
1267
|
+
if (known.has(name) || seen.has(name)) continue;
|
|
1268
|
+
seen.add(name);
|
|
1269
|
+
const suggestion = nearest(name, catalog);
|
|
1270
|
+
errors.push({
|
|
1271
|
+
source,
|
|
1272
|
+
message: `unknown role \`${name}\` \u2014 not a defined role` + (suggestion ? `; did you mean \`${suggestion}\`?` : ".") + ` Valid roles: ${catalog.join(", ")}.`
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1216
1277
|
function validateExpression(role, input, schema) {
|
|
1217
1278
|
const { dialect, source } = toSource2(input);
|
|
1218
1279
|
const errors = [];
|
|
@@ -1244,6 +1305,7 @@ function validateExpression(role, input, schema) {
|
|
|
1244
1305
|
});
|
|
1245
1306
|
} else {
|
|
1246
1307
|
checkFieldExistence(source, schema, errors);
|
|
1308
|
+
checkRoleCatalog(source, schema, errors);
|
|
1247
1309
|
if (schema?.scope === "record") {
|
|
1248
1310
|
const bare = firstUndeclaredReference(source);
|
|
1249
1311
|
if (bare) {
|
|
@@ -1276,10 +1338,32 @@ function introspectScope(role, schema) {
|
|
|
1276
1338
|
return {
|
|
1277
1339
|
dialect: expectedDialect(role),
|
|
1278
1340
|
fields: [...schema?.fields ?? []],
|
|
1279
|
-
roots: ["record", "previous", "input", "os", "vars"],
|
|
1341
|
+
roots: ["record", "previous", "input", "os", "current_user", "user", "vars"],
|
|
1342
|
+
roles: [...schema?.roleCatalog ?? []],
|
|
1280
1343
|
functions: CEL_STDLIB_FUNCTIONS
|
|
1281
1344
|
};
|
|
1282
1345
|
}
|
|
1346
|
+
function celTypeToValueType(celType) {
|
|
1347
|
+
switch (celType) {
|
|
1348
|
+
case "int":
|
|
1349
|
+
case "uint":
|
|
1350
|
+
case "double":
|
|
1351
|
+
return "number";
|
|
1352
|
+
case "string":
|
|
1353
|
+
return "text";
|
|
1354
|
+
case "bool":
|
|
1355
|
+
return "boolean";
|
|
1356
|
+
case "google.protobuf.Timestamp":
|
|
1357
|
+
return "date";
|
|
1358
|
+
default:
|
|
1359
|
+
return "unknown";
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function inferExpressionType(input, schema) {
|
|
1363
|
+
const { source } = toSource2(input);
|
|
1364
|
+
if (!source.trim()) return "unknown";
|
|
1365
|
+
return celTypeToValueType(inferCelType(source, schema?.fields));
|
|
1366
|
+
}
|
|
1283
1367
|
var CEL_STDLIB_FUNCTIONS = [
|
|
1284
1368
|
// Dates (registered stdlib)
|
|
1285
1369
|
"now",
|
|
@@ -1334,6 +1418,7 @@ var CEL_STDLIB_FUNCTIONS = [
|
|
|
1334
1418
|
formatValue,
|
|
1335
1419
|
getEngine,
|
|
1336
1420
|
hasDialect,
|
|
1421
|
+
inferExpressionType,
|
|
1337
1422
|
introspectScope,
|
|
1338
1423
|
isPushdownableCel,
|
|
1339
1424
|
lowerCelAst,
|