Skip to content

ichiriac/normal

Repository files navigation

Normal ORM

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 StatuslicenseNPM 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
classMyModel{statictable='my_table';staticfields={/* ... */};}

Database engines

NormalJS supports these SQL databases via Knex:

PostgreSQLMariaDBMySQLCockroachDBSQLiteOracleMicrosoft SQL ServerAmazon Redshift

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.tsimport{Connection,Repository}from'normaljs';// 1) Create a connection (SQLite in-memory shown; Postgres or MySQL also supported)constdb=newConnection({client: 'sqlite3',connection: {filename: ':memory:'}});// 2) Define modelsclassUsers{statictable='users';staticfields={id: 'primary'asconst,firstname: {type: 'string',required: true},lastname: {type: 'string',required: true},email: {type: 'string',unique: true,required: true},created_at: {type: 'datetime',default: ()=>newDate()},updated_at: {type: 'datetime',default: ()=>newDate()},};getname(){return`${this.firstname}${this.lastname}`;}}// Override readonly 'name' property for NormalJSObject.defineProperty(Users,'name',{value: 'Users',configurable: true});classPosts{statictable='posts';staticfields={id: 'primary'asconst,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 & syncconstrepo=newRepository(db);repo.register({ Users, Posts });awaitrepo.sync({force: true});// 4) Use itconstu=awaitrepo.get('Users').create({firstname: 'Ada',lastname: 'Lovelace',email: '[email protected]'});constp=awaitrepo.get('Posts').create({title: 'Hello',content: 'World',author_id: u.id});console.log(u.name);// "Ada Lovelace"

JavaScript (CommonJS)

// index.jsconst{ Connection, Repository }=require('normaljs');// 1) Create a connection (SQLite in-memory shown; Postgres or MySQL also supported)constdb=newConnection({client: 'sqlite3',connection: {filename: ':memory:'}});// 2) Define modelsclassUsers{statictable='users';staticfields={id: 'primary',firstname: {type: 'string',required: true},lastname: {type: 'string',required: true},email: {type: 'string',unique: true,required: true},created_at: {type: 'datetime',default: ()=>newDate()},updated_at: {type: 'datetime',default: ()=>newDate()},};getname(){return`${this.firstname}${this.lastname}`;}}// Override readonly 'name' property for NormalJSObject.defineProperty(Users,'name',{value: 'Users',configurable: true});classPosts{statictable='posts';staticfields={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 & syncconstrepo=newRepository(db);repo.register({ Users, Posts });awaitrepo.sync({force: true});// 4) Use itconstu=awaitrepo.get('Users').create({firstname: 'Ada',lastname: 'Lovelace',email: '[email protected]'});constp=awaitrepo.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 + behaviorclassUsers{static_name='Users';staticfields={id: 'primary'};}// Extend Users with fields and static/instance APIsclassUsersExt{static_name='Users';staticfields={email: 'string'};staticbyEmail(email){returnthis.where({ email }).first();// simple, model-scoped static API}getdomain(){returnthis.email?.split('@')[1]||null;// instance API on active record}}// Inheritance: child model shares base structure and behaviorclassPayment{static_name='Payment';staticfields={id: 'primary',amount: 'float'};}classCardPayment{static_name='CardPayment';staticinherits='Payment';staticfields={pan: 'string'};}repo.register(Users);repo.register(UsersExt);// extension mergedrepo.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.
  • 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' })).
  • 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.
  • 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/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)

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •