orange-orm 5.2.0-beta.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md ADDED
@@ -0,0 +1,1377 @@
1
+ # Orange ORM — Skills & Reference
2
+
3
+ > Authoritative reference for [context7.com](https://context7.com/alfateam/orange-orm) MCP consumption.
4
+ > Orange ORM is the ultimate Object Relational Mapper for Node.js, Bun, and Deno.
5
+ > It uses the **Active Record Pattern** with full TypeScript IntelliSense — no code generation required.
6
+ > Supports: PostgreSQL, SQLite, MySQL, MS SQL, Oracle, SAP ASE, PGlite, Cloudflare D1.
7
+ > Works in the browser via Express/Hono adapters.
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ 1. [Defining a Model (Table Mapping)](#defining-a-model-table-mapping)
14
+ 2. [Connecting to a Database](#connecting-to-a-database)
15
+ 3. [Inserting Rows](#inserting-rows)
16
+ 4. [Fetching Rows](#fetching-rows)
17
+ 5. [Filtering (where)](#filtering-where)
18
+ 6. [Ordering, Limit, Offset](#ordering-limit-offset)
19
+ 7. [Updating Rows (saveChanges)](#updating-rows-savechanges)
20
+ 8. [Deleting Rows](#deleting-rows)
21
+ 9. [Relationships (hasMany, hasOne, references)](#relationships-hasmany-hasone-references)
22
+ 10. [Transactions](#transactions)
23
+ 11. [acceptChanges and clearChanges](#acceptchanges-and-clearchanges)
24
+ 12. [Concurrency / Conflict Resolution](#concurrency--conflict-resolution)
25
+ 13. [Fetching Strategies (Column Selection)](#fetching-strategies-column-selection)
26
+ 14. [Aggregate Functions](#aggregate-functions)
27
+ 15. [Data Types](#data-types)
28
+ 16. [Enums](#enums)
29
+ 17. [TypeScript Type Safety](#typescript-type-safety)
30
+ 18. [Browser Usage (Express / Hono Adapters)](#browser-usage-express--hono-adapters)
31
+ 19. [Raw SQL Queries](#raw-sql-queries)
32
+ 20. [Logging](#logging)
33
+ 21. [Bulk Operations (update, replace, updateChanges)](#bulk-operations)
34
+ 22. [Batch Delete](#batch-delete)
35
+ 23. [Composite Keys](#composite-keys)
36
+ 24. [Discriminators](#discriminators)
37
+
38
+ ---
39
+
40
+ ## Defining a Model (Table Mapping)
41
+
42
+ Use `orange.map()` to define tables and columns. Each column specifies its database column name, data type, and constraints.
43
+
44
+ **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.
45
+
46
+ ```ts
47
+ import orange from 'orange-orm';
48
+
49
+ const map = orange.map(x => ({
50
+ product: x.table('product').map(({ column }) => ({
51
+ id: column('id').numeric().primary().notNullExceptInsert(),
52
+ name: column('name').string().notNull(),
53
+ price: column('price').numeric(),
54
+ }))
55
+ }));
56
+
57
+ export default map;
58
+ ```
59
+
60
+ ### Column types available
61
+
62
+ - `column('col').string()` — text/varchar
63
+ - `column('col').numeric()` — integer/decimal/float
64
+ - `column('col').bigint()` — bigint
65
+ - `column('col').boolean()` — boolean/bit
66
+ - `column('col').uuid()` — UUID as string
67
+ - `column('col').date()` — date/datetime as ISO 8601 string
68
+ - `column('col').dateWithTimeZone()` — timestamp with timezone
69
+ - `column('col').binary()` — binary/blob as base64 string
70
+ - `column('col').json()` — JSON object
71
+ - `column('col').jsonOf<T>()` — typed JSON (TypeScript generic)
72
+
73
+ ### Column modifiers
74
+
75
+ - `.primary()` — marks as primary key
76
+ - `.notNull()` — required, never null
77
+ - `.notNullExceptInsert()` — required on read, optional on insert (for autoincrement keys)
78
+ - `.default(value)` — default value or factory function
79
+ - `.validate(fn)` — custom validation function
80
+ - `.JSONSchema(schema)` — AJV JSON schema validation
81
+ - `.serializable(false)` — exclude from JSON serialization
82
+ - `.enum(values)` — restrict to enum values (array, object, or TypeScript enum)
83
+
84
+ ### Multiple tables example
85
+
86
+ ```ts
87
+ import orange from 'orange-orm';
88
+
89
+ const map = orange.map(x => ({
90
+ customer: x.table('customer').map(({ column }) => ({
91
+ id: column('id').numeric().primary().notNullExceptInsert(),
92
+ name: column('name').string(),
93
+ balance: column('balance').numeric(),
94
+ isActive: column('isActive').boolean(),
95
+ })),
96
+
97
+ order: x.table('_order').map(({ column }) => ({
98
+ id: column('id').numeric().primary().notNullExceptInsert(),
99
+ orderDate: column('orderDate').date().notNull(),
100
+ customerId: column('customerId').numeric().notNullExceptInsert(),
101
+ })),
102
+
103
+ orderLine: x.table('orderLine').map(({ column }) => ({
104
+ id: column('id').numeric().primary(),
105
+ orderId: column('orderId').numeric(),
106
+ product: column('product').string(),
107
+ amount: column('amount').numeric(),
108
+ })),
109
+
110
+ deliveryAddress: x.table('deliveryAddress').map(({ column }) => ({
111
+ id: column('id').numeric().primary(),
112
+ orderId: column('orderId').numeric(),
113
+ name: column('name').string(),
114
+ street: column('street').string(),
115
+ postalCode: column('postalCode').string(),
116
+ postalPlace: column('postalPlace').string(),
117
+ countryCode: column('countryCode').string(),
118
+ }))
119
+ }));
120
+
121
+ export default map;
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Connecting to a Database
127
+
128
+ After defining your map, call a connector method to get a `db` client.
129
+
130
+ ### SQLite
131
+
132
+ ```ts
133
+ import map from './map';
134
+ const db = map.sqlite('demo.db');
135
+ ```
136
+
137
+ With connection pool:
138
+ ```ts
139
+ const db = map.sqlite('demo.db', { size: 10 });
140
+ ```
141
+
142
+ ### PostgreSQL
143
+
144
+ ```ts
145
+ import map from './map';
146
+ const db = map.postgres('postgres://user:pass@host/dbname');
147
+ ```
148
+
149
+ ### MySQL
150
+
151
+ ```ts
152
+ import map from './map';
153
+ const db = map.mysql('mysql://user:pass@host/dbname');
154
+ ```
155
+
156
+ ### MS SQL
157
+
158
+ ```ts
159
+ import map from './map';
160
+ const db = map.mssql({
161
+ server: 'mssql',
162
+ options: { encrypt: false, database: 'test' },
163
+ authentication: {
164
+ type: 'default',
165
+ options: { userName: 'sa', password: 'password' }
166
+ }
167
+ });
168
+ ```
169
+
170
+ ### Oracle
171
+
172
+ ```ts
173
+ import map from './map';
174
+ const db = map.oracle({ user: 'sys', password: 'pass', connectString: 'oracle/XE', privilege: 2 });
175
+ ```
176
+
177
+ ### PGlite (in-memory Postgres)
178
+
179
+ ```ts
180
+ import map from './map';
181
+ const db = map.pglite();
182
+ ```
183
+
184
+ ### Cloudflare D1
185
+
186
+ ```ts
187
+ import map from './map';
188
+ const db = map.d1(env.DB);
189
+ ```
190
+
191
+ ### HTTP (browser client)
192
+
193
+ ```ts
194
+ import map from './map';
195
+ const db = map.http('http://localhost:3000/orange');
196
+ ```
197
+
198
+ ### Closing connections (important for serverless)
199
+
200
+ ```ts
201
+ await db.close();
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Inserting Rows
207
+
208
+ Use `db.<table>.insert()` to insert one or more rows. Returns the inserted row(s) with active record methods.
209
+
210
+ ### Insert a single row
211
+
212
+ ```ts
213
+ import map from './map';
214
+ const db = map.sqlite('demo.db');
215
+
216
+ const product = await db.product.insert({
217
+ name: 'Bicycle',
218
+ price: 250
219
+ });
220
+ // product = { id: 1, name: 'Bicycle', price: 250 }
221
+ // product has .saveChanges(), .delete(), etc.
222
+ ```
223
+
224
+ ### Insert multiple rows
225
+
226
+ ```ts
227
+ const products = await db.product.insert([
228
+ { name: 'Bicycle', price: 250 },
229
+ { name: 'Guitar', price: 150 }
230
+ ]);
231
+ ```
232
+
233
+ ### Insert with a fetching strategy
234
+
235
+ The second argument controls which relations to eager-load after insert:
236
+
237
+ ```ts
238
+ const order = await db.order.insert({
239
+ orderDate: new Date(),
240
+ customer: george,
241
+ deliveryAddress: {
242
+ name: 'George',
243
+ street: 'Node street 1',
244
+ postalCode: '7059',
245
+ postalPlace: 'Jakobsli',
246
+ countryCode: 'NO'
247
+ },
248
+ lines: [
249
+ { product: 'Bicycle', amount: 250 },
250
+ { product: 'Guitar', amount: 150 }
251
+ ]
252
+ }, { customer: true, deliveryAddress: true, lines: true });
253
+ ```
254
+
255
+ ### Insert and forget (no return value)
256
+
257
+ ```ts
258
+ await db.product.insertAndForget({ name: 'Bicycle', price: 250 });
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Fetching Rows
264
+
265
+ ### Get all rows
266
+
267
+ ```ts
268
+ const products = await db.product.getMany();
269
+ ```
270
+
271
+ ### Get a single row by primary key (getById)
272
+
273
+ ```ts
274
+ const product = await db.product.getById(1);
275
+ // Returns the row or undefined if not found
276
+ ```
277
+
278
+ With a fetching strategy:
279
+ ```ts
280
+ const order = await db.order.getById(1, {
281
+ customer: true,
282
+ deliveryAddress: true,
283
+ lines: true
284
+ });
285
+ ```
286
+
287
+ ### Composite primary key getById
288
+
289
+ ```ts
290
+ const line = await db.orderLine.getById('typeA', 100, 1);
291
+ // Arguments match the order of primary key columns
292
+ ```
293
+
294
+ ### Get one row (first match)
295
+
296
+ ```ts
297
+ const product = await db.product.getOne({
298
+ where: x => x.name.eq('Bicycle')
299
+ });
300
+ ```
301
+
302
+ ### Get many rows with a fetching strategy
303
+
304
+ ```ts
305
+ const orders = await db.order.getMany({
306
+ customer: true,
307
+ deliveryAddress: true,
308
+ lines: {
309
+ packages: true
310
+ }
311
+ });
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Filtering (where)
317
+
318
+ Use the `where` option in `getMany` or `getOne`. The callback receives a row reference with column filter methods.
319
+
320
+ ### Comparison operators
321
+
322
+ All column types support:
323
+ - `.equal(value)` / `.eq(value)` — equal
324
+ - `.notEqual(value)` / `.ne(value)` — not equal
325
+ - `.lessThan(value)` / `.lt(value)` — less than
326
+ - `.lessThanOrEqual(value)` / `.le(value)` — less than or equal
327
+ - `.greaterThan(value)` / `.gt(value)` — greater than
328
+ - `.greaterThanOrEqual(value)` / `.ge(value)` — greater than or equal
329
+ - `.between(from, to)` — between two values (inclusive)
330
+ - `.in(values)` — in a list of values
331
+
332
+ String columns also support:
333
+ - `.startsWith(value)` — starts with
334
+ - `.endsWith(value)` — ends with
335
+ - `.contains(value)` — contains substring
336
+ - `.iStartsWith(value)`, `.iEndsWith(value)`, `.iContains(value)`, `.iEqual(value)` — case-insensitive (Postgres only)
337
+
338
+ ### Filter by column value
339
+
340
+ ```ts
341
+ const products = await db.product.getMany({
342
+ where: x => x.price.greaterThan(50),
343
+ orderBy: 'name'
344
+ });
345
+ ```
346
+
347
+ ### Combining filters with and/or/not
348
+
349
+ ```ts
350
+ const products = await db.product.getMany({
351
+ where: x => x.price.greaterThan(50)
352
+ .and(x.name.startsWith('B'))
353
+ });
354
+
355
+ const products = await db.product.getMany({
356
+ where: x => x.name.eq('Bicycle')
357
+ .or(x.name.eq('Guitar'))
358
+ });
359
+
360
+ const products = await db.product.getMany({
361
+ where: x => x.name.eq('Bicycle').not()
362
+ });
363
+ ```
364
+
365
+ ### Filter across relations
366
+
367
+ ```ts
368
+ const orders = await db.order.getMany({
369
+ where: x => x.customer.name.startsWith('Harry')
370
+ .and(x.lines.any(line => line.product.contains('broomstick'))),
371
+ customer: true,
372
+ lines: true
373
+ });
374
+ ```
375
+
376
+ ### Relation sub-filters: any, all, none, count
377
+
378
+ ```ts
379
+ // Orders that have at least one line containing 'guitar'
380
+ const rows = await db.order.getMany({
381
+ where: x => x.lines.any(line => line.product.contains('guitar'))
382
+ });
383
+
384
+ // Orders where ALL lines contain 'a'
385
+ const rows = await db.order.getMany({
386
+ where: x => x.lines.all(line => line.product.contains('a'))
387
+ });
388
+
389
+ // Orders with NO lines equal to 'Magic wand'
390
+ const rows = await db.order.getMany({
391
+ where: x => x.lines.none(line => line.product.eq('Magic wand'))
392
+ });
393
+
394
+ // Orders with at most 1 line
395
+ const rows = await db.order.getMany({
396
+ where: x => x.lines.count().le(1)
397
+ });
398
+ ```
399
+
400
+ ### exists filter
401
+
402
+ ```ts
403
+ const rows = await db.order.getMany({
404
+ where: x => x.deliveryAddress.exists()
405
+ });
406
+ ```
407
+
408
+ ### Building filters separately (reusable)
409
+
410
+ ```ts
411
+ const filter = db.order.customer.name.startsWith('Harry');
412
+ const orders = await db.order.getMany({
413
+ where: filter,
414
+ customer: true
415
+ });
416
+ ```
417
+
418
+ ### Column-to-column comparison
419
+
420
+ ```ts
421
+ const orders = await db.order.getMany({
422
+ where: x => x.deliveryAddress.name.eq(x.customer.name)
423
+ });
424
+ ```
425
+
426
+ ### Raw SQL filter
427
+
428
+ ```ts
429
+ const rows = await db.customer.getMany({
430
+ where: () => ({
431
+ sql: 'name like ?',
432
+ parameters: ['%arry']
433
+ })
434
+ });
435
+ ```
436
+
437
+ ---
438
+
439
+ ## Ordering, Limit, Offset
440
+
441
+ ```ts
442
+ const products = await db.product.getMany({
443
+ orderBy: 'name',
444
+ limit: 10,
445
+ offset: 5
446
+ });
447
+ ```
448
+
449
+ ### Multiple order-by columns
450
+
451
+ ```ts
452
+ const products = await db.product.getMany({
453
+ orderBy: ['price desc', 'name']
454
+ });
455
+ ```
456
+
457
+ ### Ordering within relations
458
+
459
+ ```ts
460
+ const orders = await db.order.getMany({
461
+ orderBy: ['orderDate desc', 'id'],
462
+ lines: {
463
+ orderBy: 'product'
464
+ }
465
+ });
466
+ ```
467
+
468
+ ### Complete example: filter + order + limit
469
+
470
+ ```ts
471
+ const products = await db.product.getMany({
472
+ where: x => x.price.greaterThan(50),
473
+ orderBy: 'name',
474
+ limit: 10,
475
+ offset: 0
476
+ });
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Updating Rows (saveChanges)
482
+
483
+ Orange uses the **Active Record Pattern**. Fetch a row, modify its properties, then call `saveChanges()`. Only changed columns are sent to the database.
484
+
485
+ ### Update a single row
486
+
487
+ ```ts
488
+ const product = await db.product.getById(1);
489
+ product.price = 299;
490
+ await product.saveChanges();
491
+ ```
492
+
493
+ ### Update related rows (hasMany / hasOne)
494
+
495
+ ```ts
496
+ const order = await db.order.getById(1, {
497
+ deliveryAddress: true,
498
+ lines: true
499
+ });
500
+
501
+ order.orderDate = new Date();
502
+ order.deliveryAddress = null; // deletes the hasOne child
503
+ order.lines.push({ product: 'Cloak of invisibility', amount: 600 }); // adds a new line
504
+
505
+ await order.saveChanges();
506
+ ```
507
+
508
+ ### Update multiple rows at once
509
+
510
+ ```ts
511
+ let orders = await db.order.getMany({
512
+ orderBy: 'id',
513
+ lines: true,
514
+ deliveryAddress: true,
515
+ customer: true
516
+ });
517
+
518
+ orders[0].orderDate = new Date();
519
+ orders[0].deliveryAddress.street = 'Node street 2';
520
+ orders[0].lines[1].product = 'Big guitar';
521
+
522
+ orders[1].deliveryAddress = null;
523
+ orders[1].lines.push({ product: 'Cloak of invisibility', amount: 600 });
524
+
525
+ await orders.saveChanges();
526
+ ```
527
+
528
+ ### Selective update (bulk) with where
529
+
530
+ ```ts
531
+ await db.order.update(
532
+ { orderDate: new Date() },
533
+ { where: x => x.id.eq(1) }
534
+ );
535
+ ```
536
+
537
+ ### Replace a row from JSON (complete overwrite)
538
+
539
+ ```ts
540
+ const order = await db.order.replace({
541
+ id: 1,
542
+ orderDate: '2023-07-14T12:00:00',
543
+ customer: { id: 2 },
544
+ deliveryAddress: { name: 'Roger', street: 'Node street 1', postalCode: '7059', postalPlace: 'Jakobsli', countryCode: 'NO' },
545
+ lines: [
546
+ { id: 1, product: 'Bicycle', amount: 250 },
547
+ { product: 'Piano', amount: 800 }
548
+ ]
549
+ }, { customer: true, deliveryAddress: true, lines: true });
550
+ ```
551
+
552
+ ### Partial update from JSON diff (updateChanges)
553
+
554
+ ```ts
555
+ const original = { id: 1, orderDate: '2023-07-14T12:00:00', lines: [{ id: 1, product: 'Bicycle', amount: 250 }] };
556
+ const modified = JSON.parse(JSON.stringify(original));
557
+ modified.lines.push({ product: 'Piano', amount: 800 });
558
+
559
+ const order = await db.order.updateChanges(modified, original, { lines: true });
560
+ ```
561
+
562
+ ---
563
+
564
+ ## Deleting Rows
565
+
566
+ ### Delete a single row
567
+
568
+ ```ts
569
+ const product = await db.product.getById(1);
570
+ await product.delete();
571
+ ```
572
+
573
+ ### Delete an element from an array then save
574
+
575
+ ```ts
576
+ const orders = await db.order.getMany({ lines: true });
577
+ orders.splice(1, 1); // remove second order
578
+ await orders.saveChanges(); // persists the deletion
579
+ ```
580
+
581
+ ### Delete many rows (filtered)
582
+
583
+ ```ts
584
+ const orders = await db.order.getMany({
585
+ where: x => x.customer.name.eq('George')
586
+ });
587
+ await orders.delete();
588
+ ```
589
+
590
+ ### Batch delete by filter
591
+
592
+ ```ts
593
+ const filter = db.order.deliveryAddress.name.eq('George');
594
+ await db.order.delete(filter);
595
+ ```
596
+
597
+ ### Batch delete cascade
598
+
599
+ Cascade deletes also remove child rows (hasOne/hasMany):
600
+
601
+ ```ts
602
+ const filter = db.order.deliveryAddress.name.eq('George');
603
+ await db.order.deleteCascade(filter);
604
+ ```
605
+
606
+ ### Batch delete by primary key
607
+
608
+ ```ts
609
+ await db.customer.delete([{ id: 1 }, { id: 2 }]);
610
+ ```
611
+
612
+ ---
613
+
614
+ ## Relationships (hasMany, hasOne, references)
615
+
616
+ Relationships are defined in a second `.map()` call chained after the table definitions.
617
+
618
+ - **`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**.
619
+ - **`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).
620
+ - **`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).
621
+
622
+ ### Example: Author and Book (one-to-many)
623
+
624
+ ```ts
625
+ import orange from 'orange-orm';
626
+
627
+ const map = orange.map(x => ({
628
+ author: x.table('author').map(({ column }) => ({
629
+ id: column('id').numeric().primary().notNullExceptInsert(),
630
+ name: column('name').string().notNull(),
631
+ })),
632
+
633
+ book: x.table('book').map(({ column }) => ({
634
+ id: column('id').numeric().primary().notNullExceptInsert(),
635
+ authorId: column('authorId').numeric().notNull(),
636
+ title: column('title').string().notNull(),
637
+ year: column('year').numeric(),
638
+ }))
639
+ })).map(x => ({
640
+ author: x.author.map(({ hasMany }) => ({
641
+ books: hasMany(x.book).by('authorId')
642
+ })),
643
+ book: x.book.map(({ references }) => ({
644
+ author: references(x.author).by('authorId')
645
+ }))
646
+ }));
647
+
648
+ export default map;
649
+ ```
650
+
651
+ ### Query author with all their books
652
+
653
+ ```ts
654
+ import map from './map';
655
+ const db = map.sqlite('demo.db');
656
+
657
+ const author = await db.author.getById(1, {
658
+ books: true
659
+ });
660
+ // author.books is an array of { id, authorId, title, year }
661
+ console.log(author.name);
662
+ author.books.forEach(book => console.log(book.title));
663
+ ```
664
+
665
+ ### Query with nested relations
666
+
667
+ ```ts
668
+ const orders = await db.order.getMany({
669
+ customer: true,
670
+ deliveryAddress: true,
671
+ lines: {
672
+ packages: true
673
+ }
674
+ });
675
+ ```
676
+
677
+ ### Insert with nested relations
678
+
679
+ ```ts
680
+ const order = await db.order.insert({
681
+ orderDate: new Date(),
682
+ customer: george,
683
+ deliveryAddress: { name: 'George', street: 'Main St', postalCode: '12345', postalPlace: 'City', countryCode: 'US' },
684
+ lines: [
685
+ { product: 'Widget', amount: 100 }
686
+ ]
687
+ }, { customer: true, deliveryAddress: true, lines: true });
688
+ ```
689
+
690
+ ### Relationship ownership rules
691
+
692
+ - `hasMany` / `hasOne` = parent **owns** children. Deleting the parent cascades to children. Updating the parent can insert/update/delete children.
693
+ - `references` = independent reference. Deleting the referencing row does NOT delete the referenced row. You can set the reference to null to detach it.
694
+
695
+ ---
696
+
697
+ ## Transactions
698
+
699
+ Wrap operations in `db.transaction()`. Use the `tx` parameter for all operations inside the transaction. If the callback throws, the transaction is rolled back.
700
+
701
+ ```ts
702
+ import map from './map';
703
+ const db = map.sqlite('demo.db');
704
+
705
+ await db.transaction(async (tx) => {
706
+ const customer = await tx.customer.insert({
707
+ name: 'Alice',
708
+ balance: 100,
709
+ isActive: true
710
+ });
711
+
712
+ const order = await tx.order.insert({
713
+ orderDate: new Date(),
714
+ customer: customer,
715
+ lines: [
716
+ { product: 'Widget', amount: 50 }
717
+ ]
718
+ }, { customer: true, lines: true });
719
+
720
+ // If anything throws here, both inserts are rolled back
721
+ });
722
+ ```
723
+
724
+ ### Transaction with saveChanges
725
+
726
+ ```ts
727
+ await db.transaction(async (tx) => {
728
+ const customer = await tx.customer.getById(1);
729
+ customer.balance = customer.balance + 50;
730
+ await customer.saveChanges();
731
+
732
+ // This throw will rollback the balance update
733
+ throw new Error('This will rollback');
734
+ });
735
+ ```
736
+
737
+ ### Active record methods work inside transactions
738
+
739
+ The `saveChanges()` method on rows fetched via the `tx` object runs within that transaction:
740
+
741
+ ```ts
742
+ await db.transaction(async (tx) => {
743
+ const order = await tx.order.getById(1, { lines: true });
744
+ order.lines.push({ product: 'New item', amount: 100 });
745
+ await order.saveChanges();
746
+ // Committed when the callback completes without error
747
+ });
748
+ ```
749
+
750
+ **NOTE**: Transactions are not supported for Cloudflare D1.
751
+
752
+ ---
753
+
754
+ ## acceptChanges and clearChanges
755
+
756
+ These are **synchronous** Active Record methods available on both individual rows and arrays returned by `getMany`, `getById`, `insert`, etc.
757
+
758
+ ### acceptChanges()
759
+
760
+ 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.
761
+
762
+ **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.
763
+
764
+ ```ts
765
+ const product = await db.product.getById(1);
766
+ product.name = 'New name';
767
+ product.price = 999;
768
+
769
+ // Instead of saving, accept the changes as the new baseline
770
+ product.acceptChanges();
771
+
772
+ // Now modifying only price:
773
+ product.price = 500;
774
+ await product.saveChanges(); // Only sends price=500 to the DB (name='New name' is already accepted)
775
+ ```
776
+
777
+ On arrays:
778
+ ```ts
779
+ const orders = await db.order.getMany({ lines: true });
780
+ orders[0].lines.push({ product: 'Temporary', amount: 0 });
781
+ orders.acceptChanges(); // Accepts the current array state as the baseline
782
+ ```
783
+
784
+ ### clearChanges()
785
+
786
+ 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).
787
+
788
+ **Use case**: The user cancels an edit form and you want to revert to the original database state without re-fetching.
789
+
790
+ ```ts
791
+ const product = await db.product.getById(1);
792
+ // product.name = 'Bicycle'
793
+
794
+ product.name = 'Changed name';
795
+ product.price = 999;
796
+
797
+ product.clearChanges();
798
+ // product.name is back to 'Bicycle'
799
+ // product.price is back to the original value
800
+ ```
801
+
802
+ On arrays:
803
+ ```ts
804
+ const orders = await db.order.getMany({ lines: true });
805
+ orders[0].lines.push({ product: 'Temporary', amount: 0 });
806
+ orders.clearChanges(); // Reverts the array to its original state
807
+ ```
808
+
809
+ ### How they relate to saveChanges and refresh
810
+
811
+ - `saveChanges()` internally calls `acceptChanges()` after successfully persisting to the database.
812
+ - `refresh()` reloads from the database and then calls `acceptChanges()`.
813
+ - `clearChanges()` reverts to the last accepted state without hitting the database.
814
+
815
+ ---
816
+
817
+ ## Concurrency / Conflict Resolution
818
+
819
+ Orange uses **optimistic concurrency** by default. If a property was changed by another user between fetch and save, an exception is thrown.
820
+
821
+ ### Three strategies
822
+
823
+ - **`optimistic`** (default) — throws if the row was changed by another user.
824
+ - **`overwrite`** — overwrites regardless of interim changes.
825
+ - **`skipOnConflict`** — silently skips the update if the row was modified.
826
+
827
+ ### Set concurrency per-column on saveChanges
828
+
829
+ ```ts
830
+ const order = await db.order.getById(1);
831
+ order.orderDate = new Date();
832
+ await order.saveChanges({
833
+ orderDate: { concurrency: 'overwrite' }
834
+ });
835
+ ```
836
+
837
+ ### Set concurrency at the table level
838
+
839
+ ```ts
840
+ const db2 = db({
841
+ vendor: {
842
+ balance: { concurrency: 'skipOnConflict' },
843
+ concurrency: 'overwrite'
844
+ }
845
+ });
846
+ ```
847
+
848
+ ### Upsert using overwrite strategy
849
+
850
+ ```ts
851
+ const db2 = db({ vendor: { concurrency: 'overwrite' } });
852
+ await db2.vendor.insert({ id: 1, name: 'John', balance: 100, isActive: true });
853
+ // Insert again with same id — overwrites instead of throwing
854
+ await db2.vendor.insert({ id: 1, name: 'George', balance: 200, isActive: false });
855
+ ```
856
+
857
+ ---
858
+
859
+ ## Fetching Strategies (Column Selection)
860
+
861
+ Control which columns and relations to include in query results.
862
+
863
+ ### Include a relation
864
+
865
+ ```ts
866
+ const orders = await db.order.getMany({ deliveryAddress: true });
867
+ ```
868
+
869
+ ### Exclude a column
870
+
871
+ ```ts
872
+ const orders = await db.order.getMany({ orderDate: false });
873
+ // Returns all columns except orderDate
874
+ ```
875
+
876
+ ### Include only specific columns of a relation
877
+
878
+ ```ts
879
+ const orders = await db.order.getMany({
880
+ deliveryAddress: {
881
+ countryCode: true,
882
+ name: true
883
+ }
884
+ });
885
+ ```
886
+
887
+ ### Filter within a relation
888
+
889
+ ```ts
890
+ const orders = await db.order.getMany({
891
+ lines: {
892
+ where: x => x.product.contains('broomstick')
893
+ },
894
+ customer: true
895
+ });
896
+ ```
897
+
898
+ ---
899
+
900
+ ## Aggregate Functions
901
+
902
+ Supported: `count`, `sum`, `min`, `max`, `avg`.
903
+
904
+ ### Aggregates on each row
905
+
906
+ ```ts
907
+ const orders = await db.order.getMany({
908
+ numberOfLines: x => x.count(x => x.lines.id),
909
+ totalAmount: x => x.sum(x => x.lines.amount),
910
+ balance: x => x.customer.balance // elevate related data
911
+ });
912
+ ```
913
+
914
+ ### Aggregates across all rows (group by)
915
+
916
+ ```ts
917
+ const results = await db.order.aggregate({
918
+ where: x => x.orderDate.greaterThan(new Date(2022, 0, 1)),
919
+ customerId: x => x.customerId,
920
+ customerName: x => x.customer.name,
921
+ numberOfLines: x => x.count(x => x.lines.id),
922
+ totals: x => x.sum(x => x.lines.amount)
923
+ });
924
+ ```
925
+
926
+ ### Count rows
927
+
928
+ ```ts
929
+ const count = await db.order.count();
930
+
931
+ // With a filter:
932
+ const filter = db.order.lines.any(line => line.product.contains('broomstick'));
933
+ const count = await db.order.count(filter);
934
+ ```
935
+
936
+ ---
937
+
938
+ ## Data Types
939
+
940
+ | Orange Type | JS Type | SQL Types |
941
+ |---------------------|------------------|----------------------------------------------|
942
+ | `string()` | `string` | VARCHAR, TEXT |
943
+ | `numeric()` | `number` | INTEGER, DECIMAL, FLOAT, REAL, DOUBLE |
944
+ | `bigint()` | `bigint` | BIGINT, INTEGER |
945
+ | `boolean()` | `boolean` | BIT, TINYINT(1), INTEGER |
946
+ | `uuid()` | `string` | UUID, GUID, VARCHAR |
947
+ | `date()` | `string \| Date` | DATE, DATETIME, TIMESTAMP |
948
+ | `dateWithTimeZone()`| `string \| Date` | TIMESTAMP WITH TIME ZONE, DATETIMEOFFSET |
949
+ | `binary()` | `string` (base64)| BLOB, BYTEA, VARBINARY |
950
+ | `json()` | `object` | JSON, JSONB, NVARCHAR, TEXT |
951
+ | `jsonOf<T>()` | `T` | JSON, JSONB, NVARCHAR, TEXT (typed) |
952
+
953
+ ```ts
954
+ import orange from 'orange-orm';
955
+
956
+ const map = orange.map(x => ({
957
+ demo: x.table('demo').map(x => ({
958
+ id: x.column('id').uuid().primary().notNull(),
959
+ name: x.column('name').string(),
960
+ balance: x.column('balance').numeric(),
961
+ regularDate: x.column('regularDate').date(),
962
+ tzDate: x.column('tzDate').dateWithTimeZone(),
963
+ picture: x.column('picture').binary(),
964
+ isActive: x.column('isActive').boolean(),
965
+ pet: x.column('pet').jsonOf<{ name: string; kind: string }>(),
966
+ data: x.column('data').json(),
967
+ }))
968
+ }));
969
+ ```
970
+
971
+ ---
972
+
973
+ ## Enums
974
+
975
+ Enums can be defined using arrays, objects, `as const`, or TypeScript enums.
976
+
977
+ ```ts
978
+ // Array
979
+ countryCode: column('countryCode').string().enum(['NO', 'SE', 'DK', 'FI'])
980
+
981
+ // TypeScript enum
982
+ enum CountryCode { NORWAY = 'NO', SWEDEN = 'SE' }
983
+ countryCode: column('countryCode').string().enum(CountryCode)
984
+
985
+ // as const object
986
+ const Countries = { NORWAY: 'NO', SWEDEN: 'SE' } as const;
987
+ countryCode: column('countryCode').string().enum(Countries)
988
+ ```
989
+
990
+ ---
991
+
992
+ ## TypeScript Type Safety
993
+
994
+ Orange provides full IntelliSense without code generation. The `map()` function returns a fully typed `db` object.
995
+
996
+ ### Type-safe property access
997
+
998
+ ```ts
999
+ const product = await db.product.getById(1);
1000
+ // product.name is typed as string | null | undefined
1001
+ // product.price is typed as number | null | undefined
1002
+ // product.id is typed as number (notNull)
1003
+ ```
1004
+
1005
+ ### Type-safe inserts
1006
+
1007
+ ```ts
1008
+ // TypeScript error: 'name' is required (notNull)
1009
+ await db.product.insert({ price: 100 });
1010
+
1011
+ // OK: 'id' is optional because of notNullExceptInsert
1012
+ await db.product.insert({ name: 'Widget', price: 100 });
1013
+ ```
1014
+
1015
+ ### Type-safe filters
1016
+
1017
+ ```ts
1018
+ // TypeScript error: greaterThan expects number, not string
1019
+ db.product.getMany({ where: x => x.price.greaterThan('fifty') });
1020
+
1021
+ // OK
1022
+ db.product.getMany({ where: x => x.price.greaterThan(50) });
1023
+ ```
1024
+
1025
+ ### Extract TypeScript types from your map
1026
+
1027
+ ```ts
1028
+ type Product = ReturnType<typeof db.product.tsType>;
1029
+ // { id: number; name?: string | null; price?: number | null }
1030
+
1031
+ type ProductWithRelations = ReturnType<typeof db.order.tsType<{ lines: true; customer: true }>>;
1032
+ ```
1033
+
1034
+ ---
1035
+
1036
+ ## Browser Usage (Express / Hono Adapters)
1037
+
1038
+ Orange can run in the browser. The Express/Hono adapter replays client-side method calls on the server, never exposing raw SQL.
1039
+
1040
+ ### Server (Express)
1041
+
1042
+ ```ts
1043
+ import map from './map';
1044
+ import { json } from 'body-parser';
1045
+ import express from 'express';
1046
+ import cors from 'cors';
1047
+
1048
+ const db = map.sqlite('demo.db');
1049
+
1050
+ express().disable('x-powered-by')
1051
+ .use(json({ limit: '100mb' }))
1052
+ .use(cors())
1053
+ .use('/orange', db.express())
1054
+ .listen(3000);
1055
+ ```
1056
+
1057
+ ### Server (Hono)
1058
+
1059
+ ```ts
1060
+ import map from './map';
1061
+ import { Hono } from 'hono';
1062
+ import { cors } from 'hono/cors';
1063
+ import { serve } from '@hono/node-server';
1064
+
1065
+ const db = map.sqlite('demo.db');
1066
+ const app = new Hono();
1067
+
1068
+ app.use('/orange', cors());
1069
+ app.use('/orange/*', cors());
1070
+ app.all('/orange', db.hono());
1071
+ app.all('/orange/*', db.hono());
1072
+
1073
+ serve({ fetch: app.fetch, port: 3000 });
1074
+ ```
1075
+
1076
+ ### Browser client
1077
+
1078
+ ```ts
1079
+ import map from './map';
1080
+
1081
+ const db = map.http('http://localhost:3000/orange');
1082
+
1083
+ const orders = await db.order.getMany({
1084
+ where: x => x.customer.name.startsWith('Harry'),
1085
+ lines: true
1086
+ });
1087
+ ```
1088
+
1089
+ ### Interceptors (authentication)
1090
+
1091
+ ```ts
1092
+ db.interceptors.request.use((config) => {
1093
+ config.headers.Authorization = 'Bearer <token>';
1094
+ return config;
1095
+ });
1096
+ ```
1097
+
1098
+ ### Base filter (row-level security)
1099
+
1100
+ ```ts
1101
+ .use('/orange', db.express({
1102
+ order: {
1103
+ baseFilter: (db, req, _res) => {
1104
+ const customerId = Number(req.headers.authorization.split(' ')[1]);
1105
+ return db.order.customerId.eq(customerId);
1106
+ }
1107
+ }
1108
+ }))
1109
+ ```
1110
+
1111
+ ### Transaction hooks (e.g., Postgres RLS)
1112
+
1113
+ ```ts
1114
+ .use('/orange', db.express({
1115
+ hooks: {
1116
+ transaction: {
1117
+ afterBegin: async (db, req) => {
1118
+ await db.query('set local role rls_app_user');
1119
+ await db.query({ sql: "select set_config('app.tenant_id', ?, true)", parameters: [tenantId] });
1120
+ }
1121
+ }
1122
+ }
1123
+ }))
1124
+ ```
1125
+
1126
+ ---
1127
+
1128
+ ## Raw SQL Queries
1129
+
1130
+ ```ts
1131
+ const rows = await db.query({
1132
+ sql: 'SELECT * FROM customer WHERE name LIKE ?',
1133
+ parameters: ['%arry']
1134
+ });
1135
+ ```
1136
+
1137
+ Raw SQL queries are **blocked via HTTP/browser clients** (returns 403) to prevent SQL injection.
1138
+
1139
+ ---
1140
+
1141
+ ## Logging
1142
+
1143
+ ```ts
1144
+ import orange from 'orange-orm';
1145
+
1146
+ orange.on('query', (e) => {
1147
+ console.log(e.sql);
1148
+ if (e.parameters.length > 0) console.log(e.parameters);
1149
+ });
1150
+ ```
1151
+
1152
+ ---
1153
+
1154
+ ## Bulk Operations
1155
+
1156
+ ### update (selective bulk update)
1157
+
1158
+ ```ts
1159
+ await db.order.update(
1160
+ { orderDate: new Date(), customerId: 2 },
1161
+ { where: x => x.id.eq(1) }
1162
+ );
1163
+
1164
+ // With fetching strategy to return updated rows:
1165
+ const orders = await db.order.update(
1166
+ { orderDate: new Date() },
1167
+ { where: x => x.id.eq(1) },
1168
+ { customer: true, lines: true }
1169
+ );
1170
+ ```
1171
+
1172
+ ### replace (complete overwrite from JSON)
1173
+
1174
+ ```ts
1175
+ await db.order.replace({
1176
+ id: 1,
1177
+ orderDate: '2023-07-14',
1178
+ lines: [{ id: 1, product: 'Bicycle', amount: 250 }]
1179
+ }, { lines: true });
1180
+ ```
1181
+
1182
+ ### updateChanges (partial diff update)
1183
+
1184
+ ```ts
1185
+ const original = { id: 1, name: 'George' };
1186
+ const modified = { id: 1, name: 'Harry' };
1187
+ await db.customer.updateChanges(modified, original);
1188
+ ```
1189
+
1190
+ ---
1191
+
1192
+ ## Batch Delete
1193
+
1194
+ ```ts
1195
+ // By filter
1196
+ await db.order.delete(db.order.customer.name.eq('George'));
1197
+
1198
+ // Cascade (also deletes children)
1199
+ await db.order.deleteCascade(db.order.customer.name.eq('George'));
1200
+
1201
+ // By primary keys
1202
+ await db.customer.delete([{ id: 1 }, { id: 2 }]);
1203
+ ```
1204
+
1205
+ ---
1206
+
1207
+ ## Composite Keys
1208
+
1209
+ Mark multiple columns as `.primary()`:
1210
+
1211
+ ```ts
1212
+ const map = orange.map(x => ({
1213
+ order: x.table('_order').map(({ column }) => ({
1214
+ orderType: column('orderType').string().primary().notNull(),
1215
+ orderNo: column('orderNo').numeric().primary().notNull(),
1216
+ orderDate: column('orderDate').date().notNull(),
1217
+ })),
1218
+
1219
+ orderLine: x.table('orderLine').map(({ column }) => ({
1220
+ orderType: column('orderType').string().primary().notNull(),
1221
+ orderNo: column('orderNo').numeric().primary().notNull(),
1222
+ lineNo: column('lineNo').numeric().primary().notNull(),
1223
+ product: column('product').string(),
1224
+ }))
1225
+ })).map(x => ({
1226
+ order: x.order.map(v => ({
1227
+ lines: v.hasMany(x.orderLine).by('orderType', 'orderNo'),
1228
+ }))
1229
+ }));
1230
+ ```
1231
+
1232
+ ---
1233
+
1234
+ ## Discriminators
1235
+
1236
+ ### Column discriminators
1237
+
1238
+ Automatically set a discriminator column value on insert and filter by it on read/delete:
1239
+
1240
+ ```ts
1241
+ const map = orange.map(x => ({
1242
+ customer: x.table('client').map(({ column }) => ({
1243
+ id: column('id').numeric().primary(),
1244
+ name: column('name').string()
1245
+ })).columnDiscriminators(`client_type='customer'`),
1246
+
1247
+ vendor: x.table('client').map(({ column }) => ({
1248
+ id: column('id').numeric().primary(),
1249
+ name: column('name').string()
1250
+ })).columnDiscriminators(`client_type='vendor'`),
1251
+ }));
1252
+ ```
1253
+
1254
+ ### Formula discriminators
1255
+
1256
+ Use a logical expression instead of a static column value:
1257
+
1258
+ ```ts
1259
+ const map = orange.map(x => ({
1260
+ customerBooking: x.table('booking').map(({ column }) => ({
1261
+ id: column('id').uuid().primary(),
1262
+ bookingNo: column('booking_no').numeric()
1263
+ })).formulaDiscriminators('@this.booking_no between 10000 and 99999'),
1264
+
1265
+ internalBooking: x.table('booking').map(({ column }) => ({
1266
+ id: column('id').uuid().primary(),
1267
+ bookingNo: column('booking_no').numeric()
1268
+ })).formulaDiscriminators('@this.booking_no between 1000 and 9999'),
1269
+ }));
1270
+ ```
1271
+
1272
+ ---
1273
+
1274
+ ## SQLite User-Defined Functions
1275
+
1276
+ ```ts
1277
+ const db = map.sqlite('demo.db');
1278
+
1279
+ await db.function('add_prefix', (text, prefix) => `${prefix}${text}`);
1280
+
1281
+ const rows = await db.query(
1282
+ "select id, name, add_prefix(name, '[VIP] ') as prefixedName from customer"
1283
+ );
1284
+ ```
1285
+
1286
+ ---
1287
+
1288
+ ## Default Values
1289
+
1290
+ ```ts
1291
+ import orange from 'orange-orm';
1292
+ import crypto from 'crypto';
1293
+
1294
+ const map = orange.map(x => ({
1295
+ myTable: x.table('myTable').map(({ column }) => ({
1296
+ id: column('id').uuid().primary().default(() => crypto.randomUUID()),
1297
+ name: column('name').string(),
1298
+ isActive: column('isActive').boolean().default(true),
1299
+ }))
1300
+ }));
1301
+ ```
1302
+
1303
+ ---
1304
+
1305
+ ## Validation
1306
+
1307
+ ```ts
1308
+ function validateName(value?: string) {
1309
+ if (value && value.length > 10)
1310
+ throw new Error('Length cannot exceed 10 characters');
1311
+ }
1312
+
1313
+ const map = orange.map(x => ({
1314
+ demo: x.table('demo').map(x => ({
1315
+ id: x.column('id').uuid().primary().notNullExceptInsert(),
1316
+ name: x.column('name').string().validate(validateName),
1317
+ pet: x.column('pet').jsonOf<Pet>().JSONSchema(petSchema),
1318
+ }))
1319
+ }));
1320
+ ```
1321
+
1322
+ ---
1323
+
1324
+ ## Excluding Sensitive Data
1325
+
1326
+ ```ts
1327
+ const map = orange.map(x => ({
1328
+ customer: x.table('customer').map(({ column }) => ({
1329
+ id: column('id').numeric().primary().notNullExceptInsert(),
1330
+ name: column('name').string(),
1331
+ balance: column('balance').numeric().serializable(false),
1332
+ }))
1333
+ }));
1334
+
1335
+ // When serialized: balance is excluded
1336
+ const george = await db.customer.insert({ name: 'George', balance: 177 });
1337
+ JSON.stringify(george); // '{"id":1,"name":"George"}'
1338
+ ```
1339
+
1340
+ ---
1341
+
1342
+ ## Quick Reference: Active Record Methods
1343
+
1344
+ Methods available on rows returned by `getMany`, `getById`, `getOne`, `insert`:
1345
+
1346
+ | Method | On row | On array | Description |
1347
+ |--------|--------|----------|-------------|
1348
+ | `saveChanges()` | ✅ | ✅ | Persist modified properties to the database |
1349
+ | `saveChanges(concurrency)` | ✅ | ✅ | Persist with concurrency strategy |
1350
+ | `acceptChanges()` | ✅ | ✅ | Accept current values as the new baseline (sync) |
1351
+ | `clearChanges()` | ✅ | ✅ | Revert to last accepted/original state (sync) |
1352
+ | `refresh()` | ✅ | ✅ | Reload from database |
1353
+ | `refresh(strategy)` | ✅ | ✅ | Reload with fetching strategy |
1354
+ | `delete()` | ✅ | ✅ | Delete the row(s) from the database |
1355
+
1356
+ ---
1357
+
1358
+ ## Quick Reference: Table Client Methods
1359
+
1360
+ Methods available on `db.<tableName>`:
1361
+
1362
+ | Method | Description |
1363
+ |--------|-------------|
1364
+ | `getMany(strategy?)` | Fetch multiple rows with optional filter/strategy |
1365
+ | `getOne(strategy?)` | Fetch first matching row |
1366
+ | `getById(...keys, strategy?)` | Fetch by primary key |
1367
+ | `insert(row, strategy?)` | Insert one row |
1368
+ | `insert(rows, strategy?)` | Insert multiple rows |
1369
+ | `insertAndForget(row)` | Insert without returning |
1370
+ | `update(props, {where}, strategy?)` | Bulk update matching rows |
1371
+ | `replace(row, strategy?)` | Complete overwrite from JSON |
1372
+ | `updateChanges(modified, original, strategy?)` | Partial diff update |
1373
+ | `delete(filter?)` | Batch delete |
1374
+ | `deleteCascade(filter?)` | Batch delete with cascade |
1375
+ | `count(filter?)` | Count matching rows |
1376
+ | `aggregate(strategy)` | Aggregate query (group by) |
1377
+ | `proxify(row, strategy?)` | Wrap plain object with active record methods |