Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d866bab
feat: apple silicon only\n\n- Limit initial release to Apple Silicon …
kevinmichaelchen Apr 28, 2025
52cb561
fix: update CI workflow\n\n- Update test and publish jobs to only exp…
kevinmichaelchen Apr 28, 2025
daaea5e
feat: add dry-run publish to PRs\n\n- Make Publish job run on PRs wit…
kevinmichaelchen Apr 28, 2025
d38e7e6
style: add emojis to CI workflow\n\n- Add descriptive emojis to job a…
kevinmichaelchen Apr 28, 2025
82c5b08
fix: yaml syntax in CI workflow\n\n- Remove duplicate name field in b…
kevinmichaelchen Apr 28, 2025
3fa9207
fix: use consistent platform names\n\n- Use --platform instead of --t…
kevinmichaelchen Apr 28, 2025
cd1061f
ci: fix target
kevinmichaelchen Apr 28, 2025
235ea18
refactor: simplify CI workflow and use NX for native builds
kevinmichaelchen Apr 28, 2025
f05429f
refactor: update test job to use consistent matrix format
kevinmichaelchen Apr 28, 2025
736e68b
feat: support more platforms (Windows, Linux, Intel Mac)
kevinmichaelchen Apr 28, 2025
698c946
fix: validate exact number of .node files during publish
kevinmichaelchen Apr 28, 2025
ab8b7c1
fix: improve artifact handling and binary naming
kevinmichaelchen Apr 28, 2025
10dd196
fix: use explicit target flag for napi-rs build
kevinmichaelchen Apr 28, 2025
0ce8f2f
fix: use mlugg/setup-zig for cross-compilation
kevinmichaelchen Apr 28, 2025
834b775
fix: update build command and improve docs
kevinmichaelchen Apr 28, 2025
ad82ea2
fix: install zig for all ARM64 builds
kevinmichaelchen Apr 28, 2025
63db4d1
fix: install zig for all builds
kevinmichaelchen Apr 28, 2025
59be1fb
fix: only use zig for linux builds
kevinmichaelchen Apr 28, 2025
90027ee
fix: use shell script for conditional zig usage
kevinmichaelchen Apr 28, 2025
20c614f
test: more unit tests
kevinmichaelchen Apr 28, 2025
e677a80
chore: Formatting
kevinmichaelchen Apr 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 90 additions & 236 deletions .github/workflows/CI.yml

Large diffs are not rendered by default.

34 changes: 15 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# cel-typescript

