@marcbachmann/cel-js

1.2.0 • Public • Published

@marcbachmann/cel-js

A lightweight, zero-dependency implementation of the Common Expression Language (CEL) in JavaScript.

npm version License: MIT Node.js Version

Overview

CEL (Common Expression Language) is a non-Turing complete language designed for simplicity, speed, safety, and portability. This JavaScript implementation provides a fast, lightweight CEL evaluator with zero external dependencies, making it perfect for embedded expression evaluation in JavaScript applications.

Features

  • 🚀 Zero Dependencies - No external packages, minimal bundle size
  • High Performance - Optimized lexer, parser, and evaluator
  • 📦 ES Modules - Modern ESM support with full tree-shaking capabilities
  • 🔧 Type Support - Full support for CEL's type system including strings, numbers, booleans, lists, maps, and bytes
  • 🛡️ Safe Evaluation - Sandboxed expression evaluation with no access to the JavaScript runtime
  • 🎯 Complete CEL Support - Implements the full CEL specification including:
    • All arithmetic, comparison, and logical operators
    • String and bytes literals with escape sequences
    • List and map construction
    • Property access and indexing
    • Function calls and receiver-style method calls
    • Macros (has, all, exists, exists_one, map, filter)
    • Ternary conditional operator

Installation

npm install @marcbachmann/cel-js

Quick Start

import { evaluate, parse } from '@marcbachmann/cel-js'

// Simple evaluation
console.log(evaluate('1 + 2 * 3')) // 7

// With context
const context = {
  user: {
    name: 'Alice',
    age: 30,
    roles: ['admin', 'user']
  }
}

const result = evaluate('user.age >= 18 && "admin" in user.roles', context)
console.log(result) // true

// Parse once, evaluate multiple times
const expression = parse('user.age >= age_limit')
console.log(expression({ user: { age: 25 }, age_limit: 21 })) // true
console.log(expression({ user: { age: 18 }, age_limit: 21 })) // false

API Reference

evaluate(expression, context?, functions?)

Evaluates a CEL expression string and returns the result.

evaluate(expression: string, context?: object, functions?: object): any

Parameters:

  • expression - The CEL expression string to evaluate
  • context - Optional object containing variables accessible in the expression
  • functions - Optional object containing custom functions

Returns: The evaluated result

Throws: ParseError or EvaluationError on invalid expressions

parse(expression)

Parses a CEL expression and returns a reusable evaluator function.

parse(expression: string): (context?: object, functions?: object) => any

Parameters:

  • expression - The CEL expression string to parse

Returns: A function that can be called with context and functions to evaluate the expression

Throws: ParseError on syntax errors

Error Types

  • ParseError - Thrown when an expression has invalid syntax
  • EvaluationError - Thrown when an expression cannot be evaluated (e.g., undefined variable, type mismatch)

Expression Language Guide

Basic Types

// Numbers
evaluate('42')        // 42
evaluate('3.14')      // 3.14
evaluate('0xFF')      // 255 (hex)
evaluate('0xFFu')     // 255 (unsigned)

