UNPKG

orange-orm

Version:

Object Relational Mapper

1,378 lines (1,043 loc) 35.5 kB
# Orange ORM — Skills & Reference > Authoritative reference for [context7.com](https://context7.com/alfateam/orange-orm) 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. --- ## Table of Contents 1. [Defining a Model (Table Mapping)](#defining-a-model-table-mapping) 2. [Connecting to a Database](#connecting-to-a-database) 3. [Inserting Rows](#inserting-rows) 4. [Fetching Rows](#fetching-rows) 5. [Filtering (where)](#filtering-where) 6. [Ordering, Limit, Offset](#ordering-limit-offset) 7. [Updating Rows (saveChanges)](#updating-rows-savechanges) 8. [Deleting Rows](#deleting-rows) 9. [Relationships (hasMany, hasOne, references)](#relationships-hasmany-hasone-references) 10. [Transactions](#transactions) 11. [acceptChanges and clearChanges](#acceptchanges-and-clearchanges) 12. [Concurrency / Conflict Resolution](#concurrency--conflict-resolution) 13. [Fetching Strategies (Column Selection)](#fetching-strategies-column-selection) 14. [Aggregate Functions](#aggregate-functions) 15. [Data Types](#data-types) 16. [Enums](#enums) 17. [TypeScript Type Safety](#typescript-type-safety) 18. [Browser Usage (Express / Hono Adapters)](#browser-usage-express--hono-adapters) 19. [Raw SQL Queries](#raw-sql-queries) 20. [Logging](#logging) 21. [Bulk Operations (update, replace, updateChanges)](#bulk-operations) 22. [Batch Delete](#batch-delete) 23. [Composite Keys](#composite-keys) 24. [Discriminators](#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. ```ts 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/varchar - `column('col').numeric()` — integer/decimal/float - `column('col').bigint()` — bigint - `column('col').boolean()` — boolean/bit - `column('col').uuid()` — UUID as string - `column('col').date()` — date/datetime as ISO 8601 string - `column('col').dateWithTimeZone()` — timestamp with timezone - `column('col').binary()` — binary/blob as base64 string - `column('col').json()` — JSON object - `column('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 ```ts 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 ```ts import map from './map'; const db = map.sqlite('demo.db'); ``` With connection pool: ```ts const db = map.sqlite('demo.db', { size: 10 }); ``` ### PostgreSQL ```ts import map from './map'; const db = map.postgres('postgres://user:pass@host/dbname'); ``` ### MySQL ```ts import map from './map'; const db = map.mysql('mysql://user:pass@host/dbname'); ``` ### MS SQL ```ts import map from './map'; const db = map.mssql({ server: 'mssql', options: { encrypt: false, database: 'test' }, authentication: { type: 'default', options: { userName: 'sa', password: 'password' } } }); ``` ### Oracle ```ts import map from './map'; const db = map.oracle({ user: 'sys', password: 'pass', connectString: 'oracle/XE', privilege: 2 }); ``` ### PGlite (in-memory Postgres) ```ts import map from './map'; const db = map.pglite(); ``` ### Cloudflare D1 ```ts import map from './map'; const db = map.d1(env.DB); ``` ### HTTP (browser client) ```ts import map from './map'; const db = map.http('http://localhost:3000/orange'); ``` ### Closing connections (important for serverless) ```ts 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 ```ts 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 ```ts 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: ```ts 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) ```ts await db.product.insertAndForget({ name: 'Bicycle', price: 250 }); ``` --- ## Fetching Rows ### Get all rows ```ts const products = await db.product.getMany(); ``` ### Get a single row by primary key (getById) ```ts const product = await db.product.getById(1); // Returns the row or undefined if not found ``` With a fetching strategy: ```ts const order = await db.order.getById(1, { customer: true, deliveryAddress: true, lines: true }); ``` ### Composite primary key getById ```ts const line = await db.orderLine.getById('typeA', 100, 1); // Arguments match the order of primary key columns ``` ### Get one row (first match) ```ts const product = await db.product.getOne({ where: x => x.name.eq('Bicycle') }); ``` ### Get many rows with a fetching strategy ```ts 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 ```ts const products = await db.product.getMany({ where: x => x.price.greaterThan(50), orderBy: 'name' }); ``` ### Combining filters with and/or/not ```ts 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 ```ts 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 ```ts // 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 ```ts const rows = await db.order.getMany({ where: x => x.deliveryAddress.exists() }); ``` ### Building filters separately (reusable) ```ts const filter = db.order.customer.name.startsWith('Harry'); const orders = await db.order.getMany({ where: filter, customer: true }); ``` ### Column-to-column comparison ```ts const orders = await db.order.getMany({ where: x => x.deliveryAddress.name.eq(x.customer.name) }); ``` ### Raw SQL filter ```ts const rows = await db.customer.getMany({ where: () => ({ sql: 'name like ?', parameters: ['%arry'] }) }); ``` --- ## Ordering, Limit, Offset ```ts const products = await db.product.getMany({ orderBy: 'name', limit: 10, offset: 5 }); ``` ### Multiple order-by columns ```ts const products = await db.product.getMany({ orderBy: ['price desc', 'name'] }); ``` ### Ordering within relations ```ts const orders = await db.order.getMany({ orderBy: ['orderDate desc', 'id'], lines: { orderBy: 'product' } }); ``` ### Complete example: filter + order + limit ```ts 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 ```ts const product = await db.product.getById(1); product.price = 299; await product.saveChanges(); ``` ### Update related rows (hasMany / hasOne) ```ts 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 ```ts 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 ```ts await db.order.update( { orderDate: new Date() }, { where: x => x.id.eq(1) } ); ``` ### Replace a row from JSON (complete overwrite) ```ts 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) ```ts 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 ```ts const product = await db.product.getById(1); await product.delete(); ``` ### Delete an element from an array then save ```ts const orders = await db.order.getMany({ lines: true }); orders.splice(1, 1); // remove second order await orders.saveChanges(); // persists the deletion ``` ### Delete many rows (filtered) ```ts const orders = await db.order.getMany({ where: x => x.customer.name.eq('George') }); await orders.delete(); ``` ### Batch delete by filter ```ts const filter = db.order.deliveryAddress.name.eq('George'); await db.order.delete(filter); ``` ### Batch delete cascade Cascade deletes also remove child rows (hasOne/hasMany): ```ts const filter = db.order.deliveryAddress.name.eq('George'); await db.order.deleteCascade(filter); ``` ### Batch delete by primary key ```ts 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 of `hasMany` — the database models them identically (the target table has a foreign key pointing to the parent's primary key). The only difference is that `hasOne` returns 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** from `hasMany`/`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) ```ts 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 ```ts 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 ```ts const orders = await db.order.getMany({ customer: true, deliveryAddress: true, lines: { packages: true } }); ``` ### Insert with nested relations ```ts 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. ```ts 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 ```ts 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: ```ts 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. ```ts 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: ```ts 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 baseline ``` ### clearChanges() 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. ```ts 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 value ``` On arrays: ```ts const orders = await db.order.getMany({ lines: true }); orders[0].lines.push({ product: 'Temporary', amount: 0 }); orders.clearChanges(); // Reverts the array to its original state ``` ### How they relate to saveChanges and refresh - `saveChanges()` internally calls `acceptChanges()` after successfully persisting to the database. - `refresh()` reloads from the database and then calls `acceptChanges()`. - `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 ```ts const order = await db.order.getById(1); order.orderDate = new Date(); await order.saveChanges({ orderDate: { concurrency: 'overwrite' } }); ``` ### Set concurrency at the table level ```ts const db2 = db({ vendor: { balance: { concurrency: 'skipOnConflict' }, concurrency: 'overwrite' } }); ``` ### Upsert using overwrite strategy ```ts 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 ```ts const orders = await db.order.getMany({ deliveryAddress: true }); ``` ### Exclude a column ```ts const orders = await db.order.getMany({ orderDate: false }); // Returns all columns except orderDate ``` ### Include only specific columns of a relation ```ts const orders = await db.order.getMany({ deliveryAddress: { countryCode: true, name: true } }); ``` ### Filter within a relation ```ts 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 ```ts 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) ```ts 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 ```ts 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) | ```ts 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. ```ts // 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 ```ts 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 ```ts // 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 ```ts // 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 ```ts 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) ```ts 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) ```ts 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 ```ts 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) ```ts db.interceptors.request.use((config) => { config.headers.Authorization = 'Bearer <token>'; return config; }); ``` ### Base filter (row-level security) ```ts .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) ```ts .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 ```ts 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 ```ts 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) ```ts 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) ```ts await db.order.replace({ id: 1, orderDate: '2023-07-14', lines: [{ id: 1, product: 'Bicycle', amount: 250 }] }, { lines: true }); ``` ### updateChanges (partial diff update) ```ts const original = { id: 1, name: 'George' }; const modified = { id: 1, name: 'Harry' }; await db.customer.updateChanges(modified, original); ``` --- ## Batch Delete ```ts // 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()`: ```ts 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: ```ts 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: ```ts 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 ```ts 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 ```ts 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 ```ts 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 ```ts 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 |