npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@mgvdev/verdict

v0.3.1

Published

A lightweight, structured JSON decision tree library for Node.js applications

Readme

Verdict 🏛️

A lightweight, structured JSON decision tree library for Node.js applications. Build complex rule engines with an intuitive, type-safe API that supports serialization and dynamic rule evaluation.

Verdict

License: MIT TypeScript

✨ Features

  • 🚀 Lightweight & Fast - Minimal dependencies, maximum performance
  • 🔒 Type Safe - Full TypeScript support with intelligent IntelliSense
  • 📦 Serializable - Convert rules to/from JSON for storage and transmission
  • 🎯 Intuitive API - Easy-to-read, chainable rule definitions
  • 🔧 Extensible - Support for custom operators and complex nested logic
  • 📊 Context-Aware - Evaluate rules against dynamic data contexts
  • 🌐 Universal - Works in Node.js and modern browsers

📦 Installation

# Using npm
npm install @mgvdev/verdict

# Using yarn
yarn add @mgvdev/verdict

# Using pnpm
pnpm add @mgvdev/verdict

🚀 Quick Start

import { Engine, eq, and, gt } from '@mgvdev/verdict';

const engine = new Engine();

// Create a simple rule
const rule = and(
  eq('user.status', 'active'),
  gt('user.age', 18)
);

// Define your data context
const context = {
  user: {
    status: 'active',
    age: 25
  }
};

// Evaluate the rule
const result = engine.evaluate(rule, context);
console.log(result); // true

📖 Core Concepts

Engine

The Engine is the main interface for evaluating rules. It takes a rule and a context object, then returns a boolean result.

import { Engine } from '@mgvdev/verdict';

const engine = new Engine();
const result = engine.evaluate(rule, context);

Rules & Operators

Rules are built using operators that can be combined to create complex logic:

Comparison Operators

import { eq, ne, gt, gte, lt, lte } from '@mgvdev/verdict';

// Equality
eq('user.name', 'John')           // user.name === 'John'
ne('user.status', 'inactive')     // user.status !== 'inactive'

// Numeric comparisons
gt('user.age', 18)                // user.age > 18
gte('user.score', 80)             // user.score >= 80
lt('user.attempts', 3)            // user.attempts < 3
lte('user.balance', 1000)         // user.balance <= 1000

// Date and DateTime comparisons (native support)
gt('user.createdAt', '2023-01-01')              // Date strings (ISO format)
lt('event.endDate', '2024-12-31T23:59:59Z')     // DateTime with timezone
gte('user.lastLogin', new Date('2023-06-15'))   // Date objects
lte('session.expires', Date.now())              // Timestamps

Logical Operators

import { and, or, not } from '@mgvdev/verdict';

// AND - all conditions must be true
and(
  eq('user.status', 'active'),
  gt('user.age', 18)
)

// OR - at least one condition must be true
or(
  eq('user.role', 'admin'),
  eq('user.role', 'moderator')
)

// NOT - negates the condition
not(eq('user.banned', true))

Membership Operators

import { In, notIn } from '@mgvdev/verdict';

// IN - value exists in array
In('user.role', ['admin', 'moderator', 'editor'])

// NOT IN - value does not exist in array
notIn('user.status', ['banned', 'suspended'])

Array Operators

import { any, all, none } from '@mgvdev/verdict';

// ANY - at least one element matches condition
any('user.roles', eq('name', 'admin'))

// ALL - all elements match condition
all('user.permissions', eq('granted', true))

// NONE - no elements match condition
none('user.violations', eq('severity', 'critical'))

Flat array syntax:

Self reference is supported for flat arrays by using the self symbol exported from the verdict package:


import { self } from '@mgvdev/verdict';

// ANY - at least one element matches condition
any('user.roles.*', eq(self, 'admin'));

🎯 Usage Examples

Basic User Authorization

import { Engine, and, eq, gt, In } from '@mgvdev/verdict';

const engine = new Engine();

// Define authorization rule
const canAccessAdmin = and(
  eq('user.active', true),
  gt('user.age', 18),
  In('user.role', ['admin', 'moderator'])
);

// Check user access
const user = {
  active: true,
  age: 25,
  role: 'admin'
};

const hasAccess = engine.evaluate(canAccessAdmin, { user });
console.log(hasAccess); // true

Complex Business Rules

import { Engine, and, or, eq, gt, gte, In } from '@mgvdev/verdict';

const engine = new Engine();

// Complex discount eligibility rule
const discountEligible = or(
  // Premium members always eligible
  eq('user.tier', 'premium'),

  // OR high-value customers
  and(
    gte('user.totalSpent', 1000),
    gte('user.accountAge', 365)
  ),

  // OR users in specific regions with good standing
  and(
    In('user.region', ['US', 'CA', 'EU']),
    eq('user.standing', 'good'),
    gt('user.orderCount', 10)
  )
);

