edinburgh 0.4.6 → 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 +263 -381
  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 +6 -7
  6. package/build/src/edinburgh.js.map +1 -1
  7. package/build/src/indexes.d.ts +44 -113
  8. package/build/src/indexes.js +145 -175
  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 +73 -54
  13. package/build/src/models.js +110 -171
  14. package/build/src/models.js.map +1 -1
  15. package/build/src/types.d.ts +29 -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 +140 -150
  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 +6 -9
  41. package/src/indexes.ts +155 -271
  42. package/src/migrate.ts +9 -30
  43. package/src/models.ts +186 -180
  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#L209)
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#L223)
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#L228)
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#L221)
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#L225)
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#L233)
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#L255)
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,35 +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.initFields · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
648
+ #### Model.get · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
669
649
 
670
- Transform the model's `E.field` properties into the appropriate JavaScript properties. Normally this is done
671
- automatically when using `transact()`, but in case you need to access `Model.fields` directly before the first
672
- transaction, you can call this method manually.
673
-
674
- **Signature:** `(reset?: boolean) => void`
650
+ **Signature:** `(...args: any[]) => any`
675
651
 
676
652
  **Parameters:**
677
653
 
678
- - `reset?: boolean`
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`
659
+
660
+ **Parameters:**
679
661
 
680
- #### Model.findAll · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
662
+ - `args: any[]`
681
663
 
682
- Find all instances of this model in the database, ordered by primary key.
664
+ #### Model.find · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
683
665
 
684
- **Signature:** `<T extends typeof Model<unknown>>(this: T, opts?: { reverse?: boolean; }) => IndexRangeIterator<T>`
666
+ **Signature:** `(opts?: any) => any`
685
667
 
686
668
  **Parameters:**
687
669
 
688
- - `this: T`
689
- - `opts?: {reverse?: boolean}` - - Optional parameters.
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:**
690
677
 
691
- **Returns:** An iterator.
678
+ - `opts: any`
679
+ - `callback?: any`
692
680
 
693
- #### Model.replaceInto · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
681
+ #### Model.replaceInto · [static method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
694
682
 
695
683
  Load an existing instance by primary key and update it, or create a new one.
696
684
 
@@ -698,16 +686,16 @@ The provided object must contain all primary key fields. If a matching row exist
698
686
  the remaining properties from `obj` are set on the loaded instance. Otherwise a
699
687
  new instance is created with `obj` as its initial properties.
700
688
 
701
- **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>`
702
690
 
703
691
  **Parameters:**
704
692
 
705
693
  - `this: T`
706
- - `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.
707
695
 
708
696
  **Returns:** The loaded-and-updated or newly created instance.
709
697
 
710
- #### model.preCommit · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
698
+ #### model.preCommit · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
711
699
 
712
700
  Optional hook called on each modified instance right before the transaction commits.
713
701
  Runs before data is written to disk, so changes made here are included in the commit.
@@ -723,9 +711,7 @@ Common use cases:
723
711
  **Examples:**
724
712
 
725
713
  ```typescript
726
- ⁣@E.registerModel
727
- class Post extends E.Model<Post> {
728
- static pk = E.primary(Post, "id");
714
+ const Post = E.defineModel(class {
729
715
  id = E.field(E.identifier);
730
716
  title = E.field(E.string);
731
717
  slug = E.field(E.string);
@@ -733,22 +719,22 @@ class Post extends E.Model<Post> {
733
719
  preCommit() {
734
720
  this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
735
721
  }
736
- }
722
+ }, { pk: "id" });
737
723
  ```
738
724
 
739
- #### model.getPrimaryKey · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
725
+ #### model.getPrimaryKey · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
740
726
 
741
727
  **Signature:** `() => Uint8Array<ArrayBufferLike>`
742
728
 
743
729
  **Returns:** The primary key for this instance.
744
730
 
745
- #### model.getPrimaryKeyHash · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
731
+ #### model.getPrimaryKeyHash · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
746
732
 
747
733
  **Signature:** `() => number`
748
734
 
749
735
  **Returns:** A 53-bit positive integer non-cryptographic hash of the primary key, or undefined if not yet saved.
750
736
 
751
- #### model.isLazyField · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
737
+ #### model.isLazyField · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
752
738
 
753
739
  **Signature:** `(field: keyof this) => boolean`
754
740
 
@@ -756,7 +742,7 @@ class Post extends E.Model<Post> {
756
742
 
757
743
  - `field: keyof this`
758
744
 
759
- #### model.preventPersist · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
745
+ #### model.preventPersist · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
760
746
 
761
747
  Prevent this instance from being persisted to the database.
762
748
 
@@ -767,12 +753,12 @@ Prevent this instance from being persisted to the database.
767
753
  **Examples:**
768
754
 
769
755
  ```typescript
770
- const user = User.load("user123");
756
+ const user = User.get("user123");
771
757
  user.name = "New Name";
772
758
  user.preventPersist(); // Changes won't be saved
773
759
  ```
774
760
 
775
- #### model.delete · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
761
+ #### model.delete · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
776
762
 
777
763
  Delete this model instance from the database.
778
764
 
@@ -783,11 +769,11 @@ Removes the instance and all its index entries from the database and prevents fu
783
769
  **Examples:**
784
770
 
785
771
  ```typescript
786
- const user = User.load("user123");
772
+ const user = User.get("user123");
787
773
  user.delete(); // Removes from database
788
774
  ```
789
775
 
790
- #### model.validate · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
776
+ #### model.validate · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
791
777
 
792
778
  Validate all fields in this model instance.
793
779
 
@@ -809,7 +795,7 @@ if (errors.length > 0) {
809
795
  }
810
796
  ```
811
797
 
812
- #### model.isValid · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
798
+ #### model.isValid · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
813
799
 
814
800
  Check if this model instance is valid.
815
801
 
@@ -824,46 +810,42 @@ const user = new User({name: "John"});
824
810
  if (!user.isValid()) shoutAtTheUser();
825
811
  ```
826
812
 
827
- #### model.getState · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
813
+ #### model.getState · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
828
814
 
829
815
  **Signature:** `() => "created" | "deleted" | "loaded" | "lazy"`
830
816
 
831
- #### model.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
817
+ #### model.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
832
818
 
833
819
  **Signature:** `() => string`
834
820
 
835
- #### model.[Symbol.for('nodejs.util.inspect.custom')] · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
821
+ #### model.[Symbol.for('nodejs.util.inspect.custom')] · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
836
822
 
837
823
  **Signature:** `() => string`
838
824
 
839
- ### 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)
840
826
 
841
827
  Register a model class with the Edinburgh ORM system.
842
828
 
843
- **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) | ...`
844
833
 
845
834
  **Type Parameters:**
846
835
 
847
- - `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)>`
848
840
 
849
841
  **Parameters:**
850
842
 
851
- - `MyModel: T` - - The model class to register.
852
-
853
- **Returns:** The enhanced model class with ORM capabilities.
854
-
855
- **Examples:**
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.
856
845
 
857
- ```typescript
858
- ⁣@E.registerModel
859
- class User extends E.Model<User> {
860
- static pk = E.index(User, ["id"], "primary");
861
- id = E.field(E.identifier);
862
- name = E.field(E.string);
863
- }
864
- ```
846
+ **Returns:** The enhanced model constructor.
865
847
 
866
- ### 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)
867
849
 
868
850
  Create a field definition for a model property.
869
851
 
@@ -887,19 +869,26 @@ This allows for both runtime introspection and compile-time type safety.
887
869
  **Examples:**
888
870
 
889
871
  ```typescript
890
- class User extends E.Model<User> {
872
+ const User = E.defineModel(class {
891
873
  name = E.field(E.string, {description: "User's full name"});
892
874
  age = E.field(E.opt(E.number), {description: "User's age", default: 25});
893
- }
875
+ });
894
876
  ```
895
877
 
896
- ### string · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
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)
897
886
 
898
887
  Type wrapper instance for the string type.
899
888
 
900
889
  **Value:** `TypeWrapper<string>`
901
890
 
902
- ### orderedString · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
891
+ ### orderedString · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
903
892
 
904
893
  Type wrapper instance for the ordered string type, which is just like a string
905
894
  except that it sorts lexicographically in the database (instead of by incrementing
@@ -909,37 +898,37 @@ may not contain null characters.
909
898
 
910
899
  **Value:** `TypeWrapper<string>`
911
900
 
912
- ### number · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
901
+ ### number · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
913
902
 
914
903
  Type wrapper instance for the number type.
915
904
 
916
905
  **Value:** `TypeWrapper<number>`
917
906
 
918
- ### dateTime · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
907
+ ### dateTime · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
919
908
 
920
909
  Type wrapper instance for the date/time type. Stored without timezone info, rounded to whole seconds.
921
910
 
922
911
  **Value:** `TypeWrapper<Date>`
923
912
 
924
- ### boolean · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
913
+ ### boolean · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
925
914
 
926
915
  Type wrapper instance for the boolean type.
927
916
 
928
917
  **Value:** `TypeWrapper<boolean>`
929
918
 
930
- ### identifier · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
919
+ ### identifier · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
931
920
 
932
921
  Type wrapper instance for the identifier type.
933
922
 
934
923
  **Value:** `TypeWrapper<string>`
935
924
 
936
- ### undef · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
925
+ ### undef · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
937
926
 
938
927
  Type wrapper instance for the 'undefined' type.
939
928
 
940
929
  **Value:** `TypeWrapper<undefined>`
941
930
 
942
- ### opt · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
931
+ ### opt · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
943
932
 
944
933
  Create an optional type wrapper (allows undefined).
945
934
 
@@ -962,7 +951,7 @@ const optionalString = E.opt(E.string);
962
951
  const optionalNumber = E.opt(E.number);
963
952
  ```
964
953
 
965
- ### or · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
954
+ ### or · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
966
955
 
967
956
  Create a union type wrapper from multiple type choices.
968
957
 
@@ -985,7 +974,7 @@ const stringOrNumber = E.or(E.string, E.number);
985
974
  const status = E.or("active", "inactive", "pending");
986
975
  ```
987
976
 
988
- ### array · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
977
+ ### array · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
989
978
 
990
979
  Create an array type wrapper with optional length constraints.
991
980
 
@@ -1009,7 +998,7 @@ const stringArray = E.array(E.string);
1009
998
  const boundedArray = E.array(E.number, {min: 1, max: 10});
1010
999
  ```
1011
1000
 
1012
- ### set · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1001
+ ### set · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1013
1002
 
1014
1003
  Create a Set type wrapper with optional length constraints.
1015
1004
 
@@ -1033,7 +1022,7 @@ const stringSet = E.set(E.string);
1033
1022
  const boundedSet = E.set(E.number, {min: 1, max: 10});
1034
1023
  ```
1035
1024
 
1036
- ### record · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1025
+ ### record · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1037
1026
 
1038
1027
  Create a Record type wrapper for key-value objects with string or number keys.
1039
1028
 
@@ -1055,7 +1044,7 @@ Create a Record type wrapper for key-value objects with string or number keys.
1055
1044
  const scores = E.record(E.number); // Record<string | number, number>
1056
1045
  ```
1057
1046
 
1058
- ### literal · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1047
+ ### literal · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1059
1048
 
1060
1049
  Create a literal type wrapper for a constant value.
1061
1050
 
@@ -1078,15 +1067,15 @@ const statusType = E.literal("active");
1078
1067
  const countType = E.literal(42);
1079
1068
  ```
1080
1069
 
1081
- ### link · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1070
+ ### link · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1082
1071
 
1083
1072
  Create a link type wrapper for model relationships.
1084
1073
 
1085
- **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<...>; }`
1086
1075
 
1087
1076
  **Type Parameters:**
1088
1077
 
1089
- - `T extends typeof Model<any>` - The target model class.
1078
+ - `T extends new (...args: any[]) => Model<any>` - The target model class.
1090
1079
 
1091
1080
  **Parameters:**
1092
1081
 
@@ -1097,109 +1086,18 @@ Create a link type wrapper for model relationships.
1097
1086
  **Examples:**
1098
1087
 
1099
1088
  ```typescript
1100
- class User extends E.Model<User> {
1101
- posts = E.field(E.array(E.link(Post, 'author')));
1102
- }
1103
-
1104
- class Post extends E.Model<Post> {
1105
- author = E.field(E.link(User));
1106
- }
1107
- ```
1108
-
1109
- ### index · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1110
-
1111
- Create a secondary index on model fields, or a computed secondary index using a function.
1112
-
1113
- For field-based indexes, pass a field name or array of field names.
1114
- For computed indexes, pass a function that takes a model instance and returns an array of
1115
- index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1116
- separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1117
-
1118
- **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)[]>(...`
1119
-
1120
- **Type Parameters:**
1121
-
1122
- - `M extends typeof Model` - The model class.
1123
- - `V` - The computed index value type (for function-based indexes).
1124
-
1125
- **Parameters:**
1126
-
1127
- - `MyModel: M` - - The model class to create the index for.
1128
- - `fn: (instance: InstanceType<M>) => V[]`
1129
-
1130
- **Returns:** A new SecondaryIndex instance.
1131
-
1132
- **Examples:**
1133
-
1134
- ```typescript
1135
- class User extends E.Model<User> {
1136
- static byAge = E.index(User, "age");
1137
- static byTagsDate = E.index(User, ["tags", "createdAt"]);
1138
- static byWord = E.index(User, (u: User) => u.name.split(" "));
1139
- }
1140
- ```
1141
-
1142
- ### primary · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1143
-
1144
- Create a primary index on model fields.
1145
-
1146
- **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<...>; }`
1147
-
1148
- **Type Parameters:**
1149
-
1150
- - `M extends typeof Model` - The model class.
1151
- - `F extends (keyof InstanceType<M> & string)` - The field name (for single field index).
1152
-
1153
- **Parameters:**
1154
-
1155
- - `MyModel: M` - - The model class to create the index for.
1156
- - `field: F` - - Single field name for simple indexes.
1157
-
1158
- **Returns:** A new PrimaryIndex instance.
1159
-
1160
- **Examples:**
1161
-
1162
- ```typescript
1163
- class User extends E.Model<User> {
1164
- static pk = E.primary(User, ["id"]);
1165
- static pkSingle = E.primary(User, "id");
1166
- }
1167
- ```
1168
-
1169
- ### unique · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1170
-
1171
- Create a unique index on model fields, or a computed unique index using a function.
1172
-
1173
- For field-based indexes, pass a field name or array of field names.
1174
- For computed indexes, pass a function that takes a model instance and returns an array of
1175
- index keys. Return `[]` to skip indexing for that instance. Each array element creates a
1176
- separate index entry, enabling multi-value indexes (e.g., indexing by each word in a name).
1177
-
1178
- **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...`
1179
-
1180
- **Type Parameters:**
1181
-
1182
- - `M extends typeof Model` - The model class.
1183
- - `V` - The computed index value type (for function-based indexes).
1184
-
1185
- **Parameters:**
1186
-
1187
- - `MyModel: M` - - The model class to create the index for.
1188
- - `fn: (instance: InstanceType<M>) => V[]`
1189
-
1190
- **Returns:** A new UniqueIndex instance.
1191
-
1192
- **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" });
1193
1093
 
1194
- ```typescript
1195
- class User extends E.Model<User> {
1196
- static byEmail = E.unique(User, "email");
1197
- static byNameAge = E.unique(User, ["name", "age"]);
1198
- static byFullName = E.unique(User, (u: User) => [`${u.firstName} ${u.lastName}`]);
1199
- }
1094
+ const Book = E.defineModel(class {
1095
+ id = E.field(E.identifier);
1096
+ author = E.field(E.link(Author));
1097
+ }, { pk: "id" });
1200
1098
  ```
1201
1099
 
1202
- ### dump · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1100
+ ### dump · [function](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1203
1101
 
1204
1102
  Dump database contents for debugging.
1205
1103
 
@@ -1208,7 +1106,7 @@ This is primarily useful for development and debugging purposes.
1208
1106
 
1209
1107
  **Signature:** `() => void`
1210
1108
 
1211
- ### BaseIndex · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L123)
1109
+ ### BaseIndex · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L130)
1212
1110
 
1213
1111
  Base class for database indexes for efficient lookups on model fields.
1214
1112
 
@@ -1225,113 +1123,97 @@ Indexes enable fast queries on specific field combinations and enforce uniquenes
1225
1123
  - `MyModel`: - The model class this index belongs to.
1226
1124
  - `_fieldNames`: - Array of field names that make up this index.
1227
1125
 
1228
- #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1126
+ #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1229
1127
 
1230
- **Signature:** `(opts?: FindOptions<ARGS>) => IndexRangeIterator<M>`
1128
+ **Signature:** `{ (opts?: FindOptions<ARGS, "first">): InstanceType<M>; (opts: FindOptions<ARGS, "single">): InstanceType<M>; (opts?: FindOptions<...>): IndexRangeIterator<...>; }`
1231
1129
 
1232
1130
  **Parameters:**
1233
1131
 
1234
- - `opts: FindOptions<ARGS>` (optional)
1132
+ - `opts?: FindOptions<ARGS, 'first'>`
1235
1133
 
1236
- #### baseIndex.batchProcess · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1237
-
1238
- [object Object],[object Object],[object Object]
1134
+ #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1239
1135
 
