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

nobugdb-orm

v0.1.3

Published

TypeScript Data Mapper ORM for NoBugDB

Downloads

584

Readme

NoBugDB ORM

TypeScript Data Mapper ORM for NoBugDB — Node.js / Express, published as npm package nobugdb-orm.

Status

0.1.2 — plan-2 surface: set ops / windows / subqueries / scalars, CHECK & partitioning & routines/triggers in migrations, admin EXPLAIN/VACUUM, composite / non-UUID PKs.

See CHANGELOG.md. Implementation roadmap: docs/plans/README.md.

Requirements

  • Node.js >=18
  • A running NoBugDB server (default 127.0.0.1:9000)

Install

npm install nobugdb-orm

Express is an optional peer dependency — only needed if you use nobugdb-orm/express.

Stack

  • TypeScript (ESM primary, dual CJS)
  • Data Mapper + Repository (EntityManager, not Active Record)
  • TCP driver for NoBugDB wire protocol (AUTH / QUERY / PING / QUIT)
  • Connection pool with sticky transactions (default max: 4)
  • Client-generated or auto UUID primary keys (no RETURNING / SERIAL); also INT/STRING and composite PKs when supplied by the client

Data Mapper usage

Entities are plain objects. Persistence goes through Repository / EntityManager — there is no save() on the entity itself.

import { DataSource, defineEntity } from 'nobugdb-orm';

const User = defineEntity<{ id: string; email: string; name: string }>({
  name: 'User',
  tableName: 'users',
  columns: {
    id: { type: 'UUID', primary: true },
    email: { type: 'STRING', unique: true },
    name: { type: 'STRING' },
  },
});

const ds = new DataSource({
  host: '127.0.0.1',
  port: 9000,
  entities: [User],
});
await ds.initialize();

const users = ds.getRepository(User);
const u = await users.insert({ email: '[email protected]', name: 'Ada' });
const found = await users.findById(u.id);

// Explicit Unit of Work
const created = ds.manager.create(User, { email: '[email protected]', name: 'Grace' });
ds.manager.persist(created);
await ds.manager.flush();

await ds.destroy();

If the primary UUID is empty on insert / persist, the ORM generates one with randomUUID(). You may also set the UUID yourself before insert.

Relations

Declare FK columns explicitly in columns, then describe relations in metadata. Relations are eager-loaded only when requested — there is no lazy loading and no runtime DB introspection (information_schema is not available in NoBugDB).

const User = defineEntity<{ id: string; name: string }>({
  name: 'User',
  tableName: 'users',
  columns: {
    id: { type: 'UUID', primary: true },
    name: { type: 'STRING' },
  },
  relations: {
    posts: { type: 'one-to-many', target: 'Post', inverseSide: 'author' },
  },
});

const Post = defineEntity<{ id: string; title: string; authorId: string }>({
  name: 'Post',
  tableName: 'posts',
  columns: {
    id: { type: 'UUID', primary: true },
    title: { type: 'STRING' },
    authorId: { type: 'UUID' },
  },
  relations: {
    author: {
      type: 'many-to-one',
      target: 'User',
      joinColumn: 'authorId',
      inverseSide: 'posts',
    },
  },
});

const ds = new DataSource({ host: '127.0.0.1', port: 9000, entities: [User, Post] });
await ds.initialize(); // runs SchemaRegistry.assertConsistent()

const posts = await ds.getRepository(Post).find({ relations: ['author'] });
// posts[0].author is a hydrated User (shared via identity map when reused)

// Assign relation object; flush writes authorId FK (no ORM cascade insert)
const user = await ds.getRepository(User).insert({ name: 'Ada' });
const post = ds.manager.create(Post, { title: 'Hello' });
(post as { author: typeof user }).author = user;
ds.manager.persist(post);
await ds.manager.flush();

Notes:

  • Many-to-many in v1: model via an explicit join entity with two M2O relations.
  • Relation targets for writes must be base tables (views are read-only in NoBugDB).
  • Prefer find({ relations: [...] }) over manual loops to avoid N+1 queries.
  • ORM does not cascade insert/update/delete — persist related entities explicitly.

Pool usage

NoBugDB keeps transaction state on the TCP session (snapshot isolation / MVCC). Use pool.transaction (or DataSource.transaction) so BEGINCOMMIT stay on one connection:

import { Pool } from 'nobugdb-orm';

const pool = new Pool({ host: '127.0.0.1', port: 9000, max: 4 });

await pool.transaction(async (conn) => {
  await conn.query('INSERT INTO users (id, name) VALUES (...)');
});
await pool.end();

Default max: 4 matches server-side serialization (db_mutex_); raising the pool size rarely improves throughput.

Query builder usage

import { Pool, QueryBuilder } from 'nobugdb-orm';

const pool = new Pool({ host: '127.0.0.1', port: 9000, max: 4 });

const rows = await new QueryBuilder(pool)
  .select('id', 'name')
  .from('users')
  .where({ active: true })
  .orderBy('name', 'ASC')
  .limit(10)
  .execute();

await new QueryBuilder(pool)
  .insertInto('users')
  .values({ id: '...', name: 'Ada' })
  .executeCommand();

await pool.end();

toSql() renders escaped inline literals for ad-hoc SQL. execute() / executeCommand() use PREPARE / EXECUTE / DEALLOCATE on the connection session.

Supported SQL / types

| Area | Supported | |------|-----------| | DML | SELECT, INSERT, UPDATE, DELETE | | Clauses | JOIN (INNER/LEFT/RIGHT/FULL/CROSS), WHERE, GROUP BY, HAVING, ORDER BY, LIMIT / OFFSET | | Set operations | Top-level UNION / UNION ALL / INTERSECT / EXCEPT (no INTERSECT ALL / EXCEPT ALL) | | Subqueries | IN / NOT IN / EXISTS / NOT EXISTS / scalar via whereInSubquery / whereExists / sql.subquery (correlated refs via sql.ref); no set-ops inside subquery | | Aggregates | Basic (COUNT, SUM, AVG, MIN, MAX) via expression helpers | | Scalars | UPPER / LOWER / LENGTH / COALESCE / NULLIF / SUBSTRING / CAST / CURRENT_DATE via sql.* (plus generic sql.fn for builtins/UDF) | | Window functions | ROW_NUMBER / RANK / DENSE_RANK / running SUM·AVG with OVER (PARTITION BY optional, ORDER BY required); no LEAD/LAG/NTILE, named WINDOW, or explicit frame | | Types | INT, FLOAT, STRING, BOOLEAN, DATE, UUID | | Admin | DataSource.explain / explainQuery / vacuum (and EntityManager.explain / vacuum) | | Not supported | LIKE, CTE, UPSERT, RETURNING, SERIAL / sequences |

Wire values: DATE and UUID travel as strings; the ORM maps DATEDate and keeps UUID as string.

Limitations

  • Server read buffer is 1 MiB — oversized queries fail fast with REQUEST_TOO_LARGE; keep payloads within the limit.

  • No SERIAL / RETURNING — UUID PKs auto-generate by default; non-UUID and composite PKs must be supplied by the client.

  • No LIKE / CTE / UPSERT.

  • Window functions: only ROW_NUMBER / RANK / DENSE_RANK / running SUM·AVG; OVER requires ORDER BY; no LEAD/LAG/NTILE, named WINDOW, or explicit ROWS/RANGE frame.

  • Set operations are top-level only (UNION / UNION ALL / INTERSECT / EXCEPT); no INTERSECT ALL / EXCEPT ALL.

  • Subqueries support IN / EXISTS / scalar; set operations inside a subquery are rejected.

  • Partitioning: RANGE / HASH only — no SUBPARTITION; no FK on partitioned parent. Drop parent cascades children; drop child keeps parent.

  • Routines: IN params only — no OUT/INOUT or table-valued UDFs; procedure bodies cannot nest TX-BEGIN; body must not contain nested $$. CREATE/DROP require admin; reader cannot CALL. Use sql.fn('udf', …) for UDF in SELECT.

  • Admin: EXPLAIN executes the statement (side effects apply); reader may EXPLAIN allowed read statements. vacuum() is bare global VACUUM only — no per-table option; requires admin.

  • No runtime introspection (information_schema does not exist) — schema comes from entity metadata + migrations.

  • No TLS on the wire yet (development-grade auth; do not log passwords).

  • Views are read-only — never write through view targets.

  • Server uses a global DB mutex — keep pools small (default max: 4).

Migrations

NoBugDB has no information_schema — schema changes are explicit migrations, not auto-sync.

Config

Create nobugdb-orm.config.ts in your project:

export default {
  host: '127.0.0.1',
  port: 9000,
  migrationsDir: './migrations',
};

CLI

