edinburgh 0.4.5 → 0.5.0

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 (53) hide show
  1. package/README.md +268 -374
  2. package/build/src/datapack.js +1 -1
  3. package/build/src/datapack.js.map +1 -1
  4. package/build/src/edinburgh.d.ts +5 -5
  5. package/build/src/edinburgh.js +8 -7
  6. package/build/src/edinburgh.js.map +1 -1
  7. package/build/src/indexes.d.ts +46 -116
  8. package/build/src/indexes.js +148 -180
  9. package/build/src/indexes.js.map +1 -1
  10. package/build/src/migrate.js +11 -31
  11. package/build/src/migrate.js.map +1 -1
  12. package/build/src/models.d.ts +74 -49
  13. package/build/src/models.js +112 -165
  14. package/build/src/models.js.map +1 -1
  15. package/build/src/types.d.ts +30 -21
  16. package/build/src/types.js +16 -30
  17. package/build/src/types.js.map +1 -1
  18. package/package.json +1 -3
  19. package/skill/BaseIndex_batchProcess.md +1 -1
  20. package/skill/BaseIndex_find.md +2 -2
  21. package/skill/BaseIndex_find_2.md +7 -0
  22. package/skill/BaseIndex_find_3.md +7 -0
  23. package/skill/BaseIndex_find_4.md +7 -0
  24. package/skill/Model.md +5 -7
  25. package/skill/Model_batchProcess.md +8 -0
  26. package/skill/Model_delete.md +1 -1
  27. package/skill/Model_migrate.md +2 -4
  28. package/skill/Model_preCommit.md +2 -4
  29. package/skill/Model_preventPersist.md +1 -1
  30. package/skill/Model_replaceInto.md +2 -2
  31. package/skill/NonPrimaryIndex.md +10 -0
  32. package/skill/SKILL.md +146 -144
  33. package/skill/SecondaryIndex.md +2 -2
  34. package/skill/UniqueIndex.md +2 -2
  35. package/skill/defineModel.md +22 -0
  36. package/skill/field.md +2 -2
  37. package/skill/link.md +11 -9
  38. package/skill/transact.md +2 -2
  39. package/src/datapack.ts +1 -1
  40. package/src/edinburgh.ts +8 -9
  41. package/src/indexes.ts +157 -276
  42. package/src/migrate.ts +9 -30
  43. package/src/models.ts +188 -174
  44. package/src/types.ts +31 -26
  45. package/skill/Model_findAll.md +0 -12
  46. package/skill/PrimaryIndex.md +0 -8
  47. package/skill/PrimaryIndex_get.md +0 -17
  48. package/skill/PrimaryIndex_getLazy.md +0 -13
  49. package/skill/UniqueIndex_get.md +0 -17
  50. package/skill/index.md +0 -32
  51. package/skill/primary.md +0 -26
  52. package/skill/registerModel.md +0 -26
  53. package/skill/unique.md +0 -32
package/README.md CHANGED
@@ -22,39 +22,41 @@ import * as E from "edinburgh";
22
22
  // Initialize the database (optional, defaults to ".edinburgh")
23
23
  E.init("./my-database");
24
24
 
25
- // Define a model
26
- @E.registerModel
27
- class User extends E.Model<User> {
28
- // Define a primary key (optional, defaults to using the "id" field)
29
- static pk = E.primary(User, "id");
30
- // Define a unique index on the email field
31
- static byEmail = E.unique(User, "email");
32
-
33
- // Define fields with simple types -- they will be type-checked at compile time and validated at runtime.
25
+ const User = E.defineModel(class {
34
26
  id = E.field(E.identifier);
35
27
  name = E.field(E.string);
36
28
  age = E.field(E.number);
37
- email = E.field(E.opt(E.string)); // TypeScript: undefined | string
38
-
39
- // Link to another instance of this model
40
- supervisor = E.field(E.opt(E.link(User)));
41
-
42
- // A field with a more elaborate type. In TypeScript: `User | User[] | "unknown" | "whatever"`
43
- something = E.field(E.or(E.link(User), E.array(E.link(User)), E.literal("unknown"), E.literal("whatever")), { default: "unknown" });
44
- }
29
+ email = E.field(E.opt(E.string));
30
+ // Optional link to another instance of this model (needs a function as `User` is not defined yet at this point)
31
+ supervisor = E.field(E.opt(E.link(() => User)));
32
+ // A field with a more elaborate type. In TypeScript: `User | User[] | "unknown" | "whatever"`, defaulting to "unknown".
33
+ something = E.field(
34
+ E.or(
35
+ E.link(() => User),
36
+ E.array(E.link(() => User)),
37
+ E.literal("unknown"),
38
+ E.literal("whatever")
39
+ ),
40
+ { default: "unknown" }
41
+ );
42
+ }, {
43
+ pk: "id",
44
+ unique: {
45
+ byEmail: "email",
46
+ },
47
+ tableName: "User",
48
+ });
45
49
 
46
- // Use in transactions
47
50
  await E.transact(() => {
48
- const boss = new User({
49
- name: "Big Boss",
50
- age: 50,
51
- });
52
- const john = new User({ // Unique 'id' is automatically generated if not provided
53
- name: "John Doe",
51
+ // Unique 'id' values are auto-generated if not provided
52
+ const boss = new User({ name: "Big Boss", age: 50 });
53
+ new User({
54
+ name: "John Doe",
54
55
  age: 41,
55
56
  email: "john@example.com",
56
57
  supervisor: boss, // Link to another model instance
57
58
  });
59
+ // Newly instantiated models are automatically saved to the database on transaction commit
58
60
  });
59
61
 
60
62
  await E.transact(() => {
@@ -66,41 +68,31 @@ await E.transact(() => {
66
68
  john.age++;
67
69
 
68
70
  // The supervisor object is lazy loaded on first access
69
- console.log(`${john.supervisor!.name} is ${john.name}'s supervisor`);
71
+ console.log(`${john.supervisor!.name} is ${john.name}'s supervisor`);
70
72
  });
71
73
  ```
72
74
 
73
75
  ## Tutorial
74
76
 
75
- ### TypeScript Configuration
76
-
77
- When using TypeScript to transpile to JavaScript, make sure to enable the following options in your `tsconfig.json`:
78
-
79
- ```json
80
- {
81
- "compilerOptions": {
82
- "target": "es2022",
83
- "experimentalDecorators": true
84
- }
85
- }
86
- ```
87
77
 
88
78
  ### Defining Models
89
79
 
90
- Models are classes that extend `E.Model<Self>` and use the `@E.registerModel` decorator:
80
+ Models are plain, usually anonymous, classes passed to `E.defineModel()`:
91
81
 
92
82
  ```typescript
93
83
  import * as E from "edinburgh";
94
84
 
95
- @E.registerModel
96
- class User extends E.Model<User> {
97
- static pk = E.primary(User, "id");
98
-
85
+ const User = E.defineModel(class {
99
86
  id = E.field(E.identifier);
100
87
  name = E.field(E.string);
101
88
  email = E.field(E.string);
102
89
  age = E.field(E.number);
103
- }
90
+ }, {
91
+ pk: "id",
92
+ unique: {
93
+ byEmail: "email",
94
+ },
95
+ });
104
96
  ```
105
97
 
106
98
  Instance fields are declared with `E.field(type, options?)`. Available types:
@@ -124,16 +116,13 @@ Instance fields are declared with `E.field(type, options?)`. Available types:
124
116
  #### Defaults
125
117
 
126
118
  ```typescript
127
- @E.registerModel
128
- class Post extends E.Model<Post> {
129
- static pk = E.primary(Post, "id");
130
-
131
- id = E.field(E.identifier); // auto-generated
119
+ const Post = E.defineModel(class {
120
+ id = E.field(E.identifier); // auto-generated
132
121
  title = E.field(E.string);
133
122
  status = E.field(E.or("draft", "published"), {default: "draft"});
134
- tags = E.field(E.array(E.string), {default: () => []}); // use function for mutable defaults
135
- createdAt = E.field(E.dateTime); // dateTime defaults to new Date()
136
- }
123
+ tags = E.field(E.array(E.string), {default: () => []}); // use function for mutable defaults
124
+ createdAt = E.field(E.dateTime); // dateTime defaults to new Date()
125
+ }, { pk: "id" });
137
126
  ```
138
127
 
139
128
  ### Transactions
@@ -146,8 +135,8 @@ E.init("./my-database");
146
135
 
147
136
  // Create
148
137
  await E.transact(() => {
149
- const user = new User({name: "Alice", email: "alice@example.com", age: 30});
150
- // user.id is auto-generated
138
+ // User.id is auto-generated
139
+ new User({name: "Alice", email: "alice@example.com", age: 30});
151
140
  });
152
141
 
153
142
  // Read + Update
@@ -170,27 +159,26 @@ Transactions auto-retry on conflict (up to 6 times by default). Keep transaction
170
159
  Edinburgh supports three index types:
171
160
 
172
161
  ```typescript
173
- @E.registerModel
174
- class Product extends E.Model<Product> {
175
- static pk = E.primary(Product, "sku"); // primary: one per model, stores data
176
- static byName = E.unique(Product, "name"); // unique: enforces uniqueness + fast lookup
177
- static byCategory = E.index(Product, "category");// secondary: non-unique, for queries
178
-
162
+ const Product = E.defineModel(class {
179
163
  sku = E.field(E.string);
180
164
  name = E.field(E.string);
181
165
  category = E.field(E.string);
182
166
  price = E.field(E.number);
183
- }
167
+ }, {
168
+ pk: "sku",
169
+ unique: { byName: "name" },
170
+ index: { byCategory: "category" },
171
+ });
184
172
  ```
185
173
 
186
- If no `E.primary()` is declared, Edinburgh auto-creates one on an `id` field (adding `E.identifier` if missing).
174
+ If no `pk` is provided, Edinburgh auto-creates one on an `id` field, adding it as an `E.identifier` field if needed.
187
175
 
188
176
  #### Lookups
189
177
 
190
178
  ```typescript
191
179
  await E.transact(() => {
192
180
  // Primary key lookup
193
- const p = Product.pk.get("SKU-001");
181
+ const p1 = Product.get("SKU-001");
194
182
 
195
183
  // Unique index lookup
196
184
  const p2 = Product.byName.get("Widget");
@@ -211,18 +199,18 @@ await E.transact(() => {
211
199
  }
212
200
 
213
201
  // Range (inclusive)
214
- for (const p of Product.pk.find({from: "A", to: "M"})) {
202
+ for (const p of Product.find({from: "A", to: "M"})) {
215
203
  console.log(p.sku);
216
204
  }
217
205
 
218
206
  // Exclusive bounds
219
- for (const p of Product.pk.find({after: "A", before: "M"})) { ... }
207
+ for (const p of Product.find({after: "A", before: "M"})) { ... }
220
208
 
221
209
  // Open-ended
222
- for (const p of Product.pk.find({from: "M"})) { ... }
210
+ for (const p of Product.find({from: "M"})) { ... }
223
211
 
224
212
  // Reverse
225
- for (const p of Product.pk.find({reverse: true})) { ... }
213
+ for (const p of Product.find({reverse: true})) { ... }
226
214
 
227
215
  // Count and fetch helpers
228
216
  const count = Product.byCategory.find({is: "electronics"}).count();
@@ -230,25 +218,24 @@ await E.transact(() => {
230
218
  });
231
219
  ```
232
220
 
233
- #### Composite Indexes
221
+ #### Composite Primary Keys
234
222
 
235
223
  ```typescript
236
- @E.registerModel
237
- class Event extends E.Model<Event> {
238
- static pk = E.primary(Event, ["year", "month", "id"]);
239
-
224
+ const Event = E.defineModel(class {
240
225
  year = E.field(E.number);
241
226
  month = E.field(E.number);
242
227
  id = E.field(E.identifier);
243
228
  title = E.field(E.string);
244
- }
229
+ }, {
230
+ pk: ["year", "month", "id"] as const,
231
+ });
245
232
 
246
233
  await E.transact(() => {
247
234
  // Prefix matching — find all events in 2025
248
- for (const e of Event.pk.find({is: [2025]})) { ... }
235
+ for (const e of Event.find({is: [2025]})) { ... }
249
236
 
250
237
  // Find events in March 2025
251
- for (const e of Event.pk.find({is: [2025, 3]})) { ... }
238
+ for (const e of Event.find({is: [2025, 3]})) { ... }
252
239
  });
253
240
  ```
254
241
 
@@ -257,10 +244,7 @@ await E.transact(() => {
257
244
  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.
258
245
 
259
246
  ```typescript
260
- @E.registerModel
261
- class User extends E.Model<User> {
262
- static pk = E.primary(User, "id");
263
- id = E.field(E.identifier);
247
+ const User = E.defineModel(class {
264
248
  firstName = E.field(E.string);
265
249
  lastName = E.field(E.string);
266
250
 
@@ -275,27 +259,30 @@ class User extends E.Model<User> {
275
259
  greet(): string {
276
260
  return `Hello, ${this.fullName}!`;
277
261
  }
278
- }
262
+ });
279
263
  ```
280
264
 
281
265
  #### Computed Indexes
282
266
 
283
- 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).
267
+ Instead of naming fields, you can pass a function as an index specification. 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).
284
268
 
285
269
  ```typescript
286
- @E.registerModel
287
- class Article extends E.Model<Article> {
288
- static pk = E.primary(Article, "id");
289
- static byFullName = E.unique(Article, (a: Article) => [`${a.firstName} ${a.lastName}`]);
290
- static byWord = E.index(Article, (a: Article) => a.title.toLowerCase().split(" "));
291
- static byDomain = E.index(Article, (a: Article) => a.email ? [a.email.split("@")[1]] : []);
292
-
270
+ const Article = E.defineModel(class {
293
271
  id = E.field(E.identifier);
294
272
  firstName = E.field(E.string);
295
273
  lastName = E.field(E.string);
296
274
  title = E.field(E.string);
297
275
  email = E.field(E.opt(E.string));
298
- }
276
+ }, {
277
+ pk: "id",
278
+ unique: {
279
+ byFullName: (a: any) => [`${a.firstName} ${a.lastName}`], // computed covering unique index
280
+ },
281
+ index: {
282
+ byDomain: (a: any) => a.email ? [a.email.split("@")[1]] : [], // computed partial index
283
+ byWord: (a: any) => a.title.toLowerCase().split(" "), // computed multi-index
284
+ },
285
+ });
299
286
 
300
287
  await E.transact(() => {
301
288
  new Article({ firstName: "Jane", lastName: "Doe", title: "Hello World", email: "jane@acme.com" });
@@ -311,34 +298,29 @@ await E.transact(() => {
311
298
  });
312
299
  ```
313
300
 
314
- Computed indexes also support `find()` range queries and `batchProcess()`, just like field-based indexes.
315
-
316
- ### Relationships (Links)
301
+ ### Relationships
317
302
 
318
- Use `E.link(Model)` for foreign keys:
303
+ Use `E.link(Model)` for foreign keys. Use a thunk (a function that just returns a value) for forward references when needed:
319
304
 
320
305
  ```typescript
321
- @E.registerModel
322
- class Author extends E.Model<Author> {
323
- static pk = E.primary(Author, "id");
306
+ const Author = E.defineModel(class {
324
307
  id = E.field(E.identifier);
325
308
  name = E.field(E.string);
326
- }
309
+ }, { pk: "id" });
327
310
 
328
- @E.registerModel
329
- class Book extends E.Model<Book> {
330
- static pk = E.primary(Book, "id");
311
+ const Book = E.defineModel(class {
331
312
  id = E.field(E.identifier);
332
313
  title = E.field(E.string);
333
314
  author = E.field(E.link(Author));
334
- }
315
+ }, { pk: "id" });
335
316
 
336
317
  await E.transact(() => {
337
318
  const author = new Author({name: "Tolkien"});
338
319
  const book = new Book({title: "The Hobbit", author});
339
320
 
340
321
  // Later: linked models are lazy-loaded on property access
341
- const b = Book.pk.get(book.id)!;
322
+ const b = Book.get(book.id)!;
323
+ console.log(b.author.id); // no need to load yet..
342
324
  console.log(b.author.name); // loads Author automatically (~1µs)
343
325
  });
344
326
  ```
@@ -347,7 +329,7 @@ await E.transact(() => {
347
329
 
348
330
  ```typescript
349
331
  await E.transact(() => {
350
- const user = User.pk.get(someId);
332
+ const user = User.get(someId);
351
333
  if (user) user.delete();
352
334
  });
353
335
  ```
@@ -365,10 +347,16 @@ await E.transact(() => {
365
347
  user.preventPersist(); // exclude from commit
366
348
  });
367
349
 
368
- // findAll iterates all instances
350
+ // find() iterates all instances (or use range options)
369
351
  await E.transact(() => {
370
- for (const user of User.findAll()) { ... }
371
- for (const user of User.findAll({reverse: true})) { ... }
352
+ for (const user of User.find()) { ... }
353
+ for (const user of User.find({reverse: true})) { ... }
354
+
355
+ // {fetch: 'first'} returns a single instance or undefined
356
+ const first = User.find({fetch: 'first'});
357
+
358
+ // {fetch: 'single'} returns a single instance, or throws if there are none or more than one
359
+ const only = User.find({fetch: 'single'});
372
360
  });
373
361
 
374
362
  // replaceInto: upsert by primary key
@@ -382,7 +370,7 @@ await E.transact(() => {
382
370
  For large datasets, `batchProcess` auto-commits in batches:
383
371
 
384
372
  ```typescript
385
- await Product.byCategory.batchProcess({is: "old"}, (product) => {
373
+ await Product.batchProcess({ limitRows: 1000 }, (product) => {
386
374
  product.category = "archived";
387
375
  });
388
376
  // Commits every ~1 second or 4096 rows (configurable via limitSeconds, limitRows)
@@ -390,12 +378,10 @@ await Product.byCategory.batchProcess({is: "old"}, (product) => {
390
378
 
391
379
  ### Lazy Schema Migrations
392
380
 
393
- 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.
381
+ When you change a model's schema, Edinburgh lazily migrates old records on access. You can provide a `static migrate(record)` function to transform old rows:
394
382
 
395
383
  ```typescript
396
- @E.registerModel
397
- class User extends E.Model<User> {
398
- static pk = E.primary(User, "id");
384
+ const UserV2 = E.defineModel(class {
399
385
  id = E.field(E.identifier);
400
386
  name = E.field(E.string);
401
387
  role = E.field(E.string); // newly added field
@@ -438,9 +424,7 @@ console.log(result.secondaries); // { User: 1500 }
438
424
  Compute derived fields before data is written:
439
425
 
440
426
  ```typescript
441
- @E.registerModel
442
- class Article extends E.Model<Article> {
443
- static pk = E.primary(Article, "id");
427
+ const Article = E.defineModel(class {
444
428
  id = E.field(E.identifier);
445
429
  title = E.field(E.string);
446
430
  slug = E.field(E.string);
@@ -448,7 +432,7 @@ class Article extends E.Model<Article> {
448
432
  preCommit() {
449
433
  this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
450
434
  }
451
- }
435
+ }, { pk: "id" });
452
436
  ```
453
437
 
454
438
  ### Change Tracking
@@ -493,7 +477,7 @@ The following is auto-generated from `src/edinburgh.ts`:
493
477
 
494
478
  **Signature:** `() => void`
495
479
 
496
- ### init · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L67)
480
+ ### init · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L65)
497
481
 
498
482
  Initialize the database with the specified directory path.
499
483
  This function may be called multiple times with the same parameters. If it is not called before the first transact(),
@@ -511,7 +495,7 @@ the database will be automatically initialized with the default directory.
511
495
  init("./my-database");
512
496
  ```
513
497
 
514
- ### transact · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L118)
498
+ ### transact · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L116)
515
499
 
516
500
  Executes a function within a database transaction context.
517
501
 
@@ -545,7 +529,7 @@ times.
545
529
 
546
530
  ```typescript
547
531
  const paid = await E.transact(() => {
548
- const user = User.pk.get("john_doe");
532
+ const user = User.get("john_doe");
549
533
  if (user.credits > 0) {
550
534
  user.credits--;
551
535
  return true;
@@ -556,12 +540,12 @@ const paid = await E.transact(() => {
556
540
  ```typescript
557
541
  // Transaction with automatic retry on conflicts
558
542
  await E.transact(() => {
559
- const counter = Counter.pk.get("global") || new Counter({id: "global", value: 0});
543
+ const counter = Counter.get("global") || new Counter({id: "global", value: 0});
560
544
  counter.value++;
561
545
  });
562
546
  ```
563
547
 
564
- ### setMaxRetryCount · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L208)
548
+ ### setMaxRetryCount · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L206)
565
549
 
566
550
  Set the maximum number of retries for a transaction in case of conflicts.
567
551
  The default value is 6. Setting it to 0 will disable retries and cause transactions to fail immediately on conflict.
@@ -572,7 +556,7 @@ The default value is 6. Setting it to 0 will disable retries and cause transacti
572
556
 
573
557
  - `count: number` - The maximum number of retries for a transaction.
574
558
 
575
- ### setOnSaveCallback · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L222)
559
+ ### setOnSaveCallback · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L220)
576
560
 
577
561
  Set a callback function to be called after a model is saved and committed.
578
562
 
@@ -585,11 +569,11 @@ Set a callback function to be called after a model is saved and committed.
585
569
  - A sequential number. Higher numbers have been committed after lower numbers.
586
570
  - A map of model instances to their changes. The change can be "created", "deleted", or an object containing the old values.
587
571
 
588
- ### deleteEverything · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L227)
572
+ ### deleteEverything · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L225)
589
573
 
590
574
  **Signature:** `() => Promise<void>`
591
575
 
592
- ### Model · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L220)
576
+ ### Model · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
593
577
 
594
578
  [object Object],[object Object],[object Object],[object Object],[object Object]
595
579
 
@@ -600,37 +584,35 @@ Set a callback function to be called after a model is saved and committed.
600
584
  **Examples:**
601
585
 
602
586
  ```typescript
603
- ⁣@E.registerModel
604
- class User extends E.Model<User> {
605
- static pk = E.primary(User, "id");
606
-
587
+ const User = E.defineModel(class {
607
588
  id = E.field(E.identifier);
608
589
  name = E.field(E.string);
609
590
  email = E.field(E.string);
610
-
611
- static byEmail = E.unique(User, "email");
612
- }
591
+ }, {
592
+ pk: "id",
593
+ unique: { byEmail: "email" },
594
+ });
613
595
  ```
614
596
 
615
- #### Model.tableName · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L227)
597
+ #### Model.tableName · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
616
598
 
617
599
  The database table name (defaults to class name).
618
600
 
619
601
  **Type:** `string`
620
602
 
621
- #### Model.override · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L231)
603
+ #### Model.override · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
622
604
 
623
- When true, registerModel replaces an existing model with the same tableName.
605
+ When true, defineModel replaces an existing model with the same tableName.
624
606
 
625
607
  **Type:** `boolean`
626
608
 
627
- #### Model.fields · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L232)
609
+ #### Model.fields · [static property](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
628
610
 
629
611
  Field configuration metadata.
630
612
 
631
613
  **Type:** `Record<string | number | symbol, FieldConfig<unknown>>`
632
614
 
633
- #### Model.migrate · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
615
+ #### Model.migrate · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
634
616
 
635
617
  Optional migration function called when deserializing rows written with an older schema version.
636
618
  Receives a plain record with all fields (primary key fields + value fields) and should mutate it
@@ -652,9 +634,7 @@ will only be updated when `runMigration()` is run (not during lazy loading).
652
634
  **Examples:**
653
635
 
654
636
  ```typescript
655
- ⁣@E.registerModel
656
- class User extends E.Model<User> {
657
- static pk = E.primary(User, "id");
637
+ const User = E.defineModel(class {
658
638
  id = E.field(E.identifier);
659
639
  name = E.field(E.string);
660
640
  role = E.field(E.string); // new field
@@ -662,23 +642,43 @@ class User extends E.Model<User> {
662
642
  static migrate(record: Record<string, any>) {
663
643
  record.role ??= "user"; // default for rows that predate the 'role' field
664
644
  }
665
- }
645
+ }, { pk: "id" });
666
646
  ```
667
647
 
668
- #### Model.findAll · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
648
+ #### Model.get · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
669
649
 
670
- Find all instances of this model in the database, ordered by primary key.
650
+ **Signature:** `(...args: any[]) => any`
671
651
 
672
- **Signature:** `<T extends typeof Model<unknown>>(this: T, opts?: { reverse?: boolean; }) => IndexRangeIterator<T>`
652
+ **Parameters:**
653
+
654
+ - `args: any[]`
655
+
656
+ #### Model.getLazy · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
657
+
658
+ **Signature:** `(...args: any[]) => any`
673
659
 
674
660
  **Parameters:**
675
661
 
676
- - `this: T`
677
- - `opts?: {reverse?: boolean}` - - Optional parameters.
662
+ - `args: any[]`
678
663
 
679
- **Returns:** An iterator.
664
+ #### Model.find · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
680
665
 
681
- #### Model.replaceInto · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
666
+ **Signature:** `(opts?: any) => any`
667
+
668
+ **Parameters:**
669
+
670
+ - `opts?: any`
671
+
672
+ #### Model.batchProcess · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
673
+
674
+ **Signature:** `(opts: any, callback?: any) => any`
675
+
676
+ **Parameters:**
677
+
678
+ - `opts: any`
679
+ - `callback?: any`
680
+
681
+ #### Model.replaceInto · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
682
682
 
683
683
  Load an existing instance by primary key and update it, or create a new one.
684
684
 
@@ -686,16 +686,16 @@ The provided object must contain all primary key fields. If a matching row exist
686
686
  the remaining properties from `obj` are set on the loaded instance. Otherwise a
687
687
  new instance is created with `obj` as its initial properties.
688
688
 
689
- **Signature:** `<T extends typeof Model<any>>(this: T, obj: Partial<Omit<InstanceType<T>, "constructor">>) => InstanceType<T>`
689
+ **Signature:** `<T extends typeof Model<any>>(this: T, obj: Partial<Record<string, any>>) => InstanceType<T>`
690
690
 
691
691
  **Parameters:**
692
692
 
693
693
  - `this: T`
694
- - `obj: Partial<Omit<InstanceType<T>, "constructor">>` - - Partial model data that **must** include every primary key field.
694
+ - `obj: Partial<Record<string, any>>` - - Partial model data that **must** include every primary key field.
695
695
 
696
696
  **Returns:** The loaded-and-updated or newly created instance.
697
697
 
698
- #### model.preCommit · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
698
+ #### model.preCommit · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
699
699
 
700
700
  Optional hook called on each modified instance right before the transaction commits.
701
701
  Runs before data is written to disk, so changes made here are included in the commit.
@@ -711,9 +711,7 @@ Common use cases:
711
711
  **Examples:**
712
712
 
713
713
  ```typescript
714
- ⁣@E.registerModel
715
- class Post extends E.Model<Post> {
716
- static pk = E.primary(Post, "id");
714
+ const Post = E.defineModel(class {
717
715
  id = E.field(E.identifier);
718
716
  title = E.field(E.string);
719
717
  slug = E.field(E.string);
@@ -721,22 +719,22 @@ class Post extends E.Model<Post> {
721
719
  preCommit() {
722
720
  this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
723
721
  }
724
- }
722
+ }, { pk: "id" });
725
723
  ```
726
724
 
727
- #### model.getPrimaryKey · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
725
+ #### model.getPrimaryKey · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
728
726
 
729
727
  **Signature:** `() => Uint8Array<ArrayBufferLike>`
730
728
 
731
729
  **Returns:** The primary key for this instance.
732
730
 
733
- #### model.getPrimaryKeyHash · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
731
+ #### model.getPrimaryKeyHash · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
734
732
 
735
733
  **Signature:** `() => number`
736
734
 
737
735
  **Returns:** A 53-bit positive integer non-cryptographic hash of the primary key, or undefined if not yet saved.
738
736
 
739
- #### model.isLazyField · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
737
+ #### model.isLazyField · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
740
738
 
741
739
  **Signature:** `(field: keyof this) => boolean`
742
740
 
@@ -744,7 +742,7 @@ class Post extends E.Model<Post> {
744
742
 
745
743
  - `field: keyof this`
746
744
 
747
- #### model.preventPersist · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
745
+ #### model.preventPersist · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
748
746
 
749
747
  Prevent this instance from being persisted to the database.
750
748
 
@@ -755,12 +753,12 @@ Prevent this instance from being persisted to the database.
755
753
  **Examples:**
756
754
 
757
755
  ```typescript
758
- const user = User.load("user123");
756
+ const user = User.get("user123");
759
757
  user.name = "New Name";
760
758
  user.preventPersist(); // Changes won't be saved
761
759
  ```
762
760
 
763
- #### model.delete · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
761
+ #### model.delete · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
764
762
 
765
763
  Delete this model instance from the database.
766
764
 
@@ -771,11 +769,11 @@ Removes the instance and all its index entries from the database and prevents fu
771
769
  **Examples:**
772
770
 
773
771
  ```typescript
774
- const user = User.load("user123");
772
+ const user = User.get("user123");
775
773
  user.delete(); // Removes from database
776
774
  ```
777
775
 
778
- #### model.validate · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
776
+ #### model.validate · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
779
777
 
780
778
  Validate all fields in this model instance.
781
779
 
@@ -797,7 +795,7 @@ if (errors.length > 0) {
797
795
  }
798
796
  ```
799
797
 
800
- #### model.isValid · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
798
+ #### model.isValid · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
801
799
 
802
800
  Check if this model instance is valid.
803
801
 
@@ -812,46 +810,42 @@ const user = new User({name: "John"});
812
810
  if (!user.isValid()) shoutAtTheUser();
813
811
  ```
814
812
 
815
- #### model.getState · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
813
+ #### model.getState · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
816
814
 
817
815
  **Signature:** `() => "created" | "deleted" | "loaded" | "lazy"`
818
816
 
819
- #### model.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
817
+ #### model.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
820
818
 
821
819
  **Signature:** `() => string`
822
820
 
823
- #### model.[Symbol.for('nodejs.util.inspect.custom')] · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
821
+ #### model.[Symbol.for('nodejs.util.inspect.custom')] · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
824
822
 
825
823
  **Signature:** `() => string`
826
824
 
827
- ### registerModel · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L113)
825
+ ### defineModel · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L164)
828
826
 
829
827
  Register a model class with the Edinburgh ORM system.
830
828
 
831
- **Signature:** `<T extends typeof Model<unknown>>(MyModel: T) => T`
829
+ Converts a plain class into a fully-featured model with database persistence,
830
+ typed fields, primary key access, and optional secondary and unique indexes.
831
+
832
+ **Signature:** `<T extends new () => any, const PK extends (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[], const UNIQUE extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>, const INDEX extends Record<string, (keyof FieldsOf<T> & string) | ...`
832
833
 
833
834
  **Type Parameters:**
834
835
 
835
- - `T extends typeof Model<unknown>` - The model class type.
836
+ - `T extends new () => any`
837
+ - `PK extends (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[]`
838
+ - `UNIQUE extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>`
839
+ - `INDEX extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>`
836
840
 
837
841
  **Parameters:**
838
842
 
839
- - `MyModel: T` - - The model class to register.
840
-
841
- **Returns:** The enhanced model class with ORM capabilities.
843
+ - `cls: T` - - A plain class whose properties use E.field().
844
+ - `opts?: { pk?: PK, unique?: UNIQUE, index?: INDEX, tableName?: string, override?: boolean }` - - Registration options.
842
845
 
843
- **Examples:**
844
-
845
- ```typescript
846
- ⁣@E.registerModel
847
- class User extends E.Model<User> {
848
- static pk = E.index(User, ["id"], "primary");
849
- id = E.field(E.identifier);
850
- name = E.field(E.string);
851
- }
852
- ```
846
+ **Returns:** The enhanced model constructor.
853
847
 
854
- ### field · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L89)
848
+ ### field · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L87)
855
849
 
856
850
  Create a field definition for a model property.
857
851
 
@@ -875,19 +869,26 @@ This allows for both runtime introspection and compile-time type safety.
875
869
  **Examples:**
876
870
 
877
871
  ```typescript
878
- class User extends E.Model<User> {
872
+ const User = E.defineModel(class {
879
873
  name = E.field(E.string, {description: "User's full name"});
880
874
  age = E.field(E.opt(E.number), {description: "User's age", default: 25});
881
- }
875
+ });
882
876
  ```
883
877
 
884
- ### string · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
878
+ ### currentTxn · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L21)
879
+
880
+ Returns the current transaction from AsyncLocalStorage.
881
+ Throws if called outside a transact() callback.
882
+
883
+ **Signature:** `() => Transaction`
884
+
885
+ ### string · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
885
886
 
886
887
  Type wrapper instance for the string type.
887
888
 
888
889
  **Value:** `TypeWrapper<string>`
889
890
 
890
- ### orderedString · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
891
+ ### orderedString · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
891
892
 
892
893
  Type wrapper instance for the ordered string type, which is just like a string
893
894
  except that it sorts lexicographically in the database (instead of by incrementing
@@ -897,37 +898,37 @@ may not contain null characters.
897
898
 
898
899
  **Value:** `TypeWrapper<string>`
899
900
 
900
- ### number · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
901
+ ### number · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
901
902
 
902
903
  Type wrapper instance for the number type.
903
904
 
904
905
  **Value:** `TypeWrapper<number>`
905
906
 
906
- ### dateTime · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
907
+ ### dateTime · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
907
908
 
908
909
  Type wrapper instance for the date/time type. Stored without timezone info, rounded to whole seconds.
909
910
 
910
911
  **Value:** `TypeWrapper<Date>`
911
912
 
912
- ### boolean · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
913
+ ### boolean · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
913
914
 
914
915
  Type wrapper instance for the boolean type.
915
916
 
916
917
  **Value:** `TypeWrapper<boolean>`
917
918
 
918
- ### identifier · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
919
+ ### identifier · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
919
920
 
920
921
  Type wrapper instance for the identifier type.
921
922
 
922
923
  **Value:** `TypeWrapper<string>`
923
924
 
924
- ### undef · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
925
+ ### undef · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
925
926
 
926
927
  Type wrapper instance for the 'undefined' type.
927
928
 
928
929
  **Value:** `TypeWrapper<undefined>`
929
930
 
930
- ### opt · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
931
+ ### opt · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
931
932
 
932
933
  Create an optional type wrapper (allows undefined).
933
934
 
@@ -950,7 +951,7 @@ const optionalString = E.opt(E.string);
950
951
  const optionalNumber = E.opt(E.number);
951
952
  ```
952
953
 
953
- ### or · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
954
+ ### or · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
954
955
 
955
956
  Create a union type wrapper from multiple type choices.
956
957
 
@@ -973,7 +974,7 @@ const stringOrNumber = E.or(E.string, E.number);
973
974
  const status = E.or("active", "inactive", "pending");
974
975
  ```
975
976
 
976
- ### array · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
977
+ ### array · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
977
978
 
978
979
  Create an array type wrapper with optional length constraints.
979
980
 
@@ -997,7 +998,7 @@ const stringArray = E.array(E.string);
997
998
  const boundedArray = E.array(E.number, {min: 1, max: 10});
998
999
  ```
999
1000
 
1000
- ### set · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1001
+ ### set · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1001
1002
 
1002
1003
  Create a Set type wrapper with optional length constraints.
1003
1004
 
@@ -1021,7 +1022,7 @@ const stringSet = E.set(E.string);
1021
1022
  const boundedSet = E.set(E.number, {min: 1, max: 10});
1022
1023
  ```
1023
1024
 
1024
- ### record · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1025
+ ### record · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1025
1026
 
1026
1027
  Create a Record type wrapper for key-value objects with string or number keys.
1027
1028
 
@@ -1043,7 +1044,7 @@ Create a Record type wrapper for key-value objects with string or number keys.
1043
1044
  const scores = E.record(E.number); // Record<string | number, number>
1044
1045
  ```
1045
1046
 
1046
- ### literal · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1047
+ ### literal · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1047
1048
 
1048
1049
  Create a literal type wrapper for a constant value.
1049
1050
 
@@ -1066,15 +1067,15 @@ const statusType = E.literal("active");
1066
1067
  const countType = E.literal(42);
1067
1068
  ```
1068
1069
 
1069
- ### link · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1070
+ ### link · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1070
1071
 
1071
1072
  Create a link type wrapper for model relationships.
1072
1073
 
1073
- **Signature:** `<const T extends typeof Model<any>>(TargetModel: T) => TypeWrapper<InstanceType<T>>`
1074
+ **Signature:** `{ <const T extends new (...args: any[]) => Model<any>>(TargetModel: T): TypeWrapper<InstanceType<T>>; <const T extends new (...args: any[]) => Model<any>>(TargetModel: () => T): TypeWrapper<...>; }`
1074
1075
 
1075
1076
  **Type Parameters:**
1076
1077
 
1077
- - `T extends typeof Model<any>` - The target model class.
1078
+ - `T extends new (...args: any[]) => Model<any>` - The target model class.
1078
1079
 
1079
1080
  **Parameters:**
1080
1081
 
@@ -1085,109 +1086,18 @@ Create a link type wrapper for model relationships.
1085
1086
  **Examples:**
1086
1087
 
1087
1088
  ```typescript
1088
- class User extends E.Model<User> {
1089
- posts = E.field(E.array(E.link(Post, 'author')));
1090
- }
1091
-
1092
- class Post extends E.Model<Post> {
1093
- author = E.field(E.link(User));
1094
- }
1095
- ```
1096
-
1097
- ### index · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1098
-
1099
- Create a secondary index on model fields, or a computed secondary index using a function.
1100
-
1101
- For field-based indexes, pass a field name or array of field names.
1102
- For computed indexes, pass a function that takes a model instance and returns an array of
1103
- index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1104
- separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1105
-
1106
- **Signature:** `{ <M extends typeof Model, V>(MyModel: M, fn: (instance: InstanceType<M>) => V[]): SecondaryIndex<M, [], [V]>; <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): SecondaryIndex<...>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(...`
1107
-
1108
- **Type Parameters:**
1109
-
1110
- - `M extends typeof Model` - The model class.
1111
- - `V` - The computed index value type (for function-based indexes).
1112
-
1113
- **Parameters:**
1114
-
1115
- - `MyModel: M` - - The model class to create the index for.
1116
- - `fn: (instance: InstanceType<M>) => V[]`
1117
-
1118
- **Returns:** A new SecondaryIndex instance.
1119
-
1120
- **Examples:**
1121
-
1122
- ```typescript
1123
- class User extends E.Model<User> {
1124
- static byAge = E.index(User, "age");
1125
- static byTagsDate = E.index(User, ["tags", "createdAt"]);
1126
- static byWord = E.index(User, (u: User) => u.name.split(" "));
1127
- }
1128
- ```
1129
-
1130
- ### primary · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1131
-
1132
- Create a primary index on model fields.
1133
-
1134
- **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<...>; }`
1135
-
1136
- **Type Parameters:**
1137
-
1138
- - `M extends typeof Model` - The model class.
1139
- - `F extends (keyof InstanceType<M> & string)` - The field name (for single field index).
1140
-
1141
- **Parameters:**
1142
-
1143
- - `MyModel: M` - - The model class to create the index for.
1144
- - `field: F` - - Single field name for simple indexes.
1145
-
1146
- **Returns:** A new PrimaryIndex instance.
1147
-
1148
- **Examples:**
1149
-
1150
- ```typescript
1151
- class User extends E.Model<User> {
1152
- static pk = E.primary(User, ["id"]);
1153
- static pkSingle = E.primary(User, "id");
1154
- }
1155
- ```
1156
-
1157
- ### unique · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1158
-
1159
- Create a unique index on model fields, or a computed unique index using a function.
1160
-
1161
- For field-based indexes, pass a field name or array of field names.
1162
- For computed indexes, pass a function that takes a model instance and returns an array of
1163
- index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1164
- separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1165
-
1166
- **Signature:** `{ <M extends typeof Model, V>(MyModel: M, fn: (instance: InstanceType<M>) => V[]): UniqueIndex<M, [], [V]>; <M extends typeof Model, const F extends (keyof InstanceType<M> & string)>(MyModel: M, field: F): UniqueIndex<...>; <M extends typeof Model, const FS extends readonly (keyof InstanceType<M> & string)[]>(MyMode...`
1167
-
1168
- **Type Parameters:**
1169
-
1170
- - `M extends typeof Model` - The model class.
1171
- - `V` - The computed index value type (for function-based indexes).
1172
-
1173
- **Parameters:**
1174
-
1175
- - `MyModel: M` - - The model class to create the index for.
1176
- - `fn: (instance: InstanceType<M>) => V[]`
1177
-
1178
- **Returns:** A new UniqueIndex instance.
1179
-
1180
- **Examples:**
1089
+ const Author = E.defineModel(class {
1090
+ id = E.field(E.identifier);
1091
+ posts = E.field(E.array(E.link(() => Book)));
1092
+ }, { pk: "id" });
1181
1093
 
1182
- ```typescript
1183
- class User extends E.Model<User> {
1184
- static byEmail = E.unique(User, "email");
1185
- static byNameAge = E.unique(User, ["name", "age"]);
1186
- static byFullName = E.unique(User, (u: User) => [`${u.firstName} ${u.lastName}`]);
1187
- }
1094
+ const Book = E.defineModel(class {
1095
+ id = E.field(E.identifier);
1096
+ author = E.field(E.link(Author));
1097
+ }, { pk: "id" });
1188
1098
  ```
1189
1099
 
1190
- ### dump · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1100
+ ### dump · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1191
1101
 
1192
1102
  Dump database contents for debugging.
1193
1103
 
@@ -1196,7 +1106,7 @@ This is primarily useful for development and debugging purposes.
1196
1106
 
1197
1107
  **Signature:** `() => void`
1198
1108
 
1199
- ### BaseIndex · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L125)
1109
+ ### BaseIndex · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L130)
1200
1110
 
1201
1111
  Base class for database indexes for efficient lookups on model fields.
1202
1112
 
@@ -1213,113 +1123,97 @@ Indexes enable fast queries on specific field combinations and enforce uniquenes
1213
1123
  - `MyModel`: - The model class this index belongs to.
1214
1124
  - `_fieldNames`: - Array of field names that make up this index.
1215
1125
 
1216
- #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1126
+ #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1217
1127
 
1218
- **Signature:** `(opts?: FindOptions<ARGS>) => IndexRangeIterator<M>`
1128
+ **Signature:** `{ (opts?: FindOptions<ARGS, "first">): InstanceType<M>; (opts: FindOptions<ARGS, "single">): InstanceType<M>; (opts?: FindOptions<...>): IndexRangeIterator<...>; }`
1219
1129
 
1220
1130
  **Parameters:**
1221
1131
 
1222
- - `opts: FindOptions<ARGS>` (optional)
1132
+ - `opts?: FindOptions<ARGS, 'first'>`
1223
1133
 
1224
- #### baseIndex.batchProcess · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1134
+ #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1225
1135
 
1226
- [object Object],[object Object],[object Object]
1227
-
1228
- **Signature:** `(opts: FindOptions<ARGS> & { limitSeconds?: number; limitRows?: number; }, callback: (row: InstanceType<M>) => void | Promise<void>) => Promise<...>`
1136
+ **Signature:** `{ (opts?: FindOptions<ARGS, "first">): InstanceType<M>; (opts: FindOptions<ARGS, "single">): InstanceType<M>; (opts?: FindOptions<...>): IndexRangeIterator<...>; }`
1229
1137
 
1230
1138
  **Parameters:**
1231
1139
 
1232
- - `opts: FindOptions<ARGS> & { limitSeconds?: number; limitRows?: number }` (optional) - - Query options (same as `find()`), plus:
1233
- - `callback: (row: InstanceType<M>) => void | Promise<void>` - - Called for each matching row within a transaction
1234
-
1235
- #### baseIndex.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1236
-
1237
- **Signature:** `() => string`
1140
+ - `opts: FindOptions<ARGS, 'single'>`
1238
1141
 
1239
- ### UniqueIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1142
+ #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1240
1143
 
1241
- Unique index that stores references to the primary key.
1144
+ **Signature:** `{ (opts?: FindOptions<ARGS, "first">): InstanceType<M>; (opts: FindOptions<ARGS, "single">): InstanceType<M>; (opts?: FindOptions<...>): IndexRangeIterator<...>; }`
1242
1145
 
1243
- **Type Parameters:**
1244
-
1245
- - `M extends typeof Model` - The model class this index belongs to.
1246
- - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1247
- - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1146
+ **Parameters:**
1248
1147
 
1249
- #### uniqueIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1148
+ - `opts?: FindOptions<ARGS>`
1250
1149
 
1251
- Get a model instance by unique index key values.
1150
+ #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1252
1151
 
1253
- **Signature:** `(...args: ARGS) => InstanceType<M>`
1152
+ **Signature:** `{ (opts?: FindOptions<ARGS, "first">): InstanceType<M>; (opts: FindOptions<ARGS, "single">): InstanceType<M>; (opts?: FindOptions<...>): IndexRangeIterator<...>; }`
1254
1153
 
1255
1154
  **Parameters:**
1256
1155
 
1257
- - `args: ARGS` - - The unique index key values.
1258
-
1259
- **Returns:** The model instance if found, undefined otherwise.
1156
+ - `opts: any` (optional)
1260
1157
 
1261
- **Examples:**
1158
+ #### baseIndex.batchProcess · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1262
1159
 
1263
- ```typescript
1264
- const userByEmail = User.byEmail.get("john@example.com");
1265
- ```
1160
+ [object Object],[object Object],[object Object]
1266
1161
 
1267
- ### PrimaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1162
+ **Signature:** `(opts: FindOptions<ARGS, undefined> & { limitSeconds?: number; limitRows?: number; }, callback: (row: InstanceType<M>) => void | Promise<void>) => Promise<...>`
1268
1163
 
1269
- Primary index that stores the actual model data.
1164
+ **Parameters:**
1270
1165
 
1271
- **Type Parameters:**
1166
+ - `opts: FindOptions<ARGS> & { limitSeconds?: number; limitRows?: number }` (optional) - - Query options (same as `find()`), plus:
1167
+ - `callback: (row: InstanceType<M>) => void | Promise<void>` - - Called for each matching row within a transaction
1272
1168
 
1273
- - `M extends typeof Model` - The model class this index belongs to.
1274
- - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1169
+ #### baseIndex.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1275
1170
 
1276
- #### primaryIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1171
+ **Signature:** `() => string`
1277
1172
 
1278
- Get a model instance by primary key values.
1173
+ ### NonPrimaryIndex · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1279
1174
 
1280
- **Signature:** `(...args: IndexArgTypes<M, F>) => InstanceType<M>`
1175
+ Abstract base for all non-primary indexes (unique and secondary).
1176
+ Provides shared key serialization, write/delete/update logic.
1281
1177
 
1282
- **Parameters:**
1178
+ **Type Parameters:**
1283
1179
 
1284
- - `args: IndexArgTypes<M, F>` - - The primary key values.
1180
+ - `M extends typeof Model`
1181
+ - `F extends readonly (keyof InstanceType<M> & string)[]`
1182
+ - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1285
1183
 
1286
- **Returns:** The model instance if found, undefined otherwise.
1184
+ ### UniqueIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1287
1185
 
1288
- **Examples:**
1186
+ Unique index that stores references to the primary key.
1289
1187
 
1290
- ```typescript
1291
- const user = User.pk.get("john_doe");
1292
- ```
1188
+ **Type Parameters:**
1293
1189
 
1294
- #### primaryIndex.getLazy · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1190
+ - `M extends typeof Model`
1191
+ - `F extends readonly (keyof InstanceType<M> & string)[]`
1192
+ - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1295
1193
 
1296
- Does the same as as `get()`, but will delay loading the instance from disk until the first
1297
- property access. In case it turns out the instance doesn't exist, an error will be thrown
1298
- at that time.
1194
+ #### uniqueIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1299
1195
 
1300
- **Signature:** `(...args: IndexArgTypes<M, F>) => InstanceType<M>`
1196
+ **Signature:** `(...args: ARGS) => InstanceType<M>`
1301
1197
 
1302
1198
  **Parameters:**
1303
1199
 
1304
- - `args: IndexArgTypes<M, F>` - Primary key field values. (Or a single Uint8Array containing the key.)
1305
-
1306
- **Returns:** The (lazily loaded) model instance.
1200
+ - `args: ARGS`
1307
1201
 
1308
- ### SecondaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L253)
1202
+ ### SecondaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1309
1203
 
1310
1204
  Secondary index for non-unique lookups.
1311
1205
 
1312
1206
  **Type Parameters:**
1313
1207
 
1314
- - `M extends typeof Model` - The model class this index belongs to.
1315
- - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1208
+ - `M extends typeof Model`
1209
+ - `F extends readonly (keyof InstanceType<M> & string)[]`
1316
1210
  - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1317
1211
 
1318
- ### Change · [type](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L95)
1212
+ ### Change · [type](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L94)
1319
1213
 
1320
1214
  **Type:** `Record<any, any> | "created" | "deleted"`
1321
1215
 
1322
- ### Transaction · [interface](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L38)
1216
+ ### Transaction · [interface](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L39)
1323
1217
 
1324
1218
  #### transaction.id · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L41)
1325
1219
 
@@ -1329,11 +1223,11 @@ Secondary index for non-unique lookups.
1329
1223
 
1330
1224
  **Type:** `Set<Model<unknown>>`
1331
1225
 
1332
- #### transaction.instancesByPk · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L46)
1226
+ #### transaction.instancesByPk · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L44)
1333
1227
 
1334
1228
  **Type:** `Map<number, Model<unknown>>`
1335
1229
 
1336
- ### DatabaseError · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L157)
1230
+ ### DatabaseError · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L156)
1337
1231
 
1338
1232
  The DatabaseError class is used to represent errors that occur during database operations.
1339
1233
  It extends the built-in Error class and has a machine readable error code string property.
@@ -1343,7 +1237,7 @@ Invalid function arguments will throw TypeError.
1343
1237
 
1344
1238
  **Value:** `DatabaseErrorConstructor`
1345
1239
 
1346
- ### runMigration · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L124)
1240
+ ### runMigration · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L122)
1347
1241
 
1348
1242
  Run database migration: populate secondary indexes for old-version rows,
1349
1243
  convert old primary indices, rewrite row data, and clean up orphaned indices.
@@ -1354,71 +1248,71 @@ convert old primary indices, rewrite row data, and clean up orphaned indices.
1354
1248
 
1355
1249
  - `options: MigrationOptions` (optional)
1356
1250
 
1357
- ### MigrationOptions · [interface](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L16)
1251
+ ### MigrationOptions · [interface](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L13)
1358
1252
 
1359
- #### migrationOptions.tables · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L18)
1253
+ #### migrationOptions.tables · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L17)
1360
1254
 
1361
1255
  Limit migration to specific table names.
1362
1256
 
1363
1257
  **Type:** `string[]`
1364
1258
 
1365
- #### migrationOptions.populateSecondaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L23)
1259
+ #### migrationOptions.populateSecondaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L21)
1366
1260
 
1367
1261
  Populate secondary indexes for rows at old schema versions (default: true).
1368
1262
 
1369
1263
  **Type:** `boolean`
1370
1264
 
1371
- #### migrationOptions.migratePrimaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L32)
1265
+ #### migrationOptions.migratePrimaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L29)
1372
1266
 
1373
1267
  Convert old primary indices when primary key fields changed (default: true).
1374
1268
 
1375
1269
  **Type:** `boolean`
1376
1270
 
1377
- #### migrationOptions.rewriteData · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L40)
1271
+ #### migrationOptions.rewriteData · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L37)
1378
1272
 
1379
1273
  Rewrite all row data to the latest schema version (default: false).
1380
1274
 
1381
1275
  **Type:** `boolean`
1382
1276
 
1383
- #### migrationOptions.removeOrphans · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L46)
1277
+ #### migrationOptions.removeOrphans · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L44)
1384
1278
 
1385
1279
  Delete orphaned secondary/unique index entries (default: true).
1386
1280
 
1387
1281
  **Type:** `boolean`
1388
1282
 
1389
- #### migrationOptions.onProgress · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L48)
1283
+ #### migrationOptions.onProgress · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L44)
1390
1284
 
1391
1285
  Progress callback.
1392
1286
 
1393
1287
  **Type:** `(info: ProgressInfo) => void`
1394
1288
 
1395
- ### MigrationResult · [interface](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L51)
1289
+ ### MigrationResult · [interface](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L49)
1396
1290
 
1397
- #### migrationResult.secondaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L54)
1291
+ #### migrationResult.secondaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L50)
1398
1292
 
1399
1293
  Per-table counts of secondary index entries populated.
1400
1294
 
1401
1295
  **Type:** `Record<string, number>`
1402
1296
 
1403
- #### migrationResult.primaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L59)
1297
+ #### migrationResult.primaries · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L56)
1404
1298
 
1405
1299
  Per-table counts of old primary rows migrated.
1406
1300
 
1407
1301
  **Type:** `Record<string, number>`
1408
1302
 
1409
- #### migrationResult.conversionFailures · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L59)
1303
+ #### migrationResult.conversionFailures · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L57)
1410
1304
 
1411
1305
  Per-table conversion failure counts by reason.
1412
1306
 
1413
1307
  **Type:** `Record<string, Record<string, number>>`
1414
1308
 
1415
- #### migrationResult.rewritten · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L64)
1309
+ #### migrationResult.rewritten · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L60)
1416
1310
 
1417
1311
  Per-table counts of rows rewritten to latest version.
1418
1312
 
1419
1313
  **Type:** `Record<string, number>`
1420
1314
 
1421
- #### migrationResult.orphans · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L68)
1315
+ #### migrationResult.orphans · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L65)
1422
1316
 
1423
1317
  Number of orphaned index entries deleted.
1424
1318