1240
- **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<...>; }`
1241
1137
 
1242
1138
  **Parameters:**
1243
1139
 
1244
- - `opts: FindOptions<ARGS> & { limitSeconds?: number; limitRows?: number }` (optional) - - Query options (same as `find()`), plus:
1245
- - `callback: (row: InstanceType<M>) => void | Promise<void>` - - Called for each matching row within a transaction
1246
-
1247
- #### baseIndex.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1248
-
1249
- **Signature:** `() => string`
1140
+ - `opts: FindOptions<ARGS, 'single'>`
1250
1141
 
1251
- ### UniqueIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1142
+ #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1252
1143
 
1253
- Unique index that stores references to the primary key.
1254
-
1255
- **Type Parameters:**
1144
+ **Signature:** `{ (opts?: FindOptions<ARGS, "first">): InstanceType<M>; (opts: FindOptions<ARGS, "single">): InstanceType<M>; (opts?: FindOptions<...>): IndexRangeIterator<...>; }`
1256
1145
 
1257
- - `M extends typeof Model` - The model class this index belongs to.
1258
- - `F extends readonly (keyof InstanceType<M> & string)[]` - The field names that make up this index.
1259
- - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1146
+ **Parameters:**
1260
1147
 
1261
- #### uniqueIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1148
+ - `opts?: FindOptions<ARGS>`
1262
1149
 
1263
- Get a model instance by unique index key values.
1150
+ #### baseIndex.find · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1264
1151
 
1265
- **Signature:** `(...args: ARGS) => InstanceType<M>`
1152
+ **Signature:** `{ (opts?: FindOptions<ARGS, "first">): InstanceType<M>; (opts: FindOptions<ARGS, "single">): InstanceType<M>; (opts?: FindOptions<...>): IndexRangeIterator<...>; }`
1266
1153
 
1267
1154
  **Parameters:**
1268
1155
 
1269
- - `args: ARGS` - - The unique index key values.
1156
+ - `opts: any` (optional)
1270
1157
 
1271
- **Returns:** The model instance if found, undefined otherwise.
1272
-
1273
- **Examples:**
1158
+ #### baseIndex.batchProcess · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1274
1159
 
1275
- ```typescript
1276
- const userByEmail = User.byEmail.get("john@example.com");
1277
- ```
1160
+ [object Object],[object Object],[object Object]
1278
1161
 
1279
- ### PrimaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1162
+ **Signature:** `(opts: FindOptions<ARGS, undefined> & { limitSeconds?: number; limitRows?: number; }, callback: (row: InstanceType<M>) => void | Promise<void>) => Promise<...>`
1280
1163
 
1281
- Primary index that stores the actual model data.
1164
+ **Parameters:**
1282
1165
 
1283
- **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
1284
1168
 
1285
- - `M extends typeof Model` - The model class this index belongs to.
1286
- - `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)
1287
1170
 
1288
- #### primaryIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1171
+ **Signature:** `() => string`
1289
1172
 
1290
- Get a model instance by primary key values.
1173
+ ### NonPrimaryIndex · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1291
1174
 
1292
- **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.
1293
1177
 
1294
- **Parameters:**
1178
+ **Type Parameters:**
1295
1179
 
1296
- - `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>`
1297
1183
 
1298
- **Returns:** The model instance if found, undefined otherwise.
1184
+ ### UniqueIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1299
1185
 
1300
- **Examples:**
1186
+ Unique index that stores references to the primary key.
1301
1187
 
1302
- ```typescript
1303
- const user = User.pk.get("john_doe");
1304
- ```
1188
+ **Type Parameters:**
1305
1189
 
1306
- #### primaryIndex.getLazy · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1190
+ - `M extends typeof Model`
1191
+ - `F extends readonly (keyof InstanceType<M> & string)[]`
1192
+ - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1307
1193
 
1308
- Does the same as as `get()`, but will delay loading the instance from disk until the first
1309
- property access. In case it turns out the instance doesn't exist, an error will be thrown
1310
- at that time.
1194
+ #### uniqueIndex.get · [method](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1311
1195
 
1312
- **Signature:** `(...args: IndexArgTypes<M, F>) => InstanceType<M>`
1196
+ **Signature:** `(...args: ARGS) => InstanceType<M>`
1313
1197
 
1314
1198
  **Parameters:**
1315
1199
 
