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

normaljs

v1.0.6

Published

NormalJS : Node Object Relational Mapper Abstraction Layer

Readme

NormalJS

The simple, straightforward, and most advanced Node.js ORM — without the bloat.

Build data-rich apps with clean models, powerful relations, and first-class DX. NormalJS blends a tiny API with serious capabilities: schema sync, relations (1:n, n:m), transactions, model extension, and active-record-style instances. All in plain JavaScript.

Why NormalJS

  • Simple: minimal surface area. Define models with a static fields object and go.
  • Transaction-first: fully isolated repos inside transactions without leaking state.
  • Advanced caching: centralized in-memory cache shared across child processes, with UDP-based clustering for peer invalidation.
  • Powerful: relations (1:n, n:m), transactions, model mixins/extension, inheritance with discriminators, relation proxies.
  • Productive: active records you can call methods on; lazy, ID-first reads that auto-hydrate fields; request-level caching with invalidation markers.
  • Portable: works with Postgres and SQLite. Uses Knex under the hood.

What makes NormalJS different for complex domains

  • Extensible field system: add custom field types that control serialization, JSON output, schema, and lifecycle hooks.
  • Model extension and overwrite: register multiple classes with the same static _name to merge fields and attach static/instance behavior over time.
  • Inheritance with discriminators: share a base model schema and behavior; allocate correct child records automatically.
  • Schema sync (base synchronization): generate and evolve tables from model fields with migration-safe helpers.
  • Clear split of responsibilities: simple static APIs for model-level operations, and instance methods/getters for active records.

Install

Coverage Status license NPM version

npm install normaljs pg -y

TypeScript Support

NormalJS is written in TypeScript and includes full type definitions. TypeScript users get:

  • Type-safe model definitions with autocomplete for fields and relations
  • Type inference for query results and active records
  • Generic types for models and records
  • Declaration files (.d.ts) for all public APIs
npm install normaljs pg
# TypeScript types are included - no @types package needed
class MyModel {
  static table = 'my_table';
  static fields = {
    /* ... */
  };
}

Database engines

NormalJS supports these SQL databases via Knex:

Note: You only need the driver for the database(s) you use (e.g., pg for PostgreSQL, sqlite3 or better-sqlite3 for SQLite).

60‑second Quickstart

TypeScript

// index.ts
import { Connection, Repository } from 'normaljs';

// 1) Create a connection (SQLite in-memory shown; Postgres or MySQL also supported)
const db = new Connection({ client: 'sqlite3', connection: { filename: ':memory:' } });

// 2) Define models
class Users {
  static table = 'users';
  static fields = {
    id: 'primary' as const,
    firstname: { type: 'string', required: true },
    lastname: { type: 'string', required: true },
    email: { type: 'string', unique: true, required: true },
    created_at: { type: 'datetime', default: () => new Date() },
    updated_at: { type: 'datetime', default: () => new Date() },
  };

  get name() {
    return `${this.firstname} ${this.lastname}`;
  }
}
// Override readonly 'name' property for NormalJS
Object.defineProperty(Users, 'name', { value: 'Users', configurable: true });

class Posts {
  static table = 'posts';
  static fields = {
    id: 'primary' as const,
    title: { type: 'string', required: true },
    content: { type: 'string', required: true },
    author_id: { type: 'many-to-one', required: true, model: 'Users' },
  };
}
Object.defineProperty(Posts, 'name', { value: 'Posts', configurable: true });

// 3) Register & sync
const repo = new Repository(db);
repo.register({ Users, Posts });
await repo.sync({ force: true });

// 4) Use it
const u = await repo
  .get('Users')
  .create({ firstname: 'Ada', lastname: 'Lovelace', email: '[email protected]' });
const p = await repo.get('Posts').create({ title: 'Hello', content: 'World', author_id: u.id });
console.log(u.name); // "Ada Lovelace"

JavaScript (CommonJS)

// index.js
const { Connection, Repository } = require('normaljs');

// 1) Create a connection (SQLite in-memory shown; Postgres or MySQL also supported)
const db = new Connection({ client: 'sqlite3', connection: { filename: ':memory:' } });

// 2) Define models
class Users {
  static table = 'users';
  static fields = {
    id: 'primary',
    firstname: { type: 'string', required: true },
    lastname: { type: 'string', required: true },
    email: { type: 'string', unique: true, required: true },
    created_at: { type: 'datetime', default: () => new Date() },
    updated_at: { type: 'datetime', default: () => new Date() },
  };

  get name() {
    return `${this.firstname} ${this.lastname}`;
  }
}
// Override readonly 'name' property for NormalJS
Object.defineProperty(Users, 'name', { value: 'Users', configurable: true });

