edinburgh 0.4.6 → 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.
- package/README.md +403 -461
- package/build/src/datapack.d.ts +9 -9
- package/build/src/datapack.js +10 -10
- package/build/src/datapack.js.map +1 -1
- package/build/src/edinburgh.d.ts +21 -10
- package/build/src/edinburgh.js +33 -55
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +99 -288
- package/build/src/indexes.js +253 -636
- package/build/src/indexes.js.map +1 -1
- package/build/src/migrate.js +17 -39
- package/build/src/migrate.js.map +1 -1
- package/build/src/models.d.ts +177 -113
- package/build/src/models.js +487 -259
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +41 -51
- package/build/src/types.js +39 -52
- 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 +1 -3
- 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 } +2 -2
- 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 } +3 -5
- package/skill/{Model_preventPersist.md → Lifecycle Hooks_preventPersist.md } +2 -2
- 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 +253 -215
- 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 +23 -0
- package/skill/deleteEverything.md +8 -0
- package/skill/field.md +4 -4
- package/skill/link.md +12 -10
- 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 +2 -2
- package/skill/transact.md +3 -3
- package/src/datapack.ts +10 -10
- package/src/edinburgh.ts +46 -58
- package/src/indexes.ts +338 -802
- package/src/migrate.ts +15 -37
- package/src/models.ts +617 -314
- package/src/types.ts +61 -54
- 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/Model.md +0 -22
- package/skill/Model_findAll.md +0 -12
- package/skill/Model_migrate.md +0 -34
- package/skill/Model_replaceInto.md +0 -16
- package/skill/PrimaryIndex.md +0 -8
- package/skill/PrimaryIndex_get.md +0 -17
- package/skill/PrimaryIndex_getLazy.md +0 -13
- package/skill/SecondaryIndex.md +0 -9
- package/skill/UniqueIndex.md +0 -9
- package/skill/UniqueIndex_get.md +0 -17
- package/skill/dump.md +0 -8
- package/skill/index.md +0 -32
- package/skill/primary.md +0 -26
- package/skill/registerModel.md +0 -26
- package/skill/unique.md +0 -32
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 } from "./indexes.js";
|
|
23
|
-
import { addErrorPath, logLevel, dbGet, hashBytes, bytesEqual } from "./utils.js";
|
|
24
14
|
/**
|
|
25
15
|
* Create a field definition for a model property.
|
|
26
16
|
*
|
|
@@ -29,16 +19,16 @@ import { addErrorPath, logLevel, dbGet, hashBytes, bytesEqual } 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
|
-
*
|
|
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
|
+
* });
|
|
42
32
|
* ```
|
|
43
33
|
*/
|
|
44
34
|
export function field(type, options = {}) {
|
|
@@ -46,84 +36,412 @@ 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
|
}
|
|
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;
|
|
57
342
|
/**
|
|
58
343
|
* Register a model class with the Edinburgh ORM system.
|
|
59
344
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* @returns The enhanced model class with ORM capabilities.
|
|
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.
|
|
63
347
|
*
|
|
64
|
-
* @
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* ```
|
|
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.
|
|
73
356
|
*/
|
|
74
|
-
export function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
357
|
+
export function defineModel(tableName, cls, opts) {
|
|
358
|
+
Object.setPrototypeOf(cls.prototype, ModelBase.prototype);
|
|
359
|
+
const MockModel = function (initial, txn = currentTxn()) {
|
|
360
|
+
this._txn = txn;
|
|
361
|
+
txn.instances.set(nextFakePkHash--, this);
|
|
362
|
+
if (initial)
|
|
363
|
+
Object.assign(this, initial);
|
|
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);
|
|
368
|
+
cls.prototype.constructor = MockModel;
|
|
369
|
+
Object.setPrototypeOf(MockModel, ModelClassRuntime.prototype);
|
|
370
|
+
MockModel.prototype = cls.prototype;
|
|
371
|
+
copyStaticMembersFromClassChain(MockModel, cls);
|
|
372
|
+
MockModel.tableName = tableName;
|
|
84
373
|
if (MockModel.tableName in modelRegistry) {
|
|
85
|
-
if (!
|
|
374
|
+
if (!opts?.override) {
|
|
86
375
|
throw new DatabaseError(`Model with table name '${MockModel.tableName}' already registered`, 'INIT_ERROR');
|
|
87
376
|
}
|
|
88
377
|
delete modelRegistry[MockModel.tableName];
|
|
89
378
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
379
|
+
const instance = new cls();
|
|
380
|
+
if (!opts?.pk && !instance.id) {
|
|
381
|
+
instance.id = { type: identifier };
|
|
382
|
+
}
|
|
383
|
+
MockModel.fields = {};
|
|
384
|
+
for (const key in instance) {
|
|
385
|
+
const value = instance[key];
|
|
386
|
+
if (value && value.type instanceof TypeWrapper) {
|
|
387
|
+
MockModel.fields[key] = value;
|
|
388
|
+
const defObj = value.default === undefined ? value.type : value;
|
|
389
|
+
const def = defObj.default;
|
|
390
|
+
if (typeof def === 'function') {
|
|
391
|
+
Object.defineProperty(MockModel.prototype, key, {
|
|
392
|
+
get() {
|
|
393
|
+
return (this[key] = def.call(defObj, this));
|
|
394
|
+
},
|
|
395
|
+
set(val) {
|
|
396
|
+
Object.defineProperty(this, key, {
|
|
397
|
+
value: val,
|
|
398
|
+
configurable: true,
|
|
399
|
+
writable: true,
|
|
400
|
+
enumerable: true,
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
configurable: true,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
else if (def !== undefined) {
|
|
407
|
+
MockModel.prototype[key] = def;
|
|
408
|
+
}
|
|
106
409
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
410
|
+
}
|
|
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);
|
|
419
|
+
}
|
|
420
|
+
MockModel._secondaries = {};
|
|
421
|
+
MockModel._lazyDescriptors = {};
|
|
422
|
+
MockModel._resetDescriptors = {};
|
|
423
|
+
MockModel._freezePrimaryKeyDescriptors = {};
|
|
424
|
+
MockModel._versions = new Map();
|
|
425
|
+
if (opts?.unique) {
|
|
426
|
+
for (const [name, spec] of Object.entries(opts.unique)) {
|
|
427
|
+
MockModel._secondaries[name] = new UniqueIndex(tableName, normalizeSpec(spec), loadPrimary, queueInitialization);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (opts?.index) {
|
|
431
|
+
for (const [name, spec] of Object.entries(opts.index)) {
|
|
432
|
+
MockModel._secondaries[name] = new SecondaryIndex(tableName, normalizeSpec(spec), loadPrimary, queueInitialization);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
modelRegistry[MockModel.tableName] = MockModel;
|
|
436
|
+
pendingModelInits.add(MockModel);
|
|
117
437
|
return MockModel;
|
|
118
438
|
}
|
|
119
|
-
// Model base class and related symbols/state
|
|
120
|
-
const INIT_INSTANCE_SYMBOL = Symbol();
|
|
121
439
|
/**
|
|
122
440
|
* Base class for all database models in the Edinburgh ORM.
|
|
123
441
|
*
|
|
124
442
|
* Models represent database entities with typed fields, automatic serialization,
|
|
125
|
-
* change tracking, and relationship management.
|
|
126
|
-
*
|
|
443
|
+
* change tracking, and relationship management. Model classes are created using
|
|
444
|
+
* `E.defineModel()`.
|
|
127
445
|
*
|
|
128
446
|
* ### Schema Evolution
|
|
129
447
|
*
|
|
@@ -149,38 +467,27 @@ const INIT_INSTANCE_SYMBOL = Symbol();
|
|
|
149
467
|
*
|
|
150
468
|
* - **`static migrate(record)`**: Called when deserializing rows written with an older schema
|
|
151
469
|
* version. Receives a plain record object; mutate it in-place to match the current schema.
|
|
152
|
-
* See {@link Model.migrate}.
|
|
153
470
|
*
|
|
154
471
|
* - **`preCommit()`**: Called on each modified instance right before the transaction commits.
|
|
155
472
|
* Useful for computing derived fields, enforcing cross-field invariants, or creating related
|
|
156
|
-
* instances.
|
|
157
|
-
*
|
|
158
|
-
* @template SUB - The concrete model subclass (for proper typing).
|
|
473
|
+
* instances.
|
|
159
474
|
*
|
|
160
475
|
* @example
|
|
161
476
|
* ```typescript
|
|
162
|
-
*
|
|
163
|
-
* class User extends E.Model<User> {
|
|
164
|
-
* static pk = E.primary(User, "id");
|
|
165
|
-
*
|
|
477
|
+
* const User = E.defineModel("User", class {
|
|
166
478
|
* id = E.field(E.identifier);
|
|
167
479
|
* name = E.field(E.string);
|
|
168
480
|
* email = E.field(E.string);
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* }
|
|
481
|
+
* }, {
|
|
482
|
+
* pk: "id",
|
|
483
|
+
* unique: { email: "email" },
|
|
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>;
|
|
172
488
|
* ```
|
|
173
489
|
*/
|
|
174
|
-
export class
|
|
175
|
-
static _primary;
|
|
176
|
-
/** @internal All non-primary indexes for this model. */
|
|
177
|
-
static _secondaries;
|
|
178
|
-
/** The database table name (defaults to class name). */
|
|
179
|
-
static tableName;
|
|
180
|
-
/** When true, registerModel replaces an existing model with the same tableName. */
|
|
181
|
-
static override;
|
|
182
|
-
/** Field configuration metadata. */
|
|
183
|
-
static fields;
|
|
490
|
+
export class ModelBase {
|
|
184
491
|
/*
|
|
185
492
|
* IMPORTANT: We cannot use instance property initializers here, because we will be
|
|
186
493
|
* initializing the class through a fake constructor that will skip these. This is
|
|
@@ -190,92 +497,13 @@ export class Model {
|
|
|
190
497
|
* @internal
|
|
191
498
|
* - _oldValues===undefined: New instance, not yet saved.
|
|
192
499
|
* - _oldValues===null: Instance is to be deleted.
|
|
500
|
+
* - _oldValues===false: Instance excluded from persistence (preventPersist).
|
|
193
501
|
* - _oldValues is an object: Loaded (possibly only partial, still lazy) from disk, _oldValues contains (partial) old values
|
|
194
502
|
*/
|
|
195
503
|
_oldValues;
|
|
196
504
|
_primaryKey;
|
|
197
505
|
_primaryKeyHash;
|
|
198
506
|
_txn;
|
|
199
|
-
constructor(initial = {}) {
|
|
200
|
-
// This constructor will only be called once, from `initModels`. All other instances will
|
|
201
|
-
// be created by the 'fake' constructor. The typing for `initial` *is* important though.
|
|
202
|
-
if (initial === INIT_INSTANCE_SYMBOL)
|
|
203
|
-
return;
|
|
204
|
-
throw new DatabaseError("The model needs a @E.registerModel decorator", 'INIT_ERROR');
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Transform the model's `E.field` properties into the appropriate JavaScript properties. Normally this is done
|
|
208
|
-
* automatically when using `transact()`, but in case you need to access `Model.fields` directly before the first
|
|
209
|
-
* transaction, you can call this method manually.
|
|
210
|
-
*/
|
|
211
|
-
static initFields(reset) {
|
|
212
|
-
const MockModel = getMockModel(this);
|
|
213
|
-
if (reset) {
|
|
214
|
-
MockModel._primary._indexId = undefined;
|
|
215
|
-
MockModel._primary._versions.clear();
|
|
216
|
-
for (const sec of MockModel._secondaries || [])
|
|
217
|
-
sec._indexId = undefined;
|
|
218
|
-
}
|
|
219
|
-
if (MockModel.fields)
|
|
220
|
-
return;
|
|
221
|
-
// First-time init: gather field configs from a temporary instance of the original class.
|
|
222
|
-
const OrgModel = MockModel._original || this;
|
|
223
|
-
const instance = new OrgModel(INIT_INSTANCE_SYMBOL);
|
|
224
|
-
// If no primary key exists, create one using 'id' field
|
|
225
|
-
if (!MockModel._primary) {
|
|
226
|
-
if (!instance.id) {
|
|
227
|
-
instance.id = { type: identifier };
|
|
228
|
-
}
|
|
229
|
-
// @ts-ignore-next-line - `id` is not part of the type, but the user probably shouldn't touch it anyhow
|
|
230
|
-
new PrimaryIndex(MockModel, ['id']);
|
|
231
|
-
}
|
|
232
|
-
MockModel.fields = {};
|
|
233
|
-
for (const key in instance) {
|
|
234
|
-
const value = instance[key];
|
|
235
|
-
// Check if this property contains field metadata
|
|
236
|
-
if (value && value.type instanceof TypeWrapper) {
|
|
237
|
-
// Set the configuration on the constructor's `fields` property
|
|
238
|
-
MockModel.fields[key] = value;
|
|
239
|
-
// Set default value on the prototype
|
|
240
|
-
const defObj = value.default === undefined ? value.type : value;
|
|
241
|
-
const def = defObj.default;
|
|
242
|
-
if (typeof def === 'function') {
|
|
243
|
-
// The default is a function. We'll define a getter on the property in the model prototype,
|
|
244
|
-
// and once it is read, we'll run the function and set the value as a plain old property
|
|
245
|
-
// on the instance object.
|
|
246
|
-
Object.defineProperty(MockModel.prototype, key, {
|
|
247
|
-
get() {
|
|
248
|
-
// This will call set(), which will define the property on the instance.
|
|
249
|
-
return (this[key] = def.call(defObj, this));
|
|
250
|
-
},
|
|
251
|
-
set(val) {
|
|
252
|
-
Object.defineProperty(this, key, {
|
|
253
|
-
value: val,
|
|
254
|
-
configurable: true,
|
|
255
|
-
writable: true,
|
|
256
|
-
enumerable: true,
|
|
257
|
-
});
|
|
258
|
-
},
|
|
259
|
-
configurable: true,
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
else if (def !== undefined) {
|
|
263
|
-
MockModel.prototype[key] = def;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (logLevel >= 1) {
|
|
268
|
-
console.log(`[edinburgh] Registered model ${MockModel.tableName} with fields: ${Object.keys(MockModel.fields).join(' ')}`);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
static async _loadCreateIndexes() {
|
|
272
|
-
const MockModel = getMockModel(this);
|
|
273
|
-
// Always run index inits (idempotent, skip if already initialized)
|
|
274
|
-
await MockModel._primary._delayedInit();
|
|
275
|
-
for (const sec of MockModel._secondaries || [])
|
|
276
|
-
await sec._delayedInit();
|
|
277
|
-
await MockModel._primary._initVersioning();
|
|
278
|
-
}
|
|
279
507
|
_setLoadedField(fieldName, value) {
|
|
280
508
|
const oldValues = this._oldValues;
|
|
281
509
|
if (oldValues.hasOwnProperty(fieldName))
|
|
@@ -290,13 +518,25 @@ export class Model {
|
|
|
290
518
|
oldValues[fieldName] = value;
|
|
291
519
|
}
|
|
292
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
|
+
}
|
|
293
531
|
/**
|
|
294
532
|
* @returns The primary key for this instance.
|
|
295
533
|
*/
|
|
296
534
|
getPrimaryKey() {
|
|
297
535
|
let key = this._primaryKey;
|
|
298
536
|
if (key === undefined) {
|
|
299
|
-
|
|
537
|
+
if (this._oldValues === false)
|
|
538
|
+
throw new DatabaseError("Operation not allowed after preventPersist()", "NO_PERSIST");
|
|
539
|
+
key = this.constructor._serializePK(this).toUint8Array();
|
|
300
540
|
this._setPrimaryKey(key);
|
|
301
541
|
}
|
|
302
542
|
return key;
|
|
@@ -304,7 +544,7 @@ export class Model {
|
|
|
304
544
|
_setPrimaryKey(key, hash) {
|
|
305
545
|
this._primaryKey = key;
|
|
306
546
|
this._primaryKeyHash = hash ?? hashBytes(key);
|
|
307
|
-
Object.defineProperties(this, this.constructor.
|
|
547
|
+
Object.defineProperties(this, this.constructor._freezePrimaryKeyDescriptors);
|
|
308
548
|
}
|
|
309
549
|
/**
|
|
310
550
|
* @returns A 53-bit positive integer non-cryptographic hash of the primary key, or undefined if not yet saved.
|
|
@@ -315,19 +555,21 @@ export class Model {
|
|
|
315
555
|
return this._primaryKeyHash;
|
|
316
556
|
}
|
|
317
557
|
isLazyField(field) {
|
|
318
|
-
const
|
|
319
|
-
return !!(
|
|
558
|
+
const oldValues = this._oldValues;
|
|
559
|
+
return !!(oldValues && oldValues !== null && field in this.constructor._lazyDescriptors && !oldValues.hasOwnProperty(field));
|
|
320
560
|
}
|
|
321
561
|
_write(txn) {
|
|
322
562
|
const oldValues = this._oldValues;
|
|
563
|
+
if (oldValues === false)
|
|
564
|
+
return; // preventPersist() was called
|
|
323
565
|
if (oldValues === null) { // Delete instance
|
|
324
566
|
const pk = this._primaryKey;
|
|
325
567
|
// Temporarily restore _oldValues so computed indexes can trigger lazy loads
|
|
326
568
|
this._oldValues = {};
|
|
327
|
-
for (const index of this.constructor._secondaries ||
|
|
569
|
+
for (const index of Object.values(this.constructor._secondaries || {})) {
|
|
328
570
|
index._delete(txn, pk, this);
|
|
329
571
|
}
|
|
330
|
-
this.constructor.
|
|
572
|
+
this.constructor._deletePK(txn, pk, this);
|
|
331
573
|
return "deleted";
|
|
332
574
|
}
|
|
333
575
|
if (oldValues === undefined) { // Create instance
|
|
@@ -338,9 +580,9 @@ export class Model {
|
|
|
338
580
|
throw new DatabaseError("Unique constraint violation", "UNIQUE_CONSTRAINT");
|
|
339
581
|
}
|
|
340
582
|
// Insert the primary index
|
|
341
|
-
this.constructor.
|
|
583
|
+
this.constructor._writePK(txn, pk, this);
|
|
342
584
|
// Insert all secondaries
|
|
343
|
-
for (const index of this.constructor._secondaries ||
|
|
585
|
+
for (const index of Object.values(this.constructor._secondaries || {})) {
|
|
344
586
|
index._write(txn, pk, this);
|
|
345
587
|
}
|
|
346
588
|
return "created";
|
|
@@ -349,8 +591,9 @@ export class Model {
|
|
|
349
591
|
// We're doing an update. Note that we may still be in a lazy state, and we don't want to load
|
|
350
592
|
// the whole object just to see if something changed.
|
|
351
593
|
// Add old values of changed fields to 'changed'.
|
|
352
|
-
const
|
|
353
|
-
|
|
594
|
+
const changed = {};
|
|
595
|
+
const cls = this.constructor;
|
|
596
|
+
const fields = cls.fields;
|
|
354
597
|
for (const fieldName in oldValues) {
|
|
355
598
|
const oldValue = oldValues[fieldName];
|
|
356
599
|
const newValue = this[fieldName];
|
|
@@ -361,7 +604,7 @@ export class Model {
|
|
|
361
604
|
if (isObjectEmpty(changed))
|
|
362
605
|
return; // No changes, nothing to do
|
|
363
606
|
// Make sure primary has not been changed
|
|
364
|
-
for (const field of
|
|
607
|
+
for (const field of cls._indexFields.keys()) {
|
|
365
608
|
if (changed.hasOwnProperty(field)) {
|
|
366
609
|
throw new DatabaseError(`Cannot modify primary key field: ${field}`, "CHANGE_PRIMARY");
|
|
367
610
|
}
|
|
@@ -371,27 +614,10 @@ export class Model {
|
|
|
371
614
|
this.validate(true);
|
|
372
615
|
// Update the primary index
|
|
373
616
|
const pk = this._primaryKey;
|
|
374
|
-
|
|
617
|
+
cls._writePK(txn, pk, this);
|
|
375
618
|
// Update any secondaries with changed fields
|
|
376
|
-
for (const index of
|
|
377
|
-
|
|
378
|
-
// Computed indexes may depend on any field — compare serialized keys
|
|
379
|
-
const oldKeyBytes = index._serializeKeyFields(oldValues).toUint8Array();
|
|
380
|
-
const newKeyBytes = index._serializeKeyFields(this).toUint8Array();
|
|
381
|
-
if (!bytesEqual(oldKeyBytes, newKeyBytes)) {
|
|
382
|
-
index._delete(txn, pk, oldValues);
|
|
383
|
-
index._write(txn, pk, this);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
for (const field of index._fieldTypes.keys()) {
|
|
388
|
-
if (changed.hasOwnProperty(field)) {
|
|
389
|
-
index._delete(txn, pk, oldValues);
|
|
390
|
-
index._write(txn, pk, this);
|
|
391
|
-
break;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
619
|
+
for (const index of Object.values(cls._secondaries || {})) {
|
|
620
|
+
index._update(txn, pk, this, oldValues);
|
|
395
621
|
}
|
|
396
622
|
return changed;
|
|
397
623
|
}
|
|
@@ -402,56 +628,20 @@ export class Model {
|
|
|
402
628
|
*
|
|
403
629
|
* @example
|
|
404
630
|
* ```typescript
|
|
405
|
-
* const user = User.
|
|
631
|
+
* const user = User.get("user123");
|
|
406
632
|
* user.name = "New Name";
|
|
407
633
|
* user.preventPersist(); // Changes won't be saved
|
|
408
634
|
* ```
|
|
409
635
|
*/
|
|
410
636
|
preventPersist() {
|
|
411
|
-
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;
|
|
412
641
|
// Have access to '_txn' throw a descriptive error:
|
|
413
642
|
Object.defineProperty(this, "_txn", PREVENT_PERSIST_DESCRIPTOR);
|
|
414
643
|
return this;
|
|
415
644
|
}
|
|
416
|
-
/**
|
|
417
|
-
* Find all instances of this model in the database, ordered by primary key.
|
|
418
|
-
* @param opts - Optional parameters.
|
|
419
|
-
* @param opts.reverse - If true, iterate in reverse order.
|
|
420
|
-
* @returns An iterator.
|
|
421
|
-
*/
|
|
422
|
-
static findAll(opts) {
|
|
423
|
-
return this._primary.find(opts);
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Load an existing instance by primary key and update it, or create a new one.
|
|
427
|
-
*
|
|
428
|
-
* The provided object must contain all primary key fields. If a matching row exists,
|
|
429
|
-
* the remaining properties from `obj` are set on the loaded instance. Otherwise a
|
|
430
|
-
* new instance is created with `obj` as its initial properties.
|
|
431
|
-
*
|
|
432
|
-
* @param obj - Partial model data that **must** include every primary key field.
|
|
433
|
-
* @returns The loaded-and-updated or newly created instance.
|
|
434
|
-
*/
|
|
435
|
-
static replaceInto(obj) {
|
|
436
|
-
const pk = this._primary;
|
|
437
|
-
const keyArgs = [];
|
|
438
|
-
for (const fieldName of pk._fieldTypes.keys()) {
|
|
439
|
-
if (!(fieldName in obj)) {
|
|
440
|
-
throw new DatabaseError(`replaceInto: missing primary key field '${fieldName}'`, "MISSING_PRIMARY_KEY");
|
|
441
|
-
}
|
|
442
|
-
keyArgs.push(obj[fieldName]);
|
|
443
|
-
}
|
|
444
|
-
const existing = pk.get(...keyArgs);
|
|
445
|
-
if (existing) {
|
|
446
|
-
for (const key in obj) {
|
|
447
|
-
if (!pk._fieldTypes.has(key)) {
|
|
448
|
-
existing[key] = obj[key];
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
return existing;
|
|
452
|
-
}
|
|
453
|
-
return new this(obj);
|
|
454
|
-
}
|
|
455
645
|
/**
|
|
456
646
|
* Delete this model instance from the database.
|
|
457
647
|
*
|
|
@@ -459,18 +649,18 @@ export class Model {
|
|
|
459
649
|
*
|
|
460
650
|
* @example
|
|
461
651
|
* ```typescript
|
|
462
|
-
* const user = User.
|
|
652
|
+
* const user = User.get("user123");
|
|
463
653
|
* user.delete(); // Removes from database
|
|
464
654
|
* ```
|
|
465
655
|
*/
|
|
466
656
|
delete() {
|
|
467
657
|
if (this._oldValues === undefined)
|
|
468
|
-
throw new DatabaseError("Cannot delete unsaved instance", "
|
|
658
|
+
throw new DatabaseError("Cannot delete unsaved instance", "INVALID");
|
|
469
659
|
this._oldValues = null;
|
|
470
660
|
}
|
|
471
661
|
/**
|
|
472
662
|
* Validate all fields in this model instance.
|
|
473
|
-
* @param raise
|
|
663
|
+
* @param raise If true, throw on first validation error.
|
|
474
664
|
* @returns Array of validation errors (empty if valid).
|
|
475
665
|
*
|
|
476
666
|
* @example
|
|
@@ -484,10 +674,11 @@ export class Model {
|
|
|
484
674
|
*/
|
|
485
675
|
validate(raise = false) {
|
|
486
676
|
const errors = [];
|
|
487
|
-
|
|
677
|
+
const cls = this.constructor;
|
|
678
|
+
for (const [key, fieldConfig] of Object.entries(cls.fields)) {
|
|
488
679
|
let e = fieldConfig.type.getError(this[key]);
|
|
489
680
|
if (e) {
|
|
490
|
-
e = addErrorPath(e,
|
|
681
|
+
e = addErrorPath(e, cls.tableName + "." + key);
|
|
491
682
|
if (raise)
|
|
492
683
|
throw e;
|
|
493
684
|
errors.push(e);
|
|
@@ -513,7 +704,7 @@ export class Model {
|
|
|
513
704
|
return "deleted";
|
|
514
705
|
if (this._oldValues === undefined)
|
|
515
706
|
return "created";
|
|
516
|
-
for (const [key, descr] of Object.entries(this.constructor.
|
|
707
|
+
for (const [key, descr] of Object.entries(this.constructor._lazyDescriptors)) {
|
|
517
708
|
if (descr && 'get' in descr && descr.get === Reflect.getOwnPropertyDescriptor(this, key)?.get) {
|
|
518
709
|
return "lazy";
|
|
519
710
|
}
|
|
@@ -521,12 +712,49 @@ export class Model {
|
|
|
521
712
|
return "loaded";
|
|
522
713
|
}
|
|
523
714
|
toString() {
|
|
524
|
-
const
|
|
525
|
-
const pk =
|
|
526
|
-
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}}`;
|
|
527
718
|
}
|
|
528
719
|
[Symbol.for('nodejs.util.inspect.custom')]() {
|
|
529
720
|
return this.toString();
|
|
530
721
|
}
|
|
531
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;
|
|
532
760
|
//# sourceMappingURL=models.js.map
|