Skip to content

API: Utilities

Guantr exports utility functions and types for building and evaluating conditions outside the core can/cannot workflow.

Importing

ts
import {
  createMatchConditionBuilder,
  evaluateCondition,
  serializeRules,
  deserializeRules,
} from 'guantr';

import type {
  Condition,
  MatchConditionBuilder,
  MatchConditionFn,
  ResourceRef,
  ContextRef,
  LiteralRef,
  ValueRef,
  InferValueRef,
  ArrayElementType,
  OperatorNode,
  LogicalNode,
  AstNode,
} from 'guantr';

createMatchConditionBuilder

Creates a new MatchConditionBuilder instance for composing typed conditions. The returned builder is a plain object whose methods produce typed ValueRef objects carrying phantom type information, enabling type-safe operand pairing at compile time while keeping the runtime objects fully JSON-serializable.

Signature

ts
function createMatchConditionBuilder<
  Model extends Record<string, unknown> = Record<string, unknown>,
  Context extends Record<string, unknown> = Record<string, unknown>,
>(): MatchConditionBuilder<Model, Context>;

Generics

  • Model: The TypeScript type of the resource model. Used to infer field types for resource(), enabling type-safe comparisons. Defaults to Record<string, unknown>.
  • Context: The TypeScript type of the evaluation context. Used to infer field types for context(). Defaults to Record<string, unknown>.

Returns

A MatchConditionBuilder<Model, Context> instance with the following methods:

Value-source factories

MethodSignatureReturnsDescription
resource(path)(path: LeafKeys<Model>) => ResourceRef<Model>ResourceRefReference a field on the resource model
context(path)(path: LeafKeys<Context>) => ContextRef<Context>ContextRefReference a field on the evaluation context
literal(value)<T>(value: T) => LiteralRef<T>LiteralRef<T>Inline literal value

Comparison operators

OperatorSignatureDescription
eq(left, right)(ValueRef, ValueRef) => ConditionEqual to
ne(left, right)(ValueRef, ValueRef) => ConditionNot equal to
gt(left, right)(numeric ValueRef, numeric ValueRef) => ConditionGreater than
gte(left, right)(numeric ValueRef, numeric ValueRef) => ConditionGreater than or equal to
lt(left, right)(numeric ValueRef, numeric ValueRef) => ConditionLess than
lte(left, right)(numeric ValueRef, numeric ValueRef) => ConditionLess than or equal to

String operators

OperatorSignatureDescription
contains(str, substring, options?)(string ValueRef, string ValueRef, { caseInsensitive?: boolean }?) => Conditionstr contains substring
startsWith(str, prefix, options?)(string ValueRef, string ValueRef, { caseInsensitive?: boolean }?) => Conditionstr starts with prefix
endsWith(str, suffix, options?)(string ValueRef, string ValueRef, { caseInsensitive?: boolean }?) => Conditionstr ends with suffix

All string operators accept an optional { caseInsensitive: true } option.

Array membership operators

OperatorSignatureDescription
in(value, array, options?)(ValueRef, array ValueRef, { caseInsensitive?: boolean }?) => Conditionvalue is an element of array. Array position is type-checked; element-type compatibility is verified at runtime.
has(array, value, options?)(array ValueRef, ValueRef, { caseInsensitive?: boolean }?) => Conditionarray contains value
hasSome(array, values, options?)(array ValueRef, array ValueRef, { caseInsensitive?: boolean }?) => Conditionarray contains at least one of values
hasEvery(array, values, options?)(array ValueRef, array ValueRef, { caseInsensitive?: boolean }?) => Conditionarray contains all of values

All array membership operators accept an optional { caseInsensitive: true } option.

Complex array operators

OperatorSignatureDescription
some(array, fn)(array ValueRef, MatchConditionFn) => ConditionAt least one element satisfies the nested condition
every(array, fn)(array ValueRef, MatchConditionFn) => ConditionEvery element satisfies the nested condition
none(array, fn)(array ValueRef, MatchConditionFn) => ConditionNo element satisfies the nested condition

The callback fn receives a scoped builder where resource() references fields of the array element, not the parent resource.

Logical operators

OperatorSignatureDescription
and(...conditions)(...Condition[]) => ConditionAll conditions must be satisfied
or(...conditions)(...Condition[]) => ConditionAt least one condition must be satisfied
not(condition)(Condition) => ConditionNegates the condition

Example

ts
import { createMatchConditionBuilder } from 'guantr';

