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 🙏

© 2026 – Pkg Stats / Ryan Hefner

ast-predicate

v0.5.0

Published

Framework-agnostic TypeScript predicate AST utilities for building, validating, and transforming filter expressions.

Readme

ast-predicate

Framework-agnostic TypeScript predicate AST utilities for building, validating, and adapting typed predicate expressions.

ast-predicate has the ambition to be a companion to tools like Kysely: not a replacement, but a small utility layer for custom implementation details that are often too application-specific for general-purpose query builders, while still making those details easier to model, reuse, and adapt.

ast-predicate provides a small predicate AST and typed builders. It does not execute queries and does not depend on a database driver, ORM, query builder, or runtime framework.

Why ast-predicate exists

ast-predicate is useful when predicate logic should live outside a specific ORM, query builder, or SQL implementation.

Many mature backend codebases contain filtering rules in several forms at once: Sequelize scopes, Prisma filters, Kysely expressions, raw SQL fragments, service configuration, and test helpers. That makes gradual refactoring harder, because the rules are coupled to the tool that currently executes them.

With ast-predicate, a service can define filters or metadata once using a framework-agnostic AST. Adapters can then translate the same predicate into Sequelize, Kysely, raw SQL, validation logic, or tests.

This makes the package useful as a small binding layer during incremental migration or mixed-stack development. It does not replace an ORM or query builder; it only describes predicate logic in a neutral format that surrounding code can adapt to its own execution layer.

The builder syntax is intentionally SQL/Kysely-like. The goal is to keep the developer experience familiar, reduce the pain of switching between related tools, and make adoption easier in codebases where people already work with query builders, ORMs, and SQL-oriented patterns.

Installation

npm install ast-predicate

Core idea

Predicates are represented as plain TypeScript objects.

import { createAstPredicateExpressionBuilder } from 'ast-predicate';

type ArticleTable = {
    id: string;
    status: string;
    deletedAt: Date | null;
};

const eb = createAstPredicateExpressionBuilder<keyof ArticleTable & string>();

const predicate = eb.and([
    eb('deletedAt', 'is', null),
    eb('status', '=', 'PUBLISHED'),
]);

The generated AST is plain data.

{
    type: 'logical',
    op: 'and',
    nodes: [
        {
            type: 'binary',
            left: {
                type: 'ref',
                ref: 'deletedAt',
            },
            op: 'is',
            right: {
                type: 'value',
                value: null,
            },
        },
        {
            type: 'binary',
            left: {
                type: 'ref',
                ref: 'status',
            },
            op: '=',
            right: {
                type: 'value',
                value: 'PUBLISHED',
            },
        },
    ],
}

The AST is JSON-compatible except when predicate values contain values such as Date.

Expression builder

The expression builder is a callable object. The call signature creates binary predicate nodes.

const predicate = eb('status', '=', 'PUBLISHED');

It also exposes helpers for references, literal values, and logical nodes.

const predicate = eb.and([
    eb('deletedAt', 'is', null),
    eb.or([
        eb('publishedAt', 'is not', null),
        eb('status', '=', 'PUBLISHED'),
    ]),
    eb.not(eb('status', '=', 'ARCHIVED')),
]);

Supported binary operators are SQL/Kysely-like.

'=' | '!=' | '<>' | '>' | '>=' | '<' | '<=' |
'in' | 'not in' | 'is' | 'is not' | 'like' | 'not like'

Table-scoped builders

For table-local predicates, use createAstPredicateWhere or the table builder from a database definition.

import { createAstPredicateWhere } from 'ast-predicate';

type ArticleTable = {
    id: string;
    workspaceId: string;
    categoryId: string;
    status: string;
    deletedAt: Date | null;
};

const predicate = createAstPredicateWhere<ArticleTable>(({ eb, and }) =>
    and([
        eb('deletedAt', 'is', null),
        eb('status', '=', 'PUBLISHED'),
    ]),
);

Invalid column names are rejected by TypeScript.

createAstPredicateWhere<ArticleTable>(({ eb }) =>
    eb('missingColumn', '=', 'value'),
);
// Type error

Database-scoped builders

Use createAstPredicateDatabase when predicates need fully-qualified references across multiple tables.

