edinburgh 0.4.2 → 0.4.5

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.
Files changed (73) hide show
  1. package/README.md +251 -170
  2. package/build/src/datapack.d.ts +17 -1
  3. package/build/src/datapack.js +44 -5
  4. package/build/src/datapack.js.map +1 -1
  5. package/build/src/edinburgh.d.ts +1 -1
  6. package/build/src/edinburgh.js +1 -1
  7. package/build/src/edinburgh.js.map +1 -1
  8. package/build/src/indexes.d.ts +34 -17
  9. package/build/src/indexes.js +126 -55
  10. package/build/src/indexes.js.map +1 -1
  11. package/build/src/migrate-cli.d.ts +1 -16
  12. package/build/src/migrate-cli.js +56 -42
  13. package/build/src/migrate-cli.js.map +1 -1
  14. package/build/src/migrate.d.ts +15 -11
  15. package/build/src/migrate.js +62 -32
  16. package/build/src/migrate.js.map +1 -1
  17. package/build/src/models.d.ts +1 -1
  18. package/build/src/models.js +20 -7
  19. package/build/src/models.js.map +1 -1
  20. package/build/src/types.d.ts +13 -1
  21. package/build/src/types.js +89 -1
  22. package/build/src/types.js.map +1 -1
  23. package/build/src/utils.d.ts +2 -0
  24. package/build/src/utils.js +12 -0
  25. package/build/src/utils.js.map +1 -1
  26. package/package.json +7 -4
  27. package/skill/BaseIndex.md +16 -0
  28. package/skill/BaseIndex_batchProcess.md +10 -0
  29. package/skill/BaseIndex_find.md +7 -0
  30. package/skill/DatabaseError.md +9 -0
  31. package/skill/Model.md +22 -0
  32. package/skill/Model_delete.md +14 -0
  33. package/skill/Model_findAll.md +12 -0
  34. package/skill/Model_getPrimaryKeyHash.md +5 -0
  35. package/skill/Model_isValid.md +14 -0
  36. package/skill/Model_migrate.md +34 -0
  37. package/skill/Model_preCommit.md +28 -0
  38. package/skill/Model_preventPersist.md +15 -0
  39. package/skill/Model_replaceInto.md +16 -0
  40. package/skill/Model_validate.md +21 -0
  41. package/skill/PrimaryIndex.md +8 -0
  42. package/skill/PrimaryIndex_get.md +17 -0
  43. package/skill/PrimaryIndex_getLazy.md +13 -0
  44. package/skill/SKILL.md +158 -664
  45. package/skill/SecondaryIndex.md +9 -0
  46. package/skill/UniqueIndex.md +9 -0
  47. package/skill/UniqueIndex_get.md +17 -0
  48. package/skill/array.md +23 -0
  49. package/skill/dump.md +8 -0
  50. package/skill/field.md +29 -0
  51. package/skill/index.md +32 -0
  52. package/skill/init.md +17 -0
  53. package/skill/link.md +27 -0
  54. package/skill/literal.md +22 -0
  55. package/skill/opt.md +22 -0
  56. package/skill/or.md +22 -0
  57. package/skill/primary.md +26 -0
  58. package/skill/record.md +21 -0
  59. package/skill/registerModel.md +26 -0
  60. package/skill/runMigration.md +10 -0
  61. package/skill/set.md +23 -0
  62. package/skill/setMaxRetryCount.md +10 -0
  63. package/skill/setOnSaveCallback.md +12 -0
  64. package/skill/transact.md +49 -0
  65. package/skill/unique.md +32 -0
  66. package/src/datapack.ts +49 -7
  67. package/src/edinburgh.ts +2 -0
  68. package/src/indexes.ts +143 -71
  69. package/src/migrate-cli.ts +44 -46
  70. package/src/migrate.ts +71 -39
  71. package/src/models.ts +19 -7
  72. package/src/types.ts +97 -1
  73. package/src/utils.ts +12 -0
package/skill/SKILL.md CHANGED
@@ -121,6 +121,8 @@ Instance fields are declared with `E.field(type, options?)`. Available types:
121
121
  | `E.or(A, B, ...)` | `A \| B \| ...` | Union type; args can be types or literal values |
122
122
  | `E.literal(v)` | literal type | Constant value; defaults to that value |
123
123
  | `E.array(T)` | `T[]` | Optional `{min, max}` constraints |
124
+ | `E.set(T)` | `Set<T>` | Optional `{min, max}` constraints |
125
+ | `E.record(T)` | `Record<string \| number, T>` | Key-value object with string/number keys |
124
126
  | `E.link(Model)` | `Model` | Foreign key, lazy-loaded on access |
125
127
 
126
128
  #### Defaults
@@ -254,6 +256,67 @@ await E.transact(() => {
254
256
  });