interface Article {
  id: number;
  status: 'draft' | 'published' | 'archived';
  title: string;
  ownerId: string;
  tags: string[];
  comments: Array<{ approved: boolean; text: string }>;
}

interface MyContext {
  userId: string;
  role: 'admin' | 'editor' | 'viewer';
}

const builder = createMatchConditionBuilder<Article, MyContext>();

// Simple comparison
const isPublished = builder.eq(builder.resource('status'), builder.literal('published'));

// Logical AND with not
const isActiveAndNotArchived = builder.and(
  builder.eq(builder.resource('status'), builder.literal('published')),
  builder.not(builder.eq(builder.resource('status'), builder.literal('archived'))),
);

// Owner check (context vs resource)
const isOwner = builder.eq(builder.resource('ownerId'), builder.context('userId'));

// Case-insensitive string match
const titleContains = builder.contains(builder.resource('title'), builder.literal('hello'), {
  caseInsensitive: true,
});

// Array membership
const hasTag = builder.has(builder.resource('tags'), builder.literal('featured'));

// Complex array: check if any comment is approved
const hasApprovedComment = builder.some(builder.resource('comments'), ({ eq, resource, literal }) =>
  eq(resource('approved'), literal(true)),
);

// Complex nested: owner AND (published OR has approved comments)
const canManage = builder.and(
  builder.eq(builder.resource('ownerId'), builder.context('userId')),
  builder.or(isPublished, hasApprovedComment),
);

// The result is a JSON-serializable Condition object
console.log(JSON.stringify(canManage, null, 2));

evaluateCondition

Evaluates a serialized Condition AST against a resource instance and context object. This is the same runtime engine used internally by can/cannot.

Signature

ts
function evaluateCondition(
  condition: Condition,
  resource: Record<string, unknown>,
  context: Record<string, unknown>,
): boolean;

Parameters

ParameterTypeDescription
conditionConditionThe serialized condition AST to evaluate
resourceRecord<string, unknown>The resource instance to evaluate against
contextRecord<string, unknown>The evaluation context

Returns

  • booleantrue if the condition is satisfied, false otherwise.

This is a synchronous function.

Throws

  • GuantrInvalidConditionKeyError — When a referenced field does not exist on the resource or context and no nullish opt-out is present.

Example

ts
import { evaluateCondition } from 'guantr';
import type { Condition } from 'guantr';

const condition: Condition = {
  type: 'condition',
  node: {
    type: 'operator',
    operator: 'eq',
    operands: [
      { type: 'resource', path: 'status' },
      { type: 'literal', value: 'published' },
    ],
  },
};

const post = { id: 1, status: 'published', title: 'Hello' };
console.log(evaluateCondition(condition, post, {})); // true

const draft = { id: 2, status: 'draft', title: 'WIP' };
console.log(evaluateCondition(condition, draft, {})); // false

With logical operators

ts
const andCondition: Condition = {
  type: 'condition',
  node: {
    type: 'logical',
    operator: 'and',
    operands: [
      {
        type: 'condition',
        node: {
          type: 'operator',
          operator: 'eq',
          operands: [
            { type: 'resource', path: 'status' },
            { type: 'literal', value: 'published' },
          ],
        },
      },
      {
        type: 'condition',
        node: {
          type: 'operator',
          operator: 'eq',
          operands: [
            { type: 'resource', path: 'ownerId' },
            { type: 'context', path: 'userId' },
          ],
        },
      },
    ],
  },
};

const post = { id: 1, status: 'published', ownerId: 'user-1' };
const context = { userId: 'user-1' };
console.log(evaluateCondition(andCondition, post, context)); // true

serializeRules

Converts function-based matchCondition entries in a rules array into serializable Condition AST objects. This is the same logic used internally by setRules() but exposed as a standalone utility.

Use this when you want to seed rules into a custom storage backend (database, key-value store, etc.) without creating a Guantr instance or calling setRules at runtime.

Signature

ts
function serializeRules<Meta extends GuantrMeta<GuantrResourceMap> | undefined = undefined>(
  rules: readonly GuantrRule<Meta>[],
): GuantrRule<Meta>[];

Generic

  • Meta: The GuantrMeta type describing the resource map and context. Must be provided explicitly when using typed rules to preserve type information.

Parameters

ParameterTypeDescription
rulesreadonly GuantrRule<Meta>[]Array of rules, potentially containing function-based matchCondition entries.

Returns