const customer = {
  tier: 'standard',
  totalSpent: 1200,
  accountAge: 400,
  region: 'US',
  standing: 'good',
  orderCount: 15
};

const eligible = engine.evaluate(discountEligible, { user: customer });
console.log(eligible); // true

Feature Flags & A/B Testing

import { Engine, and, eq, In, gt } from '@mgvdev/verdict';

const engine = new Engine();

// Feature flag rule
const showNewFeature = and(
  eq('feature.enabled', true),
  or(
    eq('user.beta', true),
    and(
      In('user.segment', ['power_user', 'early_adopter']),
      gt('user.loginCount', 50)
    )
  )
);

const context = {
  feature: { enabled: true },
  user: {
    beta: false,
    segment: 'power_user',
    loginCount: 75
  }
};

const hasAccess = engine.evaluate(showNewFeature, context);
console.log(hasAccess); // true

Array-Based Rules

import { Engine, any, all, none, eq, gt, and } from '@mgvdev/verdict';

const engine = new Engine();

// User authorization with role-based access
const canEditPosts = any('user.roles', eq('name', 'editor'));

// Ensure all user permissions are valid
const hasValidPermissions = all('user.permissions',
  and(
    eq('granted', true),
    gt('expiresAt', Date.now())
  )
);

// Security check - no banned roles
const isSafeUser = none('user.roles', eq('status', 'banned'));

const context = {
  user: {
    roles: [
      { name: 'user', status: 'active' },
      { name: 'editor', status: 'active' }
    ],
    permissions: [
      { granted: true, expiresAt: Date.now() + 86400000 },
      { granted: true, expiresAt: Date.now() + 172800000 }
    ]
  }
};

const canEdit = engine.evaluate(canEditPosts, context);        // true
const validPerms = engine.evaluate(hasValidPermissions, context); // true
const safeUser = engine.evaluate(isSafeUser, context);         // true

Date and DateTime Comparisons

Verdict provides native support for comparing dates and datetimes in various formats:

import { Engine, eq, gt, gte, lt, lte, and, or } from '@mgvdev/verdict';

const engine = new Engine();

// Support for various date formats
const dateRules = and(
  // ISO date strings
  gte('user.birthDate', '1990-01-01'),
  lt('user.birthDate', '2005-12-31'),
  
  // ISO datetime strings with timezone
  gt('event.startTime', '2023-06-15T10:00:00Z'),
  lte('event.endTime', '2023-06-15T18:00:00Z'),
  
  // Date objects
  gt('user.lastLogin', new Date('2023-01-01')),
  
  // Timestamps (milliseconds)
  lt('session.expiresAt', Date.now() + 3600000) // 1 hour from now
);

// Real-world example: Event scheduling
const eventContext = {
  event: {
    startTime: '2023-06-15T14:30:00Z',
    endTime: '2023-06-15T16:30:00Z',
    registrationDeadline: '2023-06-10T23:59:59Z'
  },
  user: {
    registeredAt: '2023-06-05T10:00:00Z',
    lastLogin: new Date('2023-06-14T08:00:00Z')
  },
  currentTime: Date.now()
};

// Check if user can access event
const canAccessEvent = and(
  // Event hasn't started yet or is currently running
  gte('event.endTime', 'currentTime'),
  
  // User registered before deadline
  lt('user.registeredAt', 'event.registrationDeadline'),
  
  // User was active recently
  gt('user.lastLogin', '2023-06-01T00:00:00Z')
);

const hasAccess = engine.evaluate(canAccessEvent, eventContext); // true

// Comparing dates from context
const dateComparison = and(
  gt('user.lastLogin', 'user.registeredAt'),    // Last login after registration
  lt('event.startTime', 'event.endTime')        // Valid event duration
);

const isValidTiming = engine.evaluate(dateComparison, eventContext); // true

Supported Date Formats

  • ISO Date Strings: '2023-01-15', '2023-01-15T14:30:00Z'
  • Date Objects: new Date('2023-01-15')
  • Timestamps: 1673740800000 (milliseconds since epoch)
  • Mixed Formats: You can compare different formats together
// Mixed format comparison example
const mixedDateRule = and(
  gt('2023-06-15', '2023-01-01'),                    // String vs String
  lt(new Date('2023-06-15'), '2023-12-31'),          // Date vs String  
  gte('user.timestamp', new Date('2023-01-01').getTime()) // String vs Timestamp
);

🔄 Serialization

Verdict supports full serialization, allowing you to store rules as JSON and reconstruct them later.

import { RuleSerializer, and, eq, gt } from '@mgvdev/verdict';

const serializer = new RuleSerializer();

// Create a rule
const rule = and(
  eq('user.status', 'active'),
  gt('user.age', 18)
);

// Serialize to JSON
const jsonRule = serializer.serialize(rule);
console.log(JSON.stringify(jsonRule, null, 2));