255
257
  ```
256
258
 
259
+ #### Non-Persistent Properties
260
+
261
+ You can freely add regular methods, getters, and other non-persistent properties to model classes. These work normally in JavaScript but are **not stored in the database** and **not synchronized** across transactions or processes.
262
+
263
+ ```typescript
264
+ @E.registerModel
265
+ class User extends E.Model<User> {
266
+ static pk = E.primary(User, "id");
267
+ id = E.field(E.identifier);
268
+ firstName = E.field(E.string);
269
+ lastName = E.field(E.string);
270
+
271
+ // Non-persisted property
272
+ cachedFullName?: string;
273
+
274
+ get fullName(): string {
275
+ this.cachedFullName ??= `${this.firstName} ${this.lastName}`;
276
+ return this.cachedFullName;
277
+ }
278
+
279
+ greet(): string {
280
+ return `Hello, ${this.fullName}!`;
281
+ }
282
+ }
283
+ ```
284
+
285
+ #### Computed Indexes
286
+
287
+ Instead of naming fields, you can pass a **function** to `E.unique()` or `E.index()`. The function receives a model instance and returns an **array** of index key values. Each element creates a separate index entry, enabling multi-value indexes. Return `[]` to skip indexing for that instance (partial index).
288
+
289
+ ```typescript
290
+ @E.registerModel
291
+ class Article extends E.Model<Article> {
292
+ static pk = E.primary(Article, "id");
293
+ static byFullName = E.unique(Article, (a: Article) => [`${a.firstName} ${a.lastName}`]);
294
+ static byWord = E.index(Article, (a: Article) => a.title.toLowerCase().split(" "));
295
+ static byDomain = E.index(Article, (a: Article) => a.email ? [a.email.split("@")[1]] : []);
296
+
297
+ id = E.field(E.identifier);
298
+ firstName = E.field(E.string);
299
+ lastName = E.field(E.string);
300
+ title = E.field(E.string);
301
+ email = E.field(E.opt(E.string));
302
+ }
303
+
304
+ await E.transact(() => {
305
+ new Article({ firstName: "Jane", lastName: "Doe", title: "Hello World", email: "jane@acme.com" });
306
+
307
+ // Lookup via computed unique index
308
+ const jane = Article.byFullName.get("Jane Doe");
309
+
310
+ // Multi-value: each word in the title is indexed separately
311
+ for (const a of Article.byWord.find({is: "hello"})) { ... }
312
+
313
+ // Partial index: articles without email are skipped
314
+ for (const a of Article.byDomain.find({is: "acme.com"})) { ... }
315
+ });
316
+ ```
317
+
318
+ Computed indexes also support `find()` range queries and `batchProcess()`, just like field-based indexes.
319
+
257
320
  ### Relationships (Links)
258
321
 
259
322
  Use `E.link(Model)` for foreign keys:
@@ -329,13 +392,9 @@ await Product.byCategory.batchProcess({is: "old"}, (product) => {
329
392
  // Commits every ~1 second or 4096 rows (configurable via limitSeconds, limitRows)
330
393
  ```
331
394
 
332
- ### Schema Evolution
395
+ ### Lazy Schema Migrations
333
396
 
334
- Edinburgh handles schema changes automatically:
335
-
336
- - **Adding/removing fields**: Old rows are lazily migrated on read. New fields use their default value.
337
- - **Changing field types**: Requires a `static migrate()` function.
338
- - **Adding/removing indexes**: Requires running `npx migrate-edinburgh`.
397
+ When you change a model's schema, Edinburgh will lazily try to migrate old records on access. This allows you to deploy code changes without downtime or a separate migration step. Optionally, you may provide a `static migrate(record: Record<string, any>)` function on the model to transform old records during lazy migration. If there is a migration error (like a new field without a default value, or an incompatible type change), a run-time error is thrown when loading the affected model instance.
339
398
 
340
399
  ```typescript
341
400
  @E.registerModel
@@ -343,15 +402,40 @@ class User extends E.Model<User> {
343
402
  static pk = E.primary(User, "id");
344
403
  id = E.field(E.identifier);
345
404
  name = E.field(E.string);
346
- role = E.field(E.string, {default: "user"}); // new field
405
+ role = E.field(E.string); // newly added field
347
406
 
348
407
  static migrate(record: Record<string, any>) {
349
- record.role ??= "user"; // provide value for old rows
408
+ record.role ??= record.name.indexOf("admin") >= 0 ? "admin" : "user"; // set role based on name for old records
350
409
  }
351
410
  }
352
411
  ```
353
412
 
354
- Run `npx migrate-edinburgh` (or call `E.runMigration()`) after adding/removing indexes, changing index field types, or when a `migrate()` function affects indexed fields.
413
+ Edinburgh will lazily (re)run the `migrate` function on an instance whenever its implementation (the literal function code) has changed. For robustness, make sure that your `migrate` function...
414
+ - Is idempotent (meaning it can be safely run multiple times on the same row without changing the result after the first run), and
415
+ - Should perform *all* transformation steps starting from the oldest version that could possibly still be in the database. (See the next section.)
416
+
417
+ While lazy migration is convenient and often sufficient, in some cases you need migrations to happen immediately...
418
+
419
+ ### Forced Schema Migrations
420
+
421
+ The `migrate-edinburgh` CLI tool will scan the entire database, pro-actively performing the following migrations:
422
+ - **Populate secondary indexes**: If you added or changed secondary indexes, it will build them. Until you do, the indexes will be empty (or only contain instances that have been saved since the index was created).
423
+ - **Migrate primary indexes**: In case you changed the primary key fields or field types (not recommended!) of a model, it will build the new primary index, as well as all secondary indexes (to point at the new primary keys). Until you do, all of your old data will appear to be missing! Note that this may fail on duplicates.
424
+ - **Remove orphaned indexes**: If you removed or changed an index, the stale data will be deleted from the database.
425
+ - **Rewrite primary data**: These are the types of migrations that would normally be done lazily on instance access. As there's usually not much benefit to doing this forcibly, and it can be very time-consuming (and generates a lot of I/O), this is *not* done by default. It may however be useful if you want to clean up the contents of your `migrate()` function, if you have control over all application deployments. Use the `--rewrite-data` flag to enable this.
426
+
427
+ ```bash
428
+ npx migrate-edinburgh ./src/models.ts
429
+ ```
430
+
431
+ Run `npx migrate-edinburgh` without arguments to see all options. You can also call `runMigration()` programmatically:
432
+
433
+ ```typescript
434
+ import { runMigration } from "edinburgh";
435
+
436
+ const result = await runMigration({ tables: ["User"] });
437
+ console.log(result.secondaries); // { User: 1500 }
438
+ ```
355
439
 