A new array where every function-based matchCondition has been converted to a Condition AST object. Rules with non-function conditions (Condition, null, or undefined) are passed through unchanged.

Example — Basic

ts
import { serializeRules } from 'guantr';
import type { GuantrRule, GuantrMeta, GuantrResourceMap } from 'guantr';

type Post = { id: number; status: string; archived: boolean };
type ResourceMap = GuantrResourceMap<{ post: { action: 'read' | 'update'; model: Post } }>;
type Meta = GuantrMeta<ResourceMap>;

const rules: GuantrRule<Meta>[] = [
  { resource: 'post', action: 'read', effect: 'allow' },
  {
    resource: 'post',
    action: 'update',
    effect: 'allow',
    matchCondition: ({ eq, resource, context }) => eq(resource('id'), context('userId')),
  },
];

// Convert function-based conditions to serializable Condition objects
const serialized = serializeRules<Meta>(rules);
// serialized[1].matchCondition is now a Condition object, not a function
// Safe to JSON.stringify and store in a database

// Persist to database
await db.rules.insertMany(
  serialized.map((r) => ({
    action: r.action,
    resource: r.resource,
    effect: r.effect,
    condition: JSON.stringify(r.matchCondition ?? null),
  })),
);

Example — Database seeding

ts
import { serializeRules } from 'guantr';

// Rules defined in a source file (e.g., rules.ts)
export const appRules: GuantrRule<AppMeta>[] = [
  { resource: 'post', action: 'read', effect: 'allow' },
  {
    resource: 'post',
    action: 'delete',
    effect: 'deny',
    matchCondition: ({ eq, resource, context }) => eq(resource('ownerId'), context('userId')),
  },
];

// In a migration script:
async function seed() {
  const serialized = serializeRules<AppMeta>(appRules);
  for (const rule of serialized) {
    await db.insert('rules', {
      action: rule.action,
      resource: rule.resource,
      effect: rule.effect,
      match_condition: JSON.stringify(rule.matchCondition ?? null),
    });
  }
}

How it works

  1. Iterates over each rule in the input array.
  2. If matchCondition is a function, executes it with a MatchConditionBuilder and replaces it with the returned Condition AST.
  3. If matchCondition is null, undefined, or already a Condition object, leaves it unchanged.
  4. Returns a new array — the original array is not mutated.

deserializeRules

Wraps Condition objects in a rules array back into function form. This is the inverse of serializeRules.

Use this when you load rules from an external store (database, cache, etc.) and want to feed them into setRules using either the array or callback form.

Signature

ts
function deserializeRules<Meta extends GuantrMeta<GuantrResourceMap> | undefined = undefined>(
  rules: readonly GuantrRule<Meta>[],
): GuantrRule<Meta>[];

Generic

  • Meta: The GuantrMeta type describing the resource map and context. Must be provided explicitly when using typed rules.

Parameters

ParameterTypeDescription
rulesreadonly GuantrRule<Meta>[]Array of rules containing Condition objects in matchCondition.

Returns

A new array where every non-function matchCondition has been wrapped as a function. Rules with null, undefined, or already-function matchCondition are passed through unchanged.

Example

ts
import { deserializeRules } from 'guantr';

// Rules loaded from a database — matchCondition is a parsed Condition object
const rows = await db.query('SELECT * FROM rules');
const rulesFromDb: GuantrRule<Meta>[] = rows.map((r) => ({
  action: r.action,
  resource: r.resource,
  effect: r.effect,
  matchCondition: JSON.parse(r.match_condition ?? null),
}));

// Wrap Condition objects as functions for setRules
const deserialized = deserializeRules<Meta>(rulesFromDb);
await guantr.setRules(deserialized);

Example — Full round-trip

ts
import { serializeRules, deserializeRules } from 'guantr';

// 1. Define rules in code
const rules: GuantrRule<Meta>[] = [
  { resource: 'post', action: 'read', effect: 'allow' },
  {
    resource: 'post',
    action: 'delete',
    effect: 'deny',
    matchCondition: ({ eq, resource, literal }) => eq(resource('archived'), literal(true)),
  },
];

// 2. Serialize before persisting
const serialized = serializeRules<Meta>(rules);

// 3. Persist to database
const json = JSON.stringify(serialized);
await db.insert('rules', { body: json });

// 4. Later: load from database and deserialize
const fromDb = JSON.parse(await db.select('rules'));
const deserialized = deserializeRules<Meta>(fromDb);

// 5. Register with Guantr
await guantr.setRules(deserialized);