A TypeScript binding for the Common Expression Language (CEL) using
[cel-rust](https://github.com/clarkmcc/cel-rust). This project provides a
[cel-rust][cel-rust]. This project provides a
Node.js native module that allows you to use CEL in your TypeScript/JavaScript
projects.

[cel-spec]: https://github.com/google/cel-spec
[cel-rust]: https://github.com/clarkmcc/cel-rust

## What is CEL?

[Common Expression Language (CEL)](https://github.com/google/cel-spec) is an
[Common Expression Language (CEL)][cel-spec] is an
expression language created by Google that implements common semantics for
expression evaluation. It's a simple language for expressing boolean conditions,
calculations, and variable substitutions. CEL is used in various Google products
Expand All @@ -20,18 +23,7 @@ business rule evaluation.
npm install @kevinmichaelchen/cel-typescript
```

**Requirements:**

- Node.js 18 or later

This package includes pre-compiled native binaries for multiple platforms:

- macOS (x64, arm64)
- Linux (x64, arm64)
- Windows (x64)

The appropriate binary for your platform will be automatically loaded at
runtime.
Node.js 18 or later is required.

## Usage

Expand Down Expand Up @@ -169,16 +161,20 @@ pre-compiled native binaries for all supported platforms:
However, when you install this package, npm will only extract the `.node` file
for your platform. For example:

- On an M1/M2 Mac, only `cel-typescript.darwin-arm64.node` (~7.4 MB) is used
- On Apple Silicon, only `cel-typescript.darwin-arm64.node` (~7.4 MB) is used
- On Windows, only `cel-typescript.win32-x64.node` is used
- On Linux, only `cel-typescript.linux-x64.node` or
`cel-typescript.linux-arm64.node` is used

This is a common pattern for packages with native bindings. For comparison:
This is sometimes a pattern for packages with native bindings. For comparison:

- [`better-sqlite3`][better-sqlite3]: 10.2 MB unpacked
- [`canvas`][canvas]: 408 kB unpacked
- [`sharp`][sharp]: 522 kB unpacked

- `sharp` (image processing): 39.7 MB unpacked
- `better-sqlite3`: 12.8 MB unpacked
- `canvas`: 8.9 MB unpacked
[better-sqlite3]: https://www.npmjs.com/package/better-sqlite3
[canvas]: https://www.npmjs.com/package/canvas
[sharp]: https://www.npmjs.com/package/sharp

#### A Note on Tree-Shaking

Expand Down
265 changes: 262 additions & 3 deletions __tests__/cel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,274 @@ describe("Performance measurements", () => {
// Allow more variation due to system noise, optimization differences, and convenience overhead
const tolerance = 1.0; // Allow 100% variation
const expectedEvaluateTime = compileTime + executeTime;

// Log the actual ratios to help with debugging
const ratio = evaluateTime / expectedEvaluateTime;
console.log(`One-step evaluation was ${ratio.toFixed(2)}x the separate steps`);

console.log(
`One-step evaluation was ${ratio.toFixed(2)}x the separate steps`,
);

// Only fail if the difference is extreme
expect(evaluateTime).toBeGreaterThan(
expectedEvaluateTime * (1 - tolerance),
);
expect(evaluateTime).toBeLessThan(expectedEvaluateTime * (2 + tolerance));
});
});

describe("Timestamp Functions", () => {
it("should correctly parse and compare timestamps", async () => {
const expr = `[
// Test 1: Compare UTC timestamp with itself
timestamp('2025-04-28T12:00:00Z') == timestamp('2025-04-28T12:00:00Z'),
// Test 2: Compare UTC timestamp with equivalent EDT timestamp
timestamp('2025-04-28T12:00:00Z') == timestamp('2025-04-28T08:00:00-04:00'),
// Test 3: Compare timestamps one day apart
timestamp('2025-04-28T12:00:00Z') < timestamp('2025-04-29T12:00:00Z'),
// Test 4: Get components from UTC timestamp
timestamp('2025-04-28T12:00:00Z').getMonth(),
timestamp('2025-04-28T12:00:00Z').getDayOfMonth(),
timestamp('2025-04-28T12:00:00Z').getHours(),
timestamp('2025-04-28T12:00:00Z').getDayOfWeek() // 1==Monday
]`;
const result = await evaluate(expr, {});
expect(result).toEqual([true, true, true, 3, 27, 12, 1]);
});
});

describe("Travel Reservation Rules", () => {
describe("Premium Discount Eligibility", () => {
// Tests a complex pricing rule that considers:
// - Total package cost across flight, hotel, and car
// - Customer loyalty tier
// - Seasonal booking (summer months)
const expr = `
// Calculate total package cost
double(reservation.flight.price) +
double(reservation.hotel.nightlyRate) * int(reservation.hotel.nights) +
double(reservation.car.dailyRate) * int(reservation.car.days) >= 2000.0 &&
// Check loyalty tier
reservation.customer.loyaltyTier in ['GOLD', 'PLATINUM'] &&
// Check if booking is in summer months (0-based: 5=June, 6=July, 7=August)
timestamp(reservation.bookingDate).getMonth() in [5, 6, 7]
`;
let program: CelProgram;

beforeEach(async () => {
program = await CelProgram.compile(expr);
});

it("should qualify for premium discount with valid summer booking", async () => {
const result = await program.execute({
reservation: {
flight: { price: 1000.0 },
hotel: { nightlyRate: 200.0, nights: 4 },
car: { dailyRate: 100.0, days: 4 },
customer: { loyaltyTier: "PLATINUM" },
bookingDate: "2025-07-15T00:00:00Z",
},
});
expect(result).toBe(true);
});

it("should not qualify outside summer months", async () => {
const result = await program.execute({
reservation: {
flight: { price: 1000.0 },
hotel: { nightlyRate: 200.0, nights: 4 },
car: { dailyRate: 100.0, days: 4 },
customer: { loyaltyTier: "PLATINUM" },
bookingDate: "2025-12-15T00:00:00Z",
},
});
expect(result).toBe(false);
program = await CelProgram.compile(expr);
});
});

describe("Multi-condition Booking Validation", () => {
// Tests complex booking validation rules that ensure:
// - All required components are present
// - Logical time sequence of events
// - Location consistency
// - Capacity constraints
const expr = `
has(reservation.flight) &&
timestamp(reservation.flight.departureTime) < timestamp(reservation.hotel.checkIn) &&
timestamp(reservation.hotel.checkIn) < timestamp(reservation.hotel.checkOut) &&
(timestamp(reservation.hotel.checkOut) - timestamp(reservation.hotel.checkIn)) > duration("1h") &&
timestamp(reservation.hotel.checkOut) < timestamp(reservation.flight.returnTime) &&
(reservation.car.pickupLocation == reservation.flight.arrivalAirport ||
reservation.car.pickupLocation == reservation.hotel.address.city) &&
size(reservation.travelers) <= reservation.hotel.maxOccupancy &&
size(reservation.travelers) <= reservation.car.capacity
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cel-rust appears to not support the array.min() function that would simplify this

size(res.travelers) <= [a, b].min()

`;
let program: CelProgram;

beforeEach(async () => {
program = await CelProgram.compile(expr);
});

it("should validate a well-formed booking", async () => {
const result = await program.execute({
reservation: {
flight: {
departureTime: "2025-05-01T10:00:00Z",
returnTime: "2025-05-05T15:00:00Z",
arrivalAirport: "LAX",
},
hotel: {
checkIn: "2025-05-01T15:00:00Z",
checkOut: "2025-05-05T11:00:00Z",
maxOccupancy: 4,
address: { city: "Los Angeles" },
},
car: {
pickupLocation: "LAX",
capacity: 5,
},
travelers: ["person1", "person2", "person3"],
},
});
expect(result).toBe(true);
});

it("should reject invalid time sequence", async () => {
const result = await program.execute({
reservation: {
flight: {
departureTime: "2025-05-01T16:00:00Z", // Later than check-in
returnTime: "2025-05-05T15:00:00Z",
arrivalAirport: "LAX",
},
hotel: {
checkIn: "2025-05-01T15:00:00Z",
checkOut: "2025-05-05T11:00:00Z",
maxOccupancy: 4,
address: { city: "Los Angeles" },
},
car: {
pickupLocation: "LAX",
capacity: 5,
},
travelers: ["person1", "person2", "person3"],
},
});
expect(result).toBe(false);
});
});

describe.skip("Dynamic Pricing with Seasonal Adjustments", () => {
// Tests complex pricing calculations including:
// - Base rates for all components
// - Seasonal multipliers
// - Loyalty discounts
const expr = `
let basePrice = double(reservation.flight.price) +
double(reservation.hotel.nightlyRate) * int(reservation.hotel.nights) +
double(reservation.car.dailyRate) * int(reservation.car.days);
let seasonalPrice = basePrice * (timestamp(reservation.hotel.checkIn).getMonth() in [11, 0, 1] ? 1.25 : 1.0);
seasonalPrice * (1.0 - {'BRONZE': 0.05, 'SILVER': 0.10, 'GOLD': 0.15, 'PLATINUM': 0.20}[reservation.customer.loyaltyTier])
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think CEL supports let

`;
let program: CelProgram;

beforeEach(async () => {
program = await CelProgram.compile(expr);
});

it("should calculate winter pricing with loyalty discount", async () => {
const result = await program.execute({
reservation: {
flight: { price: 1000.0 },
hotel: {
nightlyRate: 200.0,
nights: 4,
checkIn: "2025-01-15T15:00:00Z", // January
},
car: { dailyRate: 100.0, days: 4 },
customer: { loyaltyTier: "GOLD" },
},
});
// Base: 1000 + (200 * 4) + (100 * 4) = 2200
// Winter multiplier: 2200 * 1.25 = 2750
// Gold discount (15%): 2750 * 0.85 = 2337.5
expect(result).toBe(2337.5);
});

it("should calculate summer pricing with loyalty discount", async () => {
const result = await program.execute({
reservation: {
flight: { price: 1000.0 },
hotel: {
nightlyRate: 200.0,
nights: 4,
checkIn: "2025-07-15T15:00:00Z", // July
},
car: { dailyRate: 100.0, days: 4 },
customer: { loyaltyTier: "PLATINUM" },
},
});
// Base: 1000 + (200 * 4) + (100 * 4) = 2200
// No seasonal multiplier
// Platinum discount (20%): 2200 * 0.80 = 1760
expect(result).toBe(1760.0);
});
});

describe("Room Upgrade Eligibility", () => {
// Tests complex upgrade eligibility rules considering:
// - Customer loyalty tier
// - Stay duration
// - Hotel occupancy
// - Existing offers
// - Total spend
// - Current booking class
const expr = `
reservation.customer.loyaltyTier in ['GOLD', 'PLATINUM'] &&
reservation.hotel.nights >= 3 &&
reservation.hotel.occupancyRate < 0.80 &&
!(reservation.specialOffers.exists(o, o.type == 'ROOM_UPGRADE')) &&
reservation.totalSpend > 5000.0 &&
[reservation.flight.class, reservation.hotel.roomType].all(t, t != 'ECONOMY')
`;
let program: CelProgram;

beforeEach(async () => {
program = await CelProgram.compile(expr);
});

it("should qualify eligible platinum member for upgrade", async () => {
const result = await program.execute({
reservation: {
customer: { loyaltyTier: "PLATINUM" },
hotel: {
nights: 4,
occupancyRate: 0.7,
roomType: "DELUXE",
},
flight: { class: "BUSINESS" },
specialOffers: [],
totalSpend: 6000.0,
},
});
expect(result).toBe(true);
});

it("should reject upgrade when conditions not met", async () => {
const result = await program.execute({
reservation: {
customer: { loyaltyTier: "PLATINUM" },
hotel: {
nights: 4,
occupancyRate: 0.85, // Too high occupancy
roomType: "DELUXE",
},
flight: { class: "BUSINESS" },
specialOffers: [],
totalSpend: 6000.0,
},
});
expect(result).toBe(false);
});
});
});
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
"lineWidth": 80,
"ignore": ["index.js"]
},
"linter": {
"enabled": true,
Expand Down
16 changes: 5 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kevinmichaelchen/cel-typescript",
"version": "0.0.8",
"version": "0.0.9",
"type": "module",
"description": "TypeScript bindings for the Common Expression Language (CEL) using cel-rust",
"repository": {
Expand All @@ -13,12 +13,7 @@
"url": "https://github.com/kevinmichaelchen/cel-typescript/issues"
},
"homepage": "https://github.com/kevinmichaelchen/cel-typescript#readme",
"files": [
"dist/**/*",
"*.node",
"index.js",
"index.d.ts"
],
"files": ["dist/**/*", "*.node", "index.js", "index.d.ts"],
"keywords": [
"cel",
"common-expression-language",
Expand All @@ -44,13 +39,12 @@
"napi": {
"name": "cel-typescript",
"triples": {
"defaults": true,
"defaults": false,
"additional": [
"aarch64-apple-darwin",
"aarch64-unknown-linux-gnu",
"aarch64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc"
]
}
Expand Down
Loading
Loading