Skip to content

Repository files navigation

NextModel

A typed, promise-based ORM for TypeScript. Define models with a factory, chain immutable scopes, plug in any storage via the Connector interface, and run schema migrations against the same connector.

At a glance

Define a model

import { defineSchema, Model } from '@next-model/core';
import { SqliteConnector } from '@next-model/sqlite-connector';

const schema = defineSchema({
  users: {
    columns: {
      id:        { type: 'integer', primary: true, autoIncrement: true },
      firstName: { type: 'string' },
      lastName:  { type: 'string' },
      age:       { type: 'integer' },
    },
  },
});

const connector = new SqliteConnector(':memory:', { schema });

class User extends Model({
  connector,
  tableName: 'users',
}) {
  get name() {
    return `${this.firstName} ${this.lastName}`;
  }
}

await connector.ensureSchema();  // creates every declared table that doesn't exist
const ada = await User.create({ firstName: 'Ada', lastName: 'Lovelace', age: 36 });
ada.name; // 'Ada Lovelace'

Models are TypeScript classes — add getters, methods, and computed properties just like normal. Swap SqliteConnector for any other connector (Postgres, MySQL, MongoDB, Redis, Supabase, MemoryConnector for tests, …) and every Model feature keeps working through the same Connector interface.

Materialise the schema with ensureSchema()

When the connector carries an attached schema, connector.ensureSchema() walks every table declared on it and creates any table that doesn't already exist. Calling it again is a no-op: existing tables are skipped, only new ones are created. It returns { created: string[], existing: string[] } so you can log the diff.

const connector = new SqliteConnector('./data/app.sqlite', { schema });
const { created, existing } = await connector.ensureSchema();
// app boot: creates missing tables; subsequent boots: returns { created: [], existing: [...] }

Implemented by SqliteConnector, MemoryConnector, and LocalStorageConnector today; other connectors fall back to the explicit createTable(name, builder) loop until they opt in.

Query with chainable, immutable scopes

// Each chain step returns a new query — the source class is untouched.
const adults = User.filterBy({ $gte: { age: 18 } });

await adults.count();                                                  // number of adults
await adults.orderBy({ key: 'age', dir: 'desc' }).first();             // oldest adult
await adults.orderBy({ age: 'desc' }).first();                         // same — conventional shape
await adults.filterBy({ $like: { lastName: 'Love%' } }).all();         // intersected filters

await User.filterBy({ $in: { age: [25, 30, 36] } })
          .orderBy({ lastName: 'asc' })
          .limitBy(10)
          .all();

await User.findOrNull(42);                                             // returns null on miss
await User.find(42);                                                   // throws NotFoundError on miss

Operators available on every connector: $eq · $gt · $gte · $lt · $lte · $in · $notIn · $null · $notNull · $like · $ilike · $between. Compose them with filterBy / orFilterBy / unfiltered / unscoped. Predeclare reusable filters via scopes on the Model({...}) definition.

Each column-style operator accepts both the nested ORM-style form filterBy({ col: { $in: [...] } }) and the top-level form filterBy({ $in: { col: [...] } }) — they produce the same query. Operators and equality keys can also be mixed in a single filter object: filterBy({ workspaceId: 1, $null: 'archivedAt' }) is composed as an AND of every predicate, no .filterBy(...).filterBy(...) chaining required.

orderBy accepts both the strict { key, dir } shape and the conventional { col: 'asc' | 'desc' } shape — connectors normalise either before building SQL, so orderBy({ createdAt: 'desc' }) and orderBy({ key: 'createdAt', dir: SortDirection.Desc }) produce the same query.

Use it from React

import { NextModelProvider, useModel } from '@next-model/react';

export function Root() {
  return <NextModelProvider><AdultUsers /></NextModelProvider>;
}

function AdultUsers() {
  const { data, isLoading } = useModel(User)
    .filterBy({ $gte: { age: 18 } })
    .orderBy({ key: 'lastName' })
    .watch({ keys: ['users:adults'] });

  if (isLoading) return <p>Loading…</p>;
  return (
    <ul>
      {data.map((u) => (
        <li key={u.id}>{u.name} · {u.age}</li>
      ))}
    </ul>
  );
}

useModel(User) returns the same chainable query builder you'd use server-side; .watch() subscribes the component so saves and deletes broadcast back into the live result set. For a one-shot read use .fetch() instead.

Packages

This repository is a pnpm workspace; each published package lives under packages/.