How it works

  1. Iterates over each rule in the input array.
  2. If matchCondition is a Condition object (not null, undefined, or a function), wraps it in a function that returns the condition directly.
  3. The wrapped function accepts a MatchConditionBuilder (ignoring it) and returns the original Condition.
  4. Returns a new array — the original array is not mutated.

Condition Types

Guantr exports all the types that compose the condition system. These are useful for type-safe manipulation of condition data, custom tooling, or serialization/deserialization scenarios.

Condition

ts
interface Condition {
  readonly type: 'condition';
  readonly node: AstNode;
}

The top-level wrapper around every condition AST. Returned by all builder methods and stored in GuantrRule.matchCondition.

MatchConditionBuilder<Model, Context>

ts
interface MatchConditionBuilder<
  Model extends Record<string, unknown>,
  Context extends Record<string, unknown>,
> {
  resource<P extends LeafKeys<Model>>(path: P): ResourceRef<Model, P>;
  context<P extends LeafKeys<Context>>(path: P): ContextRef<Context, P>;
  literal<T>(value: T): LiteralRef<T>;
  eq(...): Condition;
  ne(...): Condition;
  gt(...): Condition;
  gte(...): Condition;
  lt(...): Condition;
  lte(...): Condition;
  contains(...): Condition;
  startsWith(...): Condition;
  endsWith(...): Condition;
  in(...): Condition;
  has(...): Condition;
  hasSome(...): Condition;
  hasEvery(...): Condition;
  some(...): Condition;
  every(...): Condition;
  none(...): Condition;
  and(...conditions: Condition[]): Condition;
  or(...conditions: Condition[]): Condition;
  not(condition: Condition): Condition;
}

The builder object passed to matchCondition functions. Created by createMatchConditionBuilder() or provided automatically in rule callbacks.

MatchConditionFn<Model, Context>

ts
type MatchConditionFn<
  Model extends Record<string, unknown> = Record<string, unknown>,
  Context extends Record<string, unknown> = Record<string, unknown>,
> = (builder: MatchConditionBuilder<Model, Context>) => Condition;

The user-facing function signature for defining a matchCondition on a rule.

ResourceRef<Model, Path>

ts
interface ResourceRef<Model extends Record<string, unknown>, P extends string = string> {
  readonly type: 'resource';
  readonly path: P;
}

References a field on the resource model. Created by builder.resource('fieldName').

ContextRef<Context, Path>

ts
interface ContextRef<Context extends Record<string, unknown>, P extends string = string> {
  readonly type: 'context';
  readonly path: P;
}

References a field on the evaluation context. Created by builder.context('fieldName').

LiteralRef<T>

ts
interface LiteralRef<T = unknown> {
  readonly type: 'literal';
  readonly value: T;
}

An inline literal value. Created by builder.literal(value).

ValueRef

ts
type ValueRef = ResourceRef | ContextRef | LiteralRef;

Union of all value-reference variants. Used as operand types for operators.

InferValueRef<R>

ts
type InferValueRef<R extends ValueRef> = /* extracted phantom type */;

Extracts the value type carried by a ValueRef's phantom type parameter. Useful for type-level reasoning about condition operands.

ArrayElementType<R>

ts
type ArrayElementType<R extends ValueRef> = /* element type or never */;

Extracts the element type from a ValueRef whose phantom type is an array. Returns never if the phantom type is not an array.

OperatorNode

ts
interface OperatorNode {
  readonly type: 'operator';
  readonly operator:
    | 'eq'
    | 'ne'
    | 'gt'
    | 'gte'
    | 'lt'
    | 'lte'
    | 'contains'
    | 'startsWith'
    | 'endsWith'
    | 'in'
    | 'has'
    | 'hasSome'
    | 'hasEvery'
    | 'some'
    | 'every'
    | 'none';
  readonly operands: readonly ValueRef[];
  readonly options?: Readonly<{ caseInsensitive?: boolean }>;
  readonly condition?: Condition; // For some/every/none
}

An AST node for a comparison or membership check. Serialized as { "type": "operator", "operator": "eq", "operands": [...] }.

LogicalNode

ts
interface LogicalNode {
  readonly type: 'logical';
  readonly operator: 'and' | 'or' | 'not';
  readonly operands: readonly Condition[];
}

An AST node for logical combination of child conditions.

AstNode

ts
type AstNode = OperatorNode | LogicalNode;

Discriminated union of all AST node variants.


See also