A TypeScript ORM, inspired by Ruby's ActiveRecord, designed for PostgreSQL, MySQL, and SQLite running on bun.
- ActiveRecord Pattern: Models inherit from a base class, combining data access and business logic
- Type Safety: Full TypeScript support with compile-time type checking
- Database Agnostic: Adapter pattern allows support for multiple databases
- Query Builder: Fluent, chainable query interface
- Associations: Support for belongsTo, hasOne, hasMany, and many-to-many relationships
- Validations: Comprehensive validation system with built-in and custom validators
- Callbacks/Hooks: Lifecycle hooks for running code at specific points
- Transactions: Full ACID transaction support
- Schema Introspection: Inspect database schema at runtime
# using bun
bun add js-record# Create a database configuration file
js-record config:init # SQLite (default)
js-record config:init postgres # PostgreSQL
# This creates config/database.ts with your adapter configurationPostgreSQL:
import { PostgresAdapter } from 'js-record';
const adapter = new PostgresAdapter({
host: 'localhost',
port: 5432,
database: 'myapp_development',
user: 'postgres',
password: 'password',
});
await adapter.connect();SQLite (Bun Native):
import { SqliteAdapter } from 'js-record';
// File-based database
const adapter = new SqliteAdapter({
database: './myapp.db',
});
// Or use in-memory database
const adapter = new SqliteAdapter({
database: ':memory:',
});
await adapter.connect();import { Model } from 'js-record';
class User extends Model {
id!: number;
name!: string;
email!: string;
createdAt!: Date;
updatedAt!: Date;
}
class Post extends Model {
id!: number;
userId!: number;
title!: string;
content!: string;
published!: boolean;
createdAt!: Date;
updatedAt!: Date;
}
// Set database adapter for all models
Model.setAdapter(adapter);// Create
const user = await User.create({
name: 'John Doe',
email: '[email protected]',
});
// Read
const foundUser = await User.find(1);
const userByEmail = await User.findBy({ email: '[email protected]' });
const allUsers = await User.all();
// Update
user.name = 'Jane Doe';
await user.save();
// or
await user.update({ name: 'Jane Doe' });
// Delete
await user.destroy();// Chainable queries
const activeUsers = await User.where({ active: true }).orderBy('createdAt', 'DESC').limit(10).all();
// Complex queries
const posts = await Post.where('published = ?', true)
.where('createdAt > ?', thirtyDaysAgo)
.orderBy('views', 'DESC')
.all();
// Counting
const count = await User.where({ active: true }).count();
// Existence check
const exists = await User.where({ email: '[email protected]' }).exists();Define reusable query conditions:
// Define scopes
Post.scope('published', query => {
return query.where({ status: 'published' });
});
Post.scope('popular', (query, minViews = 100) => {
return query.where('views >= ?', minViews);
});
Post.scope('recent', query => {
return query.orderBy('createdAt', 'DESC').limit(10);
});
// Use scopes
const publishedPosts = await (Post as any).published().all();
const popularPosts = await (Post as any).popular(500).all();
// Chain scopes with queries
const featured = await (Post as any)
.published()
.where({ isFeatured: true })
.orderBy('views', 'DESC')
.all();
// Default scope (applied to all queries)
User.defaultScope({
where: { deletedAt: null }, // Soft delete
order: ['name', 'ASC'],
});
// Bypass default scope when needed
const allUsers = await User.unscoped().all();Define relationships between models:
// One-to-many
User.hasMany('posts', Post);
Post.belongsTo('user', User);
// One-to-one
User.hasOne('profile', Profile);
Profile.belongsTo('user', User);
// Many-to-many
Post.hasManyThrough('tags', Tag, {
through: 'post_tags',
foreignKey: 'postId',
throughForeignKey: 'tagId',
});
// Usage
const user = await User.find(1);
const posts = await user.posts.all();
const profile = await user.profile;
const post = await Post.find(1);
const author = await post.user;
const tags = await post.tags.all();
// Create associated records
await user.posts.create({
title: 'New Post',
content: 'Content here',
});
// Add to many-to-many
const tag = await Tag.find(1);
await post.tags.add(tag);Define validation rules to ensure data integrity:
class User extends Model {
static validations = {
name: {
presence: true,
length: { min: 2, max: 100 },
},
email: {
presence: true,
format: {
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: 'must be a valid email address',
},
uniqueness: true,
},
age: {
numericality: {
onlyInteger: true,
greaterThanOrEqualTo: 0,
lessThanOrEqualTo: 150,
},
},
username: {
presence: true,
length: { min: 3, max: 20 },
uniqueness: { caseSensitive: false },
},
password: {
length: { min: 8 },
confirmation: true, // Requires passwordConfirmation field
},
status: {
inclusion: {
in: ['active', 'inactive', 'pending'],
},
},
website: {
format: {
pattern: /^https?:\/\/.+/,
message: 'must be a valid URL',
},
},
};
}
// Usage
const user = new User();
user.name = 'John';
user.email = 'invalid-email';
const isValid = await user.validate();
console.log(isValid); // false
console.log(user.errors); // { email: ['must be a valid email address'] }
// save() automatically validates
const saved = await user.save(); // false (validation failed)
// Skip validation if needed
await user.save({ validate: false });Available Validations:
presence- Requires value to be presentlength- Min/max/exact length for strings and arraysformat- Regex pattern matchingnumericality- Number validation with constraintsuniqueness- Database uniqueness checkinclusion- Value must be in listexclusion- Value must not be in listconfirmation- Field must match confirmation fieldcustom- Custom validation function
Run code at specific points in a model's lifecycle:
class Article extends Model {
// Instance method callbacks
beforeSave() {
// Generate slug from title
if (!this.slug && this.title) {
this.slug = this.title.toLowerCase().replace(/\s+/g, '-');
}
}
afterCreate() {
console.log('Article created!');
// Send notifications, update cache, etc.
}
beforeDestroy() {
// Clean up associated records
return confirm('Are you sure?'); // Return false to halt
}
}
// Class-level callbacks
User.beforeCreate('hashPassword');
User.afterCreate('sendWelcomeEmail');
// Conditional callbacks
Post.afterCreate(
model => {
// Send publication notification
},
{ if: model => model.status === 'published' }
);Available Callbacks:
beforeValidation/afterValidationbeforeSave/afterSavebeforeCreate/afterCreatebeforeUpdate/afterUpdatebeforeDestroy/afterDestroy
Returning false from a before* callback halts the chain and prevents the operation.
const transaction = await adapter.beginTransaction();
try {
await transaction.execute('INSERT INTO users (name) VALUES ($1)', ['Alice']);
await transaction.execute('INSERT INTO profiles (userId) VALUES ($1)', [1]);
await transaction.commit();
} catch (error) {
await transaction.rollback();
}Manage your database schema with migrations:
js-record config:init # SQLite (default) js-record config:init postgres # PostgreSQL
npx js-record migration:create add_status_to_users
bunx js-record migration:create add_status_to_users
npx js-record migration:create
npx js-record migration:init
The migration:init command is useful when adding js-record to an existing project - it will introspect your current database schema and generate a migration file with all the CREATE TABLE statements.
This creates a new migration file in migrations/ with a timestamp and stub:
import { Migration } from 'js-record';
export default class AddStatusToUsers extends Migration {
async up(): Promise<void> {
// Add your migration code here
}
async down(): Promise<void> {
// Add your rollback code here
}
}export default class CreateUsersTable extends Migration {
async up(): Promise<void> {
await this.createTable('users', table => {
table.increments('id');
table.string('name').notNullable();
table.string('email').unique().notNullable();
table.boolean('active').defaultTo(true);
table.timestamps(); // created_at, updated_at
});
// Add indexes
await this.createIndex('users', ['email']);
}
async down(): Promise<void> {
await this.dropTable('users');
}
}Using the CLI (Recommended):
First, configure your database connection by creating js-record.config.js:
module.exports = {
adapter: 'postgres',
host: 'localhost',
port: 5432,
database: 'myapp_dev',
user: 'postgres',
password: 'postgres',
};Then run migrations:
# Run all pending migrations
npx js-record migrate
# Check migration status
npx js-record migrate:status
# Rollback last batch
npx js-record migrate:down
# Rollback last 2 batches
npx js-record migrate:down 2
# Reset all migrations
npx js-record migrate:resetProgrammatic API:
import { MigrationRunner } from 'js-record';
const runner = new MigrationRunner(adapter);
// Import your migrations
const migrations = new Map([
['20250111000001_create_users_table', CreateUsersTable],
['20250111000002_create_posts_table', CreatePostsTable],
]);
// Run all pending migrations
await runner.up(migrations);
// Rollback last batch
await runner.rollback(migrations, 1);
// Check migration status
const statuses = await runner.status(migrations);
// Reset all migrations
await runner.reset(migrations);See the examples/migrations-usage.ts for more examples and CLI documentation for detailed CLI usage.
π§ Work in Progress - This library is under active development.
- β Base adapter interface
- β PostgreSQL adapter with connection pooling
- β Transaction support
- β Schema introspection
- β Query Builder
- β Base Model class
- β CRUD operations
- β Associations (belongsTo, hasOne, hasMany, hasManyThrough)
- β Validations (presence, length, format, numericality, uniqueness, custom, etc.)
- β Callbacks/Hooks (lifecycle hooks with conditional execution)
- β Migrations
- β SQLite adapter with Bun's native SQLite support
- β Scopes (reusable query conditions)
- π Eager loading (includes)
- π MySQL adapter
This project uses Just as a command runner for common tasks.
# Install Just (if not already installed)
# macOS: brew install just
# Linux: curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
# List all available commands
just --list
# Common commands
just install # Install dependencies
just build # Build the project
just test # Run all tests
just quick-test # Run fast SQLite tests
just check # Type check, lint, and format check
just dev # Clean, build, and test# Install dependencies
bun install
# Build the project
bun run build
# Watch mode for development
bun run build:watch
# Type checking
bun run typecheck
# Linting
bun run lint
# Format code
bun run format
# Run tests
bun run test:sqliteSee docs/JUSTFILE.md for complete Justfile documentation.
The library is organized into modular components:
src/
βββ adapters/ # Database adapters
βββ core/ # Core ORM functionality (coming soon)
βββ associations/ # Relationship management (coming soon)
βββ validations/ # Validation framework (coming soon)
βββ callbacks/ # Hook system (coming soon)
Currently supported:
- PostgreSQL - Full support using Bun's native PostgreSQL driver
- SQLite - Full support using Bun's native SQLite (file-based and in-memory)
- See SQLite Guide for detailed documentation
Planned:
- MySQL
- SQL Server
This is an open-source project. Contributions are welcome!
MIT