Package Purpose
@next-model/core Model factory, chainable query DSL, validators, callbacks, soft deletes, associations, in-memory connector.
@next-model/knex-connector SQL connector backed by Knex 3 (sqlite3 / Postgres / MySQL / MariaDB / Oracle / MSSQL).
@next-model/postgres-connector Native PostgreSQL connector using node-postgres directly — no Knex.
@next-model/sqlite-connector Native SQLite connector using better-sqlite3 directly — no Knex.
@next-model/mysql-connector Native MySQL connector using mysql2 directly — no Knex.
@next-model/mariadb-connector Native MariaDB connector. Extends mysql-connector and uses RETURNING *.
@next-model/redis-connector Redis connector — HASH per row + ZSET of ids per table.
@next-model/valkey-connector Valkey connector. Extends redis-connector (Valkey is wire-compatible with Redis).
@next-model/mongodb-connector Native MongoDB connector using the official mongodb driver.
@next-model/aurora-data-api-connector Connector for AWS Aurora Serverless v1 (RDS Data API).
@next-model/supabase-connector Supabase connector via @supabase/supabase-js (PostgREST). Server/browser differ only by the client/key you pass.
@next-model/local-storage-connector Browser localStorage connector. Inherits from MemoryConnector.
@next-model/migrations Connector-agnostic schema migration runner with optional dependency graph.
@next-model/express-rest-api Express 5 REST adapter — eight default CRUD actions with per-action auth + response-mapping hooks.
@next-model/graphql-api GraphQL schema generator — six default CRUD operations with per-operation auth + per-row response mapping.
@next-model/nextjs-api Next.js App Router adapter — { GET, POST, PATCH, DELETE } route-handler exports with the same auth + mapping hook surface.
@next-model/migrations-generator CLI (nm-generate-migration) + library for scaffolding timestamped migration files.
@next-model/zod Bridge zod schemas into toTypedColumns() (for defineSchema), init coercion, and validators — one schema, three consumers.
@next-model/typebox TypeBox variant of @next-model/zod.
@next-model/arktype arktype variant of @next-model/zod.

Demos

End-to-end runnable projects live under demos/, grouped by runtime. node/ is split into server/ (needs Node / a DB / a native dep) and client/ (also runs in a browser).

