databaseorm
v1.0.0-alpha.2
Published
A ORM for Node.js, focusing on performance, simplicity, and developer experience.
Maintainers
Readme
DatabaseOrm
DatabaseOrm is a modern, performant, and elegant Object-Relational Mapper (ORM) for Node.js.
It aims to provide a familiar, expressive, and powerful database interaction experience 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.
- ✨ Full-Featured ORM: Implements a wide range of advanced ORM features and a fluent API, matching the capabilities of mature frameworks.
- 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_moduleslean. - 🔧 Extensibility: Easily extendable with custom database drivers.
- Reliability: Strong typing hints via JSDoc, robust error handling, and a focus on maintainable code.
DatabaseOrm strives to be a top-tier ORM choice for Node.js developers seeking performance, elegance, and a familiar API.
Table of Contents
- Why Choose DatabaseOrm?
- 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
Why Choose DatabaseOrm?
While the Node.js ecosystem has several mature ORMs (TypeORM, Sequelize, Prisma), DatabaseOrm aims to carve its niche by offering:
- The Expressive ORM Experience: : DatabaseOrm provides a powerful and intuitive database layer with a fluent, chainable API and rich features. It's designed to feel natural for developers familiar with expressive ORM tools, making the transition to Node.js smooth and productive—especially for building clean, maintainable 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.
Features
- Expressive ORM API: Models, Query Builder, Relationships, Scopes, Events, etc.
- Supported Databases: PostgreSQL (via
pgor@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/toArraymethods withhidden,visible, andappendssupport. - Events/Hooks: Lifecycle hooks (
creating,created,updating,updated,saving,saved,deleting,deleted,restoring,restored). - Scopes: Reusable query constraints (global and local).
- Soft Deletes:
deleted_attimestamp 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.
Installation
npm install databaseorm mysql2 # Example for MySQLInstall the appropriate driver for your database:
- PostgreSQL:
npm install pgornpm install @neondatabase/serverless(Neon preferred) - MySQL / MariaDB:
npm install mysql2 - SQLite:
npm install sqlite3
Quick Start
import { Model, Connection } from 'databaseorm';
// Import a driver (DatabaseOrm attempts to auto-detect and load)
// import { MySQLDriver } from 'databaseorm/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: 'Hassan Kerdash',
email: '[email protected]',
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: 'Hassanathan 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 DatabaseOrm!'
});
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();Core Concepts
Configuration & Connections
DatabaseOrm uses a Connection class to manage database interactions. Connections can be configured via:
- Environment Variables: It checks for
DATABASE_URL/DB_URLfirst, 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 'databaseorm';
// 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 'databaseorm';
const mainDb = ConnectionManager.getConnection(); // Gets 'default'
const specificDb = ConnectionManager.getConnection('mysql_test');Defining Models
Models extend the DatabaseOrm.Model class and represent a single database table.
import { Model } from 'databaseorm';
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
}
}Conventions
DatabaseOrm follows familiar active record-style conventions, while still allowing full customization and overrides when needed:
- 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 = falseorstatic keyType = 'string'. - Timestamps:
created_atandupdated_at. AssumedDATETIMEorTIMESTAMP. 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.
Basic Usage
Retrieving Models
// 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', '[email protected]').first();
// Find or create
const userOrCreate = await User.firstOrCreate(
{ email: '[email protected]' }, // 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: '[email protected]' },
{ name: 'Maybe New' }
);
if (!userOrNew.exists()) {
// userOrNew.login_count = 1; // Set additional attributes
// await userOrNew.save();
}Aggregates
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();Inserting & Updating Models
// --- Create (Mass Assignment) ---
const user = await User.create({
name: 'New User',
email: '[email protected]',
// '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: '[email protected]', name: 'Upsert 1', login_count: 1 },
{ email: '[email protected]', 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: '[email protected]' }, // 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 updatesDeleting Models
// 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();Soft Deleting
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();Query Builder
DatabaseOrm 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', '[email protected]').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'); // ['[email protected]', '[email protected]', ...]
const namesById = await User.pluck('name', 'id'); // { 1: 'Hassan', 2: 'Jane', ... }
const adminEmail = await User.where('is_admin', true).value('email'); // '[email protected]' or nullRelationships
Define relationships as getter methods on your models.
Defining Relationships
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); }
}Querying Relations
// 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();Eager Loading
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();Attributes
Accessors & Mutators
Transform attribute values when getting or setting them. DatabaseOrm 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();Attribute Casting
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 storageDate Casting
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. DatabaseOrm attempts to parse common database date/time string formats.
JSON Casting
Attributes cast to json, array, or object will be automatically JSON.parsed when retrieved from the database and JSON.stringifyd when saved.
Serialization
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);Hiding Attributes
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 undefinedVisible Attributes
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 presentNote: You can dynamically modify visibility per-instance using user.makeVisible(['password']) or user.makeHidden(['email']).
Appending Values
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 valueEvents & Hooks
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) => { /* ... */ });Scopes
Define reusable query constraints.
Global Scopes
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();Local Scopes
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();Transactions
Execute database operations within a transaction for atomicity.
import { ConnectionManager } from 'databaseorm';
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);
}Raw Queries
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 'databaseorm';
const counts = await User.select(
'status',
raw('COUNT(*) as user_count') // Use raw for aggregate function
)
.groupBy('status')
.get();Schema Builder
Define and modify database tables using a fluent, database-agnostic API.
import { Schema, Blueprint } from 'databaseorm'; // 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.
Migrations (Basic Runner)
DatabaseOrm 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
migrationsfolder (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('databaseorm/schema').Schema} Schema */ /** @typedef {import('databaseorm/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 migratenpm run migrate:rollback/yarn migrate:rollback
Performance Considerations
DatabaseOrm 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 DatabaseOrm against other popular ORMs under various workloads. We are committed to optimizing performance based on feedback and real-world data.
Extensibility (Custom Drivers)
DatabaseOrm 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(fromdatabaseorm/drivers). - Implement Methods: Implement
connect,disconnect,run,beginTransaction,commit,rollback,getTableSchema,describeTable,_createGrammar,_createSchemaGrammar. - Create Grammar Classes (Optional but recommended): Extend
BaseGrammarandBaseSchemaGrammarto handle SQL dialect specifics. - Register/Use: Configure DatabaseOrm to use your custom driver, typically by passing an instance or using a custom
drivername in the configuration object if you modify theConnection.makelogic.
import { BaseDriver, BaseGrammar, BaseSchemaGrammar } from 'databaseorm/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 });Contributing
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.
License
DatabaseOrm is open-source software licensed under the MIT license.