import { createAstPredicateDatabase } from 'ast-predicate';

type DB = {
    Article: {
        categoryId: string;
        workspaceId: string;
    };
    Category: {
        id: string;
        workspaceId: string;
    };
};

const db = createAstPredicateDatabase<DB>();

const predicate = db.where(({ eb, and, ref }) =>
    and([
        eb('Article.categoryId', '=', ref('Category.id')),
        eb('Article.workspaceId', '=', ref('Category.workspaceId')),
    ]),
);

Database references use this format:

TableName.columnName

Database aliases

Aliases are type-only. They let you define shorter reference names while still validating table and column names.

const db = createAstPredicateDatabase<
    DB,
    {
        a: 'Article';
        c: 'Category';
    }
>();

const predicate = db.where(({ eb, and, ref }) =>
    and([
        eb('a.categoryId', '=', ref('c.id')),
        eb('a.workspaceId', '=', ref('c.workspaceId')),
    ]),
);

The generated AST keeps the alias strings as given.

{
    type: 'binary',
    left: {
        type: 'ref',
        ref: 'a.categoryId',
    },
    op: '=',
    right: {
        type: 'ref',
        ref: 'c.id',
    },
}

Table metadata helpers

The database builder can create table-scoped metadata helpers. The first supported helper is uniqueIndexes.

Use the table builder to create unique-index metadata. uniqueIndexes() accepts either a metadata object or a callback. The callback form is preferred when an index predicate is needed, because it exposes the table-scoped expression context directly.

import { createAstPredicateDatabase } from 'ast-predicate';

type PredicateTestDatabase = {
    'schema.Articles': {
        id: string;
        workspaceId: string;
        categoryId: string;
        slug: string;
        title: string;
        description: string | null;
        deletedAt: Date | null;
        publishedAt: Date | null;
        createdAt: Date;
        status: string;
    };
};

const predicateDb = createAstPredicateDatabase<PredicateTestDatabase>();
const articles = predicateDb.table('schema.Articles');

const articleUniqueIndexes = articles.uniqueIndexes(({ eb, and, or, ref }) => ({
    pkey: {
        columns: ['id'],
    },
    slug_unique: {
        columns: ['workspaceId', 'categoryId', 'slug'],
        where: eb('deletedAt', 'is', null),
    },
    nullable_description_unique: {
        columns: ['workspaceId', 'categoryId', 'description'],
    },
    published_slug_unique: {
        columns: ['workspaceId', 'categoryId', 'slug'],
        where: and([
            eb('deletedAt', 'is', null),
            or([
                eb('publishedAt', 'is not', null),
                eb('status', '=', 'PUBLISHED'),
            ]),
        ]),
    },
    category_ref_unique: {
        columns: ['workspaceId', 'categoryId', 'slug'],
        where: eb('categoryId', '=', ref('id')),
    },
}));

The object form is still supported. It is useful when predicates are already built elsewhere.

const articleUniqueIndexes = articles.uniqueIndexes({
    pkey: {
        columns: ['id'],
    },
    slug_unique: {
        columns: ['workspaceId', 'categoryId', 'slug'],
        where: articles.where(({ eb }) =>
            eb('deletedAt', 'is', null),
        ),
    },
});

By default, uniqueIndexes() requires a unique index named pkey.

createAstPredicateTableBuilder<ArticleTable>().uniqueIndexes({
    pkey: {
        columns: ['id'],
    },
});

Pass another default key name to require a different index name.

createAstPredicateTableBuilder<ArticleTable, 'article_pkey'>().uniqueIndexes({
    article_pkey: {
        columns: ['id'],
    },
});

Pass never to disable the required default unique index.

createAstPredicateTableBuilder<ArticleTable, never>().uniqueIndexes({
    slug_unique: {
        columns: ['workspaceId', 'slug'],
    },
});

uniqueIndexes() preserves the metadata object as-is. Predicate values are stored as AST nodes.

This keeps column lists narrow for unique-index filter typing, while predicates can still reference any column from the table.

The resulting metadata is useful as adapter input. For example, an adapter can read columns to build unique-index filters and use where as an already-built AST node.

Namespace API

