edinburgh 0.1.3 → 0.3.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 +146 -84
- package/build/src/datapack.d.ts +119 -0
- package/build/src/datapack.js +620 -0
- package/build/src/datapack.js.map +1 -0
- package/build/src/edinburgh.d.ts +15 -3
- package/build/src/edinburgh.js +64 -30
- package/build/src/edinburgh.js.map +1 -1
- package/build/src/indexes.d.ts +58 -91
- package/build/src/indexes.js +360 -285
- package/build/src/indexes.js.map +1 -1
- package/build/src/models.d.ts +41 -45
- package/build/src/models.js +191 -239
- package/build/src/models.js.map +1 -1
- package/build/src/types.d.ts +188 -256
- package/build/src/types.js +381 -316
- package/build/src/types.js.map +1 -1
- package/build/src/utils.d.ts +9 -5
- package/build/src/utils.js +34 -5
- package/build/src/utils.js.map +1 -1
- package/package.json +13 -11
- package/src/datapack.ts +655 -0
- package/src/edinburgh.ts +68 -29
- package/src/indexes.ts +398 -319
- package/src/models.ts +224 -262
- package/src/types.ts +461 -385
- package/src/utils.ts +37 -9
- package/build/src/bytes.d.ts +0 -155
- package/build/src/bytes.js +0 -455
- package/build/src/bytes.js.map +0 -1
- package/src/bytes.ts +0 -500
package/src/models.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { DatabaseError } from "olmdb";
|
|
2
1
|
import * as olmdb from "olmdb";
|
|
2
|
+
import { DatabaseError } from "olmdb";
|
|
3
3
|
import { TypeWrapper, identifier } from "./types.js";
|
|
4
|
-
import { BaseIndex
|
|
5
|
-
import {
|
|
4
|
+
import { BaseIndex as BaseIndex, PrimaryIndex, IndexRangeIterator } from "./indexes.js";
|
|
5
|
+
import { addErrorPath, logLevel, tryDelayedInits, delayedInits } from "./utils.js";
|
|
6
|
+
import { on } from "events";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Configuration interface for model fields.
|
|
@@ -44,63 +45,31 @@ export function field<T>(type: TypeWrapper<T>, options: Partial<FieldConfig<T>>
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// Model registration and initialization
|
|
47
|
-
let uninitializedModels = new Set<typeof Model<unknown>>();
|
|
48
48
|
export const modelRegistry: Record<string, typeof Model> = {};
|
|
49
49
|
|
|
50
50
|
export function resetModelCaches() {
|
|
51
51
|
for(const model of Object.values(modelRegistry)) {
|
|
52
|
-
for(const index of model.
|
|
52
|
+
for(const index of model._secondaries || []) {
|
|
53
53
|
index._cachedIndexId = undefined;
|
|
54
54
|
}
|
|
55
|
+
delete model._primary?._cachedIndexId;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
function isObjectEmpty(obj: object) {
|
|
59
|
-
for (let
|
|
60
|
-
|
|
60
|
+
for (let _ of Object.keys(obj)) {
|
|
61
|
+
return false;
|
|
61
62
|
}
|
|
62
63
|
return true;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
type
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Set a callback function to be called after a model is saved and committed.
|
|
69
|
-
*
|
|
70
|
-
* @param callback The callback function to set. As arguments, it receives the model instance, the new key (undefined in case of a delete), and the old key (undefined in case of a create).
|
|
71
|
-
*/
|
|
72
|
-
export function setOnSaveCallback(callback: OnSaveType | undefined) {
|
|
73
|
-
onSave = callback;
|
|
74
|
-
}
|
|
75
|
-
const onSaveQueue: [InstanceType<typeof Model>, Uint8Array | undefined, Uint8Array | undefined][] = [];
|
|
76
|
-
function onSaveRevert() {
|
|
77
|
-
onSaveQueue.length = 0;
|
|
78
|
-
}
|
|
79
|
-
function onSaveCommit() {
|
|
80
|
-
if (onSave) {
|
|
81
|
-
for(let arr of onSaveQueue) {
|
|
82
|
-
onSave(...arr);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
onSaveQueue.length = 0;
|
|
86
|
-
}
|
|
87
|
-
function queueOnSave(arr: [InstanceType<typeof Model>, Uint8Array | undefined, Uint8Array | undefined]) {
|
|
88
|
-
if (onSave) {
|
|
89
|
-
if (!onSaveQueue.length) {
|
|
90
|
-
olmdb.onCommit(onSaveCommit);
|
|
91
|
-
olmdb.onRevert(onSaveRevert);
|
|
92
|
-
}
|
|
93
|
-
onSaveQueue.push(arr);
|
|
94
|
-
}
|
|
66
|
+
export type ChangedModel = Model<unknown> & {
|
|
67
|
+
changed: Record<any, any> | "updated" | "deleted";
|
|
95
68
|
}
|
|
96
69
|
|
|
97
70
|
/**
|
|
98
71
|
* Register a model class with the Edinburgh ORM system.
|
|
99
72
|
*
|
|
100
|
-
* This decorator function transforms the model class to use a proxy-based constructor
|
|
101
|
-
* that enables change tracking and automatic field initialization. It also extracts
|
|
102
|
-
* field metadata and sets up default values on the prototype.
|
|
103
|
-
*
|
|
104
73
|
* @template T - The model class type.
|
|
105
74
|
* @param MyModel - The model class to register.
|
|
106
75
|
* @returns The enhanced model class with ORM capabilities.
|
|
@@ -125,8 +94,6 @@ export function registerModel<T extends typeof Model<unknown>>(MyModel: T): T {
|
|
|
125
94
|
}
|
|
126
95
|
}
|
|
127
96
|
|
|
128
|
-
// Initialize an empty `fields` object, and set it on both constructors, as well as on the prototype.
|
|
129
|
-
MockModel.fields = MockModel.prototype._fields = {};
|
|
130
97
|
MockModel.tableName ||= MyModel.name; // Set the table name to the class name if not already set
|
|
131
98
|
|
|
132
99
|
// Register the constructor by name
|
|
@@ -134,10 +101,10 @@ export function registerModel<T extends typeof Model<unknown>>(MyModel: T): T {
|
|
|
134
101
|
modelRegistry[MockModel.tableName] = MockModel;
|
|
135
102
|
|
|
136
103
|
// Attempt to instantiate the class and gather field metadata
|
|
137
|
-
|
|
138
|
-
|
|
104
|
+
delayedInits.add(MyModel);
|
|
105
|
+
tryDelayedInits();
|
|
139
106
|
|
|
140
|
-
return MockModel;
|
|
107
|
+
return MockModel;
|
|
141
108
|
}
|
|
142
109
|
|
|
143
110
|
export function getMockModel<T extends typeof Model<unknown>>(OrgModel: T): T {
|
|
@@ -145,17 +112,16 @@ export function getMockModel<T extends typeof Model<unknown>>(OrgModel: T): T {
|
|
|
145
112
|
if (AnyOrgModel._isMock) return OrgModel;
|
|
146
113
|
if (AnyOrgModel._mock) return AnyOrgModel._mock;
|
|
147
114
|
|
|
148
|
-
const
|
|
149
|
-
|
|
115
|
+
const name = OrgModel.tableName || OrgModel.name;
|
|
116
|
+
const MockModel = function(this: any, initial?: Record<string,any>) {
|
|
117
|
+
if (delayedInits.has(this.constructor)) {
|
|
150
118
|
throw new DatabaseError("Cannot instantiate while linked models haven't been registered yet", 'INIT_ERROR');
|
|
151
119
|
}
|
|
152
120
|
if (initial && !isObjectEmpty(initial)) {
|
|
153
121
|
Object.assign(this, initial);
|
|
154
|
-
const modifiedInstances = olmdb.getTransactionData(MODIFIED_INSTANCES_SYMBOL) as Set<Model<any>>;
|
|
155
|
-
modifiedInstances.add(this);
|
|
156
122
|
}
|
|
157
|
-
|
|
158
|
-
|
|
123
|
+
const instances = olmdb.getTransactionData(INSTANCES_SYMBOL) as Set<Model<any>>;
|
|
124
|
+
instances.add(this);
|
|
159
125
|
} as any as T;
|
|
160
126
|
|
|
161
127
|
// We want .constructor to point at our fake constructor function.
|
|
@@ -169,78 +135,11 @@ export function getMockModel<T extends typeof Model<unknown>>(OrgModel: T): T {
|
|
|
169
135
|
return MockModel;
|
|
170
136
|
}
|
|
171
137
|
|
|
172
|
-
function initModels() {
|
|
173
|
-
for(const OrgModel of uninitializedModels) {
|
|
174
|
-
const MockModel = getMockModel(OrgModel);
|
|
175
|
-
// Create an instance (the only one to ever exist) of the actual class,
|
|
176
|
-
// in order to gather field config data.
|
|
177
|
-
let instance;
|
|
178
|
-
try {
|
|
179
|
-
instance = new (OrgModel as any)(INIT_INSTANCE_SYMBOL);
|
|
180
|
-
} catch(e) {
|
|
181
|
-
if (!(e instanceof ReferenceError)) throw e;
|
|
182
|
-
// ReferenceError: Cannot access 'SomeLinkedClass' before initialization.
|
|
183
|
-
// We'll try again after the next class has successfully initialized.
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
uninitializedModels.delete(OrgModel);
|
|
188
|
-
|
|
189
|
-
// If no primary key exists, create one using 'id' field
|
|
190
|
-
if (!MockModel._pk) {
|
|
191
|
-
// If no `id` field exists, add it automatically
|
|
192
|
-
if (!instance.id) {
|
|
193
|
-
instance.id = { type: identifier };
|
|
194
|
-
}
|
|
195
|
-
// @ts-ignore-next-line - `id` is not part of the type, but the user probably shouldn't touch it anyhow
|
|
196
|
-
new PrimaryIndex(MockModel, ['id']);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (const key in instance) {
|
|
200
|
-
const value = instance[key] as FieldConfig<unknown>;
|
|
201
|
-
// Check if this property contains field metadata
|
|
202
|
-
if (value && value.type instanceof TypeWrapper) {
|
|
203
|
-
// Set the configuration on the constructor's `fields` property
|
|
204
|
-
MockModel.fields[key] = value;
|
|
205
|
-
|
|
206
|
-
// Set default value on the prototype
|
|
207
|
-
const defObj = value.default===undefined ? value.type : value;
|
|
208
|
-
const def = defObj.default;
|
|
209
|
-
if (typeof def === 'function') {
|
|
210
|
-
// The default is a function. We'll define a getter on the property in the model prototype,
|
|
211
|
-
// and once it is read, we'll run the function and set the value as a plain old property
|
|
212
|
-
// on the instance object.
|
|
213
|
-
Object.defineProperty(MockModel.prototype, key, {
|
|
214
|
-
get() {
|
|
215
|
-
// This will call set(), which will define the property on the instance.
|
|
216
|
-
return (this[key] = def.call(defObj, this));
|
|
217
|
-
},
|
|
218
|
-
set(val: any) {
|
|
219
|
-
Object.defineProperty(this, key, {
|
|
220
|
-
value: val,
|
|
221
|
-
configurable: true,
|
|
222
|
-
writable: true
|
|
223
|
-
})
|
|
224
|
-
},
|
|
225
|
-
configurable: true,
|
|
226
|
-
});
|
|
227
|
-
} else if (def !== undefined) {
|
|
228
|
-
(MockModel.prototype as any)[key] = def;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (logLevel >= 1) {
|
|
234
|
-
console.log(`Registered model ${MockModel.tableName}[${MockModel._pk!._fieldNames.join(',')}] with fields: ${Object.keys(MockModel.fields).join(' ')}`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
138
|
// Model base class and related symbols/state
|
|
240
139
|
const INIT_INSTANCE_SYMBOL = Symbol();
|
|
241
140
|
|
|
242
141
|
/** @internal Symbol used to attach modified instances to running transaction */
|
|
243
|
-
export const
|
|
142
|
+
export const INSTANCES_SYMBOL = Symbol('instances');
|
|
244
143
|
|
|
245
144
|
/** @internal Symbol used to access the underlying model from a proxy */
|
|
246
145
|
|
|
@@ -275,16 +174,19 @@ export interface Model<SUB> {
|
|
|
275
174
|
* }
|
|
276
175
|
* ```
|
|
277
176
|
*/
|
|
177
|
+
|
|
178
|
+
|
|
278
179
|
export abstract class Model<SUB> {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
/** @internal All indexes for this model
|
|
282
|
-
static
|
|
180
|
+
static _primary: PrimaryIndex<any, any>;
|
|
181
|
+
|
|
182
|
+
/** @internal All non-primary indexes for this model. */
|
|
183
|
+
static _secondaries?: BaseIndex<any, readonly (keyof any & string)[]>[];
|
|
283
184
|
|
|
284
185
|
/** The database table name (defaults to class name). */
|
|
285
186
|
static tableName: string;
|
|
187
|
+
|
|
286
188
|
/** Field configuration metadata. */
|
|
287
|
-
static fields: Record<string, FieldConfig<unknown>>;
|
|
189
|
+
static fields: Record<string | symbol | number, FieldConfig<unknown>>;
|
|
288
190
|
|
|
289
191
|
/*
|
|
290
192
|
* IMPORTANT: We cannot use instance property initializers here, because we will be
|
|
@@ -292,18 +194,26 @@ export abstract class Model<SUB> {
|
|
|
292
194
|
* intentional, as we don't want to run the initializers for the fields.
|
|
293
195
|
*/
|
|
294
196
|
|
|
295
|
-
/** @internal Field configuration for this instance. */
|
|
296
|
-
_fields!: Record<string, FieldConfig<unknown>>;
|
|
297
|
-
|
|
298
197
|
/**
|
|
299
|
-
* @internal
|
|
300
|
-
* -
|
|
301
|
-
* -
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
198
|
+
* @internal
|
|
199
|
+
* - !_oldValues: New instance, not yet saved.
|
|
200
|
+
* - _oldValues && _primaryKey: Loaded (possibly only partial, still lazy) from disk, _oldValues contains (partial) old values
|
|
201
|
+
*/
|
|
202
|
+
_oldValues: Partial<Model<SUB>> | undefined;
|
|
203
|
+
_primaryKey: Uint8Array | undefined;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* This property can be used in `setOnSave` callbacks to determine how a model instance has changed.
|
|
207
|
+
* If the value is undefined, the instance has been created. If it's "deleted" the instance has
|
|
208
|
+
* been deleted. If its an object, the instance has been modified and the object contains the old values.
|
|
209
|
+
*
|
|
210
|
+
* Note: this property should **not** be accessed *during* a `transact()` -- it's state is an implementation
|
|
211
|
+
* detail that may change semantics at any minor release.
|
|
305
212
|
*/
|
|
306
|
-
|
|
213
|
+
changed?: Record<any,any> | "deleted" | "created";
|
|
214
|
+
|
|
215
|
+
// Reference the static `fields` property; the mock constructor copies it here for performance
|
|
216
|
+
_fields!: Record<string | symbol | number, FieldConfig<unknown>>;
|
|
307
217
|
|
|
308
218
|
constructor(initial: Partial<Omit<SUB, "constructor">> = {}) {
|
|
309
219
|
// This constructor will only be called once, from `initModels`. All other instances will
|
|
@@ -313,47 +223,179 @@ export abstract class Model<SUB> {
|
|
|
313
223
|
}
|
|
314
224
|
}
|
|
315
225
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
226
|
+
static _delayedInit(): boolean {
|
|
227
|
+
const MockModel = getMockModel(this);
|
|
228
|
+
// Create an instance (the only one to ever exist) of the actual class,
|
|
229
|
+
// in order to gather field config data.
|
|
230
|
+
let instance;
|
|
231
|
+
try {
|
|
232
|
+
instance = new (this as any)(INIT_INSTANCE_SYMBOL);
|
|
233
|
+
} catch(e) {
|
|
234
|
+
if (!(e instanceof ReferenceError)) throw e;
|
|
235
|
+
// ReferenceError: Cannot access 'SomeLinkedClass' before initialization.
|
|
236
|
+
// We'll try again after the next class has successfully initialized.
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// If no primary key exists, create one using 'id' field
|
|
241
|
+
if (!MockModel._primary) {
|
|
242
|
+
// If no `id` field exists, add it automatically
|
|
243
|
+
if (!instance.id) {
|
|
244
|
+
instance.id = { type: identifier };
|
|
245
|
+
}
|
|
246
|
+
// @ts-ignore-next-line - `id` is not part of the type, but the user probably shouldn't touch it anyhow
|
|
247
|
+
new PrimaryIndex(MockModel, ['id']);
|
|
248
|
+
}
|
|
319
249
|
|
|
320
|
-
|
|
250
|
+
MockModel.fields = MockModel.prototype._fields = {};
|
|
251
|
+
for (const key in instance) {
|
|
252
|
+
const value = instance[key] as FieldConfig<unknown>;
|
|
253
|
+
// Check if this property contains field metadata
|
|
254
|
+
if (value && value.type instanceof TypeWrapper) {
|
|
255
|
+
// Set the configuration on the constructor's `fields` property
|
|
256
|
+
MockModel.fields[key] = value;
|
|
321
257
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
258
|
+
// Set default value on the prototype
|
|
259
|
+
const defObj = value.default===undefined ? value.type : value;
|
|
260
|
+
const def = defObj.default;
|
|
261
|
+
if (typeof def === 'function') {
|
|
262
|
+
// The default is a function. We'll define a getter on the property in the model prototype,
|
|
263
|
+
// and once it is read, we'll run the function and set the value as a plain old property
|
|
264
|
+
// on the instance object.
|
|
265
|
+
Object.defineProperty(MockModel.prototype, key, {
|
|
266
|
+
get() {
|
|
267
|
+
// This will call set(), which will define the property on the instance.
|
|
268
|
+
return (this[key] = def.call(defObj, this));
|
|
269
|
+
},
|
|
270
|
+
set(val: any) {
|
|
271
|
+
Object.defineProperty(this, key, {
|
|
272
|
+
value: val,
|
|
273
|
+
configurable: true,
|
|
274
|
+
writable: true,
|
|
275
|
+
enumerable: true,
|
|
276
|
+
})
|
|
277
|
+
},
|
|
278
|
+
configurable: true,
|
|
279
|
+
});
|
|
280
|
+
} else if (def !== undefined) {
|
|
281
|
+
(MockModel.prototype as any)[key] = def;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
328
284
|
}
|
|
329
285
|
|
|
330
|
-
|
|
286
|
+
if (logLevel >= 1) {
|
|
287
|
+
console.log(`Registered model ${MockModel.tableName} with fields: ${Object.keys(MockModel.fields).join(' ')}`);
|
|
288
|
+
}
|
|
331
289
|
|
|
332
|
-
|
|
290
|
+
return true;
|
|
333
291
|
}
|
|
334
292
|
|
|
293
|
+
_setLoadedField(fieldName: string, value: any) {
|
|
294
|
+
const orgValues = this._oldValues ||= Object.create(Object.getPrototypeOf(this));
|
|
295
|
+
if (orgValues.hasOwnProperty(fieldName)) return; // Already loaded earlier (as part of index key?)
|
|
296
|
+
|
|
297
|
+
const fieldType = (this._fields[fieldName] as FieldConfig<unknown>).type;
|
|
298
|
+
this[fieldName as keyof Model<SUB>] = value;
|
|
299
|
+
orgValues[fieldName] = fieldType.clone(value);
|
|
300
|
+
}
|
|
335
301
|
|
|
336
302
|
/**
|
|
337
|
-
*
|
|
338
|
-
* @param args - Primary key field values.
|
|
339
|
-
* @returns The model instance if found, undefined otherwise.
|
|
340
|
-
*
|
|
341
|
-
* @example
|
|
342
|
-
* ```typescript
|
|
343
|
-
* const user = User.load("user123");
|
|
344
|
-
* const post = Post.load("post456", "en");
|
|
345
|
-
* ```
|
|
303
|
+
* @returns The primary key for this instance, or undefined if not yet saved.
|
|
346
304
|
*/
|
|
347
|
-
|
|
348
|
-
return this.
|
|
305
|
+
getPrimaryKey(): Uint8Array | undefined {
|
|
306
|
+
return this._primaryKey;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_getCreatePrimaryKey(): Uint8Array {
|
|
310
|
+
return this._primaryKey ||= this.constructor._primary!._instanceToKeySingleton(this);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
isLazyField(field: keyof this) {
|
|
314
|
+
const descr = this.constructor._primary!._lazyDescriptors[field];
|
|
315
|
+
return !!(descr && 'get' in descr && descr.get === Reflect.getOwnPropertyDescriptor(this, field)?.get);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
_onCommit(onSaveQueue: ChangedModel[] | undefined) {
|
|
319
|
+
const oldValues = this._oldValues;
|
|
320
|
+
let changed : Record<any, any> | "created" | "deleted";
|
|
321
|
+
|
|
322
|
+
if (oldValues) {
|
|
323
|
+
// We're doing an update. Note that we may still be in a lazy state, and we don't want to load
|
|
324
|
+
// the whole object just to see if something changed.
|
|
325
|
+
|
|
326
|
+
// Delete all items from this.changed that have not actually changed.
|
|
327
|
+
const fields = this._fields;
|
|
328
|
+
changed = {};
|
|
329
|
+
for(const fieldName of Object.keys(oldValues) as Iterable<keyof Model<SUB>>) {
|
|
330
|
+
const oldValue = oldValues[fieldName];
|
|
331
|
+
if (!(fields[fieldName] as FieldConfig<unknown>).type.equals(this[fieldName], oldValue)) {
|
|
332
|
+
changed[fieldName] = oldValue;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (isObjectEmpty(changed)) return; // No changes, nothing to do
|
|
336
|
+
|
|
337
|
+
// Make sure primary has not been changed
|
|
338
|
+
for (const field of this.constructor._primary!._fieldTypes.keys()) {
|
|
339
|
+
if (changed.hasOwnProperty(field)) {
|
|
340
|
+
throw new DatabaseError(`Cannot modify primary key field: ${field}`, "CHANGE_PRIMARY");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// We have changes. Now it's okay for any lazy fields to be loaded (which the validate will trigger).
|
|
345
|
+
|
|
346
|
+
// Raise any validation errors
|
|
347
|
+
this.validate(true);
|
|
348
|
+
|
|
349
|
+
// Update the primary index
|
|
350
|
+
this.constructor._primary!._write(this);
|
|
351
|
+
|
|
352
|
+
// Update any secondaries with changed fields
|
|
353
|
+
for (const index of this.constructor._secondaries || []) {
|
|
354
|
+
for (const field of index._fieldTypes.keys()) {
|
|
355
|
+
if (changed.hasOwnProperty(field)) {
|
|
356
|
+
// We need to update this index - first delete the old one
|
|
357
|
+
index._delete(oldValues);
|
|
358
|
+
index._write(this)
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} else if (this._primaryKey) { // Deleted instance
|
|
364
|
+
this.constructor._primary._delete(this);
|
|
365
|
+
for(const index of this.constructor._secondaries || []) {
|
|
366
|
+
index._delete(this);
|
|
367
|
+
}
|
|
368
|
+
changed = "deleted";
|
|
369
|
+
} else {
|
|
370
|
+
// New instance
|
|
371
|
+
// Raise any validation errors
|
|
372
|
+
this.validate(true);
|
|
373
|
+
|
|
374
|
+
// Make sure the primary key does not already exist
|
|
375
|
+
if (olmdb.get(this._getCreatePrimaryKey())) {
|
|
376
|
+
throw new DatabaseError("Unique constraint violation", "UNIQUE_CONSTRAINT");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Insert the primary index
|
|
380
|
+
this.constructor._primary!._write(this);
|
|
381
|
+
|
|
382
|
+
// Insert all secondaries
|
|
383
|
+
for (const index of this.constructor._secondaries || []) {
|
|
384
|
+
index._write(this);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
changed = "created";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (onSaveQueue) {
|
|
391
|
+
this.changed = changed;
|
|
392
|
+
onSaveQueue.push(this as ChangedModel);
|
|
393
|
+
}
|
|
349
394
|
}
|
|
350
395
|
|
|
351
396
|
/**
|
|
352
397
|
* Prevent this instance from being persisted to the database.
|
|
353
398
|
*
|
|
354
|
-
* Removes the instance from the modified instances set and disables
|
|
355
|
-
* automatic persistence at transaction commit.
|
|
356
|
-
*
|
|
357
399
|
* @returns This model instance for chaining.
|
|
358
400
|
*
|
|
359
401
|
* @example
|
|
@@ -364,14 +406,21 @@ export abstract class Model<SUB> {
|
|
|
364
406
|
* ```
|
|
365
407
|
*/
|
|
366
408
|
preventPersist() {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
modifiedInstances.delete(unproxiedModel);
|
|
370
|
-
|
|
371
|
-
unproxiedModel._state = 3; // no persist
|
|
409
|
+
const instances = olmdb.getTransactionData(INSTANCES_SYMBOL) as Set<Model<any>>;
|
|
410
|
+
instances.delete(this);
|
|
372
411
|
return this;
|
|
373
412
|
}
|
|
374
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Find all instances of this model in the database, ordered by primary key.
|
|
416
|
+
* @param opts - Optional parameters.
|
|
417
|
+
* @param opts.reverse - If true, iterate in reverse order.
|
|
418
|
+
* @returns An iterator.
|
|
419
|
+
*/
|
|
420
|
+
static findAll<T extends typeof Model<unknown>>(this: T, opts?: {reverse?: boolean}): IndexRangeIterator<T> {
|
|
421
|
+
return this._primary!.find(opts);
|
|
422
|
+
}
|
|
423
|
+
|
|
375
424
|
/**
|
|
376
425
|
* Delete this model instance from the database.
|
|
377
426
|
*
|
|
@@ -384,17 +433,8 @@ export abstract class Model<SUB> {
|
|
|
384
433
|
* ```
|
|
385
434
|
*/
|
|
386
435
|
delete() {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (this._state === 2 || typeof this._state === 'object') {
|
|
390
|
-
for(const index of unproxiedModel.constructor._indexes!) {
|
|
391
|
-
const key = index._getKeyFromModel(unproxiedModel, true);
|
|
392
|
-
olmdb.del(key);
|
|
393
|
-
if (index instanceof PrimaryIndex) queueOnSave([this, undefined, key]);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
this.preventPersist();
|
|
436
|
+
if (!this._primaryKey) throw new DatabaseError("Cannot delete unsaved instance", "NOT_SAVED");
|
|
437
|
+
this._oldValues = undefined;
|
|
398
438
|
}
|
|
399
439
|
|
|
400
440
|
/**
|
|
@@ -411,14 +451,15 @@ export abstract class Model<SUB> {
|
|
|
411
451
|
* }
|
|
412
452
|
* ```
|
|
413
453
|
*/
|
|
414
|
-
validate(raise: boolean = false):
|
|
415
|
-
const errors:
|
|
454
|
+
validate(raise: boolean = false): Error[] {
|
|
455
|
+
const errors: Error[] = [];
|
|
416
456
|
|
|
417
457
|
for (const [key, fieldConfig] of Object.entries(this._fields)) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
458
|
+
let e = fieldConfig.type.getError((this as any)[key]);
|
|
459
|
+
if (e) {
|
|
460
|
+
e = addErrorPath(e, this.constructor.tableName+"."+key);
|
|
461
|
+
if (raise) throw e;
|
|
462
|
+
errors.push(e as Error);
|
|
422
463
|
}
|
|
423
464
|
}
|
|
424
465
|
return errors;
|
|
@@ -438,82 +479,3 @@ export abstract class Model<SUB> {
|
|
|
438
479
|
return this.validate().length === 0;
|
|
439
480
|
}
|
|
440
481
|
}
|
|
441
|
-
|
|
442
|
-
// We use recursive proxies to track modifications made to, say, arrays within models. In
|
|
443
|
-
// order to know which model a nested object belongs to, we maintain a WeakMap that maps
|
|
444
|
-
// objects to their owner (unproxied) model.
|
|
445
|
-
const modificationOwnerMap = new WeakMap<object, Model<any>>();
|
|
446
|
-
|
|
447
|
-
// A cache for the proxies around nested objects, so that we don't need to recreate them
|
|
448
|
-
// every time we access a property on a nested object (and so that their identity remains
|
|
449
|
-
// the same).
|
|
450
|
-
const modificationProxyCache = new WeakMap<object, any>();
|
|
451
|
-
|
|
452
|
-
// Single proxy handler for both models and nested objects
|
|
453
|
-
export const modificationTracker: ProxyHandler<any> = {
|
|
454
|
-
get(target, prop) {
|
|
455
|
-
if (prop === TARGET_SYMBOL) return target;
|
|
456
|
-
const value = target[prop];
|
|
457
|
-
if (!value || typeof value !== 'object' || (value instanceof Model)) return value;
|
|
458
|
-
|
|
459
|
-
// Check cache first
|
|
460
|
-
let proxy = modificationProxyCache.get(value);
|
|
461
|
-
if (proxy) return proxy;
|
|
462
|
-
|
|
463
|
-
let model;
|
|
464
|
-
if (target instanceof Model) {
|
|
465
|
-
if (!target._fields[prop as string]) {
|
|
466
|
-
// No need to track properties that are not model fields.
|
|
467
|
-
return value;
|
|
468
|
-
}
|
|
469
|
-
model = target;
|
|
470
|
-
} else {
|
|
471
|
-
model = modificationOwnerMap.get(target);
|
|
472
|
-
assert(model);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
let state = model._state;
|
|
476
|
-
if (state !== undefined && state !== 2) {
|
|
477
|
-
// We don't need to track changes for this model (anymore). So we can just return the unproxied object.
|
|
478
|
-
// As we doing the modificationProxyCache lookup first, the identity of returned objects will not change:
|
|
479
|
-
// once a proxied object is returned, the same property will always return a proxied object.
|
|
480
|
-
return value;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (modificationOwnerMap.get(value)) {
|
|
484
|
-
throw new DatabaseError("Object cannot be embedded in multiple model instances", 'VALUE_ERROR');
|
|
485
|
-
}
|
|
486
|
-
modificationOwnerMap.set(value, model);
|
|
487
|
-
proxy = new Proxy(value, modificationTracker);
|
|
488
|
-
modificationProxyCache.set(value, proxy);
|
|
489
|
-
return proxy;
|
|
490
|
-
},
|
|
491
|
-
set(target, prop, value) {
|
|
492
|
-
let model;
|
|
493
|
-
if (target instanceof Model) {
|
|
494
|
-
model = target;
|
|
495
|
-
if (!model._fields[prop as string]) {
|
|
496
|
-
// No need to track properties that are not model fields.
|
|
497
|
-
(target as any)[prop] = value;
|
|
498
|
-
return true;
|
|
499
|
-
}
|
|
500
|
-
} else {
|
|
501
|
-
model = modificationOwnerMap.get(target);
|
|
502
|
-
assert(model);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
let state = model._state;
|
|
506
|
-
if (state === undefined || state === 2) {
|
|
507
|
-
const modifiedInstances = olmdb.getTransactionData(MODIFIED_INSTANCES_SYMBOL) as Set<Model<any>>;
|
|
508
|
-
modifiedInstances.add(model);
|
|
509
|
-
if (state === 2) {
|
|
510
|
-
model._state = model.constructor._indexes!.map(idx => idx._getKeyFromModel(model, true));
|
|
511
|
-
} else {
|
|
512
|
-
model._state = 1;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
target[prop] = value;
|
|
517
|
-
return true;
|
|
518
|
-
}
|
|
519
|
-
};
|