356
440
  ### preCommit Hook
357
441
 
@@ -413,125 +497,33 @@ The following is auto-generated from `src/edinburgh.ts`:
413
497
 
414
498
  **Signature:** `() => void`
415
499
 
416
- ### init · function
500
+ ### [init](init.md) · function
417
501
 
418
502
  Initialize the database with the specified directory path.
419
503
  This function may be called multiple times with the same parameters. If it is not called before the first transact(),
420
504
  the database will be automatically initialized with the default directory.
421
505
 
422
- **Signature:** `(dbDir: string) => void`
423
-
424
- **Parameters:**
425
-
426
- - `dbDir: string`
427
-
428
- **Examples:**
429
-
430
- ```typescript
431
- init("./my-database");
432
- ```
433
-
434
- ### transact · function
506
+ ### [transact](transact.md) · function
435
507
 
436
508
  Executes a function within a database transaction context.
437
509
 
438
- Loading models (also through links in other models) and changing models can only be done from
439
- within a transaction.
440
-
441
- Transactions have a consistent view of the database, and changes made within a transaction are
442
- isolated from other transactions until they are committed. In case a commit clashes with changes
443
- made by another transaction, the transaction function will automatically be re-executed up to 6
444
- times.
445
-
446
- **Signature:** `<T>(fn: () => T) => Promise<T>`
447
-
448
- **Type Parameters:**
449
-
450
- - `T` - The return type of the transaction function.
451
-
452
- **Parameters:**
453
-
454
- - `fn: () => T` - - The function to execute within the transaction context. Receives a Transaction instance.
455
-
456
- **Returns:** A promise that resolves with the function's return value.
457
-
458
- **Throws:**
459
-
460
- - With code "RACING_TRANSACTION" if the transaction fails after retries due to conflicts.
461
- - With code "TXN_LIMIT" if maximum number of transactions is reached.
462
- - With code "LMDB-{code}" for LMDB-specific errors.
463
-
464
- **Examples:**
465
-
466
- ```typescript
467
- const paid = await E.transact(() => {
468
- const user = User.pk.get("john_doe");
469
- if (user.credits > 0) {
470
- user.credits--;
471
- return true;
472
- }
473
- return false;
474
- });
475
- ```
476
- ```typescript
477
- // Transaction with automatic retry on conflicts
478
- await E.transact(() => {
479
- const counter = Counter.pk.get("global") || new Counter({id: "global", value: 0});
480
- counter.value++;
481
- });
482
- ```
483
-
484
- ### setMaxRetryCount · function
510
+ ### [setMaxRetryCount](setMaxRetryCount.md) · function
485
511
 
486
512
  Set the maximum number of retries for a transaction in case of conflicts.
487
513
  The default value is 6. Setting it to 0 will disable retries and cause transactions to fail immediately on conflict.
488
514
 
489
- **Signature:** `(count: number) => void`
490
-
491
- **Parameters:**
492
-
493
- - `count: number` - The maximum number of retries for a transaction.
494
-
495
- ### setOnSaveCallback · function
515
+ ### [setOnSaveCallback](setOnSaveCallback.md) · function
496
516
 
497
517
  Set a callback function to be called after a model is saved and committed.
498
518
 
499
- **Signature:** `(callback: (commitId: number, items: Map<Model<any>, Change>) => void) => void`
500
-
501
- **Parameters:**
502
-
503
- - `callback: ((commitId: number, items: Map<Model<any>, Change>) => void) | undefined` - The callback function to set. It gets called after each successful
504
- `transact()` commit that has changes, with the following arguments:
505
- - A sequential number. Higher numbers have been committed after lower numbers.
506
- - A map of model instances to their changes. The change can be "created", "deleted", or an object containing the old values.
507
-
508
519
  ### deleteEverything · function
509
520
 
510
521
  **Signature:** `() => Promise<void>`
511
522
 
512
- ### Model · abstract class
523
+ ### [Model](Model.md) · abstract class
513
524
 
514
525
  [object Object],[object Object],[object Object],[object Object],[object Object]
515
526
 
516
- **Type Parameters:**
517
-
518
- - `SUB` - The concrete model subclass (for proper typing).
519
-
520
- **Examples:**
521
-
522
- ```typescript
523
- ⁣@E.registerModel
524
- class User extends E.Model<User> {
525
- static pk = E.primary(User, "id");
526
-
527
- id = E.field(E.identifier);
528
- name = E.field(E.string);
529
- email = E.field(E.string);
530
-
531
- static byEmail = E.unique(User, "email");
532
- }
533
- ```
534
-
535
527
  #### Model.tableName · static property
536
528
 
537
529
  The database table name (defaults to class name).
@@ -550,120 +542,32 @@ Field configuration metadata.
550
542
 
551
543
  **Type:** `Record<string | number | symbol, FieldConfig<unknown>>`
552
544
 
553
- #### Model.migrate · static method
545
+ #### [Model.migrate](Model_migrate.md) · static method
554
546
 
555
547
  Optional migration function called when deserializing rows written with an older schema version.
556
548
  Receives a plain record with all fields (primary key fields + value fields) and should mutate it
557
549
  in-place to match the current schema.
558
550
 
