edinburgh 0.5.0 → 0.6.1
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.
- package/README.md +309 -246
- package/build/src/datapack.d.ts +9 -9
- package/build/src/datapack.js +9 -9
- package/build/src/edinburgh.d.ts +21 -7
- package/build/src/edinburgh.js +53 -67
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +85 -205
- package/build/src/indexes.js +150 -503
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate.js +8 -10
- package/build/src/migrate.js.map +1 -1
- package/build/src/models.d.ts +152 -107
- package/build/src/models.js +433 -144
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +30 -48
- package/build/src/types.js +25 -24
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +4 -4
- package/build/src/utils.js +4 -4
- package/package.json +2 -2
- package/skill/AnyModelClass.md +7 -0
- package/skill/FindOptions.md +37 -0
- package/skill/Lifecycle Hooks.md +24 -0
- package/skill/{Model_delete.md → Lifecycle Hooks_delete.md } +1 -1
- package/skill/{Model_getPrimaryKeyHash.md → Lifecycle Hooks_getPrimaryKeyHash.md } +1 -1
- package/skill/{Model_isValid.md → Lifecycle Hooks_isValid.md } +1 -1
- package/skill/Lifecycle Hooks_migrate.md +26 -0
- package/skill/{Model_preCommit.md → Lifecycle Hooks_preCommit.md } +2 -2
- package/skill/{Model_preventPersist.md → Lifecycle Hooks_preventPersist.md } +1 -1
- package/skill/{Model_validate.md → Lifecycle Hooks_validate.md } +2 -2
- package/skill/ModelBase.md +7 -0
- package/skill/ModelClass.md +8 -0
- package/skill/SKILL.md +180 -132
- package/skill/Schema Evolution.md +19 -0
- package/skill/TypeWrapper_containsNull.md +11 -0
- package/skill/TypeWrapper_deserialize.md +9 -0
- package/skill/TypeWrapper_getError.md +11 -0
- package/skill/TypeWrapper_serialize.md +10 -0
- package/skill/TypeWrapper_serializeType.md +9 -0
- package/skill/array.md +2 -2
- package/skill/defineModel.md +3 -2
- package/skill/deleteEverything.md +8 -0
- package/skill/field.md +3 -3
- package/skill/link.md +3 -3
- package/skill/literal.md +1 -1
- package/skill/opt.md +1 -1
- package/skill/or.md +1 -1
- package/skill/record.md +1 -1
- package/skill/set.md +2 -2
- package/skill/setOnSaveCallback.md +5 -2
- package/skill/transact.md +1 -1
- package/src/datapack.ts +9 -9
- package/src/edinburgh.ts +68 -68
- package/src/indexes.ts +251 -599
- package/src/migrate.ts +9 -10
- package/src/models.ts +528 -231
- package/src/types.ts +36 -34
- package/src/utils.ts +4 -4
- package/skill/BaseIndex.md +0 -16
- package/skill/BaseIndex_batchProcess.md +0 -10
- package/skill/BaseIndex_find.md +0 -7
- package/skill/BaseIndex_find_2.md +0 -7
- package/skill/BaseIndex_find_3.md +0 -7
- package/skill/BaseIndex_find_4.md +0 -7
- package/skill/Model.md +0 -20
- package/skill/Model_batchProcess.md +0 -8
- package/skill/Model_migrate.md +0 -32
- package/skill/Model_replaceInto.md +0 -16
- package/skill/NonPrimaryIndex.md +0 -10
- package/skill/SecondaryIndex.md +0 -9
- package/skill/UniqueIndex.md +0 -9
- package/skill/dump.md +0 -8
package/build/src/models.js
CHANGED
|
@@ -1,26 +1,16 @@
|
|
|
1
|
+
import * as lowlevel from "olmdb/lowlevel";
|
|
1
2
|
import { DatabaseError } from "olmdb/lowlevel";
|
|
2
|
-
import
|
|
3
|
-
import { TypeWrapper, identifier } from "./types.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
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
|
|
33
|
-
* @param 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 (
|
|
40
|
+
for (const _ of Object.keys(obj)) {
|
|
53
41
|
return false;
|
|
54
42
|
}
|
|
55
43
|
return true;
|
|
56
44
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
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,
|
|
369
|
+
Object.setPrototypeOf(MockModel, ModelClassRuntime.prototype);
|
|
68
370
|
MockModel.prototype = cls.prototype;
|
|
69
|
-
MockModel
|
|
70
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
432
|
+
MockModel._secondaries[name] = new SecondaryIndex(tableName, normalizeSpec(spec), loadPrimary, queueInitialization);
|
|
129
433
|
}
|
|
130
434
|
}
|
|
131
435
|
modelRegistry[MockModel.tableName] = MockModel;
|
|
132
|
-
|
|
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.
|
|
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: {
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
272
|
-
return !!(
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
617
|
+
cls._writePK(txn, pk, this);
|
|
328
618
|
// Update any secondaries with changed fields
|
|
329
|
-
for (const index of
|
|
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.
|
|
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", "
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
|
464
|
-
const pk =
|
|
465
|
-
return `{Model:${
|
|
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
|