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

paranorm

v0.1.1

Published

YAML schema inference, migrations, and a typed model API for Kysely.

Readme

ParanORM

Kysely on steroids: author database schemas in YAML, infer Kysely types, apply schema-diff migrations, and query through a compact model API.

Install

npm i paranorm kysely

Quick start

Keep the YAML as a literal so TypeScript can infer its database type:

import { Kysely } from "kysely";
import { paranorm, defineSchema, type InferSchema } from "paranorm";

const schema = defineSchema(`
  _version: "1.0.0"
  users:
    id: id(bigint)
    email: string unique
    name: string
    created_at: timestamp default=now
  posts:
    id: id(bigint)
    user_id: references=users.id on_delete=cascade index
    title: string
    status: string enum=[draft,published] default="draft"
    published_at: timestamp?
`);

type DB = InferSchema<typeof schema>;

const db = new Kysely<DB>({ dialect });
const orm = paranorm(db);

const posts = await orm.posts.findMany({
  where: {
    status: "published",
    OR: [{ title: { contains: "Kysely" } }, { title: { startsWith: "SQL" } }],
  },
  select: { id: true, title: true },
  orderBy: [{ published_at: "desc" }],
  take: 20,
});

paranorm accepts only Kysely<DB>. Models are created lazily through a proxy, while table names, columns, rows, filters, and operators are inferred from the Kysely database type. No table list, schema object, or relation metadata is passed to the query wrapper.

Query API

Every inferred table exposes:

orm.users.findMany(args?)
orm.users.findFirst(args?)
orm.users.findUnique({ where })
orm.users.create({ data })
orm.users.createMany({ data })
orm.users.update({ where, data })
orm.users.updateMany({ where, data })
orm.users.delete({ where })
orm.users.deleteMany({ where })
orm.users.upsert({ where, create, update })
orm.users.count({ where }?)
orm.users.exists({ where }?)
orm.users.paginate(args)

Selections return projected types instead of the full row:

const users = await orm.users.findMany({
  select: { id: true, email: true },
});
// Array<{ id: string; email: string }>

Writes use Kysely's inferred Insertable and Updateable types. Single-row writes return the affected row; updateMany and deleteMany return affected counts.

Filters

Fields accept direct equality values or type-specific operators:

await orm.users.findMany({
  where: {
    email: { endsWith: "@example.com" },
    name: { notIn: ["Bot", "Deleted"] },
    OR: [{ name: { startsWith: "A" } }, { name: { startsWith: "B" } }],
    NOT: { email: { contains: "+blocked" } },
  },
});

Supported operators:

  • Strings: equals, not, in, notIn, contains, startsWith, endsWith
  • Numbers: equals, not, in, notIn, lt, lte, gt, gte
  • Dates: equals, not, lt, lte, gt, gte
  • Booleans: equals, not
  • Nullable fields: isNull
  • Logical composition: AND, OR, NOT

LIKE wildcards in user values are escaped automatically.

Pagination

Offset pagination:

const page = await orm.posts.paginate({
  orderBy: [{ id: "asc" }],
  take: 20,
  skip: 40,
});

Cursor pagination:

const first = await orm.posts.paginate({
  orderBy: [{ published_at: "desc" }, { id: "asc" }],
  take: 20,
});

const next = await orm.posts.paginate({
  orderBy: [{ published_at: "desc" }, { id: "asc" }],
  take: 20,
  after: first.pagination.endCursor!,
});

The result includes count, hasNext, hasPrevious, startCursor, and endCursor.

YAML type inference

Use either API:

import { defineSchema, type InferDatabase, type InferSchema } from "paranorm";

const schema = defineSchema(yamlLiteral);
type DB = InferSchema<typeof schema>;

// Equivalent:
type DBDirect = InferDatabase<typeof yamlLiteral>;

Inference supports:

  • Generated IDs and columns with defaults
  • Nullable insert/select types
  • Foreign-key column types
  • String enum unions
  • string, integer, bigint, decimal, boolean, date, timestamp, JSON, and binary columns
  • Generated auth, API-key, file, and attachment tables
  • Kysely's Selectable, Insertable, and Updateable helpers

Tagged YAML templates

For IDE extensions that highlight tagged templates, use the exported schema tag directly or alias it to yaml:

import { schema as yaml } from "paranorm";

function loadSchema() {
  return yaml`
    _version: "1.0.0"
    users:
      id: id(bigint)
      email: string unique
  `;
}

The tag uses dedent, so surrounding code indentation is removed automatically. Interpolations are rejected so the template always contains one complete schema document. TypeScript does not expose tagged-template contents as a string-literal type, so use defineSchema(yamlLiteral) when InferSchema compile-time inference is needed. A const string retains its literal type without an as const assertion.

TypeScript can only infer a string known at compile time. A schema loaded with Bun.file(...).text() is a runtime string and requires generated declarations instead. Runtime parsing remains the authoritative schema validator.

See SPEC.md for the complete authoring format.

Schema migrations

createMigrator wraps Kysely's native Migrator and derives migration names from each schema's _version:

import { createMigrator, defineSchema } from "paranorm";

const v1 = defineSchema(`_version: "1.0.0"\nusers:\n  id: id\n`);
const v2 = defineSchema(`_version: "2.0.0"\nusers:\n  id: id\n  email: string?\n`);

const migrator = createMigrator(db, [v1, v2]);

await migrator.plan(); // Pending structured operations
await migrator.validate(); // Ordering and destructive-policy validation
await migrator.sql(); // Compiled SQL without execution
await migrator.migrateToLatest();
await migrator.migrateUp();
await migrator.migrateDown();
await migrator.migrateTo("1.0.0");

It returns Kysely's actual Migrator, preserving its methods, locking, migration tables, and result/error behavior. Inputs may be typed schemas, YAML strings, or { name, content } sources. A third argument accepts both schema options and Kysely migrator options:

const migrator = createMigrator(db, [v1, v2], {
  dialect: "postgres",
  allowDestructive: false,
  allowUnorderedMigrations: false,
  migrationTableName: "paranorm_migration",
  migrationLockTableName: "paranorm_migration_lock",
});

SchemaMigrationProvider and migrateSchemasToLatest remain available as lower-level and compatibility APIs. Forward destructive changes require allowDestructive: true. Migration rendering supports postgres, sqlite, mysql, and mssql type/default variants. Dialect-specific ALTER TABLE, mutation RETURNING, and upsert limitations still apply.

Schema errors include source locations when parsing strings. Named migration sources are reported as name:line:column.