559
- This is called both during lazy loading (when a row is read from disk) and during batch
560
- migration (via `runMigration()` / `npx migrate-edinburgh`). The function's source code is hashed
561
- to detect changes. Modifying `migrate()` triggers a new schema version.
562
-
563
- If `migrate()` changes values of fields used in secondary or unique indexes, those indexes
564
- will only be updated when `runMigration()` is run (not during lazy loading).
565
-
566
- **Signature:** `(record: Record<string, any>) => void`
567
-
568
- **Parameters:**
569
-
570
- - `record: Record<string, any>` - - A plain object with all field values from the old schema version.
571
-
572
- **Examples:**
573
-
574
- ```typescript
575
- ⁣@E.registerModel
576
- class User extends E.Model<User> {
577
- static pk = E.primary(User, "id");
578
- id = E.field(E.identifier);
579
- name = E.field(E.string);
580
- role = E.field(E.string); // new field
581
-
582
- static migrate(record: Record<string, any>) {
583
- record.role ??= "user"; // default for rows that predate the 'role' field
584
- }
585
- }
586
- ```
587
-
588
- #### Model.findAll · static method
551
+ #### [Model.findAll](Model_findAll.md) · static method
589
552
 
590
553
  Find all instances of this model in the database, ordered by primary key.
591
554
 
592
- **Signature:** `<T extends typeof Model<unknown>>(this: T, opts?: { reverse?: boolean; }) => IndexRangeIterator<T>`
593
-
594
- **Parameters:**
595
-
596
- - `this: T`
597
- - `opts?: {reverse?: boolean}` - - Optional parameters.
598
-
599
- **Returns:** An iterator.
600
-
601
- #### Model.replaceInto · static method
555
+ #### [Model.replaceInto](Model_replaceInto.md) · static method
602
556
 
603
557
  Load an existing instance by primary key and update it, or create a new one.
604
558
 
605
- The provided object must contain all primary key fields. If a matching row exists,
606
- the remaining properties from `obj` are set on the loaded instance. Otherwise a
607
- new instance is created with `obj` as its initial properties.
608
-
609
- **Signature:** `<T extends typeof Model<any>>(this: T, obj: Partial<Omit<InstanceType<T>, "constructor">>) => InstanceType<T>`
610
-
611
- **Parameters:**
612
-
613
- - `this: T`
614
- - `obj: Partial<Omit<InstanceType<T>, "constructor">>` - - Partial model data that **must** include every primary key field.
615
-
616
- **Returns:** The loaded-and-updated or newly created instance.
617
-
618
- #### model.preCommit · method
559
+ #### [model.preCommit](Model_preCommit.md) · method
619
560
 
620
561
  Optional hook called on each modified instance right before the transaction commits.
621
562
  Runs before data is written to disk, so changes made here are included in the commit.
622
563
 
623
- Common use cases:
624
- - Computing derived or denormalized fields
625
- - Enforcing cross-field validation rules
626
- - Creating or updating related model instances (newly created instances will also
627
- have their `preCommit()` called)
628
-
629
- **Signature:** `() => void`
630
-
631
- **Parameters:**
632
-
633
-
634
- **Examples:**
635
-
636
- ```typescript
637
- ⁣@E.registerModel
638
- class Post extends E.Model<Post> {
639
- static pk = E.primary(Post, "id");
640
- id = E.field(E.identifier);
641
- title = E.field(E.string);
642
- slug = E.field(E.string);
643
-
644
- preCommit() {
645
- this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
646
- }
647
- }
648
- ```
649
-
650
564
  #### model.getPrimaryKey · method
651
565
 
652
566
  **Signature:** `() => Uint8Array<ArrayBufferLike>`
653
567
 
654
- **Parameters:**
655
-
656
-
657
568
  **Returns:** The primary key for this instance.
658
569
 
659
- #### model.getPrimaryKeyHash · method
660
-
661
- **Signature:** `() => number`
662
-
663
- **Parameters:**
664
-
665
-
666
- **Returns:** A 53-bit positive integer non-cryptographic hash of the primary key, or undefined if not yet saved.
570
+ #### [model.getPrimaryKeyHash](Model_getPrimaryKeyHash.md) · method
667
571
 
668
572
  #### model.isLazyField · method
669
573
 
