edinburgh 0.5.0 → 0.6.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 (72) hide show
  1. package/README.md +322 -262
  2. package/build/src/datapack.d.ts +9 -9
  3. package/build/src/datapack.js +9 -9
  4. package/build/src/edinburgh.d.ts +18 -7
  5. package/build/src/edinburgh.js +30 -51
  6. package/build/src/edinburgh.js.map +1 -1
  7. package/build/src/indexes.d.ts +85 -205
  8. package/build/src/indexes.js +150 -503
  9. package/build/src/indexes.js.map +1 -1
  10. package/build/src/migrate.js +8 -10
  11. package/build/src/migrate.js.map +1 -1
  12. package/build/src/models.d.ts +152 -107
  13. package/build/src/models.js +433 -144
  14. package/build/src/models.js.map +1 -1
  15. package/build/src/types.d.ts +30 -48
  16. package/build/src/types.js +25 -24
  17. package/build/src/types.js.map +1 -1
  18. package/build/src/utils.d.ts +4 -4
  19. package/build/src/utils.js +4 -4
  20. package/package.json +1 -1
  21. package/skill/AnyModelClass.md +7 -0
  22. package/skill/FindOptions.md +37 -0
  23. package/skill/Lifecycle Hooks.md +24 -0
  24. package/skill/{Model_delete.md → Lifecycle Hooks_delete.md } +1 -1
  25. package/skill/{Model_getPrimaryKeyHash.md → Lifecycle Hooks_getPrimaryKeyHash.md } +1 -1
  26. package/skill/{Model_isValid.md → Lifecycle Hooks_isValid.md } +1 -1
  27. package/skill/Lifecycle Hooks_migrate.md +26 -0
  28. package/skill/{Model_preCommit.md → Lifecycle Hooks_preCommit.md } +2 -2
  29. package/skill/{Model_preventPersist.md → Lifecycle Hooks_preventPersist.md } +1 -1
  30. package/skill/{Model_validate.md → Lifecycle Hooks_validate.md } +2 -2
  31. package/skill/ModelBase.md +7 -0
  32. package/skill/ModelClass.md +8 -0
  33. package/skill/SKILL.md +180 -132
  34. package/skill/Schema Evolution.md +19 -0
  35. package/skill/TypeWrapper_containsNull.md +11 -0
  36. package/skill/TypeWrapper_deserialize.md +9 -0
  37. package/skill/TypeWrapper_getError.md +11 -0
  38. package/skill/TypeWrapper_serialize.md +10 -0
  39. package/skill/TypeWrapper_serializeType.md +9 -0
  40. package/skill/array.md +2 -2
  41. package/skill/defineModel.md +3 -2
  42. package/skill/deleteEverything.md +8 -0
  43. package/skill/field.md +3 -3
  44. package/skill/link.md +3 -3
  45. package/skill/literal.md +1 -1
  46. package/skill/opt.md +1 -1
  47. package/skill/or.md +1 -1
  48. package/skill/record.md +1 -1
  49. package/skill/set.md +2 -2
  50. package/skill/setOnSaveCallback.md +2 -2
  51. package/skill/transact.md +1 -1
  52. package/src/datapack.ts +9 -9
  53. package/src/edinburgh.ts +43 -52
  54. package/src/indexes.ts +251 -599
  55. package/src/migrate.ts +9 -10
  56. package/src/models.ts +528 -231
  57. package/src/types.ts +36 -34
  58. package/src/utils.ts +4 -4
  59. package/skill/BaseIndex.md +0 -16
  60. package/skill/BaseIndex_batchProcess.md +0 -10
  61. package/skill/BaseIndex_find.md +0 -7
  62. package/skill/BaseIndex_find_2.md +0 -7
  63. package/skill/BaseIndex_find_3.md +0 -7
  64. package/skill/BaseIndex_find_4.md +0 -7
  65. package/skill/Model.md +0 -20
  66. package/skill/Model_batchProcess.md +0 -8
  67. package/skill/Model_migrate.md +0 -32
  68. package/skill/Model_replaceInto.md +0 -16
  69. package/skill/NonPrimaryIndex.md +0 -10
  70. package/skill/SecondaryIndex.md +0 -9
  71. package/skill/UniqueIndex.md +0 -9
  72. package/skill/dump.md +0 -8
@@ -1,26 +1,16 @@
1
+ import * as lowlevel from "olmdb/lowlevel";
1
2
  import { DatabaseError } from "olmdb/lowlevel";
2
- import { AsyncLocalStorage } from "node:async_hooks";
3
- import { TypeWrapper, identifier } from "./types.js";
4
- import { scheduleInit } from "./edinburgh.js";
5
- export const txnStorage = new AsyncLocalStorage();
3
+ import DataPack from "./datapack.js";
4
+ import { deserializeType, serializeType, TypeWrapper, identifier } from "./types.js";
5
+ import { transact, currentTxn } from "./edinburgh.js";
6
+ import { PrimaryKey, UniqueIndex, SecondaryIndex } from "./indexes.js";
7
+ import { addErrorPath, dbGet, hashBytes, hashFunction } from "./utils.js";
8
+ let nextFakePkHash = -1;
6
9
  const PREVENT_PERSIST_DESCRIPTOR = {
7
10
  get() {
8
11
  throw new DatabaseError("Operation not allowed after preventPersist()", "NO_PERSIST");
9
12
  },
10
13
  };
