@objectstack/formula 10.2.0 → 11.0.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 +43 -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/dist/index.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Environment } from "@marcbachmann/cel-js";
|
|
3
3
|
|
|
4
4
|
// src/stdlib.ts
|
|
5
|
+
import { createEvalUser } from "@objectstack/spec";
|
|
5
6
|
function partsInTz(d, tz) {
|
|
6
7
|
const parts = new Intl.DateTimeFormat("en-US", {
|
|
7
8
|
timeZone: tz,
|
|
@@ -122,13 +123,37 @@ function registerNumericCoercions(env) {
|
|
|
122
123
|
}
|
|
123
124
|
return env;
|
|
124
125
|
}
|
|
126
|
+
function toEvalUser(u) {
|
|
127
|
+
const legacyRole = typeof u.role === "string" && u.role ? [u.role] : [];
|
|
128
|
+
const roles = Array.isArray(u.roles) ? u.roles : [];
|
|
129
|
+
const canonical = createEvalUser({
|
|
130
|
+
id: u.id,
|
|
131
|
+
name: typeof u.name === "string" ? u.name : void 0,
|
|
132
|
+
email: typeof u.email === "string" ? u.email : void 0,
|
|
133
|
+
roles: [...roles, ...legacyRole],
|
|
134
|
+
organizationId: typeof u.organizationId === "string" || u.organizationId === null ? u.organizationId : void 0
|
|
135
|
+
});
|
|
136
|
+
if (typeof u.role === "string" && u.role) {
|
|
137
|
+
canonical.role = u.role;
|
|
138
|
+
}
|
|
139
|
+
return canonical;
|
|
140
|
+
}
|
|
125
141
|
function buildScope(ctx) {
|
|
126
142
|
const scope = {};
|
|
127
143
|
if (ctx.record !== void 0) scope.record = ctx.record;
|
|
128
144
|
if (ctx.previous !== void 0) scope.previous = ctx.previous;
|
|
129
145
|
if (ctx.input !== void 0) scope.input = ctx.input;
|
|
130
146
|
const os = {};
|
|
131
|
-
if (ctx.user !== void 0)
|
|
147
|
+
if (ctx.user !== void 0) {
|
|
148
|
+
const currentUser = toEvalUser(ctx.user);
|
|
149
|
+
scope.current_user = currentUser;
|
|
150
|
+
scope.user = currentUser;
|
|
151
|
+
scope.ctx = {
|
|
152
|
+
...typeof scope.ctx === "object" && scope.ctx !== null ? scope.ctx : {},
|
|
153
|
+
user: currentUser
|
|
154
|
+
};
|
|
155
|
+
os.user = currentUser;
|
|
156
|
+
}
|
|
132
157
|
if (ctx.org !== void 0) os.org = ctx.org;
|
|
133
158
|
if (ctx.env !== void 0) os.env = ctx.env;
|
|
134
159
|
if (Object.keys(os).length > 0) scope.os = os;
|
|
@@ -218,6 +243,17 @@ function firstUndeclaredReference(source, knownFields = []) {
|
|
|
218
243
|
}
|
|
219
244
|
return null;
|
|
220
245
|
}
|
|
246
|
+
function inferCelType(source, knownFields = []) {
|
|
247
|
+
if (typeof source !== "string" || !source.trim()) return null;
|
|
248
|
+
try {
|
|
249
|
+
const env = knownFields.length === 0 ? recordScopeEnv ?? (recordScopeEnv = buildScopedEnv([])) : buildScopedEnv(knownFields);
|
|
250
|
+
const result = env.parse(source).check?.();
|
|
251
|
+
if (!result || result.valid === false) return null;
|
|
252
|
+
return typeof result.type === "string" ? result.type : null;
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
221
257
|
function coerce(value) {
|
|
222
258
|
if (typeof value === "bigint") {
|
|
223
259
|
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
@@ -1167,6 +1203,30 @@ function levenshtein(a, b) {
|
|
|
1167
1203
|
}
|
|
1168
1204
|
return dp[m];
|
|
1169
1205
|
}
|
|
1206
|
+
var ROLE_IN_RE = /(['"])([a-z0-9_]+)\1\s+in\s+(?:current_user|user|ctx\.user)\.roles\b/g;
|
|
1207
|
+
var ROLE_CONTAINS_RE = /(?:current_user|user|ctx\.user)\.roles\s*\.\s*contains\(\s*(['"])([a-z0-9_]+)\1\s*\)/g;
|
|
1208
|
+
var ROLE_EXISTS_RE = /(?:current_user|user|ctx\.user)\.roles\s*\.\s*exists\s*\([^,)]{0,64},[^)=]{0,128}==\s*(['"])([a-z0-9_]+)\1/g;
|
|
1209
|
+
var ROLE_EQ_RE = /(?:current_user|user|ctx\.user)\.role\s*==\s*(['"])([a-z0-9_]+)\1/g;
|
|
1210
|
+
function checkRoleCatalog(source, schema, errors) {
|
|
1211
|
+
const catalog = schema?.roleCatalog;
|
|
1212
|
+
if (!catalog || catalog.length === 0) return;
|
|
1213
|
+
const known = new Set(catalog);
|
|
1214
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1215
|
+
for (const re of [ROLE_IN_RE, ROLE_CONTAINS_RE, ROLE_EXISTS_RE, ROLE_EQ_RE]) {
|
|
1216
|
+
re.lastIndex = 0;
|
|
1217
|
+
let m;
|
|
1218
|
+
while ((m = re.exec(source)) !== null) {
|
|
1219
|
+
const name = m[2];
|
|
1220
|
+
if (known.has(name) || seen.has(name)) continue;
|
|
1221
|
+
seen.add(name);
|
|
1222
|
+
const suggestion = nearest(name, catalog);
|
|
1223
|
+
errors.push({
|
|
1224
|
+
source,
|
|
1225
|
+
message: `unknown role \`${name}\` \u2014 not a defined role` + (suggestion ? `; did you mean \`${suggestion}\`?` : ".") + ` Valid roles: ${catalog.join(", ")}.`
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1170
1230
|
function validateExpression(role, input, schema) {
|
|
1171
1231
|
const { dialect, source } = toSource2(input);
|
|
1172
1232
|
const errors = [];
|
|
@@ -1198,6 +1258,7 @@ function validateExpression(role, input, schema) {
|
|
|
1198
1258
|
});
|
|
1199
1259
|
} else {
|
|
1200
1260
|
checkFieldExistence(source, schema, errors);
|
|
1261
|
+
checkRoleCatalog(source, schema, errors);
|
|
1201
1262
|
if (schema?.scope === "record") {
|
|
1202
1263
|
const bare = firstUndeclaredReference(source);
|
|
1203
1264
|
if (bare) {
|
|
@@ -1230,10 +1291,32 @@ function introspectScope(role, schema) {
|
|
|
1230
1291
|
return {
|
|
1231
1292
|
dialect: expectedDialect(role),
|
|
1232
1293
|
fields: [...schema?.fields ?? []],
|
|
1233
|
-
roots: ["record", "previous", "input", "os", "vars"],
|
|
1294
|
+
roots: ["record", "previous", "input", "os", "current_user", "user", "vars"],
|
|
1295
|
+
roles: [...schema?.roleCatalog ?? []],
|
|
1234
1296
|
functions: CEL_STDLIB_FUNCTIONS
|
|
1235
1297
|
};
|
|
1236
1298
|
}
|
|
1299
|
+
function celTypeToValueType(celType) {
|
|
1300
|
+
switch (celType) {
|
|
1301
|
+
case "int":
|
|
1302
|
+
case "uint":
|
|
1303
|
+
case "double":
|
|
1304
|
+
return "number";
|
|
1305
|
+
case "string":
|
|
1306
|
+
return "text";
|
|
1307
|
+
case "bool":
|
|
1308
|
+
return "boolean";
|
|
1309
|
+
case "google.protobuf.Timestamp":
|
|
1310
|
+
return "date";
|
|
1311
|
+
default:
|
|
1312
|
+
return "unknown";
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function inferExpressionType(input, schema) {
|
|
1316
|
+
const { source } = toSource2(input);
|
|
1317
|
+
if (!source.trim()) return "unknown";
|
|
1318
|
+
return celTypeToValueType(inferCelType(source, schema?.fields));
|
|
1319
|
+
}
|
|
1237
1320
|
var CEL_STDLIB_FUNCTIONS = [
|
|
1238
1321
|
// Dates (registered stdlib)
|
|
1239
1322
|
"now",
|
|
@@ -1287,6 +1370,7 @@ export {
|
|
|
1287
1370
|
formatValue,
|
|
1288
1371
|
getEngine,
|
|
1289
1372
|
hasDialect,
|
|
1373
|
+
inferExpressionType,
|
|
1290
1374
|
introspectScope,
|
|
1291
1375
|
isPushdownableCel,
|
|
1292
1376
|
lowerCelAst,
|