NodeORM is a modern, performant, and elegant Object-Relational Mapper (ORM) for Node.js, heavily inspired by Laravel's Eloquent ORM.
It aims to provide the familiar, expressive, and powerful database interaction experience of Eloquent within the Node.js ecosystem, while prioritizing:
- 🚀 Blazing Performance: Built from the ground up with modern JavaScript (ES6+ Modules, Proxies, async/await) for maximum efficiency and minimal overhead.
- ✨ Eloquent Parity: Implements the vast majority of Eloquent's features and API, making it instantly familiar to Laravel developers and easy to learn for others.
- Simplicity & DX: Offers an intuitive, fluent API for common database tasks, reducing boilerplate and enhancing developer productivity.
- 0 Minimal Dependencies: Zero core dependencies. Relies only on specific database driver packages (
pg
,mysql2
,@neondatabase/serverless
,sqlite3
), keeping yournode_modules
lean. - đź”§ Extensibility: Easily extendable with custom database drivers.
- Reliability: Strong typing hints via JSDoc, robust error handling, and a focus on maintainable code.
NodeORM strives to be a top-tier ORM choice for Node.js developers seeking performance, elegance, and a familiar API.
- Why Choose NodeORM?
- Features
- Installation
- Quick Start
- Core Concepts
- Basic Usage
- Query Builder
- Relationships
- Attributes
- Serialization
- Events & Hooks
- Scopes
- Transactions
- Raw Queries
- Schema Builder
- Migrations (Basic Runner)
- Performance Considerations
- Extensibility (Custom Drivers)
- Contributing
- License
While the Node.js ecosystem has several mature ORMs (TypeORM, Sequelize, Prisma), NodeORM aims to carve its niche by offering:
- The Eloquent Experience: If you love Laravel's database layer, NodeORM provides the most extensive and faithful implementation of its API and features in Node.js. This significantly reduces the learning curve for Laravel developers transitioning to Node.js or using it for APIs.
- Performance Focus: Built with modern ES6+ features and minimal core abstractions. Avoiding heavy layers of compatibility code or complex type transformations aims for lower overhead compared to some traditional ORMs. Proxies are used intelligently for dynamic features without sacrificing core path speed. Note: Rigorous independent benchmarking is ongoing and encouraged.
- Simplicity & Modern JavaScript: Leverages native Promises,
async
/await
, and ES Modules. No need for separate CLI tools for basic operations (though migrations have a basic runner). The API focuses on clarity and conciseness. - Zero Core Dependencies: Reduces potential conflicts, security vulnerabilities, and bundle size. You only install the database driver(s) you need.
- Extensibility: Designed with driver extension in mind from the start.
NodeORM is ideal for developers who value the productivity and elegance of Eloquent and seek a performant, modern ORM for their Node.js projects.
- Eloquent-like API: Models, Query Builder, Relationships, Scopes, Events, etc.
- Supported Databases: PostgreSQL (via
pg
or@neondatabase/serverless
), MySQL/MariaDB (viamysql2
), SQLite (viasqlite3
). - Model Conventions: Automatic table/primary key naming (overridable).
- CRUD Operations:
find
,findOrFail
,first
,create
,update
,save
,delete
,destroy
,firstOrCreate
,updateOrCreate
, etc. - Fluent Query Builder: Chainable methods for complex SQL generation (
where
,orWhere
,whereIn
,join
,groupBy
,orderBy
, aggregates, etc.). - Relationships:
hasOne
,hasMany
,belongsTo
,belongsToMany
(with pivot table customization). - Eager Loading: Prevent N+1 query problems using
with()
(simple, nested, constrained). - Lazy Loading: Relationships loaded on demand.
- Accessors & Mutators: Transform attribute values on get/set (via Proxies).
- Attribute Casting: Automatic type casting (int, float, bool, date, datetime, json, array, object).
- Serialization:
toJSON
/toArray
methods withhidden
,visible
, andappends
support. - Events/Hooks: Lifecycle hooks (
creating
,created
,updating
,updated
,saving
,saved
,deleting
,deleted
,restoring
,restored
). - Scopes: Reusable query constraints (global and local).
- Soft Deletes:
deleted_at
timestamp support with relevant query scopes. - Transactions: Simple promise-based transaction management.
- Raw Queries: Execute raw SQL when needed.
- Schema Builder: Fluent API (
Blueprint
) for creating and modifying tables. - Basic Migration Runner: Programmatic runner for managing database schema changes.
- Modern JS: ES Modules,
async
/await
, Proxies. - Minimal Dependencies: Only requires database drivers.
npm install nodeorm mysql2 # Example for MySQL
Install the appropriate driver for your database:
- PostgreSQL:
npm install pg
ornpm install @neondatabase/serverless
(Neon preferred) - MySQL / MariaDB:
npm install mysql2
- SQLite:
npm install sqlite3
import { Model, Connection } from 'nodeorm';
// Import a driver (NodeORM attempts to auto-detect and load)
// import { MySQLDriver } from 'nodeorm/drivers'; // Optional direct import
// 1. Define Models
class User extends Model {
// Optional: Override table name (defaults to 'users')
// static table = 'my_users';
// Optional: Define fillable attributes for mass assignment
static fillable = ['name', 'email', 'login_count'];
// Optional: Define relationships
get posts() {
return this.hasMany(Post, 'author_id');
}
}
class Post extends Model {
static fillable = ['author_id', 'title', 'content'];
get author() {
return this.belongsTo(User, 'author_id');
}
}
// 2. Establish Connection (using environment variables or connection string/object)
let connection;
try {
// Option A: Auto-detect from environment (DATABASE_URL, DB_HOST etc.)
// connection = await Connection.make();
// Option B: Connection String
connection = await Connection.make('mysql://root:@localhost:3306/my_database');
// connection = await Connection.make('postgresql://user:pass@host:port/db');
// connection = await Connection.make('sqlite:/path/to/database.sqlite');
// Option C: Config Object
// connection = await Connection.make({
// driver: 'mysql',
// host: 'localhost',
// port: 3306,
// user: 'root',
// password: '',
// database: 'my_database'
// });
// 3. Initialize Models with Connection (fetches schema, associates connection)
await connection.init(User, Post);
console.log('Connection successful!');
} catch (error) {
console.error('Connection failed:', error);
process.exit(1);
}
// 4. Use Models!
async function main() {
try {
// Create
const newUser = await User.create({
name: 'John Doe',
email: 'john.doe@example.com',
login_count: 1
});
console.log('Created User:', newUser.id, newUser.name);
// Find
const foundUser = await User.find(newUser.id);
if (foundUser) {
console.log('Found User:', foundUser.name);
// Update via instance save()
foundUser.login_count = foundUser.login_count + 1;
await foundUser.save();
console.log('Updated login count (save):', foundUser.login_count);
// Update via instance update()
await foundUser.update({ name: 'Johnathan Doe' });
console.log('Updated name (update):', foundUser.name);
}
// Query Builder
const activeUsers = await User.where('login_count', '>', 0)
.orderBy('name', 'asc')
.limit(10)
.get();
console.log('Active users:', activeUsers.map(u => u.name));
// Create related model
if(foundUser) {
const newPost = await foundUser.posts().create({
title: 'My First Post',
content: 'This is NodeORM!'
});
console.log('Created Post:', newPost.id, newPost.title);
// Query relationship
const userWithPosts = await User.with('posts').find(foundUser.id);
console.log(`Posts for ${userWithPosts.name}:`, userWithPosts.posts.map(p => p.title));
}
// Delete
if (foundUser) {
await foundUser.delete();
console.log('User deleted.');
}
} catch (error) {
console.error("An error occurred:", error);
} finally {
// Close the connection pool when done
await connection.drop();
}
}
main();
NodeORM uses a Connection
class to manage database interactions. Connections can be configured via:
- Environment Variables: Mimicking Laravel, it checks for
DATABASE_URL
/DB_URL
first, then falls back toDB_CONNECTION
,DB_HOST
,DB_PORT
,DB_DATABASE
,DB_USERNAME
,DB_PASSWORD
. - Connection String: Standard URL format (
driver://user:pass@host:port/database?options
). - Configuration Object:
{ driver: 'mysql', host: '...', ... }
. - Driver Instance: Pass a pre-configured driver instance (less common).
import { Connection } from 'nodeorm';
// From Environment (implicitly uses process.env)
const defaultConn = await Connection.make();
// From Connection String
const pgConn = await Connection.make('postgresql://user:pass@host:5432/db?sslmode=require');
const sqliteConn = await Connection.make('/path/to/db.sqlite'); // Or 'sqlite::memory:'
// From Config Object
const mysqlConn = await Connection.make({
driver: 'mysql', // or 'mysql2'
host: '127.0.0.1',
user: 'root',
password: '',
database: 'test_db',
charset: 'utf8mb4'
}, 'mysql_test'); // Optional connection name
// Manage Connections (usually handled automatically by 'default')
import { ConnectionManager } from 'nodeorm';
const mainDb = ConnectionManager.getConnection(); // Gets 'default'
const specificDb = ConnectionManager.getConnection('mysql_test');
Models extend the NodeORM.Model
class and represent a single database table.
import { Model } from 'nodeorm';
class Product extends Model {
// Table name (defaults to 'products')
static table = 'store_products';
// Primary key (defaults to 'id')
static primaryKey = 'product_sku';
// Key type (defaults to 'integer')
static keyType = 'string';
// Disable auto-incrementing if PK is not AI
static incrementing = false;
// Disable timestamps (defaults to true)
static timestamps = false;
// Enable soft deletes (defaults to false)
static softDeletes = true;
// Mass assignable attributes
static fillable = ['name', 'sku', 'price', 'description'];
// Or guarded attributes (takes precedence)
// static guarded = ['id', 'internal_ref']; // Defaults to ['*']
// Attribute casting
static casts = {
price: 'float',
is_active: 'boolean',
options: 'json', // Converts DB JSON string to/from JS object/array
launched_at: 'date' // Converts DB date/datetime to JS Date object
};
// Hidden from serialization
static hidden = ['internal_ref'];
// Appended accessors for serialization
static appends = ['nameWithSku'];
// Accessor (Getter)
get nameWithSku() {
return `${this.name} (${this.sku})`; // Access attributes directly
}
// Mutator (Setter)
set name(value) {
// Directly modify internal attributes
this._attributes.name = value.trim();
}
// Relationships (defined as getters returning Relation instances)
get category() {
return this.belongsTo(Category); // Assumes Category model exists
}
get reviews() {
return this.hasMany(Review); // Assumes Review model exists
}
}
NodeORM follows conventions similar to Eloquent, but allows overrides:
- Table Name: Plural, snake_case version of the class name (e.g.,
User
->users
,ProductReview
->product_reviews
). Override withstatic table
. - Primary Key:
id
. Override withstatic primaryKey
. Assumed to be auto-incrementing integer unlessstatic incrementing = false
orstatic keyType = 'string'
. - Timestamps:
created_at
andupdated_at
. AssumedDATETIME
orTIMESTAMP
. Disable withstatic timestamps = false
. Customize names withstatic createdAtColumn
/static updatedAtColumn
. - Soft Deletes:
deleted_at
. Disable withstatic softDeletes = false
. Customize name withstatic deletedAtColumn
. - Foreign Keys:
{singular_table_name}_{primaryKey}
(e.g.,user_id
,post_id
). Inferred automatically in relationship definitions but can be overridden.
// Find by primary key
const user = await User.find(1);
// Find or fail
try {
const userOrFail = await User.findOrFail(999);
} catch (e) {
if (e instanceof errors.ModelNotFoundError) {
console.log(e.message); // "No User found for ID: 999"
}
}
// Find multiple by primary keys
const users = await User.findMany([1, 2, 5]);
// Get the first record matching constraints
const admin = await User.where('is_admin', true).first();
// Get the first record or throw
const firstUser = await User.oldest().firstOrFail();
// Retrieve all records (use with caution on large tables!)
const allUsers = await User.all(); // Equivalent to User.query().get()
// Chunking results for large tables
await User.query().orderBy('id').chunk(100, (usersChunk) => {
console.log(`Processing chunk of ${usersChunk.length} users...`);
// Process the chunk
});
// Find using attribute values
const specificUser = await User.where('email', 'jane.doe@example.com').first();
// Find or create
const userOrCreate = await User.firstOrCreate(
{ email: 'new@example.com' }, // Attributes to find by
{ name: 'New User', login_count: 0 } // Attributes to use if creating
);
// Find or create new instance (not saved)
const userOrNew = await User.firstOrNew(
{ email: 'maybe@example.com' },
{ name: 'Maybe New' }
);
if (!userOrNew.exists()) {
// userOrNew.login_count = 1; // Set additional attributes
// await userOrNew.save();
}
const totalUsers = await User.count();
const maxLogins = await User.max('login_count');
const averageLogins = await User.where('is_active', true).avg('login_count'); // Aggregates respect constraints
const totalRevenue = await Order.sum('amount');
// Check existence
const hasAdmins = await User.where('is_admin', true).exists();
const noPending = await Order.where('status', 'pending').doesntExist();
// --- Create (Mass Assignment) ---
const user = await User.create({
name: 'New User',
email: 'create@example.com',
// 'password' might be guarded by default, needs to be in fillable
});
// --- Create (New Instance + Save) ---
const product = new Product(); // Uses model constructor & proxy
product.name = 'Awesome Gadget';
product.sku = 'GADGET-001';
product.price = 99.99;
product.is_active = true;
await product.save(); // Performs INSERT
// --- Update (Instance save()) ---
const userToUpdate = await User.find(1);
if (userToUpdate) {
userToUpdate.login_count += 1;
await userToUpdate.save(); // Performs UPDATE
}
// --- Update (Instance update()) ---
const productToUpdate = await Product.find('GADGET-001');
if (productToUpdate) {
await productToUpdate.update({ price: 89.99, is_active: false }); // Performs UPDATE
}
// --- Mass Update ---
const affectedRows = await User.where('login_count', 0).update({ is_active: false });
// --- Upsert (Insert or Update) ---
// Atomicity depends on database support (MySQL, Postgres, SQLite recent versions)
await User.upsert(
[
{ email: 'upsert1@example.com', name: 'Upsert 1', login_count: 1 },
{ email: 'upsert2@example.com', name: 'Upsert 2', login_count: 5 }
],
['email'], // Unique key(s) to check for conflict
['name', 'login_count'] // Columns to update on conflict
);
// --- Update or Create ---
// Finds by first arg, updates or creates with merged args
const updatedOrCreated = await User.updateOrCreate(
{ email: 'unique@example.com' }, // Find by email
{ name: 'Updated Or Created', login_count: 10 } // Set these values
);
// --- Increment / Decrement ---
await User.where('id', 1).increment('login_count'); // Increment by 1
await User.where('id', 1).increment('login_count', 5); // Increment by 5
await User.where('id', 1).decrement('balance', 10.50, { last_activity: new Date() }); // Decrement with extra updates
// Delete by instance
const user = await User.find(1);
if (user) {
await user.delete();
}
// Delete by primary key
const affected = await User.destroy(1);
// Delete multiple by primary key
const affectedMultiple = await User.destroy([1, 2, 3]);
// Delete by query constraints
const affectedQuery = await User.where('login_count', '<', 5).delete();
// Truncate table (Deletes ALL rows - Use with extreme caution!)
// await User.truncate();
Enable by setting static softDeletes = true;
on your model.
// Enable in model:
// class Post extends Model {
// static softDeletes = true;
// }
// Soft delete an instance
const post = await Post.find(1);
await post.delete(); // Sets 'deleted_at' timestamp
// Querying (Excludes soft-deleted by default)
const activePosts = await Post.all(); // Only returns posts where deleted_at IS NULL
// Include soft-deleted results
const allPostsIncludingTrashed = await Post.withTrashed().get();
// Query ONLY soft-deleted results
const trashedPosts = await Post.onlyTrashed().get();
// Restore a soft-deleted model
const trashedPost = await Post.onlyTrashed().find(1);
if (trashedPost) {
await trashedPost.restore(); // Sets deleted_at to NULL
}
// Check if an instance is trashed
if (post.isTrashed()) { /* ... */ }
// Permanently delete (even if soft deletes are enabled)
await post.forceDelete();
// Or via query
await Post.where('author_id', 5).forceDelete();
NodeORM provides a powerful and fluent query builder. Start a query using Model.query()
or the static forwarder methods.
// Basic Where
const users = await User.where('login_count', '>', 10).get();
const user = await User.where('email', 'test@example.com').first();
const admins = await User.where({ is_admin: true, status: 'active' }).get(); // Object syntax
// Or Where
const results = await Product.where('price', '<', 10).orWhere('on_sale', true).get();
// Where In / Not In / Between / Null
const usersByIds = await User.whereIn('id', [1, 5, 10]).get();
const productsNotInCategory = await Product.whereNotIn('category_id', [3, 4]).get();
const recentOrders = await Order.whereBetween('created_at', [startDate, endDate]).get();
const incompleteProfiles = await User.whereNull('profile_bio').get();
// Ordering, Limit, Offset
const latestUsers = await User.orderBy('created_at', 'desc').limit(5).get();
const secondPage = await User.offset(10).limit(10).get();
const usersPaged = await User.forPage(3, 15).get(); // Page 3, 15 items per page
// Joins
const userOrders = await User.select('users.name', 'orders.amount')
.join('orders', 'users.id', '=', 'orders.user_id')
.where('orders.status', 'completed')
.get();
const postsWithAuthors = await Post.leftJoin('users', 'posts.author_id', '=', 'users.id')
.select('posts.*', 'users.name as author_name')
.get();
// Grouping & Having
const categoryCounts = await Product.select('category_id', raw('COUNT(*) as count'))
.groupBy('category_id')
.having('count', '>', 5)
.get();
// Raw Expressions
const activeCount = await User.where('status', 'active')
.whereRaw('MONTH(created_at) = ?', [5]) // Use driver placeholder
.count();
const users = await User.select('name', raw('IF(login_count > 100, ?, ?) as frequent_flyer', ['Yes', 'No'])).get();
// Pluck & Value
const userEmails = await User.pluck('email'); // ['a@b.com', 'c@d.com', ...]
const namesById = await User.pluck('name', 'id'); // { 1: 'John', 2: 'Jane', ... }
const adminEmail = await User.where('is_admin', true).value('email'); // 'admin@example.com' or null
Define relationships as getter methods on your models.
class User extends Model {
// One-to-One: User has one Profile
get profile() { return this.hasOne(Profile); } // Assumes Profile model, foreign key 'user_id' on profiles table
// One-to-Many: User has many Posts
get posts() { return this.hasMany(Post); } // Assumes Post model, foreign key 'user_id' on posts table
}
class Profile extends Model {
// Inverse One-to-One: Profile belongs to User
get user() { return this.belongsTo(User); } // Assumes foreign key 'user_id' on this (profiles) table
}
class Post extends Model {
// Inverse One-to-Many: Post belongs to User (Author)
get author() { return this.belongsTo(User, 'author_id'); } // Specify custom foreign key
// Many-to-Many: Post belongs to many Tags
get tags() { return this.belongsToMany(Tag); } // Assumes Tag model, pivot table 'post_tag', keys 'post_id', 'tag_id'
}
class Tag extends Model {
// Many-to-Many: Tag belongs to many Posts
get posts() { return this.belongsToMany(Post); }
}
// Lazy Loading (Loads relation when first accessed)
const user = await User.find(1);
const userProfile = await user.profile; // Query executed here
const userPosts = await user.posts; // Query executed here
// Querying Relationship Existence (has / doesntHave)
const usersWithPosts = await User.has('posts').get();
const usersWithAtLeastThreePosts = await User.has('posts', '>=', 3).get();
const usersWithoutProfile = await User.doesntHave('profile').get();
// Querying Based on Relationship Content (whereHas / orWhereHas)
const usersWithPublishedPosts = await User.whereHas('posts', (query) => {
query.where('is_published', true);
}).get();
// Accessing the Relationship Query Builder
const user = await User.find(1);
const publishedPostsCount = await user.posts().where('is_published', true).count();
Solve the N+1 query problem by loading relationships upfront.
// Eager load 'posts' relation for multiple users
const users = await User.with('posts').limit(10).get();
// Only 2 queries executed: one for users, one for posts
users.forEach(user => {
// Accessing user.posts here does NOT trigger a new query
console.log(user.name, user.posts.length);
});
// Eager load nested relations
const usersWithPostsAndComments = await User.with('posts.comments').get();
// Eager load multiple relations
const posts = await Post.with(['author', 'tags']).get();
// Eager load with constraints
const usersWithRecentPosts = await User.with({
posts: (query) => query.where('created_at', '>', someDate).orderBy('created_at', 'desc')
}).get();
Transform attribute values when getting or setting them. NodeORM uses Proxies to intercept property access, automatically calling conventional getter/setter methods.
class User extends Model {
// Accessor: Get value derived from attributes
get fullName() { // Accessed via user.fullName
return `${this.first_name} ${this.last_name}`;
}
// Or using conventional name (preferred for clarity)
// getFullNameAttribute() {
// return `${this.first_name} ${this.last_name}`;
// }
// Mutator: Modify value before setting it in attributes
set password(value) { // Called when user.password = '...' is set
this._attributes.password = await bcrypt.hash(value, 10); // Example hashing
}
// Or using conventional name
// setPasswordAttribute(value) {
// this._attributes.password = await bcrypt.hash(value, 10);
// }
}
const user = await User.find(1);
console.log(user.fullName); // Accessor called
user.password = 'new-secret'; // Mutator called
await user.save();
Automatically cast database values to common JavaScript types and back.
class Settings extends Model {
static casts = {
is_enabled: 'boolean', // DB 1/0 -> JS true/false
threshold: 'float', // DB string/numeric -> JS number
retry_count: 'integer', // DB string/numeric -> JS integer
options: 'json', // DB JSON string -> JS object/array
run_schedule: 'array', // Alias for json
config: 'object', // Alias for json
last_run_at: 'datetime', // DB datetime/timestamp -> JS Date
start_date: 'date', // DB date -> JS Date (time part ignored)
expires_at: 'timestamp' // Alias for datetime
};
}
const settings = await Settings.find(1);
console.log(typeof settings.is_enabled); // 'boolean'
console.log(typeof settings.threshold); // 'number'
console.log(Array.isArray(settings.run_schedule)); // true
console.log(settings.last_run_at instanceof Date); // true
settings.options = { timeout: 5000, retries: 3 }; // Set as object
settings.is_enabled = false;
await settings.save(); // Automatically converts back for DB storage
Attributes defined in casts
as date
, datetime
, or timestamp
, as well as the default timestamp columns (created_at
, updated_at
, deleted_at
), are automatically cast to JavaScript Date
objects. NodeORM attempts to parse common database date/time string formats.
Attributes cast to json
, array
, or object
will be automatically JSON.parse
d when retrieved from the database and JSON.stringify
d when saved.
Control how models are converted to plain objects or JSON strings.
const user = await User.find(1);
// Convert to plain object
const userArray = user.toArray();
// Convert to JSON string
const userJson = user.toJSON(); // Equivalent to JSON.stringify(user.toArray())
console.log(userJson);
Exclude sensitive attributes like passwords from serialization.
class User extends Model {
static hidden = ['password', 'remember_token'];
}
const user = await User.find(1);
const output = user.toArray();
// output.password and output.remember_token will be undefined
Explicitly define only the attributes that should be serialized. Overrides hidden
.
class User extends Model {
static visible = ['id', 'name', 'email'];
}
const user = await User.find(1);
const output = user.toArray();
// Only id, name, email will be present
Note: You can dynamically modify visibility per-instance using user.makeVisible(['password'])
or user.makeHidden(['email'])
.
Include the results of accessors in the serialized output.
class User extends Model {
static appends = ['fullName']; // Include result of getFullNameAttribute()
get fullName() {
return `${this.first_name} ${this.last_name}`;
}
}
const user = await User.find(1);
const output = user.toArray();
// output.fullName will contain the computed value
Hook into the model lifecycle (creating, created, updating, updated, saving, saved, deleting, deleted, restoring, restored).
class User extends Model {
static boot() {
super.boot(); // Call parent boot
// --- Register listeners using static methods ---
// Before creating
User.creating(async (user) => {
if (!user.uuid) {
user.uuid = generateUuid(); // Example: Set default value
}
console.log('Creating user:', user.email);
});
// After created
User.created((user) => {
sendWelcomeEmail(user); // Example: Trigger side effect
});
// Before saving (create or update)
User.saving((user) => {
console.log('Saving user:', user.name);
// Return false to halt the save operation
if (user.name === 'INVALID') {
console.log('Save halted!');
return false;
}
});
// After deleting
User.deleted((user, options) => {
console.log(`User ${user.id} deleted.`);
if(options.softDelete) console.log(' (Soft Deleted)');
else console.log(' (Hard Deleted)');
// Cleanup related data?
});
}
}
// Alternative registration (outside model class)
// User.updating(async (user) => { /* ... */ });
// User.saved((user) => { /* ... */ });
Define reusable query constraints.
Applied automatically to all queries for a model.
class Post extends Model {
static boot() {
super.boot();
// Example: Only include 'published' posts by default
// Post.addGlobalScope('published', (query) => {
// query.where('is_published', true);
// });
// Soft deleting is implemented as a global scope automatically
// if static softDeletes = true;
}
}
// Queries automatically exclude non-published posts (if scope above is active)
const posts = await Post.all();
// Temporarily remove global scope(s)
const allPostsIncludingDrafts = await Post.query().withoutGlobalScope('published').get();
const allPostsAnyStatus = await Post.query().withoutGlobalScopes().get(); // Remove all
// Soft delete scopes are removed via specific methods:
const postsWithTrashed = await Post.withTrashed().get();
const onlyTrashedPosts = await Post.onlyTrashed().get();
Applied explicitly when needed. Define as static scope<Name>
methods.
class Post extends Model {
// Local scope: scopePublished
static scopePublished(query) {
query.where('is_published', true);
}
// Local scope with parameters: scopeOfType
static scopeOfType(query, type) {
query.where('post_type', type);
}
}
// Apply local scope
const publishedPosts = await Post.query().scope('published').get();
// Or using dynamic proxy method
const publishedPostsDynamic = await Post.published().get();
// Apply scope with parameters
const articles = await Post.query().scope('ofType', 'article').get();
// Or dynamic
const articlesDynamic = await Post.ofType('article').get();
Execute database operations within a transaction for atomicity.
import { ConnectionManager } from 'nodeorm';
const connection = ConnectionManager.getConnection(); // Get default connection
try {
const result = await connection.transaction(async (trxConnection) => {
// All operations within this callback run in a transaction.
// Use the passed trxConnection or models normally (if context propagation is set up).
// For safety, explicitly use trxConnection with query builder:
const user1 = await trxConnection.query(User).find(1);
await user1.update({ login_count: user1.login_count + 1 }); // Uses instance method
// If using static methods, ensure they pick up the transaction context
// (Requires advanced setup or explicit passing)
// For safety:
await trxConnection.query(AuditLog).create({ userId: user1.id, action: 'login incremented' });
// If any awaited operation throws an error, the transaction is automatically rolled back.
// Explicit rollback (optional)
// if (someCondition) {
// await trxConnection.rollback();
// return 'Manually rolled back'; // Callback still completes
// }
return 'Transaction successful'; // Return value is passed through
});
console.log(result); // 'Transaction successful' (unless rolled back)
} catch (error) {
// Catches errors from inside the transaction callback *or* commit/rollback failures
console.error('Transaction failed:', error);
}
Execute raw SQL when the query builder isn't sufficient. Use with caution to prevent SQL injection vulnerabilities. Always prefer parameterized queries.
// Use connection.run() with parameter bindings (Safer)
const users = await connection.run(
'SELECT * FROM users WHERE login_count > ? AND is_admin = ?',
[10, true] // Bindings array
);
// Use connection.raw tagged template literal (Bindings handled automatically)
const namePattern = 'J%';
const limit = 5;
const activeUsers = await connection.raw`
SELECT id, name FROM users
WHERE name LIKE ${namePattern} AND status = ${'active'}
ORDER BY created_at DESC
LIMIT ${limit}
`;
// Raw expressions within Query Builder
import { raw } from 'nodeorm';
const counts = await User.select(
'status',
raw('COUNT(*) as user_count') // Use raw for aggregate function
)
.groupBy('status')
.get();
Define and modify database tables using a fluent, database-agnostic API.
import { Schema, Blueprint } from 'nodeorm'; // Import Schema facade and Blueprint
// Create a new table
await Schema.create('flights', (table) => {
table.id(); // Auto-incrementing BigInt primary key 'id'
table.string('name');
table.string('airline_code', 5).index(); // Add index
table.integer('capacity').unsigned();
table.decimal('price', 8, 2);
table.boolean('is_domestic').default(true);
table.timestamp('departed_at').nullable();
table.timestamps(); // Adds nullable created_at and updated_at
});
// Modify an existing table
await Schema.table('users', (table) => { // or Schema.change(...)
// Add a new column
table.string('avatar_url').nullable().after('email'); // MySQL specific placement
// Add an index
table.index(['state', 'city']);
// Note: Modifying/dropping columns requires more advanced SchemaGrammar features.
// table.renameColumn('from', 'to'); // Not yet implemented in base grammar
// table.dropColumn('obsolete_column'); // Not yet implemented in base grammar
});
// Drop a table
await Schema.dropIfExists('old_logs');
See src/schema/Blueprint.js
for all available column types and modifiers.
NodeORM includes a basic programmatic migration runner. It does not include a CLI tool like artisan migrate
.
-
Create Migration Files: Place JavaScript files in a designated
migrations
folder (e.g.,project-root/migrations
). Use timestamp-based naming for ordering (e.g.,2023_11_01_100000_create_posts_table.js
). Each file must exportasync up(schema, connection)
and optionallyasync down(schema, connection)
.// migrations/YYYY_MM_DD_HHMMSS_create_some_table.js /** @typedef {import('nodeorm/schema').Schema} Schema */ /** @typedef {import('nodeorm/schema').Blueprint} Blueprint */ export async function up(schema) { await schema.create('some_table', (table) => { table.id(); table.string('title').unique(); table.timestamps(); }); } export async function down(schema) { await schema.dropIfExists('some_table'); }
-
Create a Runner Script: (e.g.,
scripts/migrate.js
)// scripts/migrate.js import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { Connection, ConnectionManager } from '../src/index.js'; // Adjust path import { Migrator } from '../src/migrations/Migrator.js'; // Adjust path const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function run() { let connection; try { connection = await Connection.make(); // Use your connection setup const migrationsPath = path.resolve(__dirname, '../migrations'); const migrator = new Migrator(connection, migrationsPath); const command = process.argv[2] || 'latest'; if (command === 'rollback') await migrator.rollback(); else await migrator.latest(); } catch (error) { /* ... */ } finally { await ConnectionManager.disconnectAll(); } } run();
-
Add NPM Scripts:
// package.json "scripts": { "migrate": "node scripts/migrate.js latest", "migrate:rollback": "node scripts/migrate.js rollback" }
-
Run:
npm run migrate
/yarn migrate
npm run migrate:rollback
/yarn migrate:rollback
NodeORM is designed with performance as a key goal. We aim to achieve this through:
- Minimal Core: Very few dependencies outside of the database drivers themselves.
- Modern JavaScript: Leveraging native features like Promises, async/await, and Proxies efficiently.
- Direct Driver Interaction: The core query execution path aims to be thin, translating the fluent API calls into SQL and passing them to the driver with minimal intermediate layers compared to some older ORMs.
- Lazy Loading: Relationships are only loaded when explicitly accessed or eager-loaded.
However, "blazing fast" is a bold claim that requires context and evidence. Performance depends heavily on your specific queries, database schema, hardware, and driver choice.
We encourage the community to perform independent benchmarks comparing NodeORM against other popular ORMs under various workloads. We are committed to optimizing performance based on feedback and real-world data.
NodeORM is designed to be extensible. You can create your own database drivers (e.g., for NoSQL databases or less common SQL dialects) by implementing the BaseDriver
and potentially BaseGrammar
/ BaseSchemaGrammar
interfaces.
- Create Driver Class: Extend
BaseDriver
(fromnodeorm/drivers
). - Implement Methods: Implement
connect
,disconnect
,run
,beginTransaction
,commit
,rollback
,getTableSchema
,describeTable
,_createGrammar
,_createSchemaGrammar
. - Create Grammar Classes (Optional but recommended): Extend
BaseGrammar
andBaseSchemaGrammar
to handle SQL dialect specifics. - Register/Use: Configure NodeORM to use your custom driver, typically by passing an instance or using a custom
driver
name in the configuration object if you modify theConnection.make
logic.
import { BaseDriver, BaseGrammar, BaseSchemaGrammar } from 'nodeorm/drivers';
class MyCustomGrammar extends BaseGrammar { /* ... */ }
class MyCustomSchemaGrammar extends BaseSchemaGrammar { /* ... */ }
class MyCustomDriver extends BaseDriver {
_createGrammar() { return new MyCustomGrammar(); }
_createSchemaGrammar(queryGrammar) { return new MyCustomSchemaGrammar(queryGrammar); }
async connect() { /* ... */ }
async disconnect() { /* ... */ }
async run(sql, bindings) { /* ... */ }
// ... implement other abstract methods ...
}
// Usage:
// const myDriver = new MyCustomDriver({ /* config */ });
// const connection = await Connection.make(myDriver);
// or
// const connection = await Connection.make({ driver: 'custom', instance: myDriver });
Contributions are welcome! Please read the CONTRIBUTING.md file (you'll need to create this) for details on bug reports, feature requests, and pull requests.
NodeORM is open-source software licensed under the MIT license.