nobugdb-orm migration:create create_users
nobugdb-orm migration:up
nobugdb-orm migration:down        # revert last migration
nobugdb-orm migration:down 2      # revert last 2
nobugdb-orm migration:status
nobugdb-orm --config ./my.config.ts migration:up

Migration file

import type { MigrationContext } from 'nobugdb-orm';

export const id = '20260728120000_create_users';

export async function up(ctx: MigrationContext): Promise<void> {
  await ctx.schema.createTable('users', (t) => {
    t.uuid('id').primary();
    t.string('email').unique().notNull();
    t.string('name').notNull();
  });
}

export async function down(ctx: MigrationContext): Promise<void> {
  await ctx.schema.dropTable('users');
}

Filename must be {timestamp}_{slug}.ts and match exported id.

Partitioned tables

await ctx.schema.createPartitionedTable(
  'sales',
  { strategy: 'RANGE', column: 'y' },
  (t) => {
    t.int('id').primary();
    t.int('y').notNull();
  },
);
await ctx.schema.createPartition('sales_2024', 'sales', { from: 2024, to: 2025 });
await ctx.schema.createPartition('sales_h0', 'sales_hash', { modulus: 4, remainder: 0 });

createPartition does not redefine columns (schema is inherited from the parent). Use dropTable to remove a partition or the parent.

Routines (functions & procedures)

await ctx.schema.createFunction('double_it', {
  params: [{ name: 'x', type: 'INT' }],
  returns: 'INT',
  body: 'RETURN x * 2;',
});

await ctx.schema.createProcedure('add_user', {
  params: [
    { name: 'uid', type: 'INT' },
    { name: 'uname', type: 'STRING' },
  ],
  body: 'INSERT INTO users (id, name) VALUES (uid, uname);',
});

await ctx.schema.call('add_user', [1, 'Ada']);
// or at runtime:
await ds.callProcedure('add_user', [1, 'Ada']);
// SELECT UDF: sql.fn('double_it', 'id')

Use dropFunction / dropProcedure in down. Function body style 'expr' renders AS (body) instead of AS $$…$$.

Migrator API

import { DataSource, Migrator } from 'nobugdb-orm';

const ds = new DataSource({ host: '127.0.0.1', port: 9000 });
await ds.initialize();

const migrator = new Migrator(ds, { migrationsDir: './migrations' });
await migrator.up();
await migrator.status();
await migrator.down(1);

await ds.destroy();

DataSource can be initialized without entities for migration-only use.

Transactional semantics

Each migration runs inside BEGINCOMMIT on a sticky pooled connection. DDL and the history INSERT are applied in the same transaction when the server supports it. If a migration fails, the transaction rolls back and no history row is recorded.

History table (created on first migrate):

CREATE TABLE orm_migrations (
  id STRING PRIMARY KEY,
  applied_at STRING NOT NULL
);

Migration notes: no auto-generate from entity diff (v2); ALTER TABLE support matches NoBugDB engine capabilities; wide DDL may hit the 1 MiB wire buffer limit.

Express integration

nobugdb-orm ships an optional thin layer for Express: nobugdb-orm/express. It provides a request-scoped EntityManager so each request has its own Identity Map.

Middleware

import express from 'express';
import { DataSource, defineEntity } from 'nobugdb-orm';
import { nobugdbMiddleware } from 'nobugdb-orm/express';

const app = express();

// Your entities...
// const User = defineEntity(...);

const ds = new DataSource({ host: '127.0.0.1', port: 9000, entities: [] });
await ds.initialize();

app.use(nobugdbMiddleware({ dataSource: ds })); // mounts req.em

app.get('/users/:id', async (req, res, next) => {
  try {
    // Optional: type augmentation for req.em (see below).
    const user = await req.em.getRepository('User').findById(req.params.id);
    if (!user) return res.status(404).end();
    res.json(user);
  } catch (e) {
    next(e);
  }
});

Request typing (req.em)

If you want req.em typed, add this augmentation in your app:

import type { EntityManager } from 'nobugdb-orm';

declare global {
  namespace Express {
    interface Request {
      em: EntityManager;
    }
  }
}

Optional per-request transaction

By default, each ORM query uses pooled connections (request-scoped Identity Map, no automatic TCP transaction). If you need a single TCP-session transaction for the whole request, enable it:

app.use(
  '/transfer',
  nobugdbMiddleware({ dataSource: ds, perRequestTransaction: true }),
);

License

MIT