11
- /**
12
- * Returns the current transaction from AsyncLocalStorage.
13
- * Throws if called outside a transact() callback.
14
- * @internal
15
- */
16
- export function currentTxn() {
17
- const txn = txnStorage.getStore();
18
- if (!txn)
19
- throw new DatabaseError("No active transaction. Operations must be performed within a transact() callback.", 'NO_TRANSACTION');
20
- return txn;
21
- }
22
- import { PrimaryIndex, UniqueIndex, SecondaryIndex } from "./indexes.js";
23
- import { addErrorPath, dbGet, hashBytes } from "./utils.js";
24
14
  /**
25
15
  * Create a field definition for a model property.
26
16
  *
@@ -29,13 +19,13 @@ import { addErrorPath, dbGet, hashBytes } from "./utils.js";
29
19
  * This allows for both runtime introspection and compile-time type safety.
30
20
  *
31
21
  * @template T - The field type.
32
- * @param type - The type wrapper for this field.
33
- * @param options - Additional field configuration options.
22
+ * @param type The type wrapper for this field.
23
+ * @param options Additional field configuration options.
34
24
  * @returns The field value (typed as T, but actually returns FieldConfig<T>).
35
25
  *
36
26
  * @example
37
27
  * ```typescript
38
- * const User = E.defineModel(class {
28
+ * const User = E.defineModel("User", class {
39
29
  * name = E.field(E.string, {description: "User's full name"});
40
30
  * age = E.field(E.opt(E.number), {description: "User's age", default: 25});
41
31
  * });
@@ -46,33 +36,340 @@ export function field(type, options = {}) {
46
36
  options.type = type;
47
37
  return options;
48
38
  }
49
- // Model registration and initialization
50
- export const modelRegistry = {};
51
39
  function isObjectEmpty(obj) {
52
- for (let _ of Object.keys(obj)) {
40
+ for (const _ of Object.keys(obj)) {
53
41
  return false;
54
42
  }
55
43
  return true;
56
44
  }
57
- let autoTableNameId = 0;
58
- export function defineModel(cls, opts) {
59
- Object.setPrototypeOf(cls.prototype, Model.prototype);
45
+ function copyStaticMembersFromClassChain(target, source) {
46
+ for (let current = source; current && current !== Function.prototype; current = Object.getPrototypeOf(current)) {
47
+ for (const key of Object.getOwnPropertyNames(current)) {
48
+ if (key === 'length' || key === 'name' || key === 'prototype')
49
+ continue;
50
+ if (Object.prototype.hasOwnProperty.call(target, key))
51
+ continue;
52
+ Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(current, key));
53
+ }
54
+ }
55
+ }
56
+ // Model registration and initialization
57
+ export const modelRegistry = {};
58
+ export const pendingModelInits = new Set();
59
+ // These static members are attached dynamically in defineModel(), so 'declare' tells TypeScript
60
+ // they exist at runtime without emitting duplicate class fields that would shadow those assignments.
61
+ class ModelClassRuntime extends PrimaryKey {
62
+ // Cached list of non-primary fields used for value serialization.
63
+ _nonKeyFields;
64
+ // Lazy getter/setter descriptors installed on unloaded non-key fields.
65
+ _lazyDescriptors = {};
66
+ // Writable descriptors temporarily installed before hydrating value fields.
67
+ _resetDescriptors = {};
68
+ // Frozen descriptors applied to primary-key fields after key materialization.
69
+ _freezePrimaryKeyDescriptors = {};
70
+ // Active schema version number for value encoding.
71
+ _currentVersion;
72
+ // Hash of the active migrate() function for schema identity.
73
+ _currentMigrateHash;
74
+ // Cached historical schema metadata for lazy migration of old rows.
75
+ _versions = new Map();
76
+ _serializeVersionValue() {
77
+ const fields = [];
78
+ for (const fieldName of this._nonKeyFields) {
79
+ const tp = new DataPack();
80
+ serializeType(this.fields[fieldName].type, tp);
81
+ fields.push([fieldName, tp.toUint8Array()]);
82
+ }
83
+ return new DataPack().write({
84
+ migrateHash: this._currentMigrateHash,
85
+ fields,
86
+ secondaryKeys: new Set(Object.values(this._secondaries || {}).map(sec => sec._signature)),
87
+ }).toUint8Array();
88
+ }
89
+ async _initialize(reset = false) {
90
+ const allFieldTypes = new Map();
91
+ for (const [fieldName, fieldConfig] of Object.entries(this.fields)) {
92
+ allFieldTypes.set(fieldName, fieldConfig.type);
93
+ }
94
+ await super._initializeIndex(allFieldTypes, reset);
95
+ if (reset || this._nonKeyFields === undefined) {
96
+ this._nonKeyFields = Object.keys(this.fields).filter(fieldName => !this._indexFields.has(fieldName));
97
+ this._lazyDescriptors = {};
98
+ this._resetDescriptors = {};
99
+ this._freezePrimaryKeyDescriptors = {};
100
+ for (const fieldName of this._nonKeyFields) {
101
+ this._lazyDescriptors[fieldName] = {
102
+ configurable: true,
103
+ enumerable: true,
104
+ get() {
105
+ this.constructor._lazyLoad(this);
106
+ return this[fieldName];
107
+ },
108
+ set(value) {
109
+ this.constructor._lazyLoad(this);
110
+ this[fieldName] = value;
111
+ },
112
+ };
113
+ this._resetDescriptors[fieldName] = {
114
+ writable: true,
115
+ enumerable: true,
116
+ };
117
+ }
118
+ for (const fieldName of this._indexFields.keys()) {
119
+ this._freezePrimaryKeyDescriptors[fieldName] = {
120
+ writable: false,
121
+ enumerable: true,
122
+ };
123
+ }
124
+ }
125
+ for (const sec of Object.values(this._secondaries || {})) {
126
+ await sec._initializeIndex(allFieldTypes, reset, this._indexFields);
127
+ }
128
+ const migrateFn = this.migrate;
129
+ this._currentMigrateHash = migrateFn ? hashFunction(migrateFn) : 0;
130
+ const currentValueBytes = this._serializeVersionValue();
131
+ this._currentVersion = (await this._ensureVersionEntry(currentValueBytes)).version;
132
+ }
133
+ _getSecondary(name) {
134
+ const index = this._secondaries?.[name];
135
+ if (!index)
136
+ throw new DatabaseError(`Unknown index '${name}' on model '${this.tableName}'`, 'INIT_ERROR');
137
+ return index;
138
+ }
139
+ _get(txn, args, loadNow) {
140
+ let key;
141
+ let keyParts;
142
+ if (args instanceof Uint8Array) {
143
+ key = args;
144
+ }
145
+ else {
146
+ key = this._argsToKeyBytes(args, false).toUint8Array();
147
+ keyParts = args;
148
+ }
149
+ const keyHash = hashBytes(key);
150
+ const cached = txn.instances.get(keyHash);
151
+ if (cached) {
152
+ if (loadNow && loadNow !== true) {
153
+ Object.defineProperties(cached, this._resetDescriptors);
154
+ this._loadValueFields(cached, loadNow);
155
+ }
156
+ return cached;
157
+ }
158
+ let valueBuffer;
159
+ if (loadNow) {
160
+ if (loadNow === true) {
161
+ valueBuffer = dbGet(txn.id, key);
162
+ if (!valueBuffer)
163
+ return;
164
+ }
165
+ else {
166
+ valueBuffer = loadNow;
167
+ }
168
+ }
169
+ const model = Object.create(this.prototype);
170
+ model._txn = txn;
171
+ model._oldValues = {};
172
+ txn.instances.set(keyHash, model);
173
+ if (keyParts) {
174
+ let i = 0;
175
+ for (const fieldName of this._indexFields.keys()) {
176
+ model._setLoadedField(fieldName, keyParts[i++]);
177
+ }
178
+ }
179
+ else {
180
+ const keyPack = new DataPack(key);
181
+ keyPack.readNumber();
182
+ for (const [fieldName, fieldType] of this._indexFields.entries()) {
183
+ model._setLoadedField(fieldName, fieldType.deserialize(keyPack));
184
+ }
185
+ }
186
+ model._setPrimaryKey(key, keyHash);
187
+ if (valueBuffer) {
188
+ this._loadValueFields(model, valueBuffer);
189
+ }
190
+ else {
191
+ Object.defineProperties(model, this._lazyDescriptors);
192
+ }
193
+ return model;
194
+ }
195
+ _lazyLoad(model) {
196
+ const key = model._primaryKey;
197
+ const valueBuffer = dbGet(model._txn.id, key);
198
+ if (!valueBuffer)
199
+ throw new DatabaseError(`Lazy-loaded ${this.tableName}#${key} does not exist`, 'LAZY_FAIL');
200
+ Object.defineProperties(model, this._resetDescriptors);
201
+ this._loadValueFields(model, valueBuffer);
202
+ }
203
+ /**
204
+ * Load a model by primary key inside the current transaction.
205
+ *
206
+ * @returns The matching model, or `undefined` if no row exists.
207
+ */
208
+ get(...args) {
209
+ return this._get(currentTxn(), args, true);
210
+ }
211
+ /**
212
+ * Load a model by primary key without fetching its non-key fields immediately.
213
+ *
214
+ * Accessing a lazy field later will load the remaining fields transparently.
215
+ */
216
+ getLazy(...args) {
217
+ return this._get(currentTxn(), args, false);
218
+ }
219
+ _pairToInstance(txn, keyBuffer, valueBuffer) {
220
+ return this._get(txn, new Uint8Array(keyBuffer), new Uint8Array(valueBuffer));
221
+ }
222
+ /**
223
+ * Load an existing instance by primary key and update it, or create a new one.
224
+ * If a row already exists, its non-primary-key fields are updated in place.
225
+ * Otherwise, a new instance is created with `obj` as its initial properties.
226
+ *
227
+ * @param obj Partial model data that **must** include every primary key field.
228
+ * @returns The loaded-and-updated or newly created instance.
229
+ */
230
+ replaceInto(obj) {
231
+ const keyArgs = [];
232
+ for (const fieldName of this._indexFields.keys()) {
233
+ if (!(fieldName in obj)) {
234
+ throw new DatabaseError(`replaceInto: missing primary key field '${fieldName}'`, "MISSING_PRIMARY_KEY");
235
+ }
236
+ keyArgs.push(obj[fieldName]);
237
+ }
238
+ const existing = this.get(...keyArgs);
239
+ if (existing) {
240
+ for (const key in obj) {
241
+ if (!this._indexFields.has(key)) {
242
+ existing[key] = obj[key];
243
+ }
244
+ }
245
+ return existing;
246
+ }
247
+ return new this(obj);
248
+ }
249
+ /**
250
+ * Look up a model through a named unique index.
251
+ *
252
+ * @param name The name from the model's `unique` definition.
253
+ * @param args The unique-index key values.
254
+ * @returns The matching model instance, if any.
255
+ */
256
+ getBy(name, ...args) {
257
+ return this._getSecondary(name).getPK(...args);
258
+ }
259
+ findBy(name, opts) {
260
+ return this._getSecondary(name).find(opts);
261
+ }
262
+ /**
263
+ * Process rows from a named unique or secondary index in batched transactions.
264
+ *
265
+ * Uses the same range options as `findBy()`, plus batch limits.
266
+ */
267
+ batchProcessBy(name, opts, callback) {
268
+ return this._getSecondary(name).batchProcess(opts, callback);
269
+ }
270
+ _loadValueFields(model, valueArray) {
271
+ const valuePack = new DataPack(valueArray);
272
+ const version = valuePack.readNumber();
273
+ if (version === this._currentVersion) {
274
+ for (const fieldName of this._nonKeyFields) {
275
+ model._setLoadedField(fieldName, this.fields[fieldName].type.deserialize(valuePack));
276
+ }
277
+ }
278
+ else {
279
+ this._migrateValueFields(model, version, valuePack);
280
+ }
281
+ }
282
+ _loadVersionInfo(txnId, version) {
283
+ let info = this._versions.get(version);
284
+ if (info)
285
+ return info;
286
+ const key = this._versionInfoKey(version);
287
+ const raw = dbGet(txnId, key);
288
+ if (!raw)
289
+ throw new DatabaseError(`Version ${version} info not found for index ${this}`, 'CONSISTENCY_ERROR');
290
+ const obj = new DataPack(raw).read();
291
+ if (!obj || typeof obj.migrateHash !== 'number' || !Array.isArray(obj.fields) || !(obj.secondaryKeys instanceof Set)) {
292
+ throw new DatabaseError(`Version ${version} info is corrupted for index ${this}`, 'CONSISTENCY_ERROR');
293
+ }
294
+ const nonKeyFields = new Map();
295
+ for (const [name, typeBytes] of obj.fields) {
296
+ nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
297
+ }
298
+ info = { migrateHash: obj.migrateHash, nonKeyFields, secondaryKeys: obj.secondaryKeys };
299
+ this._versions.set(version, info);
300
+ return info;
301
+ }
302
+ _migrateValueFields(model, version, valuePack) {
303
+ const versionInfo = this._loadVersionInfo(model._txn.id, version);
304
+ const record = {};
305
+ for (const [name] of this._indexFields.entries())
306
+ record[name] = model[name];
307
+ for (const [name, type] of versionInfo.nonKeyFields.entries()) {
308
+ record[name] = type.deserialize(valuePack);
309
+ }
310
+ const migrateFn = this.migrate;
311
+ if (migrateFn)
312
+ migrateFn(record);
313
+ for (const fieldName of this._nonKeyFields) {
314
+ if (fieldName in record) {
315
+ model._setLoadedField(fieldName, record[fieldName]);
316
+ }
317
+ else if (fieldName in model) {
318
+ model._setLoadedField(fieldName, model[fieldName]);
319
+ }
320
+ else {
321
+ throw new DatabaseError(`Field ${fieldName} is missing in migrated data for ${model}`, 'MIGRATION_ERROR');
322
+ }
323
+ }
324
+ }
325
+ _serializeValue(data) {
326
+ const valueBytes = new DataPack();
327
+ valueBytes.write(this._currentVersion);
328
+ for (const fieldName of this._nonKeyFields) {
329
+ const fieldConfig = this.fields[fieldName];
330
+ fieldConfig.type.serialize(data[fieldName], valueBytes);
331
+ }
332
+ return valueBytes.toUint8Array();
333
+ }
334
+ }
335
+ /**
336
+ * Runtime base constructor for model classes returned by `defineModel()`.
337
+ *
338
+ * Prefer the `ModelClass` type alias for annotations and the result of
339
+ * `defineModel()` for concrete model classes.
340
+ */
341
+ export const ModelClass = ModelClassRuntime;
342
+ /**
343
+ * Register a model class with the Edinburgh ORM system.
344
+ *
345
+ * Converts a plain class into a fully-featured model with database persistence,
346
+ * typed fields, primary key access, and optional secondary and unique indexes.
347
+ *
348
+ * @param tableName The database table name for this model.
349
+ * @param cls A plain class whose properties use E.field().
350
+ * @param opts Registration options.
351
+ * @param opts.pk Primary key field name or array of field names.
352
+ * @param opts.unique Named unique index specifications (field name, field array, or compute function).
353
+ * @param opts.index Named secondary index specifications (field name, field array, or compute function).
354
+ * @param opts.override Replace a previous model with the same table name.
355
+ * @returns The enhanced model constructor.
356
+ */
357
+ export function defineModel(tableName, cls, opts) {
358
+ Object.setPrototypeOf(cls.prototype, ModelBase.prototype);
60
359
  const MockModel = function (initial, txn = currentTxn()) {
61
360
  this._txn = txn;
62
- txn.instances.add(this);
361
+ txn.instances.set(nextFakePkHash--, this);
63
362
  if (initial)
64
363
  Object.assign(this, initial);
65
364
  };
365
+ const normalizeSpec = (spec) => typeof spec === 'string' ? [spec] : spec;
366
+ const queueInitialization = () => { pendingModelInits.add(MockModel); };
367
+ const loadPrimary = (txn, primaryKey, loadNow) => MockModel._get(txn, primaryKey, loadNow);
66
368
  cls.prototype.constructor = MockModel;
67
- Object.setPrototypeOf(MockModel, Model);
369
+ Object.setPrototypeOf(MockModel, ModelClassRuntime.prototype);
68
370
  MockModel.prototype = cls.prototype;
69
- MockModel._original = cls;
70
- for (const name of Object.getOwnPropertyNames(cls)) {
71
- if (name !== 'length' && name !== 'prototype' && name !== 'name') {
72
- MockModel[name] = cls[name];
73
- }
74
- }
75
- MockModel.tableName = opts?.tableName || cls.name || `Model${++autoTableNameId}`;
371
+ copyStaticMembersFromClassChain(MockModel, cls);
372
+ MockModel.tableName = tableName;
76
373
  if (MockModel.tableName in modelRegistry) {
77
374
  if (!opts?.override) {
78
375
  throw new DatabaseError(`Model with table name '${MockModel.tableName}' already registered`, 'INIT_ERROR');
@@ -111,25 +408,32 @@ export function defineModel(cls, opts) {
111
408
  }
112
409
  }
113
410
  }
114
- if (opts?.pk) {
115
- new PrimaryIndex(MockModel, Array.isArray(opts.pk) ? opts.pk : [opts.pk]);
116
- }
117
- else {
118
- new PrimaryIndex(MockModel, ['id']);
411
+ const primaryFields = opts?.pk ? (Array.isArray(opts.pk) ? opts.pk : [opts.pk]) : ['id'];
412
+ MockModel._indexFields = new Map();
413
+ for (const fieldName of primaryFields) {
414
+ const fieldConfig = MockModel.fields[fieldName];
415
+ if (!fieldConfig) {
416
+ throw new DatabaseError(`Unknown primary key field '${fieldName}' on model '${tableName}'`, 'INIT_ERROR');
417
+ }
418
+ MockModel._indexFields.set(fieldName, fieldConfig.type);
119
419
  }
120
- const normalizeSpec = (spec) => typeof spec === 'string' ? [spec] : spec;
420
+ MockModel._secondaries = {};
421
+ MockModel._lazyDescriptors = {};
422
+ MockModel._resetDescriptors = {};
423
+ MockModel._freezePrimaryKeyDescriptors = {};
424
+ MockModel._versions = new Map();
121
425
  if (opts?.unique) {
122
426
  for (const [name, spec] of Object.entries(opts.unique)) {
123
- MockModel[name] = new UniqueIndex(MockModel, normalizeSpec(spec));
427
+ MockModel._secondaries[name] = new UniqueIndex(tableName, normalizeSpec(spec), loadPrimary, queueInitialization);
124
428
  }
125
429
  }
126
430
  if (opts?.index) {
127
431
  for (const [name, spec] of Object.entries(opts.index)) {
128
- MockModel[name] = new SecondaryIndex(MockModel, normalizeSpec(spec));
432
+ MockModel._secondaries[name] = new SecondaryIndex(tableName, normalizeSpec(spec), loadPrimary, queueInitialization);
129
433
  }
130
434
  }
131
435
  modelRegistry[MockModel.tableName] = MockModel;
132
- scheduleInit();
436
+ pendingModelInits.add(MockModel);
133
437
  return MockModel;
134
438
  }
135
439
  /**
@@ -163,42 +467,27 @@ export function defineModel(cls, opts) {
163
467
  *
164
468
  * - **`static migrate(record)`**: Called when deserializing rows written with an older schema
165
469
  * version. Receives a plain record object; mutate it in-place to match the current schema.
166
- * See {@link Model.migrate}.
167
470
  *
168
471
  * - **`preCommit()`**: Called on each modified instance right before the transaction commits.
169
472
  * Useful for computing derived fields, enforcing cross-field invariants, or creating related
170
- * instances. See {@link Model.preCommit}.
171
- *
172
- * @template SUB - The concrete model subclass (for proper typing).
473
+ * instances.
173
474
  *
174
475
  * @example
175
476
  * ```typescript
176
- * const User = E.defineModel(class {
477
+ * const User = E.defineModel("User", class {
177
478
  * id = E.field(E.identifier);
178
479
  * name = E.field(E.string);
179
480
  * email = E.field(E.string);
180
481
  * }, {
181
482
  * pk: "id",
182
- * unique: { byEmail: "email" },
483
+ * unique: { email: "email" },
183
484
  * });
485
+ * // Optional: declare a companion type so `let u: User` works.
486
+ * // Not needed if you only use `new User()`, `User.find()`, etc.
487
+ * type User = InstanceType<typeof User>;
184
488
  * ```
185
489
  */
186
- export class Model {
187
- static _primary;
188
- /** @internal All non-primary indexes for this model. */
189
- static _secondaries;
190
- /** The database table name (defaults to class name). */
191
- static tableName;
192
- /** When true, defineModel replaces an existing model with the same tableName. */
193
- static override;
194
- /** Field configuration metadata. */
195
- static fields;
196
- // Alias statics that delegate to _primary, used by migrate.ts
197
- static get _indexId() { return this._primary?._indexId; }
198
- static get _currentVersion() { return this._primary._currentVersion; }
199
- static get _pkFieldTypes() { return this._primary._fieldTypes; }
200
- static _loadVersionInfo(txnId, version) { return this._primary._loadVersionInfo(txnId, version); }
201
- static _writePrimary(txn, pk, data) { this._primary._write(txn, pk, data); }
490
+ export class ModelBase {
202
491
  /*
203
492
  * IMPORTANT: We cannot use instance property initializers here, because we will be
204
493
  * initializing the class through a fake constructor that will skip these. This is
@@ -208,27 +497,13 @@ export class Model {
208
497
  * @internal
209
498
  * - _oldValues===undefined: New instance, not yet saved.
210
499
  * - _oldValues===null: Instance is to be deleted.
500
+ * - _oldValues===false: Instance excluded from persistence (preventPersist).
211
501
  * - _oldValues is an object: Loaded (possibly only partial, still lazy) from disk, _oldValues contains (partial) old values
212
502
  */
213
503
  _oldValues;
214
504
  _primaryKey;
215
505
  _primaryKeyHash;
216
506
  _txn;
217
- constructor(initial = {}) {
218
- throw new DatabaseError("Use defineModel() to create model classes", 'INIT_ERROR');
219
- }
220
- static _resetIndexes() {
221
- this._primary._indexId = undefined;
222
- this._primary._versions.clear();
223
- for (const sec of this._secondaries || [])
224
- sec._indexId = undefined;
225
- }
226
- static async _loadCreateIndexes() {
227
- await this._primary._delayedInit();
228
- for (const sec of this._secondaries || [])
229
- await sec._delayedInit();
230
- await this._primary._initVersioning();
231
- }
232
507
  _setLoadedField(fieldName, value) {
233
508
  const oldValues = this._oldValues;
234
509
  if (oldValues.hasOwnProperty(fieldName))
@@ -243,13 +518,25 @@ export class Model {
243
518
  oldValues[fieldName] = value;
244
519
  }
245
520
  }
521
+ _restoreLazyFields() {
522
+ const oldValues = this._oldValues;
523
+ if (!oldValues || oldValues === null)
524
+ return;
525
+ for (const [fieldName, descriptor] of Object.entries(this.constructor._lazyDescriptors)) {
526
+ if (!oldValues.hasOwnProperty(fieldName)) {
527
+ Object.defineProperty(this, fieldName, descriptor);
528
+ }
529
+ }
530
+ }
246
531
  /**
247
532
  * @returns The primary key for this instance.
248
533
  */
249
534
  getPrimaryKey() {
250
535
  let key = this._primaryKey;
251
536
  if (key === undefined) {
252
- key = this.constructor._primary._serializeKey(this).toUint8Array();
537
+ if (this._oldValues === false)
538
+ throw new DatabaseError("Operation not allowed after preventPersist()", "NO_PERSIST");
539
+ key = this.constructor._serializePK(this).toUint8Array();
253
540
  this._setPrimaryKey(key);
254
541
  }
255
542
  return key;
@@ -257,7 +544,7 @@ export class Model {
257
544
  _setPrimaryKey(key, hash) {
258
545
  this._primaryKey = key;
259
546
  this._primaryKeyHash = hash ?? hashBytes(key);
260
- Object.defineProperties(this, this.constructor._primary._freezePrimaryKeyDescriptors);
547
+ Object.defineProperties(this, this.constructor._freezePrimaryKeyDescriptors);
261
548
  }
262
549
  /**
263
550
  * @returns A 53-bit positive integer non-cryptographic hash of the primary key, or undefined if not yet saved.
@@ -268,19 +555,21 @@ export class Model {
268
555
  return this._primaryKeyHash;
269
556
  }
270
557
  isLazyField(field) {
271
- const descr = this.constructor._primary._lazyDescriptors[field];
272
- return !!(descr && 'get' in descr && descr.get === Reflect.getOwnPropertyDescriptor(this, field)?.get);
558
+ const oldValues = this._oldValues;
559
+ return !!(oldValues && oldValues !== null && field in this.constructor._lazyDescriptors && !oldValues.hasOwnProperty(field));
273
560
  }
274
561
  _write(txn) {
275
562
  const oldValues = this._oldValues;
563
+ if (oldValues === false)
564
+ return; // preventPersist() was called
276
565
  if (oldValues === null) { // Delete instance
277
566
  const pk = this._primaryKey;
278
567
  // Temporarily restore _oldValues so computed indexes can trigger lazy loads
279
568
  this._oldValues = {};
280
- for (const index of this.constructor._secondaries || []) {
569
+ for (const index of Object.values(this.constructor._secondaries || {})) {
281
570
  index._delete(txn, pk, this);
282
571
  }
283
- this.constructor._primary._delete(txn, pk, this);
572
+ this.constructor._deletePK(txn, pk, this);
284
573
  return "deleted";
285
574
  }
286
575
  if (oldValues === undefined) { // Create instance
@@ -291,9 +580,9 @@ export class Model {
291
580
  throw new DatabaseError("Unique constraint violation", "UNIQUE_CONSTRAINT");
292
581
  }
293
582
  // Insert the primary index
294
- this.constructor._primary._write(txn, pk, this);
583
+ this.constructor._writePK(txn, pk, this);
295
584
  // Insert all secondaries
296
- for (const index of this.constructor._secondaries || []) {
585
+ for (const index of Object.values(this.constructor._secondaries || {})) {
297
586
  index._write(txn, pk, this);
298
587
  }
299
588
  return "created";
@@ -303,7 +592,8 @@ export class Model {
303
592
  // the whole object just to see if something changed.
304
593
  // Add old values of changed fields to 'changed'.
305
594
  const changed = {};
306
- const fields = this.constructor.fields;
595
+ const cls = this.constructor;
596
+ const fields = cls.fields;
307
597
  for (const fieldName in oldValues) {
308
598
  const oldValue = oldValues[fieldName];
309
599
  const newValue = this[fieldName];
@@ -314,7 +604,7 @@ export class Model {
314
604
  if (isObjectEmpty(changed))
315
605
  return; // No changes, nothing to do
316
606
  // Make sure primary has not been changed
317
- for (const field of this.constructor._primary._fieldTypes.keys()) {
607
+ for (const field of cls._indexFields.keys()) {
318
608
  if (changed.hasOwnProperty(field)) {
319
609
  throw new DatabaseError(`Cannot modify primary key field: ${field}`, "CHANGE_PRIMARY");
320
610
  }
@@ -324,9 +614,9 @@ export class Model {
324
614
  this.validate(true);
325
615
  // Update the primary index
326
616
  const pk = this._primaryKey;
327
- this.constructor._primary._write(txn, pk, this);
617
+ cls._writePK(txn, pk, this);
328
618
  // Update any secondaries with changed fields
329
- for (const index of this.constructor._secondaries || []) {
619
+ for (const index of Object.values(cls._secondaries || {})) {
330
620
  index._update(txn, pk, this, oldValues);
331
621
  }
332
622
  return changed;
@@ -344,53 +634,14 @@ export class Model {
344
634
  * ```
345
635
  */
346
636
  preventPersist() {
347
- this._txn.instances.delete(this);
637
+ if (this._oldValues === undefined && this._primaryKey !== undefined) {
638
+ throw new DatabaseError("Cannot preventPersist() after PK has been used", "INVALID");
639
+ }
640
+ this._oldValues = false;
348
641
  // Have access to '_txn' throw a descriptive error:
349
642
  Object.defineProperty(this, "_txn", PREVENT_PERSIST_DESCRIPTOR);
350
643
  return this;
351
644
  }
352
- static get(...args) {
353
- return this._primary.get(...args);
354
- }
355
- static getLazy(...args) {
356
- return this._primary.getLazy(...args);
357
- }
358
- static find(opts) {
359
- return this._primary.find(opts);
360
- }
361
- static batchProcess(opts, callback) {
362
- return this._primary.batchProcess(opts, callback);
363
- }
364
- /**
365
- * Load an existing instance by primary key and update it, or create a new one.
366
- *
367
- * The provided object must contain all primary key fields. If a matching row exists,
368
- * the remaining properties from `obj` are set on the loaded instance. Otherwise a
369
- * new instance is created with `obj` as its initial properties.
370
- *
371
- * @param obj - Partial model data that **must** include every primary key field.
372
- * @returns The loaded-and-updated or newly created instance.
373
- */
374
- static replaceInto(obj) {
375
- const pk = this._primary;
376
- const keyArgs = [];
377
- for (const fieldName of pk._fieldTypes.keys()) {
378
- if (!(fieldName in obj)) {
379
- throw new DatabaseError(`replaceInto: missing primary key field '${fieldName}'`, "MISSING_PRIMARY_KEY");
380
- }
381
- keyArgs.push(obj[fieldName]);
382
- }
383
- const existing = pk.get(...keyArgs);
384
- if (existing) {
385
- for (const key in obj) {
386
- if (!pk._fieldTypes.has(key)) {
387
- existing[key] = obj[key];
388
- }
389
- }
390
- return existing;
391
- }
392
- return new this(obj);
393
- }
394
645
  /**
395
646
  * Delete this model instance from the database.
396
647
  *
@@ -404,12 +655,12 @@ export class Model {
404
655
  */
405
656
  delete() {
406
657
  if (this._oldValues === undefined)
407
- throw new DatabaseError("Cannot delete unsaved instance", "NOT_SAVED");
658
+ throw new DatabaseError("Cannot delete unsaved instance", "INVALID");
408
659
  this._oldValues = null;
409
660
  }
410
661
  /**
411
662
  * Validate all fields in this model instance.
412
- * @param raise - If true, throw on first validation error.
663
+ * @param raise If true, throw on first validation error.
413
664
  * @returns Array of validation errors (empty if valid).
414
665
  *
415
666
  * @example
@@ -423,10 +674,11 @@ export class Model {
423
674
  */
424
675
  validate(raise = false) {
425
676
  const errors = [];
426
- for (const [key, fieldConfig] of Object.entries(this.constructor.fields)) {
677
+ const cls = this.constructor;
678
+ for (const [key, fieldConfig] of Object.entries(cls.fields)) {
427
679
  let e = fieldConfig.type.getError(this[key]);
428
680
  if (e) {
429
- e = addErrorPath(e, this.constructor.tableName + "." + key);
681
+ e = addErrorPath(e, cls.tableName + "." + key);
430
682
  if (raise)
431
683
  throw e;
432
684
  errors.push(e);
@@ -452,7 +704,7 @@ export class Model {
452
704
  return "deleted";
453
705
  if (this._oldValues === undefined)
454
706
  return "created";
455
- for (const [key, descr] of Object.entries(this.constructor._primary._lazyDescriptors)) {
707
+ for (const [key, descr] of Object.entries(this.constructor._lazyDescriptors)) {
456
708
  if (descr && 'get' in descr && descr.get === Reflect.getOwnPropertyDescriptor(this, key)?.get) {
457
709
  return "lazy";
458
710
  }
@@ -460,12 +712,49 @@ export class Model {
460
712
  return "loaded";
461
713
  }
462
714
  toString() {
463
- const primary = this.constructor._primary;
464
- const pk = primary._keyToArray(this._primaryKey || primary._serializeKey(this).toUint8Array(false));
465
- return `{Model:${this.constructor.tableName} ${this.getState()} ${pk}}`;
715
+ const cls = this.constructor;
716
+ const pk = cls._pkToArray(this._primaryKey || cls._serializePK(this).toUint8Array(false));
717
+ return `{Model:${cls.tableName} ${this.getState()} ${pk}}`;
466
718
  }
467
719
  [Symbol.for('nodejs.util.inspect.custom')]() {
468
720
  return this.toString();
469
721
  }
470
722
  }
723
+ /**
724
+ * Delete every key/value entry in the database and reinitialize all registered models.
725
+ *
726
+ * This clears rows, index metadata, and schema-version records. It is mainly useful
727
+ * for tests, local resets, or tooling that needs a completely empty database.
728
+ */
729
+ export async function deleteEverything() {
730
+ let done = false;
731
+ while (!done) {
732
+ await transact(() => {
733
+ const txn = currentTxn();
734
+ const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
735
+ const deadline = Date.now() + 150;
736
+ let count = 0;
737
+ try {
738
+ while (true) {
739
+ const raw = lowlevel.readIterator(iteratorId);
740
+ if (!raw) {
741
+ done = true;
742
+ break;
743
+ }
744
+ lowlevel.del(txn.id, raw.key);
745
+ if (++count >= 4096 || Date.now() >= deadline)
746
+ break;
747
+ }
748
+ }
749
+ finally {
750
+ lowlevel.closeIterator(iteratorId);
751
+ }
752
+ });
753
+ }
754
+ for (const model of Object.values(modelRegistry)) {
755
+ pendingModelInits.delete(model);
756
+ await model._initialize(true);
757
+ }
758
+ }
759
+ export const Model = ModelBase;
471
760
  //# sourceMappingURL=models.js.map