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/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, TARGET_SYMBOL, PrimaryIndex } from "./indexes.js";
5
- import { assert, addErrorPath, logLevel } from "./utils.js";
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._indexes || []) {
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 key in obj) {
60
- if (obj.hasOwnProperty(key)) return false;
60
+ for (let _ of Object.keys(obj)) {
61
+ return false;
61
62
  }
62
63
  return true;
63
64
  }
64
65
 
65
- type OnSaveType = (model: InstanceType<typeof Model>, newKey: Uint8Array | undefined, oldKey: Uint8Array | undefined) => void;
66
- let onSave: OnSaveType | undefined;
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
- uninitializedModels.add(MyModel);
138
- initModels();
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 MockModel = function (this: any, initial?: Record<string,any>) {
149
- if (uninitializedModels.has(this.constructor)) {
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
- return new Proxy(this, modificationTracker);
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 MODIFIED_INSTANCES_SYMBOL = Symbol('modifiedInstances');
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
- /** @internal Primary key index for this model. */
280
- static _pk?: PrimaryIndex<any, any>;
281
- /** @internal All indexes for this model, the primary key being first. */
282
- static _indexes?: BaseIndex<any, any>[];
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 State tracking for this model instance:
300
- * - undefined: new instance, unmodified
301
- * - 1: new instance, modified (and in modifiedInstances)
302
- * - 2: loaded from disk, unmodified
303
- * - 3: persistence disabled
304
- * - array: loaded from disk, modified (and in modifiedInstances), array values are original index buffers
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
- _state: undefined | 1 | 2 | 3 | Array<Uint8Array>;
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
- _save() {
317
- // For performance, we'll work on the unproxied object, as we know we don't require change tracking for save.
318
- const unproxiedModel = ((this as any)[TARGET_SYMBOL] || this) as Model<SUB>;
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
- unproxiedModel.validate(true);
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
- // Handle unique indexes
323
- const indexes = this.constructor._indexes!;
324
- const originalKeys = typeof unproxiedModel._state === 'object' ? unproxiedModel._state : undefined;
325
- const newPk = indexes[0]._save(unproxiedModel, originalKeys?.[0]);
326
- for (let i=1; i<indexes.length; i++) {
327
- indexes[i]._save(unproxiedModel, originalKeys?.[i]);
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
- queueOnSave([this, newPk, originalKeys?.[0]]);
286
+ if (logLevel >= 1) {
287
+ console.log(`Registered model ${MockModel.tableName} with fields: ${Object.keys(MockModel.fields).join(' ')}`);
288
+ }
331
289
 
332
- unproxiedModel._state = 2; // Loaded from disk, unmodified
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
- * Load a model instance by primary key.
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
- static load<SUB>(this: typeof Model<SUB>, ...args: any[]): SUB | undefined {
348
- return this._pk!.get(...args);
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 modifiedInstances = olmdb.getTransactionData(MODIFIED_INSTANCES_SYMBOL) as Set<Model<any>>;
368
- const unproxiedModel = (this as any)[TARGET_SYMBOL] || this;
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
- const unproxiedModel = ((this as any)[TARGET_SYMBOL] || this) as Model<SUB>;
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): DatabaseError[] {
415
- const errors: DatabaseError[] = [];
454
+ validate(raise: boolean = false): Error[] {
455
+ const errors: Error[] = [];
416
456
 
417
457
  for (const [key, fieldConfig] of Object.entries(this._fields)) {
418
- for (const error of fieldConfig.type.getErrors(this, key)) {
419
- addErrorPath(error, key);
420
- if (raise) throw error;
421
- errors.push(error);
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
- };