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.
- package/README.md +322 -262
- package/build/src/datapack.d.ts +9 -9
- package/build/src/datapack.js +9 -9
- package/build/src/edinburgh.d.ts +18 -7
- package/build/src/edinburgh.js +30 -51
- 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 +1 -1
- 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 +2 -2
- package/skill/transact.md +1 -1
- package/src/datapack.ts +9 -9
- package/src/edinburgh.ts +43 -52
- 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/src/models.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
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 {
|
|
3
|
+
import DataPack from "./datapack.js";
|
|
4
|
+
import { deserializeType, serializeType, TypeWrapper, identifier } from "./types.js";
|
|
5
|
+
import { transact, currentTxn, type Transaction } from "./edinburgh.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
import { PrimaryKey, NonPrimaryIndex, IndexRangeIterator, UniqueIndex, SecondaryIndex, FindOptions, VersionInfo } from "./indexes.js";
|
|
8
|
+
import { addErrorPath, dbGet, hashBytes, hashFunction } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
let nextFakePkHash = -1;
|
|
7
11
|
|
|
8
12
|
|
|
9
13
|
const PREVENT_PERSIST_DESCRIPTOR = {
|
|
@@ -12,25 +16,6 @@ const PREVENT_PERSIST_DESCRIPTOR = {
|
|
|
12
16
|
},
|
|
13
17
|
};
|
|
14
18
|
|
|
15
|
-
/**
|
|
16
|
-
* Returns the current transaction from AsyncLocalStorage.
|
|
17
|
-
* Throws if called outside a transact() callback.
|
|
18
|
-
* @internal
|
|
19
|
-
*/
|
|
20
|
-
export function currentTxn(): Transaction {
|
|
21
|
-
const txn = txnStorage.getStore();
|
|
22
|
-
if (!txn) throw new DatabaseError("No active transaction. Operations must be performed within a transact() callback.", 'NO_TRANSACTION');
|
|
23
|
-
return txn;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface Transaction {
|
|
27
|
-
id: number;
|
|
28
|
-
instances: Set<Model<unknown>>;
|
|
29
|
-
instancesByPk: Map<number, Model<unknown>>;
|
|
30
|
-
}
|
|
31
|
-
import { BaseIndex, NonPrimaryIndex, PrimaryIndex, IndexRangeIterator, UniqueIndex, SecondaryIndex, FindOptions } from "./indexes.js";
|
|
32
|
-
import { addErrorPath, logLevel, assert, dbGet, hashBytes } from "./utils.js";
|
|
33
|
-
|
|
34
19
|
/**
|
|
35
20
|
* Configuration interface for model fields.
|
|
36
21
|
* @template T - The field type.
|
|
@@ -52,13 +37,13 @@ export interface FieldConfig<T> {
|
|
|
52
37
|
* This allows for both runtime introspection and compile-time type safety.
|
|
53
38
|
*
|
|
54
39
|
* @template T - The field type.
|
|
55
|
-
* @param type
|
|
56
|
-
* @param options
|
|
40
|
+
* @param type The type wrapper for this field.
|
|
41
|
+
* @param options Additional field configuration options.
|
|
57
42
|
* @returns The field value (typed as T, but actually returns FieldConfig<T>).
|
|
58
43
|
*
|
|
59
44
|
* @example
|
|
60
45
|
* ```typescript
|
|
61
|
-
* const User = E.defineModel(class {
|
|
46
|
+
* const User = E.defineModel("User", class {
|
|
62
47
|
* name = E.field(E.string, {description: "User's full name"});
|
|
63
48
|
* age = E.field(E.opt(E.number), {description: "User's age", default: 25});
|
|
64
49
|
* });
|
|
@@ -70,11 +55,8 @@ export function field<T>(type: TypeWrapper<T>, options: Partial<FieldConfig<T>>
|
|
|
70
55
|
return options as any;
|
|
71
56
|
}
|
|
72
57
|
|
|
73
|
-
// Model registration and initialization
|
|
74
|
-
export const modelRegistry: Record<string, typeof Model> = {};
|
|
75
|
-
|
|
76
58
|
function isObjectEmpty(obj: object) {
|
|
77
|
-
for (
|
|
59
|
+
for (const _ of Object.keys(obj)) {
|
|
78
60
|
return false;
|
|
79
61
|
}
|
|
80
62
|
return true;
|
|
@@ -82,10 +64,7 @@ function isObjectEmpty(obj: object) {
|
|
|
82
64
|
|
|
83
65
|
export type Change = Record<any, any> | "created" | "deleted";
|
|
84
66
|
|
|
85
|
-
let autoTableNameId = 0;
|
|
86
|
-
|
|
87
67
|
type FieldsOf<T> = T extends new () => infer I ? I : never;
|
|
88
|
-
type ModelInstance<FIELDS> = FIELDS & Model<FIELDS>;
|
|
89
68
|
|
|
90
69
|
type PKArgs<FIELDS, PK> =
|
|
91
70
|
PK extends readonly (keyof FIELDS & string)[]
|
|
@@ -94,36 +73,389 @@ type PKArgs<FIELDS, PK> =
|
|
|
94
73
|
? [FIELDS[PK]]
|
|
95
74
|
: [string];
|
|
96
75
|
|
|
97
|
-
type
|
|
98
|
-
SPEC extends readonly string[]
|
|
99
|
-
|
|
76
|
+
type IndexArgs<FIELDS, SPEC> =
|
|
77
|
+
SPEC extends readonly (keyof FIELDS & string)[]
|
|
78
|
+
? { [I in keyof SPEC]: SPEC[I] extends keyof FIELDS ? FIELDS[SPEC[I]] : never }
|
|
79
|
+
: SPEC extends keyof FIELDS & string
|
|
80
|
+
? [FIELDS[SPEC]]
|
|
100
81
|
: SPEC extends (instance: any) => infer R
|
|
101
|
-
? R extends (infer V)[] ?
|
|
102
|
-
: UniqueIndex<any, [], [R]>
|
|
82
|
+
? R extends (infer V)[] ? [V] : [R]
|
|
103
83
|
: never;
|
|
104
84
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
85
|
+
/**
|
|
86
|
+
* A model constructor with its generic information erased.
|
|
87
|
+
*
|
|
88
|
+
* Useful when accepting or storing arbitrary registered model classes.
|
|
89
|
+
*/
|
|
90
|
+
export type AnyModelClass = ModelClass<new () => any, readonly any[], any, any>;
|
|
91
|
+
|
|
92
|
+
type SecondaryRegistry<FIELDS> = Record<string, NonPrimaryIndex<Model<FIELDS>, readonly (keyof FIELDS & string)[], readonly any[]>>;
|
|
93
|
+
|
|
94
|
+
function copyStaticMembersFromClassChain(target: object, source: Function) {
|
|
95
|
+
for (let current: any = source; current && current !== Function.prototype; current = Object.getPrototypeOf(current)) {
|
|
96
|
+
for (const key of Object.getOwnPropertyNames(current)) {
|
|
97
|
+
if (key === 'length' || key === 'name' || key === 'prototype') continue;
|
|
98
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) continue;
|
|
99
|
+
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(current, key)!);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Model registration and initialization
|
|
105
|
+
export const modelRegistry: Record<string, AnyModelClass> = {};
|
|
106
|
+
export const pendingModelInits = new Set<AnyModelClass>();
|
|
107
|
+
|
|
108
|
+
// These static members are attached dynamically in defineModel(), so 'declare' tells TypeScript
|
|
109
|
+
// they exist at runtime without emitting duplicate class fields that would shadow those assignments.
|
|
110
|
+
class ModelClassRuntime<FIELDS, PKA extends readonly any[], UNIQUE = {}, INDEX = {}> extends PrimaryKey<Model<FIELDS>, readonly (keyof FIELDS & string)[], PKA> {
|
|
111
|
+
// Runtime table identifier used for index naming and diagnostics.
|
|
112
|
+
declare tableName: string;
|
|
113
|
+
// Field schema map used for validation and serialization.
|
|
114
|
+
declare fields: Record<string | symbol | number, FieldConfig<unknown>>;
|
|
115
|
+
// Registered unique/secondary indexes for this model.
|
|
116
|
+
declare _secondaries?: SecondaryRegistry<FIELDS>;
|
|
117
|
+
// Cached list of non-primary fields used for value serialization.
|
|
118
|
+
_nonKeyFields!: (keyof FIELDS & string)[];
|
|
119
|
+
// Lazy getter/setter descriptors installed on unloaded non-key fields.
|
|
120
|
+
_lazyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
121
|
+
// Writable descriptors temporarily installed before hydrating value fields.
|
|
122
|
+
_resetDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
123
|
+
// Frozen descriptors applied to primary-key fields after key materialization.
|
|
124
|
+
_freezePrimaryKeyDescriptors: Record<string | symbol | number, PropertyDescriptor> = {};
|
|
125
|
+
// Active schema version number for value encoding.
|
|
126
|
+
_currentVersion!: number;
|
|
127
|
+
// Hash of the active migrate() function for schema identity.
|
|
128
|
+
_currentMigrateHash!: number;
|
|
129
|
+
// Cached historical schema metadata for lazy migration of old rows.
|
|
130
|
+
_versions: Map<number, VersionInfo> = new Map();
|
|
131
|
+
|
|
132
|
+
_serializeVersionValue(): Uint8Array {
|
|
133
|
+
const fields: [string, Uint8Array][] = [];
|
|
134
|
+
for (const fieldName of this._nonKeyFields) {
|
|
135
|
+
const tp = new DataPack();
|
|
136
|
+
serializeType(this.fields[fieldName].type, tp);
|
|
137
|
+
fields.push([fieldName, tp.toUint8Array()]);
|
|
138
|
+
}
|
|
139
|
+
return new DataPack().write({
|
|
140
|
+
migrateHash: this._currentMigrateHash,
|
|
141
|
+
fields,
|
|
142
|
+
secondaryKeys: new Set(Object.values(this._secondaries || {}).map(sec => sec._signature!)),
|
|
143
|
+
}).toUint8Array();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async _initialize(reset = false): Promise<void> {
|
|
147
|
+
const allFieldTypes = new Map<string, TypeWrapper<any>>();
|
|
148
|
+
for (const [fieldName, fieldConfig] of Object.entries(this.fields)) {
|
|
149
|
+
allFieldTypes.set(fieldName, fieldConfig.type);
|
|
150
|
+
}
|
|
151
|
+
await super._initializeIndex(allFieldTypes, reset);
|
|
152
|
+
|
|
153
|
+
if (reset || this._nonKeyFields === undefined) {
|
|
154
|
+
this._nonKeyFields = Object.keys(this.fields).filter(fieldName => !this._indexFields.has(fieldName as any)) as any;
|
|
155
|
+
this._lazyDescriptors = {};
|
|
156
|
+
this._resetDescriptors = {};
|
|
157
|
+
this._freezePrimaryKeyDescriptors = {};
|
|
158
|
+
|
|
159
|
+
for (const fieldName of this._nonKeyFields) {
|
|
160
|
+
this._lazyDescriptors[fieldName] = {
|
|
161
|
+
configurable: true,
|
|
162
|
+
enumerable: true,
|
|
163
|
+
get(this: Model<FIELDS>) {
|
|
164
|
+
this.constructor._lazyLoad(this);
|
|
165
|
+
return this[fieldName];
|
|
166
|
+
},
|
|
167
|
+
set(this: Model<FIELDS>, value: any) {
|
|
168
|
+
this.constructor._lazyLoad(this);
|
|
169
|
+
this[fieldName] = value;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
this._resetDescriptors[fieldName] = {
|
|
173
|
+
writable: true,
|
|
174
|
+
enumerable: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const fieldName of this._indexFields.keys()) {
|
|
179
|
+
this._freezePrimaryKeyDescriptors[fieldName] = {
|
|
180
|
+
writable: false,
|
|
181
|
+
enumerable: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const sec of Object.values(this._secondaries || {})) {
|
|
187
|
+
await sec._initializeIndex(allFieldTypes, reset, this._indexFields);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const migrateFn = (this as any).migrate;
|
|
191
|
+
this._currentMigrateHash = migrateFn ? hashFunction(migrateFn) : 0;
|
|
192
|
+
|
|
193
|
+
const currentValueBytes = this._serializeVersionValue();
|
|
194
|
+
this._currentVersion = (await this._ensureVersionEntry(currentValueBytes)).version;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_getSecondary(name: string) {
|
|
198
|
+
const index = this._secondaries?.[name];
|
|
199
|
+
if (!index) throw new DatabaseError(`Unknown index '${name}' on model '${this.tableName}'`, 'INIT_ERROR');
|
|
200
|
+
return index;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_get(txn: Transaction, args: PKA | Uint8Array, loadNow: false | Uint8Array): Model<FIELDS>;
|
|
204
|
+
_get(txn: Transaction, args: PKA | Uint8Array, loadNow: true): Model<FIELDS> | undefined;
|
|
205
|
+
_get(txn: Transaction, args: PKA | Uint8Array, loadNow: boolean | Uint8Array): Model<FIELDS> | undefined {
|
|
206
|
+
let key: Uint8Array;
|
|
207
|
+
let keyParts: readonly any[] | undefined;
|
|
208
|
+
if (args instanceof Uint8Array) {
|
|
209
|
+
key = args;
|
|
210
|
+
} else {
|
|
211
|
+
key = this._argsToKeyBytes(args, false).toUint8Array();
|
|
212
|
+
keyParts = args;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const keyHash = hashBytes(key);
|
|
216
|
+
const cached = txn.instances.get(keyHash) as Model<FIELDS> | undefined;
|
|
217
|
+
if (cached) {
|
|
218
|
+
if (loadNow && loadNow !== true) {
|
|
219
|
+
Object.defineProperties(cached, this._resetDescriptors);
|
|
220
|
+
this._loadValueFields(cached, loadNow);
|
|
221
|
+
}
|
|
222
|
+
return cached;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let valueBuffer: Uint8Array | undefined;
|
|
226
|
+
if (loadNow) {
|
|
227
|
+
if (loadNow === true) {
|
|
228
|
+
valueBuffer = dbGet(txn.id, key);
|
|
229
|
+
if (!valueBuffer) return;
|
|
230
|
+
} else {
|
|
231
|
+
valueBuffer = loadNow;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const model = Object.create((this as any).prototype) as Model<FIELDS>;
|
|
236
|
+
model._txn = txn;
|
|
237
|
+
model._oldValues = {};
|
|
238
|
+
txn.instances.set(keyHash, model);
|
|
239
|
+
|
|
240
|
+
if (keyParts) {
|
|
241
|
+
let i = 0;
|
|
242
|
+
for (const fieldName of this._indexFields.keys()) {
|
|
243
|
+
model._setLoadedField(fieldName, keyParts[i++]);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
const keyPack = new DataPack(key);
|
|
247
|
+
keyPack.readNumber();
|
|
248
|
+
for (const [fieldName, fieldType] of this._indexFields.entries()) {
|
|
249
|
+
model._setLoadedField(fieldName, fieldType.deserialize(keyPack));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
model._setPrimaryKey(key, keyHash);
|
|
254
|
+
if (valueBuffer) {
|
|
255
|
+
this._loadValueFields(model, valueBuffer);
|
|
256
|
+
} else {
|
|
257
|
+
Object.defineProperties(model, this._lazyDescriptors);
|
|
258
|
+
}
|
|
259
|
+
return model;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_lazyLoad(model: Model<FIELDS>) {
|
|
263
|
+
const key = model._primaryKey!;
|
|
264
|
+
const valueBuffer = dbGet(model._txn.id, key);
|
|
265
|
+
if (!valueBuffer) throw new DatabaseError(`Lazy-loaded ${this.tableName}#${key} does not exist`, 'LAZY_FAIL');
|
|
266
|
+
Object.defineProperties(model, this._resetDescriptors);
|
|
267
|
+
this._loadValueFields(model, valueBuffer);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Load a model by primary key inside the current transaction.
|
|
272
|
+
*
|
|
273
|
+
* @returns The matching model, or `undefined` if no row exists.
|
|
274
|
+
*/
|
|
275
|
+
get(...args: PKA): Model<FIELDS> | undefined {
|
|
276
|
+
return this._get(currentTxn(), args, true);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Load a model by primary key without fetching its non-key fields immediately.
|
|
281
|
+
*
|
|
282
|
+
* Accessing a lazy field later will load the remaining fields transparently.
|
|
283
|
+
*/
|
|
284
|
+
getLazy(...args: PKA): Model<FIELDS> {
|
|
285
|
+
return this._get(currentTxn(), args, false);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_pairToInstance(txn: Transaction, keyBuffer: ArrayBuffer, valueBuffer: ArrayBuffer): Model<FIELDS> {
|
|
289
|
+
return this._get(txn, new Uint8Array(keyBuffer), new Uint8Array(valueBuffer))!;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Load an existing instance by primary key and update it, or create a new one.
|
|
294
|
+
* If a row already exists, its non-primary-key fields are updated in place.
|
|
295
|
+
* Otherwise, a new instance is created with `obj` as its initial properties.
|
|
296
|
+
*
|
|
297
|
+
* @param obj Partial model data that **must** include every primary key field.
|
|
298
|
+
* @returns The loaded-and-updated or newly created instance.
|
|
299
|
+
*/
|
|
300
|
+
replaceInto(obj: Partial<FIELDS>): Model<FIELDS> {
|
|
301
|
+
const keyArgs: any[] = [];
|
|
302
|
+
for (const fieldName of this._indexFields.keys()) {
|
|
303
|
+
if (!(fieldName in (obj as any))) {
|
|
304
|
+
throw new DatabaseError(`replaceInto: missing primary key field '${fieldName}'`, "MISSING_PRIMARY_KEY");
|
|
305
|
+
}
|
|
306
|
+
keyArgs.push((obj as any)[fieldName]);
|
|
307
|
+
}
|
|
308
|
+
const existing = this.get(...keyArgs as any) as Model<FIELDS> | undefined;
|
|
309
|
+
if (existing) {
|
|
310
|
+
for (const key in obj as any) {
|
|
311
|
+
if (!this._indexFields.has(key as any)) {
|
|
312
|
+
(existing as any)[key] = (obj as any)[key];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return existing;
|
|
316
|
+
}
|
|
317
|
+
return new (this as any)(obj);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Look up a model through a named unique index.
|
|
322
|
+
*
|
|
323
|
+
* @param name The name from the model's `unique` definition.
|
|
324
|
+
* @param args The unique-index key values.
|
|
325
|
+
* @returns The matching model instance, if any.
|
|
326
|
+
*/
|
|
327
|
+
getBy<K extends string & keyof UNIQUE>(name: K, ...args: IndexArgs<FIELDS, UNIQUE[K]>): Model<FIELDS> | undefined {
|
|
328
|
+
return (this._getSecondary(name) as any).getPK(...args);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Query rows through a named unique or secondary index.
|
|
333
|
+
*
|
|
334
|
+
* This mirrors `find()`, but targets a named entry from the model's `unique`
|
|
335
|
+
* or `index` registration.
|
|
336
|
+
*/
|
|
337
|
+
findBy<K extends string & keyof (UNIQUE & INDEX)>(name: K, opts: FindOptions<IndexArgs<FIELDS, (UNIQUE & INDEX)[K]>, 'first'>): Model<FIELDS> | undefined;
|
|
338
|
+
findBy<K extends string & keyof (UNIQUE & INDEX)>(name: K, opts: FindOptions<IndexArgs<FIELDS, (UNIQUE & INDEX)[K]>, 'single'>): Model<FIELDS>;
|
|
339
|
+
findBy<K extends string & keyof (UNIQUE & INDEX)>(name: K, opts?: FindOptions<IndexArgs<FIELDS, (UNIQUE & INDEX)[K]>>): IndexRangeIterator<Model<FIELDS>>;
|
|
340
|
+
findBy(name: string, opts?: any): any {
|
|
341
|
+
return this._getSecondary(name).find(opts);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Process rows from a named unique or secondary index in batched transactions.
|
|
346
|
+
*
|
|
347
|
+
* Uses the same range options as `findBy()`, plus batch limits.
|
|
348
|
+
*/
|
|
349
|
+
batchProcessBy<K extends string & keyof (UNIQUE & INDEX)>(
|
|
350
|
+
name: K,
|
|
351
|
+
opts: FindOptions<IndexArgs<FIELDS, (UNIQUE & INDEX)[K]>> & { limitSeconds?: number; limitRows?: number },
|
|
352
|
+
callback: (row: Model<FIELDS>) => any,
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
return this._getSecondary(name).batchProcess(opts, callback as any);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_loadValueFields(model: Model<FIELDS>, valueArray: Uint8Array) {
|
|
358
|
+
const valuePack = new DataPack(valueArray);
|
|
359
|
+
const version = valuePack.readNumber();
|
|
360
|
+
|
|
361
|
+
if (version === this._currentVersion) {
|
|
362
|
+
for (const fieldName of this._nonKeyFields) {
|
|
363
|
+
model._setLoadedField(fieldName, this.fields[fieldName].type.deserialize(valuePack));
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
this._migrateValueFields(model, version, valuePack);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
_loadVersionInfo(txnId: number, version: number): VersionInfo {
|
|
371
|
+
let info = this._versions.get(version);
|
|
372
|
+
if (info) return info;
|
|
373
|
+
|
|
374
|
+
const key = this._versionInfoKey(version);
|
|
375
|
+
const raw = dbGet(txnId, key);
|
|
376
|
+
if (!raw) throw new DatabaseError(`Version ${version} info not found for index ${this}`, 'CONSISTENCY_ERROR');
|
|
377
|
+
|
|
378
|
+
const obj = new DataPack(raw).read() as any;
|
|
379
|
+
if (!obj || typeof obj.migrateHash !== 'number' || !Array.isArray(obj.fields) || !(obj.secondaryKeys instanceof Set)) {
|
|
380
|
+
throw new DatabaseError(`Version ${version} info is corrupted for index ${this}`, 'CONSISTENCY_ERROR');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const nonKeyFields = new Map<string, TypeWrapper<any>>();
|
|
384
|
+
for (const [name, typeBytes] of obj.fields) {
|
|
385
|
+
nonKeyFields.set(name, deserializeType(new DataPack(typeBytes), 0));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
info = { migrateHash: obj.migrateHash, nonKeyFields, secondaryKeys: obj.secondaryKeys as Set<string> };
|
|
389
|
+
this._versions.set(version, info);
|
|
390
|
+
return info;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
_migrateValueFields(model: Model<FIELDS>, version: number, valuePack: DataPack) {
|
|
394
|
+
const versionInfo = this._loadVersionInfo(model._txn.id, version);
|
|
395
|
+
const record: Record<string, any> = {};
|
|
396
|
+
for (const [name] of this._indexFields.entries()) record[name] = (model as any)[name];
|
|
397
|
+
for (const [name, type] of versionInfo.nonKeyFields.entries()) {
|
|
398
|
+
record[name] = type.deserialize(valuePack);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const migrateFn = (this as any).migrate;
|
|
402
|
+
if (migrateFn) migrateFn(record);
|
|
403
|
+
|
|
404
|
+
for (const fieldName of this._nonKeyFields) {
|
|
405
|
+
if (fieldName in record) {
|
|
406
|
+
model._setLoadedField(fieldName, record[fieldName]);
|
|
407
|
+
} else if (fieldName in model) {
|
|
408
|
+
model._setLoadedField(fieldName, (model as any)[fieldName]);
|
|
409
|
+
} else {
|
|
410
|
+
throw new DatabaseError(`Field ${fieldName} is missing in migrated data for ${model}`, 'MIGRATION_ERROR');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_serializeValue(data: Record<string, any>): Uint8Array {
|
|
416
|
+
const valueBytes = new DataPack();
|
|
417
|
+
valueBytes.write(this._currentVersion);
|
|
418
|
+
for (const fieldName of this._nonKeyFields) {
|
|
419
|
+
const fieldConfig = this.fields[fieldName] as FieldConfig<unknown>;
|
|
420
|
+
fieldConfig.type.serialize(data[fieldName], valueBytes);
|
|
421
|
+
}
|
|
422
|
+
return valueBytes.toUint8Array();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Runtime base constructor for model classes returned by `defineModel()`.
|
|
428
|
+
*
|
|
429
|
+
* Prefer the `ModelClass` type alias for annotations and the result of
|
|
430
|
+
* `defineModel()` for concrete model classes.
|
|
431
|
+
*/
|
|
432
|
+
export const ModelClass = ModelClassRuntime;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The static side of a model class returned by `defineModel()`.
|
|
436
|
+
*
|
|
437
|
+
* Besides the class constructor itself, this includes primary-key lookup
|
|
438
|
+
* helpers like `get()` and `getLazy()`, range-query helpers like `find()`, and
|
|
439
|
+
* named-index helpers like `getBy()` and `findBy()`.
|
|
440
|
+
*
|
|
441
|
+
* @template T - The original class passed to `defineModel()`.
|
|
442
|
+
* @template PKA - Tuple of primary-key argument types.
|
|
443
|
+
* @template UNIQUE - Named unique-index specifications.
|
|
444
|
+
* @template INDEX - Named secondary-index specifications.
|
|
445
|
+
*/
|
|
446
|
+
export type ModelClass<T extends new () => any, PKA extends readonly any[], UNIQUE = {}, INDEX = {}> =
|
|
447
|
+
T
|
|
448
|
+
& ModelClassRuntime<FieldsOf<T>, PKA, UNIQUE, INDEX>
|
|
449
|
+
& {
|
|
450
|
+
new (initial?: Partial<FieldsOf<T>>, txn?: Transaction): Model<FieldsOf<T>>;
|
|
451
|
+
};
|
|
112
452
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
find(opts: FindOptions<PKA, 'first'>): ModelInstance<FIELDS> | undefined;
|
|
120
|
-
find(opts: FindOptions<PKA, 'single'>): ModelInstance<FIELDS>;
|
|
121
|
-
find(opts?: FindOptions<PKA>): IndexRangeIterator<any>;
|
|
122
|
-
batchProcess(opts?: FindOptions<PKA> & { limitSeconds?: number; limitRows?: number }, callback?: (row: ModelInstance<FIELDS>) => any): Promise<void>;
|
|
123
|
-
replaceInto(obj: Partial<FIELDS>): ModelInstance<FIELDS>;
|
|
124
|
-
} &
|
|
125
|
-
{ [K in keyof UNIQUE]: UniqueFor<UNIQUE[K]> } &
|
|
126
|
-
{ [K in keyof INDEX]: SecondaryFor<INDEX[K]> };
|
|
453
|
+
/**
|
|
454
|
+
* Minimal instance-side model shape used for typing the constructor property.
|
|
455
|
+
*/
|
|
456
|
+
export interface ModelBase {
|
|
457
|
+
constructor: AnyModelClass;
|
|
458
|
+
}
|
|
127
459
|
|
|
128
460
|
/**
|
|
129
461
|
* Register a model class with the Edinburgh ORM system.
|
|
@@ -131,13 +463,13 @@ type RegisteredModel<FIELDS, PKA extends readonly any[], UNIQUE, INDEX> = {
|
|
|
131
463
|
* Converts a plain class into a fully-featured model with database persistence,
|
|
132
464
|
* typed fields, primary key access, and optional secondary and unique indexes.
|
|
133
465
|
*
|
|
134
|
-
* @param
|
|
135
|
-
* @param
|
|
136
|
-
* @param opts
|
|
137
|
-
* @param opts.
|
|
138
|
-
* @param opts.
|
|
139
|
-
* @param opts.
|
|
140
|
-
* @param opts.override
|
|
466
|
+
* @param tableName The database table name for this model.
|
|
467
|
+
* @param cls A plain class whose properties use E.field().
|
|
468
|
+
* @param opts Registration options.
|
|
469
|
+
* @param opts.pk Primary key field name or array of field names.
|
|
470
|
+
* @param opts.unique Named unique index specifications (field name, field array, or compute function).
|
|
471
|
+
* @param opts.index Named secondary index specifications (field name, field array, or compute function).
|
|
472
|
+
* @param opts.override Replace a previous model with the same table name.
|
|
141
473
|
* @returns The enhanced model constructor.
|
|
142
474
|
*/
|
|
143
475
|
export function defineModel<
|
|
@@ -146,31 +478,27 @@ export function defineModel<
|
|
|
146
478
|
const UNIQUE extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>,
|
|
147
479
|
const INDEX extends Record<string, (keyof FieldsOf<T> & string) | readonly (keyof FieldsOf<T> & string)[] | ((instance: any) => any)>,
|
|
148
480
|
>(
|
|
481
|
+
tableName: string,
|
|
149
482
|
cls: T,
|
|
150
|
-
opts?: { pk?: PK, unique?: UNIQUE, index?: INDEX,
|
|
151
|
-
):
|
|
152
|
-
|
|
153
|
-
export function defineModel(cls: any, opts?: any): any {
|
|
154
|
-
Object.setPrototypeOf(cls.prototype, Model.prototype);
|
|
155
|
-
|
|
483
|
+
opts?: { pk?: PK, unique?: UNIQUE, index?: INDEX, override?: boolean }
|
|
484
|
+
): ModelClass<T, PKArgs<FieldsOf<T>, PK>, UNIQUE, INDEX> {
|
|
485
|
+
Object.setPrototypeOf(cls.prototype, ModelBase.prototype);
|
|
156
486
|
const MockModel = function(this: any, initial?: Record<string, any>, txn: Transaction = currentTxn()) {
|
|
157
487
|
this._txn = txn;
|
|
158
|
-
txn.instances.
|
|
488
|
+
txn.instances.set(nextFakePkHash--, this);
|
|
159
489
|
if (initial) Object.assign(this, initial);
|
|
160
490
|
} as any;
|
|
161
491
|
|
|
492
|
+
const normalizeSpec = (spec: any) => typeof spec === 'string' ? [spec] : spec;
|
|
493
|
+
const queueInitialization = () => { pendingModelInits.add(MockModel); };
|
|
494
|
+
const loadPrimary = (txn: Transaction, primaryKey: Uint8Array, loadNow: boolean | Uint8Array) => MockModel._get(txn, primaryKey, loadNow);
|
|
495
|
+
|
|
162
496
|
cls.prototype.constructor = MockModel;
|
|
163
|
-
Object.setPrototypeOf(MockModel,
|
|
497
|
+
Object.setPrototypeOf(MockModel, ModelClassRuntime.prototype);
|
|
164
498
|
MockModel.prototype = cls.prototype;
|
|
165
|
-
MockModel
|
|
499
|
+
copyStaticMembersFromClassChain(MockModel, cls);
|
|
166
500
|
|
|
167
|
-
|
|
168
|
-
if (name !== 'length' && name !== 'prototype' && name !== 'name') {
|
|
169
|
-
MockModel[name] = cls[name];
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
MockModel.tableName = opts?.tableName || cls.name || `Model${++autoTableNameId}`;
|
|
501
|
+
MockModel.tableName = tableName;
|
|
174
502
|
|
|
175
503
|
if (MockModel.tableName in modelRegistry) {
|
|
176
504
|
if (!opts?.override) {
|
|
@@ -213,38 +541,38 @@ export function defineModel(cls: any, opts?: any): any {
|
|
|
213
541
|
}
|
|
214
542
|
}
|
|
215
543
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
544
|
+
const primaryFields = opts?.pk ? (Array.isArray(opts.pk) ? opts.pk : [opts.pk]) : ['id'];
|
|
545
|
+
MockModel._indexFields = new Map();
|
|
546
|
+
for (const fieldName of primaryFields) {
|
|
547
|
+
const fieldConfig = MockModel.fields[fieldName];
|
|
548
|
+
if (!fieldConfig) {
|
|
549
|
+
throw new DatabaseError(`Unknown primary key field '${fieldName}' on model '${tableName}'`, 'INIT_ERROR');
|
|
550
|
+
}
|
|
551
|
+
MockModel._indexFields.set(fieldName, fieldConfig.type);
|
|
220
552
|
}
|
|
221
553
|
|
|
222
|
-
|
|
554
|
+
MockModel._secondaries = {};
|
|
555
|
+
MockModel._lazyDescriptors = {};
|
|
556
|
+
MockModel._resetDescriptors = {};
|
|
557
|
+
MockModel._freezePrimaryKeyDescriptors = {};
|
|
558
|
+
MockModel._versions = new Map();
|
|
223
559
|
|
|
224
560
|
if (opts?.unique) {
|
|
225
561
|
for (const [name, spec] of Object.entries<any>(opts.unique)) {
|
|
226
|
-
MockModel[name] = new UniqueIndex(
|
|
562
|
+
MockModel._secondaries[name] = new UniqueIndex(tableName, normalizeSpec(spec), loadPrimary, queueInitialization);
|
|
227
563
|
}
|
|
228
564
|
}
|
|
229
565
|
if (opts?.index) {
|
|
230
566
|
for (const [name, spec] of Object.entries<any>(opts.index)) {
|
|
231
|
-
MockModel[name] = new SecondaryIndex(
|
|
567
|
+
MockModel._secondaries[name] = new SecondaryIndex(tableName, normalizeSpec(spec), loadPrimary, queueInitialization);
|
|
232
568
|
}
|
|
233
569
|
}
|
|
234
570
|
|
|
235
571
|
modelRegistry[MockModel.tableName] = MockModel;
|
|
236
|
-
|
|
572
|
+
pendingModelInits.add(MockModel);
|
|
237
573
|
return MockModel;
|
|
238
574
|
}
|
|
239
575
|
|
|
240
|
-
/**
|
|
241
|
-
* Model interface that ensures proper typing for the constructor property.
|
|
242
|
-
* @template SUB - The concrete model subclass.
|
|
243
|
-
*/
|
|
244
|
-
export interface Model<SUB> {
|
|
245
|
-
constructor: typeof Model<SUB>;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
576
|
/**
|
|
249
577
|
* Base class for all database models in the Edinburgh ORM.
|
|
250
578
|
*
|
|
@@ -276,73 +604,44 @@ export interface Model<SUB> {
|
|
|
276
604
|
*
|
|
277
605
|
* - **`static migrate(record)`**: Called when deserializing rows written with an older schema
|
|
278
606
|
* version. Receives a plain record object; mutate it in-place to match the current schema.
|
|
279
|
-
* See {@link Model.migrate}.
|
|
280
607
|
*
|
|
281
608
|
* - **`preCommit()`**: Called on each modified instance right before the transaction commits.
|
|
282
609
|
* Useful for computing derived fields, enforcing cross-field invariants, or creating related
|
|
283
|
-
* instances.
|
|
610
|
+
* instances.
|
|
284
611
|
*
|
|
285
|
-
* @template SUB - The concrete model subclass (for proper typing).
|
|
286
|
-
*
|
|
287
612
|
* @example
|
|
288
613
|
* ```typescript
|
|
289
|
-
* const User = E.defineModel(class {
|
|
614
|
+
* const User = E.defineModel("User", class {
|
|
290
615
|
* id = E.field(E.identifier);
|
|
291
616
|
* name = E.field(E.string);
|
|
292
617
|
* email = E.field(E.string);
|
|
293
618
|
* }, {
|
|
294
619
|
* pk: "id",
|
|
295
|
-
* unique: {
|
|
620
|
+
* unique: { email: "email" },
|
|
296
621
|
* });
|
|
622
|
+
* // Optional: declare a companion type so `let u: User` works.
|
|
623
|
+
* // Not needed if you only use `new User()`, `User.find()`, etc.
|
|
624
|
+
* type User = InstanceType<typeof User>;
|
|
297
625
|
* ```
|
|
298
626
|
*/
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
export abstract class Model<SUB> {
|
|
302
|
-
static _primary: PrimaryIndex<any, any>;
|
|
303
|
-
|
|
304
|
-
/** @internal All non-primary indexes for this model. */
|
|
305
|
-
static _secondaries?: NonPrimaryIndex<any, readonly (keyof any & string)[]>[];
|
|
306
|
-
|
|
307
|
-
/** The database table name (defaults to class name). */
|
|
308
|
-
static tableName: string;
|
|
309
|
-
|
|
310
|
-
/** When true, defineModel replaces an existing model with the same tableName. */
|
|
311
|
-
static override?: boolean;
|
|
312
|
-
|
|
313
|
-
/** Field configuration metadata. */
|
|
314
|
-
static fields: Record<string | symbol | number, FieldConfig<unknown>>;
|
|
315
|
-
|
|
316
|
-
// Alias statics that delegate to _primary, used by migrate.ts
|
|
317
|
-
static get _indexId() { return this._primary?._indexId; }
|
|
318
|
-
static get _currentVersion() { return this._primary._currentVersion; }
|
|
319
|
-
static get _pkFieldTypes() { return this._primary._fieldTypes; }
|
|
320
|
-
static _loadVersionInfo(txnId: number, version: number) { return this._primary._loadVersionInfo(txnId, version); }
|
|
321
|
-
static _writePrimary(txn: Transaction, pk: Uint8Array, data: Record<string, any>) { this._primary._write(txn, pk, data as any); }
|
|
322
|
-
|
|
627
|
+
export abstract class ModelBase {
|
|
323
628
|
/**
|
|
324
629
|
* Optional migration function called when deserializing rows written with an older schema version.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
* This is called both during lazy loading (when a row is read from disk) and during batch
|
|
329
|
-
* migration (via `runMigration()` / `npx migrate-edinburgh`). The function's source code is hashed
|
|
330
|
-
* to detect changes. Modifying `migrate()` triggers a new schema version.
|
|
630
|
+
* Receives a plain record with all fields and should mutate it in-place to match the current schema.
|
|
631
|
+
* It runs during lazy loading and during `runMigration()`. Changing this method creates a new schema version.
|
|
632
|
+
* If it updates values used by secondary or unique indexes, those index entries are refreshed only by `runMigration()`.
|
|
331
633
|
*
|
|
332
|
-
*
|
|
333
|
-
* will only be updated when `runMigration()` is run (not during lazy loading).
|
|
334
|
-
*
|
|
335
|
-
* @param record - A plain object with all field values from the old schema version.
|
|
634
|
+
* @param record A plain object containing the row's field values from the older schema version.
|
|
336
635
|
*
|
|
337
636
|
* @example
|
|
338
637
|
* ```typescript
|
|
339
|
-
* const User = E.defineModel(class {
|
|
638
|
+
* const User = E.defineModel("User", class {
|
|
340
639
|
* id = E.field(E.identifier);
|
|
341
640
|
* name = E.field(E.string);
|
|
342
|
-
* role = E.field(E.string);
|
|
641
|
+
* role = E.field(E.string);
|
|
343
642
|
*
|
|
344
643
|
* static migrate(record: Record<string, any>) {
|
|
345
|
-
* record.role ??= "user";
|
|
644
|
+
* record.role ??= "user";
|
|
346
645
|
* }
|
|
347
646
|
* }, { pk: "id" });
|
|
348
647
|
* ```
|
|
@@ -359,17 +658,14 @@ export abstract class Model<SUB> {
|
|
|
359
658
|
* @internal
|
|
360
659
|
* - _oldValues===undefined: New instance, not yet saved.
|
|
361
660
|
* - _oldValues===null: Instance is to be deleted.
|
|
661
|
+
* - _oldValues===false: Instance excluded from persistence (preventPersist).
|
|
362
662
|
* - _oldValues is an object: Loaded (possibly only partial, still lazy) from disk, _oldValues contains (partial) old values
|
|
363
663
|
*/
|
|
364
|
-
_oldValues: Record<string, any> | undefined | null;
|
|
664
|
+
_oldValues: Record<string, any> | undefined | null | false;
|
|
365
665
|
_primaryKey: Uint8Array | undefined;
|
|
366
666
|
_primaryKeyHash: number | undefined;
|
|
367
667
|
_txn!: Transaction;
|
|
368
668
|
|
|
369
|
-
constructor(initial: Partial<Omit<SUB, "constructor">> = {}) {
|
|
370
|
-
throw new DatabaseError("Use defineModel() to create model classes", 'INIT_ERROR');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
669
|
/**
|
|
374
670
|
* Optional hook called on each modified instance right before the transaction commits.
|
|
375
671
|
* Runs before data is written to disk, so changes made here are included in the commit.
|
|
@@ -382,7 +678,7 @@ export abstract class Model<SUB> {
|
|
|
382
678
|
*
|
|
383
679
|
* @example
|
|
384
680
|
* ```typescript
|
|
385
|
-
* const Post = E.defineModel(class {
|
|
681
|
+
* const Post = E.defineModel("Post", class {
|
|
386
682
|
* id = E.field(E.identifier);
|
|
387
683
|
* title = E.field(E.string);
|
|
388
684
|
* slug = E.field(E.string);
|
|
@@ -395,23 +691,11 @@ export abstract class Model<SUB> {
|
|
|
395
691
|
*/
|
|
396
692
|
preCommit?(): void;
|
|
397
693
|
|
|
398
|
-
static _resetIndexes(): void {
|
|
399
|
-
this._primary._indexId = undefined;
|
|
400
|
-
this._primary._versions.clear();
|
|
401
|
-
for (const sec of this._secondaries || []) sec._indexId = undefined;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
static async _loadCreateIndexes(): Promise<void> {
|
|
405
|
-
await this._primary._delayedInit();
|
|
406
|
-
for (const sec of this._secondaries || []) await sec._delayedInit();
|
|
407
|
-
await this._primary._initVersioning();
|
|
408
|
-
}
|
|
409
|
-
|
|
410
694
|
_setLoadedField(fieldName: string, value: any) {
|
|
411
|
-
const oldValues = this._oldValues
|
|
695
|
+
const oldValues = this._oldValues! as Record<string, any>;
|
|
412
696
|
if (oldValues.hasOwnProperty(fieldName)) return; // Already loaded earlier (as part of index key?)
|
|
413
697
|
|
|
414
|
-
this
|
|
698
|
+
(this as any)[fieldName] = value;
|
|
415
699
|
if (typeof value === 'object' && value !== null) {
|
|
416
700
|
const fieldType = (this.constructor.fields[fieldName] as FieldConfig<unknown>).type;
|
|
417
701
|
oldValues[fieldName] = fieldType.clone(value);
|
|
@@ -421,13 +705,24 @@ export abstract class Model<SUB> {
|
|
|
421
705
|
}
|
|
422
706
|
}
|
|
423
707
|
|
|
708
|
+
_restoreLazyFields() {
|
|
709
|
+
const oldValues = this._oldValues;
|
|
710
|
+
if (!oldValues || oldValues === null) return;
|
|
711
|
+
for (const [fieldName, descriptor] of Object.entries(this.constructor._lazyDescriptors)) {
|
|
712
|
+
if (!oldValues.hasOwnProperty(fieldName)) {
|
|
713
|
+
Object.defineProperty(this, fieldName, descriptor);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
424
718
|
/**
|
|
425
719
|
* @returns The primary key for this instance.
|
|
426
720
|
*/
|
|
427
721
|
getPrimaryKey(): Uint8Array {
|
|
428
722
|
let key = this._primaryKey;
|
|
429
723
|
if (key === undefined) {
|
|
430
|
-
|
|
724
|
+
if (this._oldValues === false) throw new DatabaseError("Operation not allowed after preventPersist()", "NO_PERSIST");
|
|
725
|
+
key = this.constructor._serializePK(this).toUint8Array();
|
|
431
726
|
this._setPrimaryKey(key);
|
|
432
727
|
}
|
|
433
728
|
return key;
|
|
@@ -436,7 +731,7 @@ export abstract class Model<SUB> {
|
|
|
436
731
|
_setPrimaryKey(key: Uint8Array, hash?: number) {
|
|
437
732
|
this._primaryKey = key;
|
|
438
733
|
this._primaryKeyHash = hash ?? hashBytes(key);
|
|
439
|
-
Object.defineProperties(this, this.constructor.
|
|
734
|
+
Object.defineProperties(this, this.constructor._freezePrimaryKeyDescriptors);
|
|
440
735
|
}
|
|
441
736
|
|
|
442
737
|
/**
|
|
@@ -448,21 +743,23 @@ export abstract class Model<SUB> {
|
|
|
448
743
|
}
|
|
449
744
|
|
|
450
745
|
isLazyField(field: keyof this) {
|
|
451
|
-
const
|
|
452
|
-
return !!(
|
|
746
|
+
const oldValues = this._oldValues;
|
|
747
|
+
return !!(oldValues && oldValues !== null && field in this.constructor._lazyDescriptors && !oldValues.hasOwnProperty(field));
|
|
453
748
|
}
|
|
454
749
|
|
|
455
750
|
_write(txn: Transaction): undefined | Change {
|
|
456
751
|
const oldValues = this._oldValues;
|
|
457
752
|
|
|
753
|
+
if (oldValues === false) return; // preventPersist() was called
|
|
754
|
+
|
|
458
755
|
if (oldValues === null) { // Delete instance
|
|
459
756
|
const pk = this._primaryKey;
|
|
460
757
|
// Temporarily restore _oldValues so computed indexes can trigger lazy loads
|
|
461
758
|
this._oldValues = {};
|
|
462
|
-
for(const index of this.constructor._secondaries ||
|
|
759
|
+
for (const index of Object.values(this.constructor._secondaries || {})) {
|
|
463
760
|
index._delete(txn, pk!, this);
|
|
464
761
|
}
|
|
465
|
-
this.constructor.
|
|
762
|
+
this.constructor._deletePK(txn, pk!, this);
|
|
466
763
|
|
|
467
764
|
return "deleted";
|
|
468
765
|
}
|
|
@@ -477,10 +774,10 @@ export abstract class Model<SUB> {
|
|
|
477
774
|
}
|
|
478
775
|
|
|
479
776
|
// Insert the primary index
|
|
480
|
-
this.constructor.
|
|
777
|
+
this.constructor._writePK(txn, pk!, this);
|
|
481
778
|
|
|
482
779
|
// Insert all secondaries
|
|
483
|
-
for (const index of this.constructor._secondaries ||
|
|
780
|
+
for (const index of Object.values(this.constructor._secondaries || {})) {
|
|
484
781
|
index._write(txn, pk!, this);
|
|
485
782
|
}
|
|
486
783
|
|
|
@@ -493,10 +790,11 @@ export abstract class Model<SUB> {
|
|
|
493
790
|
|
|
494
791
|
// Add old values of changed fields to 'changed'.
|
|
495
792
|
const changed: Record<string, any> = {};
|
|
496
|
-
const
|
|
793
|
+
const cls = this.constructor;
|
|
794
|
+
const fields = cls.fields;
|
|
497
795
|
for(const fieldName in oldValues) {
|
|
498
796
|
const oldValue = oldValues[fieldName];
|
|
499
|
-
const newValue = this[fieldName as keyof
|
|
797
|
+
const newValue = this[fieldName as keyof ModelBase];
|
|
500
798
|
if (newValue !== oldValue && !(fields[fieldName] as FieldConfig<unknown>).type.equals(newValue, oldValue)) {
|
|
501
799
|
changed[fieldName] = oldValue;
|
|
502
800
|
}
|
|
@@ -505,7 +803,7 @@ export abstract class Model<SUB> {
|
|
|
505
803
|
if (isObjectEmpty(changed)) return; // No changes, nothing to do
|
|
506
804
|
|
|
507
805
|
// Make sure primary has not been changed
|
|
508
|
-
for (const field of
|
|
806
|
+
for (const field of cls._indexFields.keys()) {
|
|
509
807
|
if (changed.hasOwnProperty(field)) {
|
|
510
808
|
throw new DatabaseError(`Cannot modify primary key field: ${field}`, "CHANGE_PRIMARY");
|
|
511
809
|
}
|
|
@@ -518,10 +816,10 @@ export abstract class Model<SUB> {
|
|
|
518
816
|
|
|
519
817
|
// Update the primary index
|
|
520
818
|
const pk = this._primaryKey!;
|
|
521
|
-
|
|
819
|
+
cls._writePK(txn, pk, this);
|
|
522
820
|
|
|
523
821
|
// Update any secondaries with changed fields
|
|
524
|
-
for (const index of
|
|
822
|
+
for (const index of Object.values(cls._secondaries || {})) {
|
|
525
823
|
index._update(txn, pk, this, oldValues);
|
|
526
824
|
}
|
|
527
825
|
return changed;
|
|
@@ -540,60 +838,15 @@ export abstract class Model<SUB> {
|
|
|
540
838
|
* ```
|
|
541
839
|
*/
|
|
542
840
|
preventPersist() {
|
|
543
|
-
this.
|
|
841
|
+
if (this._oldValues === undefined && this._primaryKey !== undefined) {
|
|
842
|
+
throw new DatabaseError("Cannot preventPersist() after PK has been used", "INVALID");
|
|
843
|
+
}
|
|
844
|
+
this._oldValues = false;
|
|
544
845
|
// Have access to '_txn' throw a descriptive error:
|
|
545
846
|
Object.defineProperty(this, "_txn", PREVENT_PERSIST_DESCRIPTOR);
|
|
546
847
|
return this;
|
|
547
848
|
}
|
|
548
849
|
|
|
549
|
-
static get(...args: any[]): any {
|
|
550
|
-
return this._primary!.get(...args);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
static getLazy(...args: any[]): any {
|
|
554
|
-
return this._primary!.getLazy(...args);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
static find(opts?: any): any {
|
|
558
|
-
return this._primary!.find(opts);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
static batchProcess(opts: any, callback?: any): any {
|
|
562
|
-
return this._primary!.batchProcess(opts, callback);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Load an existing instance by primary key and update it, or create a new one.
|
|
567
|
-
*
|
|
568
|
-
* The provided object must contain all primary key fields. If a matching row exists,
|
|
569
|
-
* the remaining properties from `obj` are set on the loaded instance. Otherwise a
|
|
570
|
-
* new instance is created with `obj` as its initial properties.
|
|
571
|
-
*
|
|
572
|
-
* @param obj - Partial model data that **must** include every primary key field.
|
|
573
|
-
* @returns The loaded-and-updated or newly created instance.
|
|
574
|
-
*/
|
|
575
|
-
static replaceInto<T extends typeof Model<any>>(this: T, obj: Partial<Record<string, any>>): InstanceType<T> {
|
|
576
|
-
const pk = this._primary!;
|
|
577
|
-
const keyArgs = [];
|
|
578
|
-
for (const fieldName of pk._fieldTypes.keys()) {
|
|
579
|
-
if (!(fieldName in (obj as any))) {
|
|
580
|
-
throw new DatabaseError(`replaceInto: missing primary key field '${fieldName}'`, "MISSING_PRIMARY_KEY");
|
|
581
|
-
}
|
|
582
|
-
keyArgs.push((obj as any)[fieldName]);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const existing = pk.get(...keyArgs as any) as InstanceType<T> | undefined;
|
|
586
|
-
if (existing) {
|
|
587
|
-
for (const key in obj as any) {
|
|
588
|
-
if (!pk._fieldTypes.has(key as any)) {
|
|
589
|
-
(existing as any)[key] = (obj as any)[key];
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
return existing;
|
|
593
|
-
}
|
|
594
|
-
return new (this as any)(obj) as InstanceType<T>;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
850
|
/**
|
|
598
851
|
* Delete this model instance from the database.
|
|
599
852
|
*
|
|
@@ -606,13 +859,13 @@ export abstract class Model<SUB> {
|
|
|
606
859
|
* ```
|
|
607
860
|
*/
|
|
608
861
|
delete() {
|
|
609
|
-
if (this._oldValues === undefined) throw new DatabaseError("Cannot delete unsaved instance", "
|
|
862
|
+
if (this._oldValues === undefined) throw new DatabaseError("Cannot delete unsaved instance", "INVALID");
|
|
610
863
|
this._oldValues = null;
|
|
611
864
|
}
|
|
612
865
|
|
|
613
866
|
/**
|
|
614
867
|
* Validate all fields in this model instance.
|
|
615
|
-
* @param raise
|
|
868
|
+
* @param raise If true, throw on first validation error.
|
|
616
869
|
* @returns Array of validation errors (empty if valid).
|
|
617
870
|
*
|
|
618
871
|
* @example
|
|
@@ -626,11 +879,12 @@ export abstract class Model<SUB> {
|
|
|
626
879
|
*/
|
|
627
880
|
validate(raise: boolean = false): Error[] {
|
|
628
881
|
const errors: Error[] = [];
|
|
882
|
+
const cls = this.constructor;
|
|
629
883
|
|
|
630
|
-
for (const [key, fieldConfig] of Object.entries(
|
|
884
|
+
for (const [key, fieldConfig] of Object.entries(cls.fields)) {
|
|
631
885
|
let e = fieldConfig.type.getError((this as any)[key]);
|
|
632
886
|
if (e) {
|
|
633
|
-
e = addErrorPath(e,
|
|
887
|
+
e = addErrorPath(e, cls.tableName+"."+key);
|
|
634
888
|
if (raise) throw e;
|
|
635
889
|
errors.push(e as Error);
|
|
636
890
|
}
|
|
@@ -655,7 +909,7 @@ export abstract class Model<SUB> {
|
|
|
655
909
|
getState(): "deleted" | "created" | "loaded" | "lazy" {
|
|
656
910
|
if (this._oldValues === null) return "deleted";
|
|
657
911
|
if (this._oldValues === undefined) return "created";
|
|
658
|
-
for(const [key,descr] of Object.entries(this.constructor.
|
|
912
|
+
for(const [key,descr] of Object.entries(this.constructor._lazyDescriptors)) {
|
|
659
913
|
if (descr && 'get' in descr && descr.get === Reflect.getOwnPropertyDescriptor(this, key)?.get) {
|
|
660
914
|
return "lazy";
|
|
661
915
|
}
|
|
@@ -664,12 +918,55 @@ export abstract class Model<SUB> {
|
|
|
664
918
|
}
|
|
665
919
|
|
|
666
920
|
toString(): string {
|
|
667
|
-
const
|
|
668
|
-
const pk =
|
|
669
|
-
return `{Model:${
|
|
921
|
+
const cls = this.constructor;
|
|
922
|
+
const pk = cls._pkToArray(this._primaryKey || cls._serializePK(this).toUint8Array(false));
|
|
923
|
+
return `{Model:${cls.tableName} ${this.getState()} ${pk}}`;
|
|
670
924
|
}
|
|
671
925
|
|
|
672
926
|
[Symbol.for('nodejs.util.inspect.custom')]() {
|
|
673
927
|
return this.toString();
|
|
674
928
|
}
|
|
675
929
|
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Delete every key/value entry in the database and reinitialize all registered models.
|
|
933
|
+
*
|
|
934
|
+
* This clears rows, index metadata, and schema-version records. It is mainly useful
|
|
935
|
+
* for tests, local resets, or tooling that needs a completely empty database.
|
|
936
|
+
*/
|
|
937
|
+
export async function deleteEverything(): Promise<void> {
|
|
938
|
+
let done = false;
|
|
939
|
+
while (!done) {
|
|
940
|
+
await transact(() => {
|
|
941
|
+
const txn = currentTxn();
|
|
942
|
+
const iteratorId = lowlevel.createIterator(txn.id, undefined, undefined, false);
|
|
943
|
+
const deadline = Date.now() + 150;
|
|
944
|
+
let count = 0;
|
|
945
|
+
try {
|
|
946
|
+
while (true) {
|
|
947
|
+
const raw = lowlevel.readIterator(iteratorId);
|
|
948
|
+
if (!raw) {
|
|
949
|
+
done = true;
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
lowlevel.del(txn.id, raw.key);
|
|
953
|
+
if (++count >= 4096 || Date.now() >= deadline) break;
|
|
954
|
+
}
|
|
955
|
+
} finally {
|
|
956
|
+
lowlevel.closeIterator(iteratorId);
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
for (const model of Object.values(modelRegistry)) {
|
|
962
|
+
pendingModelInits.delete(model);
|
|
963
|
+
await model._initialize(true);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* A model instance, including its user-defined fields.
|
|
969
|
+
* @template FIELDS - The fields defined on this model.
|
|
970
|
+
*/
|
|
971
|
+
export type Model<FIELDS> = FIELDS & ModelBase;
|
|
972
|
+
export const Model = ModelBase;
|