Demo Adapter / connector Infra
node/client/memory @next-model/core (MemoryConnector) none
node/client/local-storage @next-model/local-storage-connector none
node/server/sqlite @next-model/sqlite-connector none (in-memory db)
node/server/postgres @next-model/postgres-connector docker compose up -d (postgres:17)
node/server/mysql @next-model/mysql-connector docker compose up -d (mysql:8)
node/server/mariadb @next-model/mariadb-connector docker compose up -d (mariadb:11)
node/server/redis @next-model/redis-connector docker compose up -d (redis:7)
node/server/valkey @next-model/valkey-connector docker compose up -d (valkey:8)
node/server/mongodb @next-model/mongodb-connector docker compose up -d (mongo:7)
node/server/knex @next-model/knex-connector sqlite: none; `docker compose --profile pg
node/server/aurora-data-api @next-model/aurora-data-api-connector via MockDataApiClient none
node/server/supabase @next-model/supabase-connector via MockSupabaseClient none
node/server/express-rest-api Express 5 + @next-model/express-rest-api + @next-model/sqlite-connector none
node/server/graphql-api graphql-http + @next-model/graphql-api + @next-model/sqlite-connector none
react/todo React 19 + Vite + @next-model/local-storage-connector none (browser)
nextjs/todo Next.js 15 App Router + @next-model/sqlite-connector — server components + server actions none
nextjs/api Next.js 15 + @next-model/nextjs-api — REST route handlers none

The demos/README.md covers the running convention in detail.

Schema mutations

Migrations need more than createTable / dropTable. Each connector implements alterTable(spec) so existing tables can grow new columns, indexes, foreign keys, and check constraints in place — no destructive recreate.

import { defineAlter } from '@next-model/core';

await connector.alterTable(defineAlter('users', (a) => {
  a.addColumn('lastSeenAt', 'datetime', { null: true });
  a.renameColumn('first_name', 'firstName');
  a.changeColumn('age', 'integer', { null: false });
  a.addIndex(['firstName', 'lastName'], { name: 'idx_users_full_name' });
  a.addForeignKey('teams', { onDelete: 'cascade' });
  a.addCheckConstraint('age >= 0', { name: 'chk_age_non_negative' });
}));

The full op set: addColumn / removeColumn / renameColumn / changeColumn, addIndex / removeIndex / renameIndex, addForeignKey / removeForeignKey, addCheckConstraint / removeCheckConstraint, plus addReference / removeReference sugar (column + index + optional FK in one call). Constraints get stable default names (fk_<table>_<refTable>, idx_<table>_<columns>) so the matching remove* ops can target them without bookkeeping.

Connector Behaviour
KnexConnector, PostgresConnector, MysqlConnector, MariaDbConnector, DataApiConnector All ops translate to native ALTER TABLE / CREATE INDEX DDL.
SqliteConnector addColumn / removeColumn / renameColumn use native ALTER TABLE (SQLite ≥ 3.35 / 3.25). changeColumn, addForeignKey / removeForeignKey, and addCheckConstraint / removeCheckConstraint use the standard "create new table + copy + drop + rename" recreate dance internally.
MemoryConnector / LocalStorageConnector Column rename / remove rewrites the in-memory rows; addColumn back-fills the default. Index ops are no-ops (no indexing). Foreign keys + check constraints throw UnsupportedOperationError since they cannot be enforced.
RedisConnector / ValkeyConnector removeColumn / renameColumn rewrite hash fields. Other ops are no-ops or throw UnsupportedOperationError (relational constraints aren't enforced).
MongoDbConnector removeColumn / renameColumn use $unset / $rename. addIndex / removeIndex map to createIndex / dropIndex. Foreign keys + check constraints throw UnsupportedOperationError.

The same spec passed through SchemaCollector.alterTable(...) is mirrored into the schema snapshot, so collector.writeSchema(path) continues to round-trip cleanly through createTable + a series of mutations.

Reversible migrations (Rails-style change)

@next-model/migrations accepts both styles of migration. Define a single change(connector) block and the runner records every schema mutation, then replays the inverse on down() automatically:

import type { ChangeMigration } from '@next-model/migrations';

const addEmailToUsers: ChangeMigration = {
  version: '20260101120000',
  name: 'add_email_to_users',
  async change(connector) {
    await connector.alterTable(defineAlter('users', (a) => {
      a.addColumn('email', 'string', { null: false });
      a.addIndex('email', { unique: true, name: 'idx_users_email' });
    }));
  },
};

await migrator.migrate([addEmailToUsers]);
await migrator.rollback([addEmailToUsers]);   // auto-derives removeIndex + removeColumn

Inversion table:

Recorded op Inverse
createTable(name, ...) dropTable(name)
addColumn(name, type, opts) removeColumn(name)
renameColumn(from, to) renameColumn(to, from)
changeColumn(name, type, opts, previous) changeColumn(...) back to previous
addIndex(cols, { name? }) removeIndex(name ?? cols)
renameIndex(from, to) renameIndex(to, from)
addForeignKey(toTable, opts) removeForeignKey(opts.name ?? "fk_<from>_<to>")
addCheckConstraint(expr, { name }) removeCheckConstraint(name)

Operations that lose information when applied (dropTable, removeColumn, removeIndex, removeForeignKey, removeCheckConstraint, changeColumn without a previous snapshot, addCheckConstraint without an explicit name) raise IrreversibleMigrationError on down(). Write explicit up() / down() for those — both styles can coexist in the same migration list. Inside a change() block you can only call schema-mutating methods (createTable / dropTable / alterTable); use up() / down() for any data-touching work.

Agent skills

This repo ships agent skills under skills/ — one SKILL.md per package plus an overview that helps an AI agent pick the right adapter for a use case. They follow the open agent skills spec and are installable with the skills CLI for Claude Code, Cursor, OpenCode, Codex, and 50+ other coding agents.

# install every skill from this repo (Claude Code, Cursor, OpenCode, …)
npx skills add tamino-martinius/node-next-model

# list before installing
npx skills add tamino-martinius/node-next-model --list

# pick just the ones you need
npx skills add tamino-martinius/node-next-model \
  --skill next-model \
  --skill next-model-core \
  --skill next-model-postgres-connector

# install globally (~/.claude/skills/, ~/.cursor/skills/, …)
npx skills add tamino-martinius/node-next-model -g

# target one agent
npx skills add tamino-martinius/node-next-model -a claude-code -y

Available skills (next-model is the index; the rest map 1:1 to the packages above):

next-model · next-model-core · next-model-knex-connector · next-model-postgres-connector · next-model-sqlite-connector · next-model-mysql-connector · next-model-mariadb-connector · next-model-redis-connector · next-model-valkey-connector · next-model-mongodb-connector · next-model-aurora-data-api-connector · next-model-supabase-connector · next-model-local-storage-connector · next-model-migrations · next-model-migrations-generator · next-model-express-rest-api · next-model-graphql-api · next-model-nextjs-api · next-model-zod · next-model-typebox · next-model-arktype · next-model-react

Supported runtime

  • Node.js ≥ 22 (required by every package).
  • Pure ESM (type: "module", exports field). Built with TypeScript 6 / module: NodeNext.
  • Browser support is limited to @next-model/local-storage-connector.

Development

pnpm install
pnpm -r build
pnpm typecheck
pnpm -r coverage

CI matrix: Node 22 + 24 on every push, plus a real-database leg that boots Postgres 17 and MySQL 8 service containers and runs the knex-connector spec against each.

Contributing

Open an issue or PR on github.com/tamino-martinius/node-next-model. Each package keeps a rolling vNext section in its HISTORY.md; please append a one-line entry under the appropriate subheading when you ship a feature, fix or tooling change.

License

MIT, copyright 2017–2026 Tamino Martinius.

About

Rails like models using ES6.

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages