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/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
+ }
142
221
 
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;
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
+ }
234
+
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,87 +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
- /**
317
- * Transform the model's `E.field` properties into the appropriate JavaScript properties. Normally this is done
318
- * automatically when using `transact()`, but in case you need to access `Model.fields` directly before the first
319
- * transaction, you can call this method manually.
320
- */
321
- static initFields(reset?: boolean): void {
322
- const MockModel = getMockModel(this);
323
-
324
- if (reset) {
325
- MockModel._primary._indexId = undefined;
326
- MockModel._primary._versions.clear();
327
- for (const sec of MockModel._secondaries || []) sec._indexId = undefined;
328
- }
329
-
330
- if (MockModel.fields) return;
331
-
332
- // First-time init: gather field configs from a temporary instance of the original class.
333
- const OrgModel = (MockModel as any)._original || this;
334
- const instance = new (OrgModel as any)(INIT_INSTANCE_SYMBOL);
335
-
336
- // If no primary key exists, create one using 'id' field
337
- if (!MockModel._primary) {
338
- if (!instance.id) {
339
- instance.id = { type: identifier };
340
- }
341
- // @ts-ignore-next-line - `id` is not part of the type, but the user probably shouldn't touch it anyhow
342
- new PrimaryIndex(MockModel, ['id']);
343
- }
344
-
345
- MockModel.fields = {};
346
- for (const key in instance) {
347
- const value = instance[key] as FieldConfig<unknown>;
348
- // Check if this property contains field metadata
349
- if (value && value.type instanceof TypeWrapper) {
350
- // Set the configuration on the constructor's `fields` property
351
- MockModel.fields[key] = value;
352
-
353
- // Set default value on the prototype
354
- const defObj = value.default===undefined ? value.type : value;
355
- const def = defObj.default;
356
- if (typeof def === 'function') {
357
- // The default is a function. We'll define a getter on the property in the model prototype,
358
- // and once it is read, we'll run the function and set the value as a plain old property
359
- // on the instance object.
360
- Object.defineProperty(MockModel.prototype, key, {
361
- get() {
362
- // This will call set(), which will define the property on the instance.
363
- return (this[key] = def.call(defObj, this));
364
- },
365
- set(val: any) {
366
- Object.defineProperty(this, key, {
367
- value: val,
368
- configurable: true,
369
- writable: true,
370
- enumerable: true,
371
- })
372
- },
373
- configurable: true,
374
- });
375
- } else if (def !== undefined) {
376
- (MockModel.prototype as any)[key] = def;
377
- }
378
- }
379
- }
380
-
381
- if (logLevel >= 1) {
382
- console.log(`[edinburgh] Registered model ${MockModel.tableName} with fields: ${Object.keys(MockModel.fields).join(' ')}`);
383
- }
398
+ static _resetIndexes(): void {
399
+ this._primary._indexId = undefined;
400
+ this._primary._versions.clear();
401
+ for (const sec of this._secondaries || []) sec._indexId = undefined;
384
402
  }
385
403
 
386
404
  static async _loadCreateIndexes(): Promise<void> {
387
- const MockModel = getMockModel(this);
388
- // Always run index inits (idempotent, skip if already initialized)
389
- await MockModel._primary._delayedInit();
390
- for (const sec of MockModel._secondaries || []) await sec._delayedInit();
391
- await MockModel._primary._initVersioning();
405
+ await this._primary._delayedInit();
406
+ for (const sec of this._secondaries || []) await sec._delayedInit();
407
+ await this._primary._initVersioning();
392
408
  }
393
409
 
394
410
  _setLoadedField(fieldName: string, value: any) {
@@ -411,7 +427,7 @@ export abstract class Model<SUB> {
411
427
  getPrimaryKey(): Uint8Array {
412
428
  let key = this._primaryKey;
413
429
  if (key === undefined) {
414
- key = this.constructor._primary!._serializeKeyFields(this).toUint8Array();
430
+ key = this.constructor._primary!._serializeKey(this).toUint8Array();
415
431
  this._setPrimaryKey(key);
416
432
  }
417
433
  return key;
@@ -476,8 +492,8 @@ export abstract class Model<SUB> {
476
492
  // the whole object just to see if something changed.
477
493
 
478
494
  // Add old values of changed fields to 'changed'.
495
+ const changed: Record<string, any> = {};
479
496
  const fields = this.constructor.fields;
480
- let changed : Record<any, any> = {};
481
497
  for(const fieldName in oldValues) {
482
498
  const oldValue = oldValues[fieldName];
483
499
  const newValue = this[fieldName as keyof Model<SUB>];
@@ -506,23 +522,7 @@ export abstract class Model<SUB> {
506
522
 
507
523
  // Update any secondaries with changed fields
508
524
  for (const index of this.constructor._secondaries || []) {
509
- if (index._computeFn) {
510
- // Computed indexes may depend on any field — compare serialized keys
511
- const oldKeyBytes = index._serializeKeyFields(oldValues).toUint8Array();
512
- const newKeyBytes = index._serializeKeyFields(this as any).toUint8Array();
513
- if (!bytesEqual(oldKeyBytes, newKeyBytes)) {
514
- index._delete(txn, pk, oldValues);
515
- index._write(txn, pk, this);
516
- }
517
- } else {
518
- for (const field of index._fieldTypes.keys()) {
519
- if (changed.hasOwnProperty(field)) {
520
- index._delete(txn, pk, oldValues);
521
- index._write(txn, pk, this);
522
- break;
523
- }
524
- }
525
- }
525
+ index._update(txn, pk, this, oldValues);
526
526
  }
527
527
  return changed;
528
528
  }
@@ -534,7 +534,7 @@ export abstract class Model<SUB> {
534
534
  *
535
535
  * @example
536
536
  * ```typescript
537
- * const user = User.load("user123");
537
+ * const user = User.get("user123");
538
538
  * user.name = "New Name";
539
539
  * user.preventPersist(); // Changes won't be saved
540
540
  * ```
@@ -546,16 +546,22 @@ export abstract class Model<SUB> {
546
546
  return this;
547
547
  }
548
548
 
549
- /**
550
- * Find all instances of this model in the database, ordered by primary key.
551
- * @param opts - Optional parameters.
552
- * @param opts.reverse - If true, iterate in reverse order.
553
- * @returns An iterator.
554
- */
555
- 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 {
556
558
  return this._primary!.find(opts);
557
559
  }
558
560
 
561
+ static batchProcess(opts: any, callback?: any): any {
562
+ return this._primary!.batchProcess(opts, callback);
563
+ }
564
+
559
565
  /**
560
566
  * Load an existing instance by primary key and update it, or create a new one.
561
567
  *
@@ -566,7 +572,7 @@ export abstract class Model<SUB> {
566
572
  * @param obj - Partial model data that **must** include every primary key field.
567
573
  * @returns The loaded-and-updated or newly created instance.
568
574
  */
569
- 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> {
570
576
  const pk = this._primary!;
571
577
  const keyArgs = [];
572
578
  for (const fieldName of pk._fieldTypes.keys()) {
@@ -595,7 +601,7 @@ export abstract class Model<SUB> {
595
601
  *
596
602
  * @example
597
603
  * ```typescript
598
- * const user = User.load("user123");
604
+ * const user = User.get("user123");
599
605
  * user.delete(); // Removes from database
600
606
  * ```
601
607
  */
@@ -659,7 +665,7 @@ export abstract class Model<SUB> {
659
665
 
660
666
  toString(): string {
661
667
  const primary = this.constructor._primary;
662
- const pk = primary._keyToArray(this._primaryKey || primary._serializeKeyFields(this).toUint8Array(false));
668
+ const pk = primary._keyToArray(this._primaryKey || primary._serializeKey(this).toUint8Array(false));
663
669
  return `{Model:${this.constructor.tableName} ${this.getState()} ${pk}}`;
664
670
  }
665
671