| `proxify(row, strategy?)` | Wrap plain object with active record methods |
Resources
21Install
npx skillscat add alfateam/orange-orm Install via the SkillsCat registry.
Orange ORM — Skills & Reference
Authoritative reference for context7.com MCP consumption.
Orange ORM is the ultimate Object Relational Mapper for Node.js, Bun, and Deno.
It uses the Active Record Pattern with full TypeScript IntelliSense — no code generation required.
Supports: PostgreSQL, SQLite, MySQL, MS SQL, Oracle, SAP ASE, PGlite, Cloudflare D1.
Works in the browser via Express/Hono adapters.
Repository maintenance note
- Treat
dist/index.mjsanddist/index.browser.mjsas generated build output from the build command, not as source files to review or edit directly unless the task explicitly targets build artifacts.
Table of Contents
- Defining a Model (Table Mapping)
- Connecting to a Database
- Inserting Rows
- Fetching Rows
- Filtering (where)
- Ordering, Limit, Offset
- Updating Rows (saveChanges)
- Deleting Rows
- Relationships (hasMany, hasOne, references)
- Transactions
- acceptChanges and clearChanges
- Concurrency / Conflict Resolution
- Fetching Strategies (Column Selection)
- Aggregate Functions
- Data Types
- Enums
- TypeScript Type Safety
- Browser Usage (Express / Hono Adapters)
- Raw SQL Queries
- Logging
- Bulk Operations (update, replace, updateChanges)
- Batch Delete
- Composite Keys
- Discriminators
Defining a Model (Table Mapping)
Use orange.map() to define tables and columns. Each column specifies its database column name, data type, and constraints.
IMPORTANT: The .map() method maps JavaScript property names to database column names. Always call .primary() on primary key columns. Use .notNullExceptInsert() for autoincrement keys. Use .notNull() for required columns.
import orange from 'orange-orm';
const map = orange.map(x => ({
product: x.table('product').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
name: column('name').string().notNull(),
price: column('price').numeric(),
}))
}));
export default map;Column types available
column('col').string()— text/varcharcolumn('col').numeric()— integer/decimal/floatcolumn('col').bigint()— bigintcolumn('col').boolean()— boolean/bitcolumn('col').uuid()— UUID as stringcolumn('col').date()— date/datetime as ISO 8601 stringcolumn('col').dateWithTimeZone()— timestamp with timezonecolumn('col').binary()— binary/blob as base64 stringcolumn('col').json()— JSON objectcolumn('col').jsonOf<T>()— typed JSON (TypeScript generic)
Column modifiers
.primary()— marks as primary key.notNull()— required, never null.notNullExceptInsert()— required on read, optional on insert (for autoincrement keys).default(value)— default value or factory function.validate(fn)— custom validation function.JSONSchema(schema)— AJV JSON schema validation.serializable(false)— exclude from JSON serialization.enum(values)— restrict to enum values (array, object, or TypeScript enum)
Multiple tables example
import orange from 'orange-orm';
const map = orange.map(x => ({
customer: x.table('customer').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
name: column('name').string(),
balance: column('balance').numeric(),
isActive: column('isActive').boolean(),
})),
order: x.table('_order').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
orderDate: column('orderDate').date().notNull(),
customerId: column('customerId').numeric().notNullExceptInsert(),
})),
orderLine: x.table('orderLine').map(({ column }) => ({
id: column('id').numeric().primary(),
orderId: column('orderId').numeric(),
product: column('product').string(),
amount: column('amount').numeric(),
})),
deliveryAddress: x.table('deliveryAddress').map(({ column }) => ({
id: column('id').numeric().primary(),
orderId: column('orderId').numeric(),
name: column('name').string(),
street: column('street').string(),
postalCode: column('postalCode').string(),
postalPlace: column('postalPlace').string(),
countryCode: column('countryCode').string(),
}))
}));
export default map;Connecting to a Database
After defining your map, call a connector method to get a db client.
SQLite
import map from './map';
const db = map.sqlite('demo.db');With connection pool:
const db = map.sqlite('demo.db', { size: 10 });PostgreSQL
import map from './map';
const db = map.postgres('postgres://user:pass@host/dbname');MySQL
import map from './map';
const db = map.mysql('mysql://user:pass@host/dbname');MS SQL
import map from './map';
const db = map.mssql({
server: 'mssql',
options: { encrypt: false, database: 'test' },
authentication: {
type: 'default',
options: { userName: 'sa', password: 'password' }
}
});Oracle
import map from './map';
const db = map.oracle({ user: 'sys', password: 'pass', connectString: 'oracle/XE', privilege: 2 });PGlite (in-memory Postgres)
import map from './map';
const db = map.pglite();Cloudflare D1
import map from './map';
const db = map.d1(env.DB);HTTP (browser client)
import map from './map';
const db = map.http('http://localhost:3000/orange');Closing connections (important for serverless)
await db.close();Inserting Rows
Use db.<table>.insert() to insert one or more rows. Returns the inserted row(s) with active record methods.
Insert a single row
import map from './map';
const db = map.sqlite('demo.db');
const product = await db.product.insert({
name: 'Bicycle',
price: 250
});
// product = { id: 1, name: 'Bicycle', price: 250 }
// product has .saveChanges(), .delete(), etc.Insert multiple rows
const products = await db.product.insert([
{ name: 'Bicycle', price: 250 },
{ name: 'Guitar', price: 150 }
]);Insert with a fetching strategy
The second argument controls which relations to eager-load after insert:
const order = await db.order.insert({
orderDate: new Date(),
customer: george,
deliveryAddress: {
name: 'George',
street: 'Node street 1',
postalCode: '7059',
postalPlace: 'Jakobsli',
countryCode: 'NO'
},
lines: [
{ product: 'Bicycle', amount: 250 },
{ product: 'Guitar', amount: 150 }
]
}, { customer: true, deliveryAddress: true, lines: true });Insert and forget (no return value)
await db.product.insertAndForget({ name: 'Bicycle', price: 250 });Fetching Rows
Get all rows
const products = await db.product.getMany();Get a single row by primary key (getById)
const product = await db.product.getById(1);
// Returns the row or undefined if not foundWith a fetching strategy:
const order = await db.order.getById(1, {
customer: true,
deliveryAddress: true,
lines: true
});Composite primary key getById
const line = await db.orderLine.getById('typeA', 100, 1);
// Arguments match the order of primary key columnsGet one row (first match)
const product = await db.product.getOne({
where: x => x.name.eq('Bicycle')
});Get many rows with a fetching strategy
const orders = await db.order.getMany({
customer: true,
deliveryAddress: true,
lines: {
packages: true
}
});Filtering (where)
Use the where option in getMany or getOne. The callback receives a row reference with column filter methods.
Comparison operators
All column types support:
.equal(value)/.eq(value)— equal.notEqual(value)/.ne(value)— not equal.lessThan(value)/.lt(value)— less than.lessThanOrEqual(value)/.le(value)— less than or equal.greaterThan(value)/.gt(value)— greater than.greaterThanOrEqual(value)/.ge(value)— greater than or equal.between(from, to)— between two values (inclusive).in(values)— in a list of values
String columns also support:
.startsWith(value)— starts with.endsWith(value)— ends with.contains(value)— contains substring.iStartsWith(value),.iEndsWith(value),.iContains(value),.iEqual(value)— case-insensitive (Postgres only)
Filter by column value
const products = await db.product.getMany({
where: x => x.price.greaterThan(50),
orderBy: 'name'
});Combining filters with and/or/not
const products = await db.product.getMany({
where: x => x.price.greaterThan(50)
.and(x.name.startsWith('B'))
});
const products = await db.product.getMany({
where: x => x.name.eq('Bicycle')
.or(x.name.eq('Guitar'))
});
const products = await db.product.getMany({
where: x => x.name.eq('Bicycle').not()
});Filter across relations
const orders = await db.order.getMany({
where: x => x.customer.name.startsWith('Harry')
.and(x.lines.any(line => line.product.contains('broomstick'))),
customer: true,
lines: true
});Relation sub-filters: any, all, none, count
// Orders that have at least one line containing 'guitar'
const rows = await db.order.getMany({
where: x => x.lines.any(line => line.product.contains('guitar'))
});
// Orders where ALL lines contain 'a'
const rows = await db.order.getMany({
where: x => x.lines.all(line => line.product.contains('a'))
});
// Orders with NO lines equal to 'Magic wand'
const rows = await db.order.getMany({
where: x => x.lines.none(line => line.product.eq('Magic wand'))
});
// Orders with at most 1 line
const rows = await db.order.getMany({
where: x => x.lines.count().le(1)
});exists filter
const rows = await db.order.getMany({
where: x => x.deliveryAddress.exists()
});Building filters separately (reusable)
const filter = db.order.customer.name.startsWith('Harry');
const orders = await db.order.getMany({
where: filter,
customer: true
});Column-to-column comparison
const orders = await db.order.getMany({
where: x => x.deliveryAddress.name.eq(x.customer.name)
});Raw SQL filter
const rows = await db.customer.getMany({
where: () => ({
sql: 'name like ?',
parameters: ['%arry']
})
});Ordering, Limit, Offset
const products = await db.product.getMany({
orderBy: 'name',
limit: 10,
offset: 5
});Multiple order-by columns
const products = await db.product.getMany({
orderBy: ['price desc', 'name']
});Ordering within relations
const orders = await db.order.getMany({
orderBy: ['orderDate desc', 'id'],
lines: {
orderBy: 'product'
}
});Complete example: filter + order + limit
const products = await db.product.getMany({
where: x => x.price.greaterThan(50),
orderBy: 'name',
limit: 10,
offset: 0
});Updating Rows (saveChanges)
Orange uses the Active Record Pattern. Fetch a row, modify its properties, then call saveChanges(). Only changed columns are sent to the database.
Update a single row
const product = await db.product.getById(1);
product.price = 299;
await product.saveChanges();Update related rows (hasMany / hasOne)
const order = await db.order.getById(1, {
deliveryAddress: true,
lines: true
});
order.orderDate = new Date();
order.deliveryAddress = null; // deletes the hasOne child
order.lines.push({ product: 'Cloak of invisibility', amount: 600 }); // adds a new line
await order.saveChanges();Update multiple rows at once
let orders = await db.order.getMany({
orderBy: 'id',
lines: true,
deliveryAddress: true,
customer: true
});
orders[0].orderDate = new Date();
orders[0].deliveryAddress.street = 'Node street 2';
orders[0].lines[1].product = 'Big guitar';
orders[1].deliveryAddress = null;
orders[1].lines.push({ product: 'Cloak of invisibility', amount: 600 });
await orders.saveChanges();Selective update (bulk) with where
await db.order.update(
{ orderDate: new Date() },
{ where: x => x.id.eq(1) }
);Replace a row from JSON (complete overwrite)
const order = await db.order.replace({
id: 1,
orderDate: '2023-07-14T12:00:00',
customer: { id: 2 },
deliveryAddress: { name: 'Roger', street: 'Node street 1', postalCode: '7059', postalPlace: 'Jakobsli', countryCode: 'NO' },
lines: [
{ id: 1, product: 'Bicycle', amount: 250 },
{ product: 'Piano', amount: 800 }
]
}, { customer: true, deliveryAddress: true, lines: true });Partial update from JSON diff (updateChanges)
const original = { id: 1, orderDate: '2023-07-14T12:00:00', lines: [{ id: 1, product: 'Bicycle', amount: 250 }] };
const modified = JSON.parse(JSON.stringify(original));
modified.lines.push({ product: 'Piano', amount: 800 });
const order = await db.order.updateChanges(modified, original, { lines: true });Deleting Rows
Delete a single row
const product = await db.product.getById(1);
await product.delete();Delete an element from an array then save
const orders = await db.order.getMany({ lines: true });
orders.splice(1, 1); // remove second order
await orders.saveChanges(); // persists the deletionDelete many rows (filtered)
const orders = await db.order.getMany({
where: x => x.customer.name.eq('George')
});
await orders.delete();Batch delete by filter
const filter = db.order.deliveryAddress.name.eq('George');
await db.order.delete(filter);Batch delete cascade
Cascade deletes also remove child rows (hasOne/hasMany):
const filter = db.order.deliveryAddress.name.eq('George');
await db.order.deleteCascade(filter);Batch delete by primary key
await db.customer.delete([{ id: 1 }, { id: 2 }]);Relationships (hasMany, hasOne, references)
Relationships are defined in a second .map() call chained after the table definitions.
hasMany(targetTable).by('foreignKeyColumn')— one-to-many. The target table has a foreign key pointing to the parent's primary key. The parent owns the children (cascade delete). Returns an array.hasOne(targetTable).by('foreignKeyColumn')— one-to-one. This is a special case ofhasMany— the database models them identically (the target table has a foreign key pointing to the parent's primary key). The only difference is thathasOnereturns a single object (or null) instead of an array. The parent owns the child (cascade delete).references(targetTable).by('foreignKeyColumn')— many-to-one. This is the opposite direction fromhasMany/hasOne: the current table has a foreign key pointing to the target's primary key. The target is independent (no cascade delete). Returns a single object (or null).
Example: Author and Book (one-to-many)
import orange from 'orange-orm';
const map = orange.map(x => ({
author: x.table('author').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
name: column('name').string().notNull(),
})),
book: x.table('book').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
authorId: column('authorId').numeric().notNull(),
title: column('title').string().notNull(),
year: column('year').numeric(),
}))
})).map(x => ({
author: x.author.map(({ hasMany }) => ({
books: hasMany(x.book).by('authorId')
})),
book: x.book.map(({ references }) => ({
author: references(x.author).by('authorId')
}))
}));
export default map;Query author with all their books
import map from './map';
const db = map.sqlite('demo.db');
const author = await db.author.getById(1, {
books: true
});
// author.books is an array of { id, authorId, title, year }
console.log(author.name);
author.books.forEach(book => console.log(book.title));Query with nested relations
const orders = await db.order.getMany({
customer: true,
deliveryAddress: true,
lines: {
packages: true
}
});Insert with nested relations
const order = await db.order.insert({
orderDate: new Date(),
customer: george,
deliveryAddress: { name: 'George', street: 'Main St', postalCode: '12345', postalPlace: 'City', countryCode: 'US' },
lines: [
{ product: 'Widget', amount: 100 }
]
}, { customer: true, deliveryAddress: true, lines: true });Relationship ownership rules
hasMany/hasOne= parent owns children. Deleting the parent cascades to children. Updating the parent can insert/update/delete children.references= independent reference. Deleting the referencing row does NOT delete the referenced row. You can set the reference to null to detach it.
Transactions
Wrap operations in db.transaction(). Use the tx parameter for all operations inside the transaction. If the callback throws, the transaction is rolled back.
import map from './map';
const db = map.sqlite('demo.db');
await db.transaction(async (tx) => {
const customer = await tx.customer.insert({
name: 'Alice',
balance: 100,
isActive: true
});
const order = await tx.order.insert({
orderDate: new Date(),
customer: customer,
lines: [
{ product: 'Widget', amount: 50 }
]
}, { customer: true, lines: true });
// If anything throws here, both inserts are rolled back
});Transaction with saveChanges
await db.transaction(async (tx) => {
const customer = await tx.customer.getById(1);
customer.balance = customer.balance + 50;
await customer.saveChanges();
// This throw will rollback the balance update
throw new Error('This will rollback');
});Active record methods work inside transactions
The saveChanges() method on rows fetched via the tx object runs within that transaction:
await db.transaction(async (tx) => {
const order = await tx.order.getById(1, { lines: true });
order.lines.push({ product: 'New item', amount: 100 });
await order.saveChanges();
// Committed when the callback completes without error
});NOTE: Transactions are not supported for Cloudflare D1.
acceptChanges and clearChanges
These are synchronous Active Record methods available on both individual rows and arrays returned by getMany, getById, insert, etc.
acceptChanges()
Marks the current in-memory values as the new "original" baseline for change tracking. After calling acceptChanges(), the ORM treats the current property values as the unchanged state. This means a subsequent saveChanges() will only send properties modified after the acceptChanges() call.
Use case: You have modified a row in memory but want to skip persisting those changes. Or you want to reset the change-tracking baseline after performing your own custom persistence logic.
const product = await db.product.getById(1);
product.name = 'New name';
product.price = 999;
// Instead of saving, accept the changes as the new baseline
product.acceptChanges();
// Now modifying only price:
product.price = 500;
await product.saveChanges(); // Only sends price=500 to the DB (name='New name' is already accepted)On arrays:
const orders = await db.order.getMany({ lines: true });
orders[0].lines.push({ product: 'Temporary', amount: 0 });
orders.acceptChanges(); // Accepts the current array state as the baselineclearChanges()
Reverts the row (or array) back to the last accepted/original state. It undoes all in-memory mutations since the last acceptChanges() (or since the row was fetched).
Use case: The user cancels an edit form and you want to revert to the original database state without re-fetching.
const product = await db.product.getById(1);
// product.name = 'Bicycle'
product.name = 'Changed name';
product.price = 999;
product.clearChanges();
// product.name is back to 'Bicycle'
// product.price is back to the original valueOn arrays:
const orders = await db.order.getMany({ lines: true });
orders[0].lines.push({ product: 'Temporary', amount: 0 });
orders.clearChanges(); // Reverts the array to its original stateHow they relate to saveChanges and refresh
saveChanges()internally callsacceptChanges()after successfully persisting to the database.refresh()reloads from the database and then callsacceptChanges().clearChanges()reverts to the last accepted state without hitting the database.
Concurrency / Conflict Resolution
Orange uses optimistic concurrency by default. If a property was changed by another user between fetch and save, an exception is thrown.
Three strategies
optimistic(default) — throws if the row was changed by another user.overwrite— overwrites regardless of interim changes.skipOnConflict— silently skips the update if the row was modified.
Set concurrency per-column on saveChanges
const order = await db.order.getById(1);
order.orderDate = new Date();
await order.saveChanges({
orderDate: { concurrency: 'overwrite' }
});Set concurrency at the table level
const db2 = db({
vendor: {
balance: { concurrency: 'skipOnConflict' },
concurrency: 'overwrite'
}
});Upsert using overwrite strategy
const db2 = db({ vendor: { concurrency: 'overwrite' } });
await db2.vendor.insert({ id: 1, name: 'John', balance: 100, isActive: true });
// Insert again with same id — overwrites instead of throwing
await db2.vendor.insert({ id: 1, name: 'George', balance: 200, isActive: false });Fetching Strategies (Column Selection)
Control which columns and relations to include in query results.
Include a relation
const orders = await db.order.getMany({ deliveryAddress: true });Exclude a column
const orders = await db.order.getMany({ orderDate: false });
// Returns all columns except orderDateInclude only specific columns of a relation
const orders = await db.order.getMany({
deliveryAddress: {
countryCode: true,
name: true
}
});Filter within a relation
const orders = await db.order.getMany({
lines: {
where: x => x.product.contains('broomstick')
},
customer: true
});Aggregate Functions
Supported: count, sum, min, max, avg.
Aggregates on each row
const orders = await db.order.getMany({
numberOfLines: x => x.count(x => x.lines.id),
totalAmount: x => x.sum(x => x.lines.amount),
balance: x => x.customer.balance // elevate related data
});Aggregates across all rows (group by)
const results = await db.order.aggregate({
where: x => x.orderDate.greaterThan(new Date(2022, 0, 1)),
customerId: x => x.customerId,
customerName: x => x.customer.name,
numberOfLines: x => x.count(x => x.lines.id),
totals: x => x.sum(x => x.lines.amount)
});Count rows
const count = await db.order.count();
// With a filter:
const filter = db.order.lines.any(line => line.product.contains('broomstick'));
const count = await db.order.count(filter);Data Types
| Orange Type | JS Type | SQL Types |
|---|---|---|
string() |
string |
VARCHAR, TEXT |
numeric() |
number |
INTEGER, DECIMAL, FLOAT, REAL, DOUBLE |
bigint() |
bigint |
BIGINT, INTEGER |
boolean() |
boolean |
BIT, TINYINT(1), INTEGER |
uuid() |
string |
UUID, GUID, VARCHAR |
date() |
string | Date |
DATE, DATETIME, TIMESTAMP |
dateWithTimeZone() |
string | Date |
TIMESTAMP WITH TIME ZONE, DATETIMEOFFSET |
binary() |
string (base64) |
BLOB, BYTEA, VARBINARY |
json() |
object |
JSON, JSONB, NVARCHAR, TEXT |
jsonOf<T>() |
T |
JSON, JSONB, NVARCHAR, TEXT (typed) |
import orange from 'orange-orm';
const map = orange.map(x => ({
demo: x.table('demo').map(x => ({
id: x.column('id').uuid().primary().notNull(),
name: x.column('name').string(),
balance: x.column('balance').numeric(),
regularDate: x.column('regularDate').date(),
tzDate: x.column('tzDate').dateWithTimeZone(),
picture: x.column('picture').binary(),
isActive: x.column('isActive').boolean(),
pet: x.column('pet').jsonOf<{ name: string; kind: string }>(),
data: x.column('data').json(),
}))
}));Enums
Enums can be defined using arrays, objects, as const, or TypeScript enums.
// Array
countryCode: column('countryCode').string().enum(['NO', 'SE', 'DK', 'FI'])
// TypeScript enum
enum CountryCode { NORWAY = 'NO', SWEDEN = 'SE' }
countryCode: column('countryCode').string().enum(CountryCode)
// as const object
const Countries = { NORWAY: 'NO', SWEDEN: 'SE' } as const;
countryCode: column('countryCode').string().enum(Countries)TypeScript Type Safety
Orange provides full IntelliSense without code generation. The map() function returns a fully typed db object.
Type-safe property access
const product = await db.product.getById(1);
// product.name is typed as string | null | undefined
// product.price is typed as number | null | undefined
// product.id is typed as number (notNull)Type-safe inserts
// TypeScript error: 'name' is required (notNull)
await db.product.insert({ price: 100 });
// OK: 'id' is optional because of notNullExceptInsert
await db.product.insert({ name: 'Widget', price: 100 });Type-safe filters
// TypeScript error: greaterThan expects number, not string
db.product.getMany({ where: x => x.price.greaterThan('fifty') });
// OK
db.product.getMany({ where: x => x.price.greaterThan(50) });Extract TypeScript types from your map
type Product = ReturnType<typeof db.product.tsType>;
// { id: number; name?: string | null; price?: number | null }
type ProductWithRelations = ReturnType<typeof db.order.tsType<{ lines: true; customer: true }>>;Browser Usage (Express / Hono Adapters)
Orange can run in the browser. The Express/Hono adapter replays client-side method calls on the server, never exposing raw SQL.
Server (Express)
import map from './map';
import { json } from 'body-parser';
import express from 'express';
import cors from 'cors';
const db = map.sqlite('demo.db');
express().disable('x-powered-by')
.use(json({ limit: '100mb' }))
.use(cors())
.use('/orange', db.express())
.listen(3000);Server (Hono)
import map from './map';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { serve } from '@hono/node-server';
const db = map.sqlite('demo.db');
const app = new Hono();
app.use('/orange', cors());
app.use('/orange/*', cors());
app.all('/orange', db.hono());
app.all('/orange/*', db.hono());
serve({ fetch: app.fetch, port: 3000 });Browser client
import map from './map';
const db = map.http('http://localhost:3000/orange');
const orders = await db.order.getMany({
where: x => x.customer.name.startsWith('Harry'),
lines: true
});Interceptors (authentication)
db.interceptors.request.use((config) => {
config.headers.Authorization = 'Bearer <token>';
return config;
});Base filter (row-level security)
.use('/orange', db.express({
order: {
baseFilter: (db, req, _res) => {
const customerId = Number(req.headers.authorization.split(' ')[1]);
return db.order.customerId.eq(customerId);
}
}
}))Transaction hooks (e.g., Postgres RLS)
.use('/orange', db.express({
hooks: {
transaction: {
afterBegin: async (db, req) => {
await db.query('set local role rls_app_user');
await db.query({ sql: "select set_config('app.tenant_id', ?, true)", parameters: [tenantId] });
}
}
}
}))Raw SQL Queries
const rows = await db.query({
sql: 'SELECT * FROM customer WHERE name LIKE ?',
parameters: ['%arry']
});Raw SQL queries are blocked via HTTP/browser clients (returns 403) to prevent SQL injection.
Logging
import orange from 'orange-orm';
orange.on('query', (e) => {
console.log(e.sql);
if (e.parameters.length > 0) console.log(e.parameters);
});Bulk Operations
update (selective bulk update)
await db.order.update(
{ orderDate: new Date(), customerId: 2 },
{ where: x => x.id.eq(1) }
);
// With fetching strategy to return updated rows:
const orders = await db.order.update(
{ orderDate: new Date() },
{ where: x => x.id.eq(1) },
{ customer: true, lines: true }
);replace (complete overwrite from JSON)
await db.order.replace({
id: 1,
orderDate: '2023-07-14',
lines: [{ id: 1, product: 'Bicycle', amount: 250 }]
}, { lines: true });updateChanges (partial diff update)
const original = { id: 1, name: 'George' };
const modified = { id: 1, name: 'Harry' };
await db.customer.updateChanges(modified, original);Batch Delete
// By filter
await db.order.delete(db.order.customer.name.eq('George'));
// Cascade (also deletes children)
await db.order.deleteCascade(db.order.customer.name.eq('George'));
// By primary keys
await db.customer.delete([{ id: 1 }, { id: 2 }]);Composite Keys
Mark multiple columns as .primary():
const map = orange.map(x => ({
order: x.table('_order').map(({ column }) => ({
orderType: column('orderType').string().primary().notNull(),
orderNo: column('orderNo').numeric().primary().notNull(),
orderDate: column('orderDate').date().notNull(),
})),
orderLine: x.table('orderLine').map(({ column }) => ({
orderType: column('orderType').string().primary().notNull(),
orderNo: column('orderNo').numeric().primary().notNull(),
lineNo: column('lineNo').numeric().primary().notNull(),
product: column('product').string(),
}))
})).map(x => ({
order: x.order.map(v => ({
lines: v.hasMany(x.orderLine).by('orderType', 'orderNo'),
}))
}));Discriminators
Column discriminators
Automatically set a discriminator column value on insert and filter by it on read/delete:
const map = orange.map(x => ({
customer: x.table('client').map(({ column }) => ({
id: column('id').numeric().primary(),
name: column('name').string()
})).columnDiscriminators(`client_type='customer'`),
vendor: x.table('client').map(({ column }) => ({
id: column('id').numeric().primary(),
name: column('name').string()
})).columnDiscriminators(`client_type='vendor'`),
}));Formula discriminators
Use a logical expression instead of a static column value:
const map = orange.map(x => ({
customerBooking: x.table('booking').map(({ column }) => ({
id: column('id').uuid().primary(),
bookingNo: column('booking_no').numeric()
})).formulaDiscriminators('@this.booking_no between 10000 and 99999'),
internalBooking: x.table('booking').map(({ column }) => ({
id: column('id').uuid().primary(),
bookingNo: column('booking_no').numeric()
})).formulaDiscriminators('@this.booking_no between 1000 and 9999'),
}));SQLite User-Defined Functions
const db = map.sqlite('demo.db');
await db.function('add_prefix', (text, prefix) => `${prefix}${text}`);
const rows = await db.query(
"select id, name, add_prefix(name, '[VIP] ') as prefixedName from customer"
);Default Values
import orange from 'orange-orm';
import crypto from 'crypto';
const map = orange.map(x => ({
myTable: x.table('myTable').map(({ column }) => ({
id: column('id').uuid().primary().default(() => crypto.randomUUID()),
name: column('name').string(),
isActive: column('isActive').boolean().default(true),
}))
}));Validation
function validateName(value?: string) {
if (value && value.length > 10)
throw new Error('Length cannot exceed 10 characters');
}
const map = orange.map(x => ({
demo: x.table('demo').map(x => ({
id: x.column('id').uuid().primary().notNullExceptInsert(),
name: x.column('name').string().validate(validateName),
pet: x.column('pet').jsonOf<Pet>().JSONSchema(petSchema),
}))
}));Excluding Sensitive Data
const map = orange.map(x => ({
customer: x.table('customer').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
name: column('name').string(),
balance: column('balance').numeric().serializable(false),
}))
}));
// When serialized: balance is excluded
const george = await db.customer.insert({ name: 'George', balance: 177 });
JSON.stringify(george); // '{"id":1,"name":"George"}'Quick Reference: Active Record Methods
Methods available on rows returned by getMany, getById, getOne, insert:
| Method | On row | On array | Description |
|---|---|---|---|
saveChanges() |
✅ | ✅ | Persist modified properties to the database |
saveChanges(concurrency) |
✅ | ✅ | Persist with concurrency strategy |
acceptChanges() |
✅ | ✅ | Accept current values as the new baseline (sync) |
clearChanges() |
✅ | ✅ | Revert to last accepted/original state (sync) |
refresh() |
✅ | ✅ | Reload from database |
refresh(strategy) |
✅ | ✅ | Reload with fetching strategy |
delete() |
✅ | ✅ | Delete the row(s) from the database |
Quick Reference: Table Client Methods
Methods available on db.<tableName>:
| Method | Description |
|---|---|
getMany(strategy?) |
Fetch multiple rows with optional filter/strategy |
getOne(strategy?) |
Fetch first matching row |
getById(...keys, strategy?) |
Fetch by primary key |
insert(row, strategy?) |
Insert one row |
insert(rows, strategy?) |
Insert multiple rows |
insertAndForget(row) |
Insert without returning |
update(props, {where}, strategy?) |
Bulk update matching rows |
replace(row, strategy?) |
Complete overwrite from JSON |
updateChanges(modified, original, strategy?) |
Partial diff update |
delete(filter?) |
Batch delete |
deleteCascade(filter?) |
Batch delete with cascade |
count(filter?) |
Count matching rows |
aggregate(strategy) |
Aggregate query (group by) |
proxify(row, strategy?) |
Wrap plain object with active record methods |