1316
- - `args: IndexArgTypes<M, F>` - Primary key field values. (Or a single Uint8Array containing the key.)
1317
-
1318
- **Returns:** The (lazily loaded) model instance.
1200
+ - `args: ARGS`
1319
1201
 
1320
- ### SecondaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L255)
1202
+ ### SecondaryIndex · [class](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L252)
1321
1203
 
1322
1204
  Secondary index for non-unique lookups.
1323
1205
 
1324
1206
  **Type Parameters:**
1325
1207
 
1326
- - `M extends typeof Model` - The model class this index belongs to.
1327
- - `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)[]`
1328
1210
  - `ARGS extends readonly any[] = IndexArgTypes<M, F>`
1329
1211
 
1330
- ### 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)
1331
1213
 
1332
1214
  **Type:** `Record<any, any> | "created" | "deleted"`
1333
1215
 
1334
- ### 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)
1335
1217
 
1336
1218
  #### transaction.id · [member](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L41)
1337
1219
 
@@ -1341,11 +1223,11 @@ Secondary index for non-unique lookups.
1341
1223
 
1342
1224
  **Type:** `Set<Model<unknown>>`
1343
1225
 
1344
- #### 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)
1345
1227
 
1346
1228
  **Type:** `Map<number, Model<unknown>>`
1347
1229
 
1348
- ### DatabaseError · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L158)
1230
+ ### DatabaseError · [constant](https://github.com/vanviegen/edinburgh/blob/main/src/edinburgh.ts#L156)
1349
1231
 
1350
1232
  The DatabaseError class is used to represent errors that occur during database operations.
1351
1233
  It extends the built-in Error class and has a machine readable error code string property.
@@ -1355,7 +1237,7 @@ Invalid function arguments will throw TypeError.
1355
1237
 
1356
1238
  **Value:** `DatabaseErrorConstructor`
1357
1239
 
1358
- ### 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)
1359
1241
 
1360
1242
  Run database migration: populate secondary indexes for old-version rows,
1361
1243
  convert old primary indices, rewrite row data, and clean up orphaned indices.
@@ -1366,71 +1248,71 @@ convert old primary indices, rewrite row data, and clean up orphaned indices.
1366
1248
 
1367
1249
  - `options: MigrationOptions` (optional)
1368
1250
 
1369
- ### 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)
1370
1252
 
1371
- #### 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)
1372
1254
 
1373
1255
  Limit migration to specific table names.
1374
1256
 
1375
1257
  **Type:** `string[]`
1376
1258
 
1377
- #### 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)
1378
1260
 
1379
1261
  Populate secondary indexes for rows at old schema versions (default: true).
1380
1262
 
1381
1263
  **Type:** `boolean`
1382
1264
 
1383
- #### 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)
1384
1266
 
1385
1267
  Convert old primary indices when primary key fields changed (default: true).
1386
1268
 
1387
1269
  **Type:** `boolean`
1388
1270
 
1389
- #### 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)
1390
1272
 
1391
1273
  Rewrite all row data to the latest schema version (default: false).
1392
1274
 
1393
1275
  **Type:** `boolean`
1394
1276
 
1395
- #### 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)
1396
1278
 
1397
1279
  Delete orphaned secondary/unique index entries (default: true).
1398
1280
 
1399
1281
  **Type:** `boolean`
1400
1282
 
1401
- #### 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)
1402
1284
 
1403
1285
  Progress callback.
1404
1286
 
1405
1287
  **Type:** `(info: ProgressInfo) => void`
1406
1288
 
1407
- ### 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)
1408
1290
 
1409
- #### 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)
1410
1292
 
1411
1293
  Per-table counts of secondary index entries populated.
1412
1294
 
1413
1295
  **Type:** `Record<string, number>`
1414
1296
 
1415
- #### 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)
1416
1298
 
1417
1299
  Per-table counts of old primary rows migrated.
1418
1300
 
1419
1301
  **Type:** `Record<string, number>`
1420
1302
 
1421
- #### 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)
1422
1304
 
1423
1305
  Per-table conversion failure counts by reason.
1424
1306
 
1425
1307
  **Type:** `Record<string, Record<string, number>>`
1426
1308
 
1427
- #### 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)
1428
1310
 
1429
1311
  Per-table counts of rows rewritten to latest version.
1430
1312
 
1431
1313
  **Type:** `Record<string, number>`
1432
1314
 
1433
- #### 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)
1434
1316
 
1435
1317
  Number of orphaned index entries deleted.
1436
1318