@@ -673,161 +577,42 @@ class Post extends E.Model<Post> {
673
577
 
674
578
  - `field: keyof this`
675
579
 
676
- #### model.preventPersist · method
580
+ #### [model.preventPersist](Model_preventPersist.md) · method
677
581
 
678
582
  Prevent this instance from being persisted to the database.
679
583
 
680
- **Signature:** `() => this`
681
-
682
- **Parameters:**
683
-
684
-
685
- **Returns:** This model instance for chaining.
686
-
687
- **Examples:**
688
-
689
- ```typescript
690
- const user = User.load("user123");
691
- user.name = "New Name";
692
- user.preventPersist(); // Changes won't be saved
693
- ```
694
-
695
- #### model.delete · method
584
+ #### [model.delete](Model_delete.md) · method
696
585
 
697
586
  Delete this model instance from the database.
698
587
 
699
- Removes the instance and all its index entries from the database and prevents further persistence.
700
-
701
- **Signature:** `() => void`
702
-
703
- **Parameters:**
704
-
705
-
706
- **Examples:**
707
-
708
- ```typescript
709
- const user = User.load("user123");
710
- user.delete(); // Removes from database
711
- ```
712
-
713
- #### model.validate · method
588
+ #### [model.validate](Model_validate.md) · method
714
589
 
715
590
  Validate all fields in this model instance.
716
591
 
717
- **Signature:** `(raise?: boolean) => Error[]`
718
-
719
- **Parameters:**
720
-
721
- - `raise: boolean` (optional) - - If true, throw on first validation error.
722
-
723
- **Returns:** Array of validation errors (empty if valid).
724
-
725
- **Examples:**
726
-
727
- ```typescript
728
- const user = new User();
729
- const errors = user.validate();
730
- if (errors.length > 0) {
731
- console.log("Validation failed:", errors);
732
- }
733
- ```
734
-
735
- #### model.isValid · method
592
+ #### [model.isValid](Model_isValid.md) · method
736
593
 
737
594
  Check if this model instance is valid.
738
595
 
739
- **Signature:** `() => boolean`
740
-
741
- **Parameters:**
742
-
743
-
744
- **Returns:** true if all validations pass.
745
-
746
- **Examples:**
747
-
748
- ```typescript
749
- const user = new User({name: "John"});
750
- if (!user.isValid()) shoutAtTheUser();
751
- ```
752
-
753
596
  #### model.getState · method
754
597
 
755
598
  **Signature:** `() => "created" | "deleted" | "loaded" | "lazy"`
756
599
 
757
- **Parameters:**
758
-
759
-
760
600
  #### model.toString · method
761
601
 
762
602
  **Signature:** `() => string`
763
603
 
764
- **Parameters:**
765
-
766
-
767
604
  #### model.[Symbol.for('nodejs.util.inspect.custom')] · method
768
605
 
769
606
  **Signature:** `() => string`
770
607
 
771
- **Parameters:**
772
-
773
-
774
- ### registerModel · function
608
+ ### [registerModel](registerModel.md) · function
775
609
 
776
610
  Register a model class with the Edinburgh ORM system.
777
611
 
778
- **Signature:** `<T extends typeof Model<unknown>>(MyModel: T) => T`
779
-
780
- **Type Parameters:**
781
-
782
- - `T extends typeof Model<unknown>` - The model class type.
783
-
784
- **Parameters:**
785
-
786
- - `MyModel: T` - - The model class to register.
787
-
788
- **Returns:** The enhanced model class with ORM capabilities.
789
-
790
- **Examples:**
791
-
792
- ```typescript
793
- ⁣@E.registerModel
794
- class User extends E.Model<User> {
795
- static pk = E.index(User, ["id"], "primary");
796
- id = E.field(E.identifier);
797
- name = E.field(E.string);
798
- }
799
- ```
800
-
801
- ### field · function
612
+ ### [field](field.md) · function
802
613
 
803
614
  Create a field definition for a model property.
804
615
 
805
- This function uses TypeScript magic to return the field configuration object
806
- while appearing to return the actual field value type to the type system.
807
- This allows for both runtime introspection and compile-time type safety.
808
-
809
- **Signature:** `<T>(type: TypeWrapper<T>, options?: Partial<FieldConfig<T>>) => T`
810
-
811
- **Type Parameters:**
812
-
813
- - `T` - The field type.
814
-
815
- **Parameters:**
816
-
817
- - `type: TypeWrapper<T>` - - The type wrapper for this field.
818
- - `options: Partial<FieldConfig<T>>` (optional) - - Additional field configuration options.
819
-
820
- **Returns:** The field value (typed as T, but actually returns FieldConfig<T>).
821
-
822
- **Examples:**
823
-
824
- ```typescript
825
- class User extends E.Model<User> {
826
- name = E.field(E.string, {description: "User's full name"});
827
- age = E.field(E.opt(E.number), {description: "User's age", default: 25});
828
- }
829
- ```
830
-
831
616
  ### string · constant
832
617
 
833
618
  Type wrapper instance for the string type.
@@ -852,7 +637,7 @@ Type wrapper instance for the number type.
852
637
 
853
638
  ### dateTime · constant
854
639
 
855
- Type wrapper instance for the date/time type.
640
+ Type wrapper instance for the date/time type. Stored without timezone info, rounded to whole seconds.
856
641
 
857
642
  **Value:** `TypeWrapper<Date>`
858
643
 
@@ -874,336 +659,90 @@ Type wrapper instance for the 'undefined' type.
874
659
 
875
660
  **Value:** `TypeWrapper<undefined>`
876
661
 
877
- ### opt · function
662
+ ### [opt](opt.md) · function
878
663
 
879
664
  Create an optional type wrapper (allows undefined).
880
665
 
881
- **Signature:** `<const T extends TypeWrapper<unknown> | BasicType>(inner: T) => TypeWrapper<T extends TypeWrapper<infer U> ? U : T>`
882
-
883
- **Type Parameters:**
884
-
885
- - `T extends TypeWrapper<unknown>|BasicType` - Type wrapper or basic type to make optional.
886
-
887
- **Parameters:**
888
-
889
- - `inner: T` - - The inner type to make optional.
890
-
891
- **Returns:** A union type that accepts the inner type or undefined.
892
-
893
- **Examples:**
894
-
895
- ```typescript
896
- const optionalString = E.opt(E.string);
897
- const optionalNumber = E.opt(E.number);
898
- ```
899
-
900
- ### or · function
666
+ ### [or](or.md) · function
901
667
 
902
668
  Create a union type wrapper from multiple type choices.
903
669
 
904
- **Signature:** `<const T extends (TypeWrapper<unknown> | BasicType)[]>(...choices: T) => TypeWrapper<UnwrapTypes<T>>`
905
-
906
- **Type Parameters:**
907
-
908
- - `T extends (TypeWrapper<unknown>|BasicType)[]` - Array of type wrapper or basic types.
909
-
910
- **Parameters:**
911
-
912
- - `choices: T` - - The type choices for the union.
913
-
914
- **Returns:** A union type instance.
915
-
916
- **Examples:**
917
-
918
- ```typescript
919
- const stringOrNumber = E.or(E.string, E.number);
920
- const status = E.or("active", "inactive", "pending");
921
- ```
922
-
923
- ### array · function
670
+ ### [array](array.md) · function
924
671
 
925
672
  Create an array type wrapper with optional length constraints.
926
673
 
927
- **Signature:** `<const T>(inner: TypeWrapper<T>, opts?: { min?: number; max?: number; }) => TypeWrapper<T[]>`
928
-
929
- **Type Parameters:**
674
+ ### [set](set.md) · function
930
675
 
931
- - `T` - The element type.
676
+ Create a Set type wrapper with optional length constraints.
932
677
 
933
- **Parameters:**
678
+ ### [record](record.md) · function
934
679
 
935
- - `inner: TypeWrapper<T>` - - Type wrapper for array elements.
936
- - `opts: {min?: number, max?: number}` (optional) - - Optional constraints (min/max length).
680
+ Create a Record type wrapper for key-value objects with string or number keys.
937
681
 
938
- **Returns:** An array type instance.
939
-
940
- **Examples:**
941
-
942
- ```typescript
943
- const stringArray = E.array(E.string);
944
- const boundedArray = E.array(E.number, {min: 1, max: 10});
945
- ```
946
-
947
- ### literal · function
682
+ ### [literal](literal.md) · function
948
683
 
949
684
  Create a literal type wrapper for a constant value.
950
685
 
951
- **Signature:** `<const T>(value: T) => TypeWrapper<T>`
952
-
953
- **Type Parameters:**
954
-
955
- - `T` - The literal type.
956
-
957
- **Parameters:**
958
-
959
- - `value: T` - - The literal value.
960
-
961
- **Returns:** A literal type instance.
962
-
963
- **Examples:**
964
-
965
- ```typescript
966
- const statusType = E.literal("active");
967
- const countType = E.literal(42);
968
- ```
969
-
970
- ### link · function
686
+ ### [link](link.md) · function
971
687
 
972
688
  Create a link type wrapper for model relationships.
973
689
 
974
- **Signature:** `<const T extends typeof Model<any>>(TargetModel: T) => TypeWrapper<InstanceType<T>>`
975
-
976
- **Type Parameters:**
977
-
978
- - `T extends typeof Model<any>` - The target model class.
979
-
980
- **Parameters:**
981
-
982
- - `TargetModel: T` - - The model class this link points to.
983
-
984
- **Returns:** A link type instance.
985
-
986
- **Examples:**
987
-
988
- ```typescript
989
- class User extends E.Model<User> {
990
- posts = E.field(E.array(E.link(Post, 'author')));
991
- }
992
-
993
- class Post extends E.Model<Post> {
994
- author = E.field(E.link(User));
995
- }
996
- ```
997
-
998
- ### index · function
690
+ ### [index](index.md) · function
999
691
 
1000
- Create a secondary index on model fields.
692
+ Create a secondary index on model fields, or a computed secondary index using a function.
1001
693
 
1002
- **Signature:** `{ <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): SecondaryIndex<M, [F]>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): SecondaryIndex<...>; }`
1003
-
1004
- **Type Parameters:**
1005
-
1006
- - `M extends typeof Model` - The model class.
1007
- - `F extends (keyof InstanceType<M> & string)` - The field name (for single field index).
1008
-
1009
- **Parameters:**
1010
-
1011
- - `MyModel: M` - - The model class to create the index for.
1012
- - `field: F` - - Single field name for simple indexes.
1013
-
1014
- **Returns:** A new SecondaryIndex instance.
1015
-
1016
- **Examples:**
1017
-
1018
- ```typescript
1019
- class User extends E.Model<User> {
1020
- static byAge = E.index(User, "age");
1021
- static byTagsDate = E.index(User, ["tags", "createdAt"]);
1022
- }
1023
- ```
1024
-
1025
- ### primary · function
694
+ ### [primary](primary.md) · function
1026
695
 
1027
696
  Create a primary index on model fields.
1028
697
 
1029
- **Signature:** `{ <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): PrimaryIndex<M, [F]>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): PrimaryIndex<...>; }`
1030
-
1031
- **Type Parameters:**
1032
-
1033
- - `M extends typeof Model` - The model class.
1034
- - `F extends (keyof InstanceType<M> & string)` - The field name (for single field index).
1035
-
1036
- **Parameters:**
1037
-
1038
- - `MyModel: M` - - The model class to create the index for.
1039
- - `field: F` - - Single field name for simple indexes.
1040
-
1041
- **Returns:** A new PrimaryIndex instance.
1042
-
1043
- **Examples:**
1044
-
1045
- ```typescript
1046
- class User extends E.Model<User> {
1047
- static pk = E.primary(User, ["id"]);
1048
- static pkSingle = E.primary(User, "id");
1049
- }
1050
- ```
1051
-
1052
- ### unique · function
698
+ ### [unique](unique.md) · function
1053
699
 
1054
- Create a unique index on model fields.
700
+ Create a unique index on model fields, or a computed unique index using a function.
1055
701
 
1056
- **Signature:** `{ <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): UniqueIndex<M, [F]>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyModel: M, fields: FS): UniqueIndex<...>; }`
1057
-
1058
- **Type Parameters:**
1059
-
1060
- - `M extends typeof Model` - The model class.
1061
- - `F extends (keyof InstanceType<M> & string)` - The field name (for single field index).
1062
-
1063
- **Parameters:**
1064
-
1065
- - `MyModel: M` - - The model class to create the index for.
1066
- - `field: F` - - Single field name for simple indexes.
1067
-
1068
- **Returns:** A new UniqueIndex instance.
1069
-
1070
- **Examples:**
1071
-
1072
- ```typescript
1073
- class User extends E.Model<User> {
1074
- static byEmail = E.unique(User, "email");
1075
- static byNameAge = E.unique(User, ["name", "age"]);
1076
- }
1077
- ```
1078
-
1079
- ### dump · function
702
+ ### [dump](dump.md) · function
1080
703
 
1081
704
  Dump database contents for debugging.
1082
705
 
1083
- Prints all indexes and their data to the console for inspection.
1084
- This is primarily useful for development and debugging purposes.
1085
-
1086
- **Signature:** `() => void`
1087
-
1088
- ### BaseIndex · abstract class
706
+ ### [BaseIndex](BaseIndex.md) · abstract class
1089
707
 
1090
708
  Base class for database indexes for efficient lookups on model fields.
1091
709
 
1092
- Indexes enable fast queries on specific field combinations and enforce uniqueness constraints.
1093
-
1094
- **Type Parameters:**
1095
-
1096
- - `M extends typeof Model` - The model class this index belongs to.
1097
- - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1098
-
1099
- **Constructor Parameters:**
1100
-
1101
- - `MyModel`: - The model class this index belongs to.
1102
- - `_fieldNames`: - Array of field names that make up this index.
710
+ #### [baseIndex.find](BaseIndex_find.md) · method
1103
711
 
1104
- #### baseIndex.find · method
1105
-
1106
- **Signature:** `(opts?: FindOptions<IndexArgTypes<M, F>>) => IndexRangeIterator<M>`
1107
-
1108
- **Parameters:**
1109
-
1110
- - `opts: FindOptions<IndexArgTypes<M, F>>` (optional)
1111
-
1112
- #### baseIndex.batchProcess · method
712
+ #### [baseIndex.batchProcess](BaseIndex_batchProcess.md) · method
1113
713
 
1114
714
  [object Object],[object Object],[object Object]
1115
715
 
1116
- **Signature:** `(opts: FindOptions<IndexArgTypes<M, F>> & { limitSeconds?: number; limitRows?: number; }, callback: (row: InstanceType<M>) => void | Promise<...>) => Promise<...>`
1117
-
1118
- **Parameters:**
1119
-
1120
- - `opts: FindOptions<IndexArgTypes<M, F>> & { limitSeconds?: number; limitRows?: number }` (optional) - - Query options (same as `find()`), plus:
1121
- - `callback: (row: InstanceType<M>) => void | Promise<void>` - - Called for each matching row within a transaction
1122
-
1123
716
  #### baseIndex.toString · method
1124
717
 
1125
718
  **Signature:** `() => string`
1126
719
 
1127
- **Parameters:**
1128
-
1129
-
1130
- ### UniqueIndex · class
720
+ ### [UniqueIndex](UniqueIndex.md) · class
1131
721
 
1132
722
  Unique index that stores references to the primary key.
1133
723
 
1134
- **Type Parameters:**
1135
-
1136
- - `M extends typeof Model` - The model class this index belongs to.
1137
- - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1138
-
1139
- #### uniqueIndex.get · method
724
+ #### [uniqueIndex.get](UniqueIndex_get.md) · method
1140
725
 
1141
726
  Get a model instance by unique index key values.
1142
727
 
1143
- **Signature:** `(...args: IndexArgTypes<M, F>) => InstanceType<M>`
1144
-
1145
- **Parameters:**
1146
-
1147
- - `args: IndexArgTypes<M, F>` - - The unique index key values.
1148
-
1149
- **Returns:** The model instance if found, undefined otherwise.
1150
-
1151
- **Examples:**
1152
-
1153
- ```typescript
1154
- const userByEmail = User.byEmail.get("john@example.com");
1155
- ```
1156
-
1157
- ### PrimaryIndex · class
728
+ ### [PrimaryIndex](PrimaryIndex.md) · class
1158
729
 
1159
730
  Primary index that stores the actual model data.
1160
731
 
1161
- **Type Parameters:**
1162
-
1163
- - `M extends typeof Model` - The model class this index belongs to.
1164
- - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1165
-
1166
- #### primaryIndex.get · method
732
+ #### [primaryIndex.get](PrimaryIndex_get.md) · method
1167
733
 
1168
734
  Get a model instance by primary key values.
1169
735
 
1170
- **Signature:** `(...args: IndexArgTypes<M, F>) => InstanceType<M>`
1171
-
1172
- **Parameters:**
1173
-
1174
- - `args: IndexArgTypes<M, F>` - - The primary key values.
1175
-
1176
- **Returns:** The model instance if found, undefined otherwise.
1177
-
1178
- **Examples:**
1179
-
1180
- ```typescript
1181
- const user = User.pk.get("john_doe");
1182
- ```
1183
-
1184
- #### primaryIndex.getLazy · method
736
+ #### [primaryIndex.getLazy](PrimaryIndex_getLazy.md) · method
1185
737
 
1186
738
  Does the same as as `get()`, but will delay loading the instance from disk until the first
1187
739
  property access. In case it turns out the instance doesn't exist, an error will be thrown
1188
740
  at that time.
1189
741
 
1190
- **Signature:** `(...args: IndexArgTypes<M, F>) => InstanceType<M>`
1191
-
1192
- **Parameters:**
1193
-
1194
- - `args: IndexArgTypes<M, F>` - Primary key field values. (Or a single Uint8Array containing the key.)
1195
-
1196
- **Returns:** The (lazily loaded) model instance.
1197
-
1198
- ### SecondaryIndex · class
742
+ ### [SecondaryIndex](SecondaryIndex.md) · class
1199
743
 
1200
744
  Secondary index for non-unique lookups.
1201
745
 
1202
- **Type Parameters:**
1203
-
1204
- - `M extends typeof Model` - The model class this index belongs to.
1205
- - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1206
-
1207
746
  ### Change · type
1208
747
 
1209
748
  **Type:** `Record<any, any> | "created" | "deleted"`
@@ -1222,26 +761,15 @@ Secondary index for non-unique lookups.
1222
761
 
1223
762
  **Type:** `Map<number, Model<unknown>>`
1224
763
 
1225
- ### DatabaseError · constant
764
+ ### [DatabaseError](DatabaseError.md) · constant
1226
765
 
1227
766
  The DatabaseError class is used to represent errors that occur during database operations.
1228
767
  It extends the built-in Error class and has a machine readable error code string property.
1229
768
 
1230
- The lowlevel API will throw DatabaseError instances for all database-related errors.
1231
- Invalid function arguments will throw TypeError.
1232
-
1233
- **Value:** `DatabaseErrorConstructor`
1234
-
1235
- ### runMigration · function
1236
-
1237
- Run database migration: upgrade all rows to the latest schema version,
1238
- convert old primary indices, and clean up orphaned secondary indices.
1239
-
1240
- **Signature:** `(options?: MigrationOptions) => Promise<MigrationResult>`
769
+ ### [runMigration](runMigration.md) · function
1241
770
 
1242
- **Parameters:**
1243
-
1244
- - `options: MigrationOptions` (optional)
771
+ Run database migration: populate secondary indexes for old-version rows,
772
+ convert old primary indices, rewrite row data, and clean up orphaned indices.
1245
773
 
1246
774
  ### MigrationOptions · interface
1247
775
 
@@ -1251,21 +779,27 @@ Limit migration to specific table names.
1251
779
 
1252
780
  **Type:** `string[]`
1253
781
 
1254
- #### migrationOptions.convertOldPrimaries · member
782
+ #### migrationOptions.populateSecondaries · member
1255
783
 
1256
- Whether to convert old primary indices for known tables (default: true).
784
+ Populate secondary indexes for rows at old schema versions (default: true).
1257
785
 
1258
786
  **Type:** `boolean`
1259
787
 
1260
- #### migrationOptions.deleteOrphanedIndexes · member
788
+ #### migrationOptions.migratePrimaries · member
1261
789
 
1262
- Whether to delete orphaned secondary/unique indices (default: true).
790
+ Convert old primary indices when primary key fields changed (default: true).
1263
791
 
1264
792
  **Type:** `boolean`
1265
793
 
1266
- #### migrationOptions.upgradeVersions · member
794
+ #### migrationOptions.rewriteData · member
1267
795
 
1268
- Whether to upgrade rows to the latest version (default: true).
796
+ Rewrite all row data to the latest schema version (default: false).
797
+
798
+ **Type:** `boolean`
799
+
800
+ #### migrationOptions.removeOrphans · member
801
+
802
+ Delete orphaned secondary/unique index entries (default: true).
1269
803
 
1270
804
  **Type:** `boolean`
1271
805
 
@@ -1279,13 +813,13 @@ Progress callback.
1279
813
 
1280
814
  #### migrationResult.secondaries · member
1281
815
 
1282
- Per-table stats for row upgrades.
816
+ Per-table counts of secondary index entries populated.
1283
817
 
1284
818
  **Type:** `Record<string, number>`
1285
819
 
1286
820
  #### migrationResult.primaries · member
1287
821
 
1288
- Per-table stats for old primary conversions.
822
+ Per-table counts of old primary rows migrated.
1289
823
 
1290
824
  **Type:** `Record<string, number>`
1291
825
 
@@ -1295,55 +829,15 @@ Per-table conversion failure counts by reason.
1295
829
 
1296
830
  **Type:** `Record<string, Record<string, number>>`
1297
831
 
1298
- #### migrationResult.orphaned · member
1299
-
1300
- Number of orphaned index entries deleted.
1301
-
1302
- **Type:** `number`
1303
-
1304
- ## Schema Migrations
1305
-
1306
- Edinburgh automatically tracks the schema version of each model. When you change fields, field types, indexes, or the `migrate()` function, Edinburgh detects a new schema version.
1307
-
1308
- ### What happens automatically (lazy migration)
832
+ #### migrationResult.rewritten · member
1309
833
 
1310
- Changes to regular (non-index) field values are migrated lazily. When a row with an old schema version is loaded from disk, it is deserialized using the old field types and transformed by the optional static `migrate()` function. This is transparent and requires no downtime.
834
+ Per-table counts of rows rewritten to latest version.
1311
835
 
1312
- ```typescript
1313
- @E.registerModel
1314
- class User extends E.Model<User> {
1315
- static pk = E.primary(User, "id");
1316
- id = E.field(E.identifier);
1317
- name = E.field(E.string);
1318
- role = E.field(E.string); // newly added field
1319
-
1320
- static migrate(record: Record<string, any>) {
1321
- record.role ??= "user"; // provide a default for old rows
1322
- }
1323
- }
1324
- ```
1325
-
1326
- ### What requires `migrate-edinburgh`
1327
-
1328
- The `migrate-edinburgh` CLI tool (or the `runMigration()` API) must be run when:
1329
-
1330
- - **Adding or removing** secondary or unique indexes
1331
- - **Changing the fields or types** of an existing index
1332
- - A **`migrate()` function changes values** that are used in index fields
1333
-
1334
- The tool populates new indexes, removes orphaned ones, and updates index entries whose values were changed by `migrate()`. It does *not* rewrite primary data rows - lazy migration handles that on read.
1335
-
1336
- ```bash
1337
- npx migrate-edinburgh --import ./src/models.ts
1338
- ```
836
+ **Type:** `Record<string, number>`
1339
837
 
1340
- Run `npx migrate-edinburgh` to see all of its options.
838
+ #### migrationResult.orphans · member
1341
839
 
1342
- You can also call `runMigration()` programmatically:
840
+ Number of orphaned index entries deleted.
1343
841
 
1344
- ```typescript
1345
- import { runMigration } from "edinburgh";
842
+ **Type:** `number`
1346
843
 
1347
- const result = await runMigration({ tables: ["User"] });
1348
- console.log(result.upgraded); // { User: 1500 }
1349
- ```