The package also exposes the same main entry points through the AstPredicate namespace.

import { AstPredicate } from 'ast-predicate';

const db = AstPredicate.database<PredicateTestDatabase>();
const eb = AstPredicate.expressionBuilder<'status' | 'deletedAt'>();

This style is useful when you prefer fewer named imports.

Predicate input callbacks

Many APIs accept AstPredicateInput<TRef>. It can be either an already-built AST node or a callback that receives an expression context.

import {
    createAstPredicateExpressionBuilder,
    resolveAstPredicateInput,
} from 'ast-predicate';

const eb = createAstPredicateExpressionBuilder<'deletedAt'>();

const predicate = resolveAstPredicateInput(
    ({ eb }) => eb('deletedAt', 'is', null),
    eb,
);

The callback context contains:

eb
ref
val
and
or
not

For table metadata such as uniqueIndexes(), prefer the callback form when defining predicates inline:

const indexes = articles.uniqueIndexes(({ eb }) => ({
    pkey: {
        columns: ['id'],
    },
    slug_unique: {
        columns: ['workspaceId', 'categoryId', 'slug'],
        where: eb('deletedAt', 'is', null),
    },
}));

This keeps predicate inference inside the table-scoped callback and stores only resolved AST nodes in the metadata object.

The object form is still supported. It is useful when predicates are built elsewhere or when you want to reuse the table helper explicitly:

const articles = predicateDb.table('schema.Articles');

const indexes = articles.uniqueIndexes({
    pkey: {
        columns: ['id'],
    },
    slug_unique: {
        columns: ['workspaceId', 'categoryId', 'slug'],
        where: articles.where(({ eb }) =>
            eb('deletedAt', 'is', null),
        ),
    },
});

AST node shape

The current AST uses binary, logical, and unary nodes.

type AstPredicateNode<TRef extends string = string> =
    | AstPredicateBinaryNode<TRef>
    | AstPredicateLogicalNode<TRef>
    | AstPredicateUnaryNode<TRef>;

Binary node:

type AstPredicateBinaryNode<TRef extends string = string> = {
    readonly type: 'binary';
    readonly left: AstPredicateRefOperand<TRef>;
    readonly op: AstPredicateBinaryOperator;
    readonly right: AstPredicateOperand<TRef>;
};

Logical node:

type AstPredicateLogicalNode<TRef extends string = string> = {
    readonly type: 'logical';
    readonly op: 'and' | 'or';
    readonly nodes: readonly AstPredicateNode<TRef>[];
};

Unary node:

type AstPredicateUnaryNode<TRef extends string = string> = {
    readonly type: 'unary';
    readonly op: 'not';
    readonly node: AstPredicateNode<TRef>;
};

Reference operand:

type AstPredicateRefOperand<TRef extends string = string> = {
    readonly type: 'ref';
    readonly ref: TRef;
};

Value operand:

type AstPredicateValueOperand = {
    readonly type: 'value';
    readonly value: AstPredicateValue;
};

Validating unknown input

Use assertAstPredicateNode when accepting a predicate from an unknown source.

import { assertAstPredicateNode } from 'ast-predicate';

const input: unknown = JSON.parse(payload);

assertAstPredicateNode(input);

// input is now typed as AstPredicateNode

This only validates the AST structure. It does not decide whether a reference is safe for a specific adapter or database schema. Adapters should still validate or map references before converting predicates into executable queries.

Test utilities

ast-predicate exposes optional SQL assertion helpers from a separate subpath.

import { createCompiledSqlExpect } from 'ast-predicate/test-utils';

These helpers are intended for tests around SQL-producing adapters or services. They are PostgreSQL-oriented and expect the test framework expect function to be passed in by the consumer.

See test utilities documentation for examples.

Current scope

The package currently contains:

  • framework-agnostic predicate AST types
  • typed expression builders
  • table-scoped metadata helpers
  • database-scoped reference helpers
  • runtime AST assertions
  • optional PostgreSQL-oriented test utilities

The package intentionally does not execute queries and does not provide database connectivity.

Adapters may be added later as separate packages or optional modules, for example:

ast-predicate-kysely
ast-predicate-sequelize

License

MIT