// Strings
evaluate('"Hello"')   // "Hello"
evaluate("'World'")   // "World"
evaluate('"""
Multi
line
"""')                // "Multi\nline\n"

// Booleans
evaluate('true')      // true
evaluate('false')     // false

// Null
evaluate('null')      // null

Operators

// Arithmetic
evaluate('2 + 3')     // 5
evaluate('5 - 2')     // 3
evaluate('3 * 4')     // 12
evaluate('10 / 2')    // 5
evaluate('10 % 3')    // 1
evaluate('-5')        // -5

// Comparison
evaluate('5 == 5')    // true
evaluate('5 != 4')    // true
evaluate('5 > 4')     // true
evaluate('5 >= 5')    // true
evaluate('3 < 4')     // true
evaluate('3 <= 3')    // true

// Logical
evaluate('true && false')  // false
evaluate('true || false')  // true
evaluate('!true')          // false

// String concatenation
evaluate('"Hello" + " " + "World"')  // "Hello World"

// Membership
evaluate('2 in [1, 2, 3]')           // true
evaluate('"lo" in "Hello"')          // true

Collections

// Lists
evaluate('[1, 2, 3]')              // [1, 2, 3]
evaluate('[1, 2, 3][1]')           // 2
evaluate('[1, 2, 3].size()')       // 3

// Maps
evaluate('{name: "John", age: 30}')         // {name: "John", age: 30}
evaluate('{"key": "value"}.key')             // "value"
evaluate('{1: "one", 2: "two"}[2]')         // "two"

Built-in Functions

// Type conversion
evaluate('string(123)')           // "123"
evaluate('bytes("hello")')        // Uint8Array

// Size
evaluate('size("hello")')         // 5
evaluate('size([1, 2, 3])')      // 3
evaluate('size({a: 1, b: 2})')   // 2

// Timestamps
evaluate('timestamp("2023-01-01T00:00:00Z")')  // Date object

String Methods

evaluate('"hello".size()')                    // 5
evaluate('"hello".startsWith("he")')          // true
evaluate('"hello".endsWith("lo")')            // true
evaluate('"hello".contains("ell")')           // true
evaluate('"hello".matches("h.*o")')           // true

Bytes Operations

// Bytes literals
evaluate('b"hello"')                         // Uint8Array
evaluate('b"\\xFF\\x00"')                   // Uint8Array with hex values

// Bytes methods
evaluate('b"hello".size()')                  // 5
evaluate('b"hello"[0]')                      // 104
evaluate('b"hello".toString()')              // "hello"
evaluate('b"hello".toString("base64")')      // "aGVsbG8="
evaluate('b"hello".toString("hex")')         // "68656c6c6f"

Macros

// has - check if field exists
evaluate('has(user.email)', { user: { name: 'John' } })  // false

// all - check if all elements match
evaluate('numbers.all(x, x > 0)', { numbers: [1, 2, 3] })  // true

// exists - check if any element matches
evaluate('numbers.exists(x, x > 5)', { numbers: [1, 5, 10] })  // true

// exists_one - check if exactly one element matches
evaluate('numbers.exists_one(x, x == 5)', { numbers: [1, 5, 10] })  // true

// map - transform elements
evaluate('numbers.map(x, x * 2)', { numbers: [1, 2, 3] })  // [2, 4, 6]

// filter - filter elements
evaluate('numbers.filter(x, x > 2)', { numbers: [1, 2, 3, 4] })  // [3, 4]

Date/Time Operations

const context = {
  now: new Date('2024-01-15T10:30:00Z')
}

evaluate('now.getFullYear()', context)           // 2024
evaluate('now.getMonth()', context)               // 0 (January)
evaluate('now.getDate()', context)                // 15
evaluate('now.getHours()', context)               // 10
evaluate('now.getMinutes()', context)             // 30

// With timezone
evaluate('now.getHours("America/New_York")', context)  // 5 (UTC-5)

Custom Functions

const functions = {
  // Simple function
  double: (x) => x * 2,

  // Multiple parameters
  add: (a, b) => a + b,

  // String manipulation
  capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1),

  // Complex logic
  discount: (price, percentage) => price * (1 - percentage / 100)
}

evaluate('double(21)', {}, functions)                    // 42
evaluate('add(10, 5)', {}, functions)                   // 15
evaluate('capitalize("hello")', {}, functions)          // "Hello"
evaluate('discount(100, 20)', {}, functions)            // 80

Advanced Usage

Expression Compilation

For better performance when evaluating the same expression multiple times:

import { parse } from '@marcbachmann/cel-js'

// Parse once
const checkAccess = parse('user.role == "admin" && user.active')

// Evaluate many times with different contexts
const users = [
  { role: 'admin', active: true },
  { role: 'user', active: true },
  { role: 'admin', active: false }
]

users.forEach(user => {
  console.log(checkAccess({ user }))
})
// Output: true, false, false

Error Handling

import { evaluate, ParseError, EvaluationError } from '@marcbachmann/cel-js'

try {
  const result = evaluate('invalid syntax +++')
} catch (error) {
  if (error instanceof ParseError) {
    console.error('Syntax error:', error.message)
  } else if (error instanceof EvaluationError) {
    console.error('Runtime error:', error.message)
  }
}

Complex Example

import {parse} from '@marcbachmann/cel-js'
const context = {
  request: {
    method: 'POST',
    path: '/api/users',
    user: {
      id: 123,
      role: 'admin',
      permissions: ['read', 'write', 'delete']
    },
    body: {
      name: 'New User',
      email: 'newuser@example.com'
    }
  },
  resource: {
    owner_id: 456,
    public: false
  }
}

const rules = {
  // Admin can do anything
  isAdmin: 'request.user.role == "admin"',

  // User owns the resource
  isOwner: 'request.user.id == resource.owner_id',

  // Has specific permission
  canDelete: '"delete" in request.user.permissions',

  // Complex access rule
  canAccess: `
    resource.public ||
    request.user.role == "admin" ||
    (isOwner && request.method in ["GET", "PUT"])
  `,

  // Validate request
  validRequest: `
    request.method == "POST" &&
    has(request.body.name) &&
    has(request.body.email) &&
    request.body.email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$")
  `
}

// Compile all rules
const compiledRules = {}
for (const [name, expr] of Object.entries(rules)) {
  compiledRules[name] = parse(expr)
}

// Evaluate all rules
const results = {}
for (const [name, rule] of Object.entries(compiledRules)) {
  results[name] = rule(context)
}

console.log(results)
// {
//   isAdmin: true,
//   isOwner: false,
//   canDelete: true,
//   canAccess: true,
//   validRequest: true
// }

Performance

This implementation is designed for high performance:

  • Hand-written lexer - No regex overhead, direct character processing
  • Efficient parser - Recursive descent with minimal allocations
  • Optimized AST - Array-based representation for cache efficiency
  • Fast evaluation - Direct interpretation without intermediate representations

Benchmark Results

Comparison with the cel-js package shows significant performance improvements:

Parsing Performance

  • 7x faster on average across all expressions
  • Simple literals: 2.5-6x faster
  • Complex expressions: 5-16x faster
  • Best performance on arithmetic operations and string methods

Evaluation Performance

  • 12.5x faster on average for supported operations
  • Simple value access: 15-21x faster
  • Property access: 7-13x faster
  • Complex logic: 8-17x faster

Memory Usage

  • 8-13x less memory for parsed ASTs
  • Number literals use 342 bytes vs 4.5KB (13x less)
  • Complex expressions use 5KB vs 34KB (7x less)
  • More stable memory growth patterns

Combined Parse + Evaluate

  • 11x faster on average
  • Simple expressions: 20x faster
  • Complex authorization checks: 6x faster

Feature Advantages

@marcbachmann/cel-js supports many features not available in cel-js:

  • ✅ String methods (startsWith, endsWith, contains, matches)
  • ✅ Macros (has, all, exists, exists_one, map, filter)
  • ✅ Type functions (string, bytes, timestamp)
  • ✅ Bytes literals and operations
  • ✅ Raw strings and escape sequences
  • ✅ Triple-quoted strings

Running Benchmarks

# Performance benchmark
npm run benchmark

# Memory usage benchmark
npm run benchmark:memory

# With detailed GC stats
node --expose-gc benchmark/memory.js

Test Environment: Node.js v24.1.0 on Apple Silicon (M1/M2) Iterations: 10,000 parse operations, 10,000 evaluate operations

See the benchmark directory for detailed benchmark documentation and results.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development

# Run tests
npm test

# Run tests in watch mode
npm run test:watch

# Run benchmarks
npm run benchmark

# Run memory benchmarks
npm run benchmark:memory

License

MIT © Marc Bachmann

See Also

Acknowledgments

This implementation follows the CEL specification created by Google. CEL was designed to be simple, fast, and safe for expression evaluation in various contexts including security policies, protocols, and configurations.

Package Sidebar

Install

npm i @marcbachmann/cel-js

Weekly Downloads

362

Version

1.2.0

License

MIT

Unpacked Size

70.5 kB

Total Files

6

Last publish

Collaborators

  • marcbachmann