class Posts {
  static table = 'posts';
  static fields = {
    id: 'primary',
    title: { type: 'string', required: true },
    content: { type: 'string', required: true },
    author_id: { type: 'many-to-one', required: true, model: 'Users' },
  };
}
Object.defineProperty(Posts, 'name', { value: 'Posts', configurable: true });

// 3) Register & sync
const repo = new Repository(db);
repo.register({ Users, Posts });
await repo.sync({ force: true });

// 4) Use it
const u = await repo
  .get('Users')
  .create({ firstname: 'Ada', lastname: 'Lovelace', email: '[email protected]' });
const p = await repo.get('Posts').create({ title: 'Hello', content: 'World', author_id: u.id });
console.log(u.name); // "Ada Lovelace"

Modeling big domains, simply

Static methods live on models; instance methods live on records. You can extend models incrementally or inherit from a base model.

// Extension: register the same model name again to add fields + behavior
class Users {
  static _name = 'Users';
  static fields = { id: 'primary' };
}

// Extend Users with fields and static/instance APIs
class UsersExt {
  static _name = 'Users';
  static fields = { email: 'string' };
  static byEmail(email) {
    return this.where({ email }).first(); // simple, model-scoped static API
  }
  get domain() {
    return this.email?.split('@')[1] || null; // instance API on active record
  }
}

// Inheritance: child model shares base structure and behavior
class Payment {
  static _name = 'Payment';
  static fields = { id: 'primary', amount: 'float' };
}
class CardPayment {
  static _name = 'CardPayment';
  static inherits = 'Payment';
  static fields = { pan: 'string' };
}

repo.register(Users);
repo.register(UsersExt); // extension merged
repo.register({ Payment, CardPayment });

Features at a glance

  • Models
    • Simple class with static _name, static table, static fields.
    • Extension system: register multiple times with same static _name to add/override fields and behavior.
    • Inheritance with discriminators for polymorphic models.
    • NEW: Model scopes for reusable query filters and eager loading patterns.
  • Fields
    • Built-ins: primary, integer/float, string/text, boolean, date/datetime, enum, json, reference.
    • Constraints: default, required, unique, index.
    • Custom fields: implement serialization, JSON, schema, and lifecycle hooks.
    • NEW: Model-level indexes for composite, unique, and partial indexes (e.g., static indexes = { idx_name: { fields: ['email', 'company'], unique: true } }).
  • Relations
    • 1:n via one-to-many (e.g., comments: { type: 'one-to-many', foreign: 'Comments.post_id' }).
    • n:m via paired many-to-many (auto-join table).
    • Relation proxies on instances: add, remove, load.
    • NEW: Automatic join generation for relational filters (e.g., where({ 'author.organization.name': 'ACME' })).
  • Scopes
    • Define reusable query patterns at the model level.
    • Support for defaultScope applied to all queries unless bypassed.
    • Parameterized scopes via functions.
    • Composable scopes with deterministic merging.
    • NEW: See docs/scopes.md for comprehensive guide.
  • Transactions
    • repo.transaction(async (tx) => { /* ... */ }) gives an isolated tx-bound repository.
    • Post-commit cache flush of changed records.
  • Active records
    • Rows are wrapped into instances; instance methods and getters work naturally.
    • Default reads select only id (fast), with lazy hydration from cache/DB.
  • Cache and discovery
    • Request-level caching via .cache(ttl) and entry cache per Model:ID.
    • Per-model invalidation markers ($Model) to evict request caches without dropping entry caches.
    • Centralized in-memory cache across processes with UDP-based clustering.
    • Discovery protocol auto-syncs peer list for invalidations.
  • Repository Context
    • NEW: Repository-wide context accessible in transactions, models, and records.
    • get_context(key, defaultValue) retrieves context values.
    • set_context(key, value) stores context values.
    • Transactions inherit parent context but modifications are isolated.
    • Use cases: multi-tenancy, user context, feature flags, request metadata.
  • Schema sync
    • Create/update tables from model fields with repo.sync().
    • Migration-safe helpers for column replacement and index updates.

See full field reference in docs/fields.md.

More docs

  • docs/models.md — Model definitions, inheritance, and extension system.
  • docs/fields.md — Built-in field types and options.
  • docs/scopes.mdNEW: Model scopes for reusable query filters.
  • docs/requests.md — Request API, criteria DSL, and request-level caching.
  • docs/relational-filters.mdNEW: Automatic joins for relational field filters.
  • docs/cache.md — Cache architecture, connection options, discovery, and model cache options.
  • docs/custom-fields.md — In-depth custom fields with hooks and a file-storage example.
  • docs/adoption-sequelize.md — Step-by-step migration guide from Sequelize.

Demo

Explore demo/ for a working blog schema (Users, Posts, Tags, Comments) and a CRM and Stocks example.

License

The MIT License (MIT)