🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@marcbachmann/cel-js

Package Overview
Dependencies
Maintainers
1
Versions
87
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@marcbachmann/cel-js - npm Package Compare versions

Comparing version
7.4.0
to
7.5.0
+2
-2
lib/evaluator.js

@@ -152,3 +152,3 @@ import {createRegistry, RootContext} from './registry.js'

debugOperandType(value, checkedType) {
if (checkedType?.hasNoDynTypes()) return checkedType
if (checkedType?.hasDynType === false) return checkedType
return this.debugRuntimeType(value, checkedType).wrappedType

@@ -158,3 +158,3 @@ }

debugRuntimeType(value, checkedType) {
if (checkedType?.hasNoDynTypes()) return checkedType
if (checkedType?.hasDynType === false) return checkedType

@@ -161,0 +161,0 @@ const runtimeType = this.debugType(value)

@@ -390,4 +390,4 @@ import {EvaluationError} from './errors.js'

const GPD = 'google.protobuf.Duration'
const TimestampType = registry.registerType(TS, Date).getObjectType(TS).typeType
const DurationType = registry.registerType(GPD, Duration).getObjectType(GPD).typeType
const TimestampType = registry.registerType(TS, Date).typeType
const DurationType = registry.registerType(GPD, Duration).typeType
registry.registerConstant('google', 'map<string, map<string, type>>', {

@@ -394,0 +394,0 @@ protobuf: {Duration: DurationType, Timestamp: TimestampType}

import {EvaluationError, ParseError} from './errors.js'
import {ASTNode} from './parser.js'
import {OPERATORS as OPS} from './operators.js'

@@ -11,6 +10,2 @@ const identity = (x) => x

function ast(callAst, op, args) {
return new ASTNode(callAst.input, callAst.pos, op, args)
}
function createMapExpander(hasFilter) {

@@ -24,14 +19,14 @@ const functionDesc = hasFilter ? 'map(var, filter, transform)' : 'map(var, transform)'

let step = ast(transform, OPS.accuPush, transform)
let step = transform.clone(OPS.accuPush, transform)
if (predicate) {
const accuValue = ast(predicate, OPS.accuValue)
step = ast(predicate, OPS.ternary, [predicate.setMeta('label', label), step, accuValue])
const accuValue = predicate.clone(OPS.accuValue)
step = predicate.clone(OPS.ternary, [predicate.setMeta('label', label), step, accuValue])
}
return {
callAst: ast(callAst, OPS.comprehension, {
callAst: callAst.clone(OPS.comprehension, {
errorsAreFatal: true,
iterable: receiver,
iterVarName: assertIdentifier(iterVar, invalidMsg),
init: ast(callAst, OPS.list, []),
init: callAst.clone(OPS.list, []),
step,

@@ -51,13 +46,13 @@ result: identity

const iterVarName = assertIdentifier(args[0], invalidMsg)
const accuValue = ast(callAst, OPS.accuValue)
const accuValue = callAst.clone(OPS.accuValue)
const predicate = args[1].setMeta('label', label)
const appendItem = ast(callAst, OPS.accuPush, ast(callAst, OPS.id, iterVarName))
const step = ast(predicate, OPS.ternary, [predicate, appendItem, accuValue])
const appendItem = callAst.clone(OPS.accuPush, callAst.clone(OPS.id, iterVarName))
const step = predicate.clone(OPS.ternary, [predicate, appendItem, accuValue])
return {
callAst: ast(callAst, OPS.comprehension, {
callAst: callAst.clone(OPS.comprehension, {
errorsAreFatal: true,
iterable: receiver,
iterVarName,
init: ast(callAst, OPS.list, []),
init: callAst.clone(OPS.list, []),
step,

@@ -78,3 +73,3 @@ result: identity

return {
callAst: ast(callAst, OPS.comprehension, {
callAst: callAst.clone(OPS.comprehension, {
kind: 'quantifier',

@@ -140,8 +135,8 @@ errorsAreFatal: opts.errorsAreFatal || false,

return {
init: ast(callAst, OPS.value, true),
init: callAst.clone(OPS.value, true),
condition: identity,
step: ast(predicate, OPS.ternary, [
step: predicate.clone(OPS.ternary, [
predicate,
ast(predicate, OPS.value, true),
ast(predicate, OPS.value, false)
predicate.clone(OPS.value, true),
predicate.clone(OPS.value, false)
])

@@ -162,8 +157,8 @@ }

return {
init: ast(callAst, OPS.value, false),
init: callAst.clone(OPS.value, false),
condition: opts.condition,
step: ast(predicate, OPS.ternary, [
step: predicate.clone(OPS.ternary, [
predicate,
ast(predicate, OPS.value, true),
ast(predicate, OPS.value, false)
predicate.clone(OPS.value, true),
predicate.clone(OPS.value, false)
])

@@ -184,6 +179,6 @@ }

transform({ast: callAst, predicate, opts}) {
const accuValue = ast(callAst, OPS.accuValue)
const accuValue = callAst.clone(OPS.accuValue)
return {
init: ast(callAst, OPS.value, 0),
step: ast(predicate, OPS.ternary, [predicate, ast(callAst, OPS.accuInc), accuValue]),
init: callAst.clone(OPS.value, 0),
step: predicate.clone(OPS.ternary, [predicate, callAst.clone(OPS.accuInc), accuValue]),
result: opts.result

@@ -199,5 +194,5 @@ }

function bindOptionalEvaluate(ev, macro, ctx, boundValue) {
const res = ev.eval(macro.exp, (ctx = macro.bindCtx.reuse(ctx).setIterValue(boundValue)))
if (res instanceof Promise && ctx === macro.bindCtx) ctx.async = true
function bindOptionalEvaluate(ev, exp, bindCtx, ctx, boundValue) {
const res = ev.eval(exp, (ctx = bindCtx.reuse(ctx).setIterValue(boundValue, ev, exp)))
if (res instanceof Promise && ctx === bindCtx) ctx.async = true
return res

@@ -216,6 +211,6 @@ }

function bindEvaluate(ev, macro, ctx) {
const v = ev.eval(macro.val, ctx)
if (v instanceof Promise) return v.then((_v) => bindOptionalEvaluate(ev, macro, ctx, _v))
return bindOptionalEvaluate(ev, macro, ctx, v)
function bindEvaluate(ev, {val, exp, bindCtx}, ctx) {
const v = ev.eval(val, ctx)
if (v instanceof Promise) return v.then((_v) => bindOptionalEvaluate(ev, exp, bindCtx, ctx, _v))
return bindOptionalEvaluate(ev, exp, bindCtx, ctx, v)
}

@@ -222,0 +217,0 @@

import {celTypes} from './registry.js'
import {objKeys, isArray} from './globals.js'
const dynType = celTypes.dyn

@@ -202,3 +203,3 @@ export class Base {

if (!(left.hasDyn() || right.hasDyn())) {
if (!(left.hasDynType || right.hasDynType)) {
ast.staticHandler = chk.registry.findBinaryOverload(op, left, right)

@@ -234,3 +235,3 @@ }

const types = (ast.argTypes ??= new Array(argLen))
const types = ast.argTypes
let i = argLen

@@ -260,6 +261,6 @@ while (i--) types[i] = ev.debugOperandType(args[i], argAst[i].checkedType)

let i = args.length
const types = (ast.argTypes ??= new Array(i))
const types = ast.argTypes
while (i--) types[i] = ev.debugOperandType(args[i], argAst[i].checkedType)
const receiverType = ev.debugRuntimeType(receiver, receiverAst.checkedType || ev.dynType)
const receiverType = ev.debugRuntimeType(receiver, receiverAst.checkedType || dynType)
const decl = candidates.findMatch(types, receiverType)

@@ -333,3 +334,3 @@ if (decl) return decl.handler.call(ev, receiver, ...args)

if (condition && !condition(accu)) break
accu = ev.eval(step, ctx.setIterValue(items[i++]))
accu = ev.eval(step, ctx.setIterValue(items[i++], ev, step))
if (accu instanceof Promise) return continueLoop(ev, ctx, args, items, accu, i)

@@ -348,3 +349,3 @@ }

if (condition && !condition(accu)) return args.result(accu)
accu = ev.eval(step, ctx.setIterValue(items[i++]))
accu = ev.eval(step, ctx.setIterValue(items[i++], ev, step))
if (accu instanceof Promise) accu = await accu

@@ -361,3 +362,3 @@ }

if (!condition(accu)) return args.result(accu)
stp = ev.tryEval(step, ctx.setIterValue(items[i++]))
stp = ev.tryEval(step, ctx.setIterValue(items[i++], ev, step))
if (stp instanceof Promise) return continueQuantifier(ev, ctx, args, items, accu, i, error, stp)

@@ -385,3 +386,3 @@ if (stp instanceof Error && (error ??= stp)) continue

if (!condition(accu)) return args.result(accu)
stp = ev.tryEval(step, ctx.setIterValue(items[i++]))
stp = ev.tryEval(step, ctx.setIterValue(items[i++], ev, step))
if (stp instanceof Promise) stp = await stp

@@ -402,4 +403,4 @@ if (stp instanceof Error && (error ??= stp)) continue

function fieldAccess(ev, ast, left, right) {
// const leftType = ast.args[0].checkedType
// if (leftType.name !== 'dyn') return leftType.field(left, right, ast, ev)
const leftType = ast.args[0].checkedType
if (leftType !== dynType) return leftType.field(left, right, ast, ev)
return ev.debugType(left).field(left, right, ast, ev)

@@ -419,19 +420,13 @@ }

check(chk, ast, ctx) {
const varType = ctx.getType(ast.args)
if (varType !== undefined) return varType
throw new chk.Error(`Unknown variable: ${ast.args}`, ast)
const variable = ctx.getVariable(ast.args)
if (!variable) throw new chk.Error(`Unknown variable: ${ast.args}`, ast)
if (variable.constant) {
const alternate = ast.clone(OPERATORS.value, variable.value)
ast.setMeta('alternate', alternate)
return chk.check(alternate, ctx)
}
return variable.type
},
evaluate(ev, ast, ctx) {
const type = ast.checkedType || ctx.getType(ast.args)
const value = type && ctx.getValue(ast.args)
if (value === undefined) throw new ev.Error(`Unknown variable: ${ast.args}`, ast)
const valueType = ev.debugType(value)
switch (type) {
case valueType:
case celTypes.dyn:
return value
default:
if (type.matches(valueType)) return value
}
throw new ev.Error(`Variable '${ast.args}' is not of type '${type}', got '${valueType}'`, ast)
return ctx.getCheckedValue(ev, ast)
}

@@ -490,3 +485,3 @@ },

const argTypes = args.map((a) => chk.check(a, ctx))
const argTypes = (ast.argTypes = args.map((a) => chk.check(a, ctx)))
const decl = candidates.findMatch(argTypes)

@@ -501,3 +496,3 @@

if (!argTypes.some((t) => t.hasDyn())) ast.staticHandler = decl
if (!argTypes.some((t) => t.hasDynType)) ast.staticHandler = decl
return decl.returnType

@@ -521,3 +516,3 @@ },

const argTypes = args.map((a) => chk.check(a, ctx))
const argTypes = (ast.argTypes = args.map((a) => chk.check(a, ctx)))
if (receiverType.kind === 'dyn' && candidates.returnType) return candidates.returnType

@@ -535,3 +530,3 @@ const decl = candidates.findMatch(argTypes, receiverType)

if (!receiverType.hasPlaceholder() && !argTypes.some((t) => t.hasDyn())) {
if (!receiverType.hasPlaceholderType && !argTypes.some((t) => t.hasDynType)) {
ast.staticHandler = decl

@@ -538,0 +533,0 @@ }

@@ -55,3 +55,5 @@ import {EvaluationError} from './errors.js'

export function toggleOptionalTypes(registry, enable) {
registry.constants.set('optional', enable ? optionalNamespace : undefined)
const optionalConstant = enable ? optionalNamespace : undefined
registry.variables.set('optional', undefined)
registry.registerConstant('optional', 'OptionalNamespace', optionalConstant)
}

@@ -58,0 +60,0 @@

@@ -192,3 +192,5 @@ import {UnsignedInt, Duration} from './functions.js'

switch (typeof a) {
case 'undefined':
case 'string':
case 'boolean':
return false

@@ -203,11 +205,8 @@ case 'bigint':

return false
case 'boolean':
return false
case 'object':
if (typeof b !== 'object' || a === null || b === null) return false
if (typeof b !== 'object') return false
const leftType = ev.debugType(a)
const rightType = ev.debugType(b)
if (leftType !== rightType) return false
const leftType = ev.objectTypesByConstructor.get(a.constructor)?.type
const rightType = ev.objectTypesByConstructor.get(b.constructor)?.type
if (!leftType || leftType !== rightType) return false
const overload = ev.registry.findBinaryOverload('==', leftType, rightType)

@@ -214,0 +213,0 @@ if (!overload) return false

@@ -85,2 +85,6 @@ import {UnsignedInt} from './functions.js'

clone(op, args) {
return new ASTNode(this.#meta.input, this.#meta.pos, op, args)
}
get meta() {

@@ -87,0 +91,0 @@ return this.#meta

@@ -96,2 +96,12 @@ /**

/**
* Schema definition for inline typed object registration.
* Maps field names to type strings or nested schemas.
* When used with `registerVariable`, internally calls `registerType` to create
* a named type with runtime conversion support.
*/
export interface ObjectSchema {
[field: string]: string | ObjectSchema
}
/**
* Registry for managing function overloads, operator overloads, and type mappings.

@@ -130,10 +140,13 @@ */

* Register a custom type with its constructor and optional field definitions.
* When `ctor` is omitted but `fields` is provided, an internal wrapper class is auto-generated
* and a default `convert` function is created to wrap plain objects at runtime.
* @param typename - The name of the type
* @param definition - Either a constructor function or an object with ctor and fields
* @param withoutDynRegistration - If true, skip automatic dyn() and type() function registration
* @param definition - Either a constructor function or an object with ctor/fields/convert
*/
registerType(
typename: string,
definition: Function | {ctor: Function; fields?: Record<string, any>},
withoutDynRegistration?: boolean
definition:
| Function
| {ctor: Function; fields?: Record<string, string>; convert?: (value: any) => any}
| {fields: Record<string, string>; convert?: (value: any) => any}
): void

@@ -150,6 +163,15 @@

* Register a variable with its type, throwing if it already exists.
* When an ObjectSchema is provided via `{schema: ...}`, a type is auto-registered
* via `registerType` with runtime conversion support.
* @param name - The variable name
* @param type - The variable type name or declaration
* @param type - The variable type name, declaration, or options with schema
*/
registerVariable(name: string, type: string | TypeDeclaration): this
registerVariable(
name: string,
type:
| string
| TypeDeclaration
| {type: string; description?: string}
| {schema: ObjectSchema; description?: string}
): this

@@ -253,3 +275,3 @@ /**

/** Set the iteration variable value */
setIterValue(iterValue: any): this
setIterValue(iterValue: any, evaluator: any, ast: any): this

@@ -256,0 +278,0 @@ /** Resolve a value by name, falling back to parent scopes. */

@@ -70,3 +70,4 @@ import {EvaluationError} from './errors.js'

has(key) {
return this.#entries.has(key) || (this.#parent ? this.#parent.has(key) : false)
if (this.#entries.has(key)) return !!this.#entries.get(key)
return this.#parent ? this.#parent.has(key) : false
}

@@ -94,3 +95,5 @@

get(name) {
return super.get(name) ?? (RESERVED.has(name) ? undefined : dynVariableDeclaration)
return (
super.get(name) ?? (RESERVED.has(name) ? undefined : new VariableDeclaration(name, dynType))
)
}

@@ -106,16 +109,21 @@ }

#matchesCache = new WeakMap()
#hasDynTypes = null
#hasPlaceholderTypes = null
constructor({kind, type, name, keyType, valueType, values}) {
constructor({kind, type, name, keyType, valueType}) {
this.kind = kind
this.type = type
this.name = name
this.keyType = keyType
this.valueType = valueType
if (keyType) this.keyType = keyType
if (valueType) this.valueType = valueType
if (values) this.values = values
this.unwrappedType = kind === 'dyn' && valueType ? valueType.unwrappedType : this
this.wrappedType = kind === 'dyn' ? this : _createDynType(this.unwrappedType)
this.hasDynType =
this.kind === 'dyn' || this.valueType?.hasDynType || this.keyType?.hasDynType || false
this.hasPlaceholderType =
this.kind === 'param' ||
this.keyType?.hasPlaceholderType ||
this.valueType?.hasPlaceholderType ||
false
if (kind === 'list') this.fieldLazy = this.#getListField

@@ -126,20 +134,13 @@ else if (kind === 'map') this.fieldLazy = this.#getMapField

this.#hasDynTypes =
this.kind === 'dyn' || this.valueType?.hasDyn() || this.keyType?.hasDyn() || false
this.#hasPlaceholderTypes =
this.kind === 'param' ||
this.keyType?.hasPlaceholder() ||
this.valueType?.hasPlaceholder() ||
false
objFreeze(this)
}
/** @deprecated Please use .hasDynType */
hasDyn() {
return this.#hasDynTypes
return this.hasDynType
}
/** @deprecated Please use .hasDynType === false */
hasNoDynTypes() {
return this.#hasDynTypes === false
return this.hasDynType === false
}

@@ -155,4 +156,5 @@

/** @deprecated Please use .hasPlaceholderType */
hasPlaceholder() {
return this.#hasPlaceholderTypes
return this.hasPlaceholderType
}

@@ -165,3 +167,4 @@

if (t1.kind !== t2.kind) return null
if (!(t1.hasPlaceholder() || t2.hasPlaceholder() || t1.hasDyn() || t2.hasDyn())) return null
if (!(t1.hasPlaceholderType || t2.hasPlaceholderType || t1.hasDynType || t2.hasDynType))
return null

@@ -182,3 +185,3 @@ const valueType = t1.valueType.unify(r, t2.valueType)

templated(r, bind) {
if (!this.hasPlaceholder()) return this
if (!this.hasPlaceholderType) return this

@@ -219,13 +222,14 @@ switch (this.kind) {

#getMessageField(obj, key, ast, ev) {
const message = ev.objectTypesByConstructor.get(obj.constructor)
if (!message || !(obj instanceof message.ctor)) return
if (!message.fields) return hasOwn(obj, key) ? obj[key] : undefined
const message = obj ? ev.objectTypesByConstructor.get(obj.constructor) : undefined
if (!message) return
const type = message.fields[key]
if (!type) return
const type = message.fields ? message.fields[key] : dynType
if (!type) return undefined
const value = obj[key]
const value = obj instanceof Map ? obj.get(key) : obj[key]
if (value === undefined) return
const valueType = ev.debugType(value)
switch (type) {
case celTypes.dyn:
case dynType:
case valueType:

@@ -240,22 +244,39 @@ return value

#getMapField(obj, key, ast, ev) {
let value
if (obj instanceof Map) value = obj.get(key)
else value = hasOwn(obj, key) ? obj[key] : undefined
// eslint-disable-next-line no-nested-ternary
const value = obj instanceof Map ? obj.get(key) : obj && hasOwn(obj, key) ? obj[key] : undefined
if (value === undefined) return
const valueType = this.valueType
const type = ev.debugType(value)
if (valueType.matches(type)) return value
if (this.valueType.matches(type)) return value
throw new EvaluationError(`Field '${key}' is not of type '${valueType}', got '${type}'`, ast)
throw new EvaluationError(
`Field '${key}' is not of type '${this.valueType}', got '${type}'`,
ast
)
}
#getListElementAtIndex(list, pos) {
switch (list?.constructor) {
case Array:
return list[pos]
case Set: {
let i = 0
for (const item of list) {
if (i++ !== pos) continue
return item
}
}
}
}
#getListField(obj, key, ast, ev) {
if (!(typeof key === 'number' || typeof key === 'bigint')) return
if (typeof key === 'bigint') key = Number(key)
else if (typeof key !== 'number') return
const value = obj[key]
const value = this.#getListElementAtIndex(obj, key)
if (value === undefined) {
if (!obj) return
throw new EvaluationError(
`No such key: index out of bounds, index ${key} ${
key < 0 ? '< 0' : `>= size ${obj.length}`
key < 0 ? '< 0' : `>= size ${obj.length || obj.size}`
}`,

@@ -276,2 +297,3 @@ ast

fieldLazy() {}
field(obj, key, ast, ev) {

@@ -326,6 +348,8 @@ const v = this.fieldLazy(obj, key, ast, ev)

export class VariableDeclaration {
constructor({name, type, description}) {
constructor(name, type, description, value) {
this.name = name
this.type = type
this.description = description ?? null
this.constant = value !== undefined
this.value = value
objFreeze(this)

@@ -336,3 +360,2 @@ }

export class FunctionDeclaration {
#hasPlaceholderTypes
constructor({name, receiverType, returnType, handler, description, params}) {

@@ -354,6 +377,6 @@ if (typeof name !== 'string') throw new Error('name must be a string')

this.#hasPlaceholderTypes =
this.returnType.hasPlaceholder() ||
this.receiverType?.hasPlaceholder() ||
this.argTypes.some((t) => t.hasPlaceholder()) ||
this.hasPlaceholderType =
this.returnType.hasPlaceholderType ||
this.receiverType?.hasPlaceholderType ||
this.argTypes.some((t) => t.hasPlaceholderType) ||
false

@@ -365,3 +388,3 @@

hasPlaceholder() {
return this.#hasPlaceholderTypes
return this.hasPlaceholderType
}

@@ -378,3 +401,2 @@

export class OperatorDeclaration {
#hasPlaceholderTypes
constructor({operator, leftType, rightType, handler, returnType}) {

@@ -390,4 +412,4 @@ this.operator = operator

this.#hasPlaceholderTypes =
this.leftType.hasPlaceholder() || this.rightType?.hasPlaceholder() || false
this.hasPlaceholderType =
this.leftType.hasPlaceholderType || this.rightType?.hasPlaceholderType || false

@@ -398,3 +420,3 @@ objFreeze(this)

hasPlaceholder() {
return this.#hasPlaceholderTypes
return this.hasPlaceholderType
}

@@ -454,7 +476,5 @@

const dynType = _createDynType()
const dynVariableDeclaration = new VariableDeclaration({name: 'dyn', type: dynType})
const astType = _createPrimitiveType('ast')
const listType = _createListType(dynType)
const mapType = _createMapType(dynType, dynType)
export const celTypes = {

@@ -511,3 +531,3 @@ string: _createPrimitiveType('string'),

#matchesFunction(fn, argTypes, receiverType) {
if (fn.hasPlaceholder()) return this.#matchWithPlaceholders(fn, argTypes, receiverType)
if (fn.hasPlaceholderType) return this.#matchWithPlaceholders(fn, argTypes, receiverType)
if (receiverType && fn.receiverType && !receiverType.matches(fn.receiverType)) return

@@ -561,6 +581,2 @@ return fn.matchesArgs(argTypes)

const objTypesDecls = [
[undefined, 'map', TYPES.map, celTypes.map],
[Object, 'map', TYPES.map, celTypes.map],
[Map, 'map', TYPES.map, celTypes.map],
[Array, 'list', TYPES.list, celTypes.list],
[UnsignedInt, 'uint', TYPES.uint, celTypes.uint],

@@ -576,3 +592,4 @@ [Type, 'type', TYPES.type, celTypes.type],

const invalidVar = 'Invalid variable declaration:'
const invalidVar = (postfix) => new Error(`Invalid variable declaration: ${postfix}`)
const invalidType = (postfix) => new Error(`Invalid type declaration: ${postfix}`)

@@ -622,2 +639,3 @@ export class Registry {

let description = opts?.description
let value
if (

@@ -629,29 +647,42 @@ typeof name === 'string' &&

description = type.description
type = type.type
value = type.value
if (type.schema) type = this.registerType({name: `$${name}`, schema: type.schema}).type
else type = type.type
} else if (typeof name === 'object') {
type = name.type
if (name.schema) type = this.registerType({name: `$${name.name}`, schema: name.schema}).type
else type = name.type
description = name.description
value = name.value
name = name.name
}
if (typeof name !== 'string' || !name) throw new Error(`${invalidVar} name must be a string`)
if (RESERVED.has(name)) throw new Error(`${invalidVar} '${name}' is a reserved name`)
if (this.variables.has(name)) throw new Error(`${invalidVar} '${name}' is already registered`)
if (typeof name !== 'string' || !name) throw invalidVar(`name must be a string`)
if (RESERVED.has(name)) throw invalidVar(`'${name}' is a reserved name`)
if (this.variables.has(name)) throw invalidVar(`'${name}' is already registered`)
if (typeof type === 'string') type = this.getType(type)
else if (!(type instanceof TypeDeclaration)) throw new Error(`${invalidVar} type is required`)
else if (!(type instanceof TypeDeclaration)) throw invalidVar(`type is required`)
this.variables.set(name, new VariableDeclaration({name, type, description}))
this.variables.set(name, new VariableDeclaration(name, type, description, value))
return this
}
#registerSchemaAsType(name, schema) {
const fields = Object.create(null)
for (const key of objKeys(schema)) {
const def = schema[key]
if (typeof def === 'object' && def) {
fields[key] = this.registerType({name: `${name}.${key}`, schema: def}).type.name
} else if (typeof def === 'string') {
fields[key] = def
} else {
throw new Error(`Invalid field definition for '${name}.${key}'`)
}
}
return fields
}
registerConstant(name, type, value) {
if (typeof name === 'object') {
value = name.value
this.registerVariable(name)
name = name.name
} else {
this.registerVariable(name, type)
}
this.constants.set(name, value)
if (typeof name === 'object') this.registerVariable(name)
else this.registerVariable({name, type, value})
return this

@@ -721,31 +752,33 @@ }

if (typeof name !== 'string' || name.length < 2 || RESERVED.has(name)) {
throw new Error(`Message type name invalid: ${name}`)
throw invalidType(`name '${name}' is not valid`)
}
if (this.objectTypes.has(name)) throw new Error(`Message type already registered: ${name}`)
if (this.objectTypes.has(name)) throw invalidType(`type '${name}' already registered`)
const type = this.#parseTypeString(name, false)
if (type.kind !== 'message') throw new Error(`Message type invalid: ${name}`)
if (type.kind !== 'message') throw invalidType(`type '${name}' is not valid`)
const ctor = typeof _d === 'function' ? _d : _d?.ctor
if (typeof ctor !== 'function') throw new Error(`Message type constructor invalid: '${name}'`)
const decl = Object.freeze({
const decl = {
name,
typeType: new Type(name),
type,
ctor,
fields: this.#normalizeFields(name, _d?.fields)
})
ctor: typeof _d === 'function' ? _d : _d?.ctor,
convert: typeof _d === 'function' ? undefined : _d?.convert,
fields:
typeof _d?.schema === 'object'
? this.#normalizeFields(name, this.#registerSchemaAsType(name, _d.schema))
: this.#normalizeFields(name, typeof _d === 'function' ? undefined : _d?.fields)
}
this.objectTypes.set(name, decl)
this.objectTypesByConstructor.set(ctor, decl)
if (typeof decl.ctor !== 'function') {
if (!decl.fields) throw invalidType(`type '${name}' requires a constructor or fields`)
Object.assign(decl, this.#createDefaultConvert(name, decl.fields))
}
this.objectTypes.set(name, Object.freeze(decl))
this.objectTypesByConstructor.set(decl.ctor, decl)
this.registerFunctionOverload(`type(${name}): type`, () => decl.typeType)
return this
return decl
}
getObjectType(name) {
return this.objectTypes.get(name)
}
/** @returns {TypeDeclaration} */

@@ -893,3 +926,3 @@ #parseTypeString(typeStr, requireKnownTypes = true) {

}
return celTypes.dyn
return dynType
}

@@ -905,3 +938,3 @@

#matchesOverload(decl, actualLeft, actualRight) {
const bindings = decl.hasPlaceholder() ? new Map() : null
const bindings = decl.hasPlaceholderType ? new Map() : null
const leftType = this.matchTypeWithPlaceholders(decl.leftType, actualLeft, bindings)

@@ -916,3 +949,3 @@ if (!leftType) return

return decl.hasPlaceholder()
return decl.hasPlaceholderType
? {

@@ -928,3 +961,3 @@ handler: decl.handler,

matchTypeWithPlaceholders(declared, actual, bindings) {
if (!declared.hasPlaceholder()) return actual.matches(declared) ? actual : null
if (!declared.hasPlaceholderType) return actual.matches(declared) ? actual : null

@@ -946,3 +979,3 @@ const treatAsDyn = actual.kind === 'dyn'

#collectPlaceholderBindings(declared, actual, bindings, fromDyn = false) {
if (!declared.hasPlaceholder()) return true
if (!declared.hasPlaceholderType) return true
if (!actual) return false

@@ -955,3 +988,3 @@

case 'param': {
const candidateType = treatAsDyn ? celTypes.dyn : actual
const candidateType = treatAsDyn ? dynType : actual
return this.#bindPlaceholder(declared.name, candidateType, bindings)

@@ -1027,2 +1060,50 @@ }

#createDefaultConvert(name, fields) {
const keys = objKeys(fields)
const verifiedProto = Object.create(null)
for (const k of keys) {
const type = fields[k]
if (type.kind !== 'message') verifiedProto[k] = true
else verifiedProto[k] = this.objectTypes.get(type.name)
}
const Ctor = {
[name]: class extends Map {
#raw
constructor(v) {
super()
this.#raw = v
}
[Symbol.iterator]() {
if (this.size !== keys.length) for (const k of keys) this.get(k)
return super[Symbol.iterator]()
}
get(field) {
const v = super.get(field)
if (v !== undefined) return v
const dec = verifiedProto[field]
if (dec === undefined || this.has(field)) return
const value = this.#raw instanceof Map ? this.#raw.get(field) : this.#raw[field]
if (dec === true || value === undefined) return (super.set(field, value), value)
super.set(field, dec.convert ? dec.convert(value) : value)
return super.get(field)
}
}
}[name]
return {
ctor: Ctor,
convert(v) {
if (!v) return
if (v.constructor === Ctor) return v
return new Ctor(v)
}
}
}
clone(opts) {

@@ -1227,3 +1308,3 @@ return new Registry({

const dec = new OperatorDeclaration({operator: op, leftType, rightType, returnType, handler})
if (dec.hasPlaceholder() && !(rightType.hasPlaceholder() && leftType.hasPlaceholder())) {
if (dec.hasPlaceholderType && !(rightType.hasPlaceholderType && leftType.hasPlaceholderType)) {
throw new Error(

@@ -1305,32 +1386,51 @@ `Operator overload with placeholders must use them in both left and right types: ${sig}`

#variables
#constants
#context
#contextObj
#contextMap
#convertCache
constructor(registry, context) {
this.#variables = registry.variables
this.#constants = registry.constants
if (context === undefined || context === null) return
if (typeof context !== 'object') throw new EvaluationError('Context must be an object')
this.#context = context
if (context instanceof Map) this.getValue = this.#getValueFromMap
else this.getValue = this.#getValueFromObject
if (context instanceof Map) this.#contextMap = context
else this.#contextObj = context
}
#getValueFromObject(key) {
const v = this.#context[key]
if (v !== undefined) return v
return this.#constants.get(key)
getType(name) {
return this.#variables.get(name)?.type
}
#getValueFromMap(key) {
const v = this.#context.get(key)
if (v !== undefined) return v
return this.#variables.get(key)
getValue(key) {
return (
this.#convertCache?.get(key) ||
(this.#contextMap ? this.#contextMap.get(key) : this.#contextObj?.[key])
)
}
getType(name) {
return this.#variables.get(name)?.type
getVariable(name) {
return this.#variables.get(name)
}
getValue(name) {
return this.#constants.get(name)
getCheckedValue(ev, ast) {
const value = this.getValue(ast.args)
if (value === undefined) throw new ev.Error(`Unknown variable: ${ast.args}`, ast)
const valueType = ev.debugType(value)
switch (ast.checkedType) {
case valueType:
case dynType:
return value
default:
if (ast.checkedType.matches(valueType)) return value
// Convert plain objects to typed instances when a convert function is registered
if (ast.checkedType.kind === 'message' && valueType.kind === 'map') {
const converted = ev.objectTypes.get(ast.checkedType.name)?.convert?.(value)
if (converted)
return ((this.#convertCache ??= new Map()).set(ast.args, converted), converted)
}
}
throw new ev.Error(
`Variable '${ast.args}' is not of type '${ast.checkedType}', got '${valueType}'`,
ast
)
}

@@ -1359,3 +1459,3 @@

reuse(parent) {
if (!this.async) return (this.#parent = parent) && this
if (!this.async) return ((this.#parent = parent), this)
const ctx = new OverlayContext(parent, this.iterVar, this.iterType)

@@ -1366,4 +1466,22 @@ ctx.accuType = this.accuType

setIterValue(v) {
return ((this.iterValue = v), this)
setIterValue(v, ev, ast) {
const valueType = ev.debugType(v)
switch (this.iterType) {
case valueType:
case dynType:
return ((this.iterValue = v), this)
default:
if (this.iterType.matches(valueType)) return ((this.iterValue = v), this)
// Convert plain objects to typed instances when a convert function is registered
if (this.iterType.kind === 'message' && valueType.kind === 'map') {
const converted = ev.objectTypes.get(this.iterType.name)?.convert?.(v)
if (converted) return ((this.iterValue = converted), this)
}
}
throw new ev.Error(
`Variable '${this.iterVar}' is not of type '${this.iterType}', got '${valueType}'`,
ast
)
}

@@ -1383,2 +1501,17 @@

getCheckedValue(ev, ast) {
if (this.iterVar === ast.args) return this.iterValue
return this.#parent.getCheckedValue(ev, ast)
}
getVariable(name) {
if (this.iterVar === name) return new VariableDeclaration(name, this.iterType)
return this.#parent.getVariable(name)
}
setConverted(name, value) {
return this.iterVar === name ? (this.iterValue = value) : this.#parent.setConverted(name, value)
}
// getVariable makes this obsolete
getType(key) {

@@ -1385,0 +1518,0 @@ return this.iterVar === key ? this.iterType : this.#parent.getType(key)

@@ -54,4 +54,7 @@ import {TypeError, EvaluationError} from './errors.js'

if (customType.fields) {
const keyName = ast.op === '.' || ast.op === '.?' ? ast.args[1] : undefined
if (keyName) {
let keyName
if (ast.op === '.' || ast.op === '.?') keyName = ast.args[1]
else if (ast.args[1].op === 'value') keyName = ast.args[1].args
if (typeof keyName === 'string') {
const fieldType = customType.fields[keyName]

@@ -72,3 +75,3 @@ if (fieldType) return fieldType

formatType(type) {
if (!type.hasPlaceholder()) return type.name
if (!type.hasPlaceholderType) return type.name
return type.templated(this.registry, toDynTypeBinding).name

@@ -75,0 +78,0 @@ }

{
"name": "@marcbachmann/cel-js",
"version": "7.4.0",
"version": "7.5.0",
"description": "A lightweight Common Expression Language (CEL) implementation in JavaScript with zero dependencies",

@@ -72,12 +72,12 @@ "keywords": [

"service": "drone",
"commit": "74bb594614b78ef4b8b47b1b4c344f6848e9fd02",
"build": "235",
"buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/235",
"commit": "0a47f533eb9b3772529f1a829b2bfbe957b654bb",
"build": "241",
"buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/241",
"branch": "main",
"jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/235",
"jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/241",
"isPr": false,
"slug": "marcbachmann/cel-js",
"root": "/drone/src",
"date": "2026-02-11T23:27:43.430Z"
"date": "2026-02-16T17:48:36.155Z"
}
}

@@ -73,3 +73,9 @@ # @marcbachmann/cel-js [![npm version](https://img.shields.io/npm/v/@marcbachmann/cel-js.svg)](https://www.npmjs.com/package/@marcbachmann/cel-js) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

const env = new Environment()
.registerVariable('user', 'map')
.registerVariable('skipAgeCheck', 'bool')
.registerVariable('user', {
schema: {
email: 'string',
age: 'int',
}
})
.registerConstant('minAge', 'int', 18n)

@@ -80,4 +86,4 @@ .registerFunction('isAdult(int): bool', age => age >= 18n)

// Type-checked evaluation with constant
env.evaluate('isAdult(user.age) && user.age >= minAge', {
user: {age: 25n}
env.evaluate('isAdult(user.age) && (user.age >= minAge || skipAgeCheck)', {
user: {age: 25n}, skipAgeCheck: true
})

@@ -140,3 +146,3 @@

- **`registerConstant(name, type, value)`** - Provide immutable values without passing them in context
- **`clone()`** - Create a fast, isolated copy that stops the parent from registering more entries
- **`clone()`** - Create an isolated copy. Call that stops the parent from registering more entries.
- **`hasVariable(name)`** - Check if variable is registered

@@ -155,2 +161,17 @@ - **`parse(expression)`** - Parse expression for reuse

env.registerVariable({name: 'user', type: 'map', description: 'The current user'})
// Passing a schema property will implicitly create a custom type and
// convert objects/maps to a new class instance. Those values then behave similar like
// explicit types that are created using a constructor.
env.registerVariable({
name: 'user',
schema: {
email: 'string',
age: 'int',
profile: {
tags: 'list<string>',
avatar: 'string'
}
}
})
```

@@ -160,2 +181,29 @@

#### `registerType` signatures
```javascript
// Name + constructor class
// when fields are not provided, own properties are accessible automatically
env.registerType('Vector', Vector)
// Name + object with constructor and field types
env.registerType('Vector', {ctor: Vector, fields: {x: 'double', y: 'double'}})
// Name + object with fields only (auto-generates a wrapper class and convert function)
env.registerType('Vector', {fields: {x: 'double', y: 'double'}})
// Name + object with nested schema (registers nested types automatically)
env.registerType('Vector', {schema: {x: 'double', y: 'double'}})
// Single object with name and schema
env.registerType({name: 'Vector', schema: {x: 'double', y: 'double'}})
// Single object with constructor (name inferred from constructor)
env.registerType({ctor: Vector, fields: {x: 'double', y: 'double'}})
```
When `fields` or `schema` is provided without a `ctor`, an internal wrapper class is auto-generated and plain objects are automatically converted at runtime. A custom `convert` function can be passed to override this default conversion.
When using the `schema` declaration, we're creating a new Map instance for the specific type when retrieving the values by variable from a context object.
#### `registerFunction` signatures

@@ -162,0 +210,0 @@