Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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.

About

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

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages