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

@vibeorm/dsl

v5.0.4

Published

TypeScript schema DSL for VibeORM v2 — compiles to canonical SchemaIR, with .prisma generation for interop

Readme

@vibeorm/dsl

Part of VibeORM — a type-safe TypeScript ORM for Bun and Node. Prisma-schema or TypeScript-DSL input, a canonical schema IR, a generated client, and a dialect-aware SQL layer over PostgreSQL, PGlite, SQLite and MySQL.

The code-first schema input: models and relations written in TypeScript, compiled straight to the canonical SchemaIR. It is the peer of the .prisma parser, and it also emits .prisma text for interop.

bun add @vibeorm/dsl

Stable. 2.0.0, published under the latest dist-tag. The v1 line stays installable as vibeorm@1.

Most users never install this directly — it ships as a dependency of vibeorm. Reach for it when you are building tooling on the IR or SQL layer.

Usage

// src/schema.ts
import { dsl } from "@vibeorm/dsl";

const Role = dsl.enum({ name: "Role", options: ["USER", "ADMIN"] });

const User = dsl.create({
  name: "User",
  schema: {
    id: dsl.id({ type: "autoincrement" }),
    createdAt: dsl.datetime().defaultNow(),
    updatedAt: dsl.datetime().updatedAt(),
    email: dsl.string().unique(),
    name: dsl.string().optional(),
    role: Role.field({ default: "USER" }),
  },
});

const Post = dsl.create({
  name: "Post",
  schema: {
    id: dsl.id({ type: "uuid" }),
    title: dsl.string(),
    published: dsl.boolean().default(false),
    meta: dsl.json().optional(),
    authorId: dsl.int().index(),
  },
  indexes: (i) => [i.unique("authorId", "title")],
});

const relations = dsl.defineRelations({ User, Post }, (r) => ({
  User: { posts: r.many.Post({ from: r.User.id, to: r.Post.authorId }) },
  Post: { author: r.one.User({ from: r.Post.authorId, to: r.User.id, onDelete: "Cascade" }) },
}));

export const schema = dsl.schema({ Role, User, Post }, { relations });
export default schema;

Point vibeorm.config.ts at the .ts entry (schema: "./src/schema.ts") and the CLI takes it from there. The module must export the composed schema as default, as export const schema, or as a schema property on the default export.

To go to the IR yourself: dsl.toIR({ schema }) returns a SchemaIR, optionally taking provider and an extensions registry. dsl.validateSchema(schema) returns ValidationError[] without throwing.

Column factories

dsl.string() · dsl.int() · dsl.bigint() · dsl.float() · dsl.decimal() · dsl.boolean() · dsl.datetime() · dsl.json() · dsl.bytes() · dsl.id({ type }) · dsl.unsupported("tsvector") · dsl.extensionType({ ... }) · dsl.enum(...).

dsl.enum has two forms: with a name it creates a reusable enum whose .field() yields a column, without one it creates an inline single-use enum column.

Id strategies are dsl.id({ type: "ulid" | "uuid" | "cuid" | "nanoid" | "autoincrement" | "none" }). The default is "ulid", a String column generated in the app; "autoincrement" makes it an Int. A key the database mints takes type: "none" with a database default: dsl.id({ type: "none" }).uuid().dbgenerated("uuidv7()") is Prisma's String @id @db.Uuid @default(dbgenerated("uuidv7()")).

Chainable modifiers

On every column: .optional(), .unique(), .index(), .array(), .default(value), .map("db_col"), .dbgenerated("expr"), .comment("...").

Kind-specific: .maxLength(n), .text() and .uuid() on strings (.uuid() also on ids), .precision(p, s) on decimals, .defaultNow(), .updatedAt() and .tz() on datetimes. .tz({ precision: 3 }) declares @db.Timestamptz(3): it stores exactly what a JavaScript Date holds, so a database-written time reads back unchanged and findPage can order by the column without an assertion. .uuid() declares @db.Uuid: PostgreSQL's native uuid type (the value stays a string); SQLite stores it as TEXT; MySQL has no uuid type and refuses the schema with VIBE_UNSUPPORTED_CAPABILITY.

Database defaults

arrival: dsl.bigint().autoincrement(),
settings: dsl.json().default({ notifications: true, channels: [] }),
tags: dsl.string().array().default([]),

dsl.int() also supports .autoincrement(). The config form is { autoincrement: true }. Neither form implies a primary key or uniqueness. PostgreSQL/PGlite allow non-primary counters; SQLite requires the single integer primary key; MySQL requires a leading index and permits only one counter per table. Existing serial columns compare equal to the declaration without replacing their sequences. Adding auto-increment to an existing ordinary column remains an explicit migration operation, not a sequence reset.

JSON objects, arrays, booleans, numbers and JSON null become database defaults. String arguments retain the Prisma-compatible serialized-JSON convention, for example .default('"hello"'). Cycles, undefined members, non-finite numbers and class instances are refused. Scalar-list defaults are supported on PostgreSQL/PGlite, including empty lists and exact BigInt elements. Other dialects cannot store scalar-list columns. Defaults are database-owned, so inserts may omit these fields. A literal default cannot be combined with .dbgenerated() or .autoincrement().

Physical index names use map, separately from a compound unique's client-selector name:

indexes: (i) => [i.on(["arrival"], { map: "arrival_lookup" })],

On PostgreSQL and PGlite a non-unique index can set an operator class per field with ops, keyed by field name, each value a qualified name:

indexes: (i) => [i.on(["email"], { ops: { email: { namespace: "pg_catalog", name: "text_pattern_ops" } } })],

ops on i.unique(...) is refused (VIBE_UNSUPPORTED_CAPABILITY), as are operator classes on SQLite and MySQL. dsl.generate.prisma refuses a schema that uses them, because Prisma's ops: syntax cannot carry the qualified name.

Relations

Relations are declared separately from columns, in dsl.defineRelations(models, (r) => ...), which keeps column references typed on both ends.

  • One side: r.one.X({ from, to, optional?, alias?, ownsForeignKey?, onDelete?, onUpdate? }).
  • Many side: r.many.X({ from, to }).
  • Implicit many-to-many: both sides r.many, between two DIFFERENT models. A many-to-many from a model to itself is refused (VIBE_SCHEMA_INVALID, rule implicit-self-many-to-many) because both sides would own the same join-table column; use an explicit junction model with one foreign key per direction.
  • Explicit many-to-many, through a junction model: r.many.Group({ from: r.User.id.through(r.UserGroup.userId), to: r.Group.id.through(r.UserGroup.groupId) }).
  • Self-relations, and several relations between the same pair of models, disambiguate with alias.

For a shared-primary-key one-to-one, set ownsForeignKey: true on the side whose table carries the foreign key. Ambiguous pairs now fail instead of deriving ownership from declaration order. ownsForeignKey: false marks the inverse side; conflicting declarations fail. Conversion emits ownership explicitly.

Large schemas can split relations across files with dsl.defineRelationsPart and recombine them with dsl.mergeRelations; dsl.domain({ name, models }) groups models.

Templates

dsl.templates.base spreads id, createdAt, updatedAt and deletedAt into a model. Also available: baseWithWorkspace, withAudit, withActive, withMetadata, withNameDescription, withExternalId, and fields.

const Workspace = dsl.create({
  name: "Workspace",
  schema: { ...dsl.templates.base, name: dsl.string() },
});

Interop

dsl.generate.prisma(schema) renders the schema as .prisma text, and dsl.generate(schema) returns it as a GeneratorOutput. That is what backs the CLI's conversion in both directions:

vibeorm convert --to dsl --out src/schema.ts              # .prisma → TS DSL
vibeorm convert --to prisma --out prisma/schema.prisma    # TS DSL → .prisma

Conversion goes through the IR and round-trips are tested: emitted DSL compiles back to the same IR. convert --to dsl refuses constructs the DSL cannot express — views, composite-column foreign keys, non-standard index kinds, enum @@map names, exotic @db.* types — with one VIBE_CONFIG error naming every offender.

Links