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/src/migrate.ts CHANGED
@@ -2,7 +2,6 @@ import * as lowlevel from "olmdb/lowlevel";
2
2
  import DataPack from "./datapack.js";
3
3
  import { modelRegistry, currentTxn, Transaction } from "./models.js";
4
4
  import { dbDel, toBuffer, bytesEqual } from "./utils.js";
5
- import { PrimaryIndex } from "./indexes.js";
6
5
  import { deserializeType, TypeWrapper } from "./types.js";
7
6
  import { transact } from "./edinburgh.js";
8
7
 
@@ -118,13 +117,12 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
118
117
 
119
118
  // Build maps of known index IDs
120
119
  const knownIndexIds = new Set<number>();
121
- const primaryByIndexId = new Map<number, { model: typeof modelRegistry[string], primary: PrimaryIndex<any, any> }>();
120
+ const modelByPkIndexId = new Map<number, typeof modelRegistry[string]>();
122
121
 
123
122
  for (const model of Object.values(modelRegistry)) {
124
123
  if (options.tables && !options.tables.includes(model.tableName)) continue;
125
- const primary = model._primary;
126
- knownIndexIds.add(primary._indexId!);
127
- primaryByIndexId.set(primary._indexId!, { model, primary });
124
+ knownIndexIds.add(model._indexId!);
125
+ modelByPkIndexId.set(model._indexId!, model);
128
126
  for (const sec of model._secondaries || []) {
129
127
  knownIndexIds.add(sec._indexId!);
130
128
  }
@@ -155,7 +153,7 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
155
153
 
156
154
  // Phase 1: Populate secondary indexes and/or rewrite row data
157
155
  if (populateSecondaries || rewriteData) {
158
- for (const [indexId, { model, primary }] of primaryByIndexId) {
156
+ for (const [indexId, model] of modelByPkIndexId) {
159
157
  let secondaryCount = 0;
160
158
  let rewrittenCount = 0;
161
159
  const migrateFn = (model as any).migrate as ((record: Record<string, any>) => void) | undefined;
@@ -164,15 +162,15 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
164
162
  await forEachRow(indexId, (txn, keyBuf, valueBuf) => {
165
163
  const valuePack = new DataPack(valueBuf);
166
164
  const version = valuePack.readNumber();
167
- if (version === primary._currentVersion) return; // Already current
165
+ if (version === model._currentVersion) return; // Already current
168
166
 
169
- const versionInfo = primary._loadVersionInfo(txn.id, version);
167
+ const versionInfo = model._loadVersionInfo(txn.id, version);
170
168
 
171
169
  // Deserialize pre-migrate values from key + old-format value
172
170
  const record: Record<string, any> = {};
173
171
  const keyPack = new DataPack(keyBuf);
174
172
  keyPack.readNumber(); // skip indexId
175
- for (const [name, type] of primary._fieldTypes.entries()) {
173
+ for (const [name, type] of model._pkFieldTypes.entries()) {
176
174
  record[name] = type.deserialize(keyPack);
177
175
  }
178
176
  for (const [name, type] of versionInfo.nonKeyFields.entries()) {
@@ -191,33 +189,14 @@ export async function runMigration(options: MigrationOptions = {}): Promise<Migr
191
189
  sec._write(txn, keyBuf, record as any);
192
190
  secondaryCount++;
193
191
  } else if (preMigrate) {
194
- if (sec._computeFn) {
195
- // Computed indexes: compare serialized keys to avoid unnecessary re-indexing
196
- const oldKeyBytes = sec._serializeKeyFields(preMigrate).toUint8Array();
197
- const newKeyBytes = sec._serializeKeyFields(record).toUint8Array();
198
- if (!bytesEqual(oldKeyBytes, newKeyBytes)) {
199
- sec._delete(txn, keyBuf, preMigrate as any);
200
- sec._write(txn, keyBuf, record as any);
201
- secondaryCount++;
202
- }
203
- } else {
204
- // Existing secondary, update if migrate changed any of its fields
205
- for (const [field, type] of sec._fieldTypes.entries()) {
206
- if (!type.equals(preMigrate[field], record[field])) {
207
- sec._delete(txn, keyBuf, preMigrate as any);
208
- sec._write(txn, keyBuf, record as any);
209
- secondaryCount++;
210
- break;
211
- }
212
- }
213
- }
192
+ if (sec._update(txn, keyBuf, record, preMigrate)) secondaryCount++;
214
193
  }
215
194
  }
216
195
  }
217
196
 
218
197
  // Rewrite primary row data to current version
219
198
  if (rewriteData) {
220
- primary._write(txn, keyBuf, record);
199
+ model._writePrimary(txn, keyBuf, record);
221
200
  rewrittenCount++;
222
201
  }
223
202
  });
package/src/models.ts CHANGED
@@ -28,8 +28,8 @@ export interface Transaction {
28
28
  instances: Set<Model<unknown>>;
29
29
  instancesByPk: Map<number, Model<unknown>>;
30
30
  }
31
- import { BaseIndex as BaseIndex, PrimaryIndex, IndexRangeIterator } from "./indexes.js";
32
- import { addErrorPath, logLevel, assert, dbGet, hashBytes, bytesEqual } from "./utils.js";
31
+ import { BaseIndex, NonPrimaryIndex, PrimaryIndex, IndexRangeIterator, UniqueIndex, SecondaryIndex, FindOptions } from "./indexes.js";
32
+ import { addErrorPath, logLevel, assert, dbGet, hashBytes } from "./utils.js";
33
33
 
34
34
  /**
35
35
  * Configuration interface for model fields.
@@ -58,10 +58,10 @@ export interface FieldConfig<T> {
58
58
  *
59
59
  * @example
60
60
  * ```typescript
61
- * class User extends E.Model<User> {
61
+ * const User = E.defineModel(class {
62
62
  * name = E.field(E.string, {description: "User's full name"});
63
63
  * age = E.field(E.opt(E.number), {description: "User's age", default: 25});
64
- * }
64
+ * });
65
65
  * ```
66
66
  */
67
67
  export function field<T>(type: TypeWrapper<T>, options: Partial<FieldConfig<T>> = {}): T {
@@ -82,77 +82,161 @@ function isObjectEmpty(obj: object) {
82
82
 
83
83
  export type Change = Record<any, any> | "created" | "deleted";
84
84
 
85
+ let autoTableNameId = 0;
86
+
87
+ type FieldsOf<T> = T extends new () => infer I ? I : never;
88
+ type ModelInstance<FIELDS> = FIELDS & Model<FIELDS>;
89
+
90
+ type PKArgs<FIELDS, PK> =
91
+ PK extends readonly (keyof FIELDS & string)[]
92
+ ? { [I in keyof PK]: PK[I] extends keyof FIELDS ? FIELDS[PK[I]] : never }
93
+ : PK extends keyof FIELDS & string
94
+ ? [FIELDS[PK]]
95
+ : [string];
96
+
97
+ type UniqueFor<SPEC> =
98
+ SPEC extends readonly string[] ? UniqueIndex<any, SPEC>
99
+ : SPEC extends string ? UniqueIndex<any, [SPEC]>
100
+ : SPEC extends (instance: any) => infer R
101
+ ? R extends (infer V)[] ? UniqueIndex<any, [], [V]>
102
+ : UniqueIndex<any, [], [R]>
103
+ : never;
104
+
105
+ type SecondaryFor<SPEC> =
106
+ SPEC extends readonly string[] ? SecondaryIndex<any, SPEC>
107
+ : SPEC extends string ? SecondaryIndex<any, [SPEC]>
108
+ : SPEC extends (instance: any) => infer R
109
+ ? R extends (infer V)[] ? SecondaryIndex<any, [], [V]>
110
+ : SecondaryIndex<any, [], [R]>
111
+ : never;
112
+
113
+ type RegisteredModel<FIELDS, PKA extends readonly any[], UNIQUE, INDEX> = {
114
+ new (initial?: Partial<FIELDS>): ModelInstance<FIELDS>;
115
+ tableName: string;
116
+ fields: Record<string | symbol | number, FieldConfig<unknown>>;
117
+ get(...args: PKA): ModelInstance<FIELDS> | undefined;
118
+ getLazy(...args: PKA): ModelInstance<FIELDS>;
119
+ find(opts: FindOptions<PKA, 'first'>): ModelInstance<FIELDS> | undefined;
120
+ find(opts: FindOptions<PKA, 'single'>): ModelInstance<FIELDS>;
121
+ find(opts?: FindOptions<PKA>): IndexRangeIterator<any>;
122
+ batchProcess(opts?: FindOptions<PKA> & { limitSeconds?: number; limitRows?: number }, callback?: (row: ModelInstance<FIELDS>) => any): Promise<void>;
123
+ replaceInto(obj: Partial<FIELDS>): ModelInstance<FIELDS>;
124
+ } &
125
+ { [K in keyof UNIQUE]: UniqueFor<UNIQUE[K]> } &
126
+ { [K in keyof INDEX]: SecondaryFor<INDEX[K]> };
127
+
85
128
  /**
86
129
  * Register a model class with the Edinburgh ORM system.
87
- *
88
- * @template T - The model class type.
89
- * @param MyModel - The model class to register.
90
- * @returns The enhanced model class with ORM capabilities.
91
- *
92
- * @example
93
- * ```typescript
94
- * ⁣@E.registerModel
95
- * class User extends E.Model<User> {
96
- * static pk = E.index(User, ["id"], "primary");
97
- * id = E.field(E.identifier);
98
- * name = E.field(E.string);
99
- * }
100
- * ```
130
+ *
131
+ * Converts a plain class into a fully-featured model with database persistence,
132
+ * typed fields, primary key access, and optional secondary and unique indexes.
133
+ *
134
+ * @param cls - A plain class whose properties use E.field().
135
+ * @param opts - Registration options.
136
+ * @param opts.pk - Primary key field name or array of field names.
137
+ * @param opts.unique - Named unique index specifications (field name, field array, or compute function).
138
+ * @param opts.index - Named secondary index specifications (field name, field array, or compute function).
139
+ * @param opts.tableName - Explicit database table name.
140
+ * @param opts.override - Replace a previous model with the same table name.
141
+ * @returns The enhanced model constructor.
101
142
  */
102
- export function registerModel<T extends typeof Model<unknown>>(MyModel: T): T {
103
- const MockModel = getMockModel(MyModel);
143
+ export function defineModel<
144
+ T extends new () => any,
145
+ const PK extends (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[],
146
+ const UNIQUE extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>,
147
+ const INDEX extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>,
148
+ >(
149
+ cls: T,
150
+ opts?: { pk?: PK, unique?: UNIQUE, index?: INDEX, tableName?: string, override?: boolean }
151
+ ): RegisteredModel<FieldsOf<T>, PKArgs<FieldsOf<T>, PK>, UNIQUE, INDEX>;
152
+
153
+ export function defineModel(cls: any, opts?: any): any {
154
+ Object.setPrototypeOf(cls.prototype, Model.prototype);
155
+
156
+ const MockModel = function(this: any, initial?: Record<string, any>, txn: Transaction = currentTxn()) {
157
+ this._txn = txn;
158
+ txn.instances.add(this);
159
+ if (initial) Object.assign(this, initial);
160
+ } as any;
104
161
 
105
- // Copy own static methods/properties
106
- for(const name of Object.getOwnPropertyNames(MyModel)) {
107
- if (name !== 'length' && name !== 'prototype' && name !== 'name' && name !== 'mock' && name !== 'override') {
108
- (MockModel as any)[name] = (MyModel as any)[name];
162
+ cls.prototype.constructor = MockModel;
163
+ Object.setPrototypeOf(MockModel, Model);
164
+ MockModel.prototype = cls.prototype;
165
+ MockModel._original = cls;
166
+
167
+ for (const name of Object.getOwnPropertyNames(cls)) {
168
+ if (name !== 'length' && name !== 'prototype' && name !== 'name') {
169
+ MockModel[name] = cls[name];
109
170
  }
110
171
  }
111
- MockModel.tableName ||= MyModel.name;
112
172
 
113
- // Register the constructor by name
173
+ MockModel.tableName = opts?.tableName || cls.name || `Model${++autoTableNameId}`;
174
+
114
175
  if (MockModel.tableName in modelRegistry) {
115
- if (!(MyModel as any).override) {
176
+ if (!opts?.override) {
116
177
  throw new DatabaseError(`Model with table name '${MockModel.tableName}' already registered`, 'INIT_ERROR');
117
178
  }
118
179
  delete modelRegistry[MockModel.tableName];
119
180
  }
120
- modelRegistry[MockModel.tableName] = MockModel;
121
181
 
122
- return MockModel;
123
- }
124
-
125
- export function getMockModel<T extends typeof Model<unknown>>(OrgModel: T): T {
126
- const AnyOrgModel = OrgModel as any;
127
- if (AnyOrgModel._isMock) return OrgModel;
128
- if (AnyOrgModel._mock) return AnyOrgModel._mock;
182
+ const instance = new cls();
183
+ if (!opts?.pk && !instance.id) {
184
+ instance.id = { type: identifier };
185
+ }
129
186
 
130
- const MockModel = function(this: any, initial?: Record<string,any> | undefined, txn: Transaction = currentTxn()) {
131
- // This constructor should only be called when the user does 'new Model'. We'll bypass this when
132
- // loading objects. Add to 'instances', so the object will be saved.
133
- this._txn = txn;
134
- txn.instances.add(this);
135
- if (initial) {
136
- Object.assign(this, initial);
187
+ MockModel.fields = {};
188
+ for (const key in instance) {
189
+ const value = instance[key] as FieldConfig<unknown>;
190
+ if (value && value.type instanceof TypeWrapper) {
191
+ MockModel.fields[key] = value;
192
+
193
+ const defObj = value.default === undefined ? value.type : value;
194
+ const def = defObj.default;
195
+ if (typeof def === 'function') {
196
+ Object.defineProperty(MockModel.prototype, key, {
197
+ get() {
198
+ return (this[key] = def.call(defObj, this));
199
+ },
200
+ set(val: any) {
201
+ Object.defineProperty(this, key, {
202
+ value: val,
203
+ configurable: true,
204
+ writable: true,
205
+ enumerable: true,
206
+ });
207
+ },
208
+ configurable: true,
209
+ });
210
+ } else if (def !== undefined) {
211
+ MockModel.prototype[key] = def;
212
+ }
137
213
  }
138
- } as any as T;
214
+ }
139
215
 
140
- // We want .constructor to point at our fake constructor function.
141
- OrgModel.prototype.constructor = MockModel as any;
216
+ if (opts?.pk) {
217
+ new PrimaryIndex(MockModel, Array.isArray(opts.pk) ? opts.pk : [opts.pk]);
218
+ } else {
219
+ new PrimaryIndex(MockModel, ['id']);
220
+ }
221
+
222
+ const normalizeSpec = (spec: any) => typeof spec === 'string' ? [spec] : spec;
223
+
224
+ if (opts?.unique) {
225
+ for (const [name, spec] of Object.entries<any>(opts.unique)) {
226
+ MockModel[name] = new UniqueIndex(MockModel, normalizeSpec(spec));
227
+ }
228
+ }
229
+ if (opts?.index) {
230
+ for (const [name, spec] of Object.entries<any>(opts.index)) {
231
+ MockModel[name] = new SecondaryIndex(MockModel, normalizeSpec(spec));
232
+ }
233
+ }
142
234
 
143
- // Copy the prototype chain for the constructor as well as for instantiated objects
144
- Object.setPrototypeOf(MockModel, Object.getPrototypeOf(OrgModel));
145
- MockModel.prototype = OrgModel.prototype;
146
- (MockModel as any)._isMock = true;
147
- (MockModel as any)._original = OrgModel;
148
- AnyOrgModel._mock = MockModel;
235
+ modelRegistry[MockModel.tableName] = MockModel;
149
236
  scheduleInit();
150
237
  return MockModel;
151
238
  }
152
239
 
153
- // Model base class and related symbols/state
154
- const INIT_INSTANCE_SYMBOL = Symbol();
155
-
156
240
  /**
157
241
  * Model interface that ensures proper typing for the constructor property.
158
242
  * @template SUB - The concrete model subclass.
@@ -165,8 +249,8 @@ export interface Model<SUB> {
165
249
  * Base class for all database models in the Edinburgh ORM.
166
250
  *
167
251
  * Models represent database entities with typed fields, automatic serialization,
168
- * change tracking, and relationship management. All model classes should extend
169
- * this base class and be decorated with `@E.registerModel`.
252
+ * change tracking, and relationship management. Model classes are created using
253
+ * `E.defineModel()`.
170
254
  *
171
255
  * ### Schema Evolution
172
256
  *
@@ -202,16 +286,14 @@ export interface Model<SUB> {
202
286
  *
203
287
  * @example
204
288
  * ```typescript
205
- * ⁣@E.registerModel
206
- * class User extends E.Model<User> {
207
- * static pk = E.primary(User, "id");
208
- *
289
+ * const User = E.defineModel(class {
209
290
  * id = E.field(E.identifier);
210
291
  * name = E.field(E.string);
211
292
  * email = E.field(E.string);
212
- *
213
- * static byEmail = E.unique(User, "email");
214
- * }
293
+ * }, {
294
+ * pk: "id",
295
+ * unique: { byEmail: "email" },
296
+ * });
215
297
  * ```
216
298
  */
217
299
 
@@ -220,17 +302,24 @@ export abstract class Model<SUB> {
220
302
  static _primary: PrimaryIndex<any, any>;
221
303
 
222
304
  /** @internal All non-primary indexes for this model. */
223
- static _secondaries?: BaseIndex<any, readonly (keyof any & string)[]>[];
305
+ static _secondaries?: NonPrimaryIndex<any, readonly (keyof any & string)[]>[];
224
306
 
225
307
  /** The database table name (defaults to class name). */
226
308
  static tableName: string;
227
309
 
228
- /** When true, registerModel replaces an existing model with the same tableName. */
310
+ /** When true, defineModel replaces an existing model with the same tableName. */
229
311
  static override?: boolean;
230
312
 
231
313
  /** Field configuration metadata. */
232
314
  static fields: Record<string | symbol | number, FieldConfig<unknown>>;
233
315
 
316
+ // Alias statics that delegate to _primary, used by migrate.ts
317
+ static get _indexId() { return this._primary?._indexId; }
318
+ static get _currentVersion() { return this._primary._currentVersion; }
319
+ static get _pkFieldTypes() { return this._primary._fieldTypes; }
320
+ static _loadVersionInfo(txnId: number, version: number) { return this._primary._loadVersionInfo(txnId, version); }
321
+ static _writePrimary(txn: Transaction, pk: Uint8Array, data: Record<string, any>) { this._primary._write(txn, pk, data as any); }
322
+
234
323
  /**
235
324
  * Optional migration function called when deserializing rows written with an older schema version.
236
325
  * Receives a plain record with all fields (primary key fields + value fields) and should mutate it
@@ -247,9 +336,7 @@ export abstract class Model<SUB> {
247
336
  *
248
337
  * @example
249
338
  * ```typescript
250
- * ⁣@E.registerModel
251
- * class User extends E.Model<User> {
252
- * static pk = E.primary(User, "id");
339
+ * const User = E.defineModel(class {
253
340
  * id = E.field(E.identifier);
254
341
  * name = E.field(E.string);
255
342
  * role = E.field(E.string); // new field
@@ -257,7 +344,7 @@ export abstract class Model<SUB> {
257
344
  * static migrate(record: Record<string, any>) {
258
345
  * record.role ??= "user"; // default for rows that predate the 'role' field
259
346
  * }
260
- * }
347
+ * }, { pk: "id" });
261
348
  * ```
262
349
  */
263
350
  static migrate?(record: Record<string, any>): void;
@@ -280,10 +367,7 @@ export abstract class Model<SUB> {
280
367
  _txn!: Transaction;
281
368
 
282
369
  constructor(initial: Partial<Omit<SUB, "constructor">> = {}) {
283
- // This constructor will only be called once, from `initModels`. All other instances will
284
- // be created by the 'fake' constructor. The typing for `initial` *is* important though.
285
- if (initial as any === INIT_INSTANCE_SYMBOL) return;
286
- throw new DatabaseError("The model needs a @E.registerModel decorator", 'INIT_ERROR');
370
+ throw new DatabaseError("Use defineModel() to create model classes", 'INIT_ERROR');
287
371
  }
288
372
 
289
373
  /**
@@ -298,9 +382,7 @@ export abstract class Model<SUB> {
298
382
  *
299
383
  * @example
300
384
  * ```typescript
301
- * ⁣@E.registerModel
302
- * class Post extends E.Model<Post> {
303
- * static pk = E.primary(Post, "id");
385
+ * const Post = E.defineModel(class {
304
386
  * id = E.field(E.identifier);
305
387
  * title = E.field(E.string);
306
388
  * slug = E.field(E.string);
@@ -308,79 +390,21 @@ export abstract class Model<SUB> {
308
390
  * preCommit() {
309
391
  * this.slug = this.title.toLowerCase().replace(/\s+/g, "-");
310
392
  * }
311
- * }
393
+ * }, { pk: "id" });
312
394
  * ```
313
395
  */
314
396
  preCommit?(): void;
315
397
 
316
- static async _delayedInit(cleared?: boolean): Promise<void> {
317
- const MockModel = getMockModel(this);
318
-
319
- if (cleared) {
320
- MockModel._primary._indexId = undefined;
321
- MockModel._primary._versions.clear();
322
- for (const sec of MockModel._secondaries || []) sec._indexId = undefined;
323
- }
324
-
325
- if (!MockModel.fields) {
326
- // First-time init: gather field configs from a temporary instance of the original class.
327
- const OrgModel = (MockModel as any)._original || this;
328
- const instance = new (OrgModel as any)(INIT_INSTANCE_SYMBOL);
329
-
330
- // If no primary key exists, create one using 'id' field
331
- if (!MockModel._primary) {
332
- if (!instance.id) {
333
- instance.id = { type: identifier };
334
- }
335
- // @ts-ignore-next-line - `id` is not part of the type, but the user probably shouldn't touch it anyhow
336
- new PrimaryIndex(MockModel, ['id']);
337
- }
338
-
339
- MockModel.fields = {};
340
- for (const key in instance) {
341
- const value = instance[key] as FieldConfig<unknown>;
342
- // Check if this property contains field metadata
343
- if (value && value.type instanceof TypeWrapper) {
344
- // Set the configuration on the constructor's `fields` property
345
- MockModel.fields[key] = value;
346
-
347
- // Set default value on the prototype
348
- const defObj = value.default===undefined ? value.type : value;
349
- const def = defObj.default;
350
- if (typeof def === 'function') {
351
- // The default is a function. We'll define a getter on the property in the model prototype,
352
- // and once it is read, we'll run the function and set the value as a plain old property
353
- // on the instance object.
354
- Object.defineProperty(MockModel.prototype, key, {
355
- get() {
356
- // This will call set(), which will define the property on the instance.
357
- return (this[key] = def.call(defObj, this));
358
- },
359
- set(val: any) {
360
- Object.defineProperty(this, key, {
361
- value: val,
362
- configurable: true,
363
- writable: true,
364
- enumerable: true,
365
- })
366
- },
367
- configurable: true,
368
- });
369
- } else if (def !== undefined) {
370
- (MockModel.prototype as any)[key] = def;
371
- }
372
- }
373
- }
374
-
375
- if (logLevel >= 1) {
376
- console.log(`[edinburgh] Registered model ${MockModel.tableName} with fields: ${Object.keys(MockModel.fields).join(' ')}`);
377
- }
378
- }
398
+ static _resetIndexes(): void {
399
+ this._primary._indexId = undefined;
400
+ this._primary._versions.clear();
401
+ for (const sec of this._secondaries || []) sec._indexId = undefined;
402
+ }
379
403
 
380
- // Always run index inits (idempotent, skip if already initialized)
381
- await MockModel._primary._delayedInit();
382
- for (const sec of MockModel._secondaries || []) await sec._delayedInit();
383
- await MockModel._primary._initVersioning();
404
+ static async _loadCreateIndexes(): Promise<void> {
405
+ await this._primary._delayedInit();
406
+ for (const sec of this._secondaries || []) await sec._delayedInit();
407
+ await this._primary._initVersioning();
384
408
  }
385
409
 
386
410
  _setLoadedField(fieldName: string, value: any) {
@@ -403,7 +427,7 @@ export abstract class Model<SUB> {
403
427
  getPrimaryKey(): Uint8Array {
404
428
  let key = this._primaryKey;
405
429
  if (key === undefined) {
406
- key = this.constructor._primary!._serializeKeyFields(this).toUint8Array();
430
+ key = this.constructor._primary!._serializeKey(this).toUint8Array();
407
431
  this._setPrimaryKey(key);
408
432
  }
409
433
  return key;
@@ -468,8 +492,8 @@ export abstract class Model<SUB> {
468
492
  // the whole object just to see if something changed.
469
493
 
470
494
  // Add old values of changed fields to 'changed'.
495
+ const changed: Record<string, any> = {};
471
496
  const fields = this.constructor.fields;
472
- let changed : Record<any, any> = {};
473
497
  for(const fieldName in oldValues) {
474
498
  const oldValue = oldValues[fieldName];
475
499
  const newValue = this[fieldName as keyof Model<SUB>];
@@ -498,23 +522,7 @@ export abstract class Model<SUB> {
498
522
 
499
523
  // Update any secondaries with changed fields
500
524
  for (const index of this.constructor._secondaries || []) {
501
- if (index._computeFn) {
502
- // Computed indexes may depend on any field — compare serialized keys
503
- const oldKeyBytes = index._serializeKeyFields(oldValues).toUint8Array();
504
- const newKeyBytes = index._serializeKeyFields(this as any).toUint8Array();
505
- if (!bytesEqual(oldKeyBytes, newKeyBytes)) {
506
- index._delete(txn, pk, oldValues);
507
- index._write(txn, pk, this);
508
- }
509
- } else {
510
- for (const field of index._fieldTypes.keys()) {
511
- if (changed.hasOwnProperty(field)) {
512
- index._delete(txn, pk, oldValues);
513
- index._write(txn, pk, this);
514
- break;
515
- }
516
- }
517
- }
525
+ index._update(txn, pk, this, oldValues);
518
526
  }
519
527
  return changed;
520
528
  }
@@ -526,7 +534,7 @@ export abstract class Model<SUB> {
526
534
  *
527
535
  * @example
528
536
  * ```typescript
529
- * const user = User.load("user123");
537
+ * const user = User.get("user123");
530
538
  * user.name = "New Name";
531
539
  * user.preventPersist(); // Changes won't be saved
532
540
  * ```
@@ -538,16 +546,22 @@ export abstract class Model<SUB> {
538
546
  return this;
539
547
  }
540
548
 
541
- /**
542
- * Find all instances of this model in the database, ordered by primary key.
543
- * @param opts - Optional parameters.
544
- * @param opts.reverse - If true, iterate in reverse order.
545
- * @returns An iterator.
546
- */
547
- static findAll<T extends typeof Model<unknown>>(this: T, opts?: {reverse?: boolean}): IndexRangeIterator<T> {
549
+ static get(...args: any[]): any {
550
+ return this._primary!.get(...args);
551
+ }
552
+
553
+ static getLazy(...args: any[]): any {
554
+ return this._primary!.getLazy(...args);
555
+ }
556
+
557
+ static find(opts?: any): any {
548
558
  return this._primary!.find(opts);
549
559
  }
550
560
 
561
+ static batchProcess(opts: any, callback?: any): any {
562
+ return this._primary!.batchProcess(opts, callback);
563
+ }
564
+
551
565
  /**
552
566
  * Load an existing instance by primary key and update it, or create a new one.
553
567
  *
@@ -558,7 +572,7 @@ export abstract class Model<SUB> {
558
572
  * @param obj - Partial model data that **must** include every primary key field.
559
573
  * @returns The loaded-and-updated or newly created instance.
560
574
  */
561
- static replaceInto<T extends typeof Model<any>>(this: T, obj: Partial<Omit<InstanceType<T>, "constructor">>): InstanceType<T> {
575
+ static replaceInto<T extends typeof Model<any>>(this: T, obj: Partial<Record<string, any>>): InstanceType<T> {
562
576
  const pk = this._primary!;
563
577
  const keyArgs = [];
564
578
  for (const fieldName of pk._fieldTypes.keys()) {
@@ -587,7 +601,7 @@ export abstract class Model<SUB> {
587
601
  *
588
602
  * @example
589
603
  * ```typescript
590
- * const user = User.load("user123");
604
+ * const user = User.get("user123");
591
605
  * user.delete(); // Removes from database
592
606
  * ```
593
607
  */
@@ -651,7 +665,7 @@ export abstract class Model<SUB> {
651
665
 
652
666
  toString(): string {
653
667
  const primary = this.constructor._primary;
654
- const pk = primary._keyToArray(this._primaryKey || primary._serializeKeyFields(this).toUint8Array(false));
668
+ const pk = primary._keyToArray(this._primaryKey || primary._serializeKey(this).toUint8Array(false));
655
669
  return `{Model:${this.constructor.tableName} ${this.getState()} ${pk}}`;
656
670
  }
657
671