Output:

{
  "operator": "and",
  "args": [
    {
      "operator": "eq",
      "args": ["user.status", "active"]
    },
    {
      "operator": "gt",
      "args": ["user.age", 18]
    }
  ]
}
// Deserialize from JSON
const reconstructedRule = serializer.deserialize(jsonRule);

// Use the reconstructed rule
const result = engine.evaluate(reconstructedRule, context);

🗃️ Database Storage Example

// Store rules in database
const ruleDefinition = {
  name: 'Premium User Access',
  rule: serializer.serialize(premiumAccessRule),
  createdAt: new Date()
};

await database.rules.create(ruleDefinition);

// Later, retrieve and use the rule
const storedRule = await database.rules.findByName('Premium User Access');
const rule = serializer.deserialize(storedRule.rule);
const hasAccess = engine.evaluate(rule, userContext);

🔍 Context Path Resolution

Verdict supports deep object path resolution using dot notation:

const context = {
  user: {
    profile: {
      personal: {
        name: 'John Doe',
        age: 30
      },
      preferences: {
        theme: 'dark',
        notifications: true
      }
    },
    account: {
      tier: 'premium',
      balance: 1500
    }
  },
  session: {
    loginCount: 42,
    lastActive: '2024-01-15'
  }
};

// Access nested values
const rule = and(
  eq('user.profile.personal.name', 'John Doe'),
  gt('user.profile.personal.age', 18),
  eq('user.account.tier', 'premium'),
  gt('session.loginCount', 40)
);

Array Access

const context = {
  user: {
    permissions: ['read', 'write', 'admin'],
    recentOrders: [
      { id: 1, amount: 100 },
      { id: 2, amount: 250 }
    ]
  }
};

// Access array elements by index
const rule = and(
  eq('user.permissions.0', 'read'),        // First permission
  gt('user.recentOrders.1.amount', 200)   // Second order amount
);

Wildcard Array Queries

Verdict supports wildcard (*) syntax for querying arrays:

const context = {
  user: {
    roles: [
      { name: 'admin', active: true },
      { name: 'user', active: true },
      { name: 'guest', active: false }
    ],
    orders: [
      { id: 1, items: [{ price: 100 }, { price: 50 }] },
      { id: 2, items: [{ price: 200 }, { price: 75 }] }
    ]
  }
};

// Extract all role names: ['admin', 'user', 'guest']
const roleNames = getValueFromPath(context, 'user.roles.*.name');

// Extract all item prices: [100, 50, 200, 75]
const allPrices = getValueFromPath(context, 'user.orders.*.items.*.price');

// Use wildcard with operators
const rule = In('admin', 'user.roles.*.name');  // Check if user has admin role

// Your specific use case: Check if user has a role with specific name
const hasSpecificRole = any('user.roles', eq('name', 'admin'));

// Or using wildcard syntax with existing operators
const adminNames = getValueFromPath(context, 'user.roles.*.name');
const hasAdminRole = adminNames.includes('admin');

📋 API Reference

Engine

class Engine {
  evaluate(rule: OperatorInterface, context?: object): boolean
}

Operators

Comparison Operators

  • eq(left, right) - Equality comparison (supports dates)
  • ne(left, right) - Inequality comparison (supports dates)
  • gt(left, right) - Greater than (supports dates)
  • gte(left, right) - Greater than or equal (supports dates)
  • lt(left, right) - Less than (supports dates)
  • lte(left, right) - Less than or equal (supports dates)

Date Support: All comparison operators natively support:

  • ISO date strings ('2023-01-15', '2023-01-15T14:30:00Z')
  • JavaScript Date objects (new Date('2023-01-15'))
  • Unix timestamps (1673740800000)
  • Mixed format comparisons

Logical Operators

  • and(...conditions) - Logical AND
  • or(...conditions) - Logical OR
  • not(condition) - Logical NOT

Membership Operators

  • In(value, array) - Value exists in array
  • notIn(value, array) - Value does not exist in array

Array Operators

  • any(arrayPath, condition) - At least one element matches condition
  • all(arrayPath, condition) - All elements match condition
  • none(arrayPath, condition) - No elements match condition

Serialization

class RuleSerializer {
  serialize(rule: OperatorInterface): RuleJson
  deserialize(json: RuleJson): OperatorInterface
}

🧪 Testing

# Run all tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Run tests with coverage
pnpm test:coverage

🔧 Development

# Clone the repository
git clone https://github.com/mgvdev/verdict.git
cd verdict

# Install dependencies
pnpm install

# Build the project
pnpm build

# Run tests
pnpm test

# Lint code
pnpm lint

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • Inspired by business rule engines and decision trees
  • Built with TypeScript for type safety and developer experience
  • Designed for modern Node.js and browser environments

📞 Support


Made with ❤️ by Maxence Guyonvarho