@acodeninja/persist 3.0.0-next.26 → 3.0.0-next.28

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/Connection.js CHANGED
@@ -7,6 +7,104 @@ import Model from './data/Model.js';
7
7
  import SearchIndex from './data/SearchIndex.js';
8
8
  import _ from 'lodash';
9
9
 
10
+ /**
11
+ * @class State
12
+ */
13
+ class State {
14
+ /**
15
+ * @private
16
+ * @property {StorageEngine}
17
+ */
18
+ #storage;
19
+
20
+ constructor(storage) {
21
+ this.modelCache = new Map();
22
+ this.indexCache = new Map();
23
+ this.searchIndexCache = new Map();
24
+ this.#storage = storage;
25
+ }
26
+
27
+ /**
28
+ * Get a given index
29
+ * @param {Model.constructor} modelConstructor
30
+ * @return {Object}
31
+ */
32
+ async getIndex(modelConstructor) {
33
+ const modelConstructorName = modelConstructor.name;
34
+
35
+ if (!this.indexCache.has(modelConstructorName)) {
36
+ this.indexCache.set(modelConstructorName, {
37
+ changed: false,
38
+ index: await this.#storage.getIndex(modelConstructor),
39
+ });
40
+ }
41
+
42
+ return this.indexCache.get(modelConstructorName)?.index;
43
+ }
44
+
45
+ /**
46
+ * Get a Map of indexes that have been tainted
47
+ * @return {Map<String, Object>}
48
+ */
49
+ getTaintedIndexes() {
50
+ return new Map(
51
+ this.indexCache
52
+ .entries()
53
+ .filter(([_name, cache]) => cache.changed)
54
+ .map(([name, {index}]) => [name, index]),
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Update a given index
60
+ * @param {string} modelConstructorName
61
+ * @return {Object}
62
+ */
63
+ updateIndex(modelConstructorName, index) {
64
+ this.indexCache.set(modelConstructorName, {index, changed: true});
65
+ }
66
+
67
+ /**
68
+ * Get a given search index
69
+ * @param {Model.constructor} modelConstructor
70
+ * @return {Object}
71
+ */
72
+ async getSearchIndex(modelConstructor) {
73
+ const modelConstructorName = modelConstructor.name;
74
+
75
+ if (!this.searchIndexCache.has(modelConstructorName)) {
76
+ this.searchIndexCache.set(modelConstructorName, {
77
+ changed: false,
78
+ index: await this.#storage.getSearchIndex(modelConstructor),
79
+ });
80
+ }
81
+
82
+ return this.searchIndexCache.get(modelConstructorName)?.index;
83
+ }
84
+
85
+ /**
86
+ * Get a Map of search indexes that have been tainted
87
+ * @return {Map<String, Object>}
88
+ */
89
+ getTaintedSearchIndexes() {
90
+ return new Map(
91
+ this.searchIndexCache
92
+ .entries()
93
+ .filter(([_name, cache]) => cache.changed)
94
+ .map(([name, {index}]) => [name, index]),
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Update a given search index
100
+ * @param {string} modelConstructorName
101
+ * @return {Object}
102
+ */
103
+ updateSearchIndex(modelConstructorName, index) {
104
+ this.searchIndexCache.set(modelConstructorName, {index, changed: true});
105
+ }
106
+ }
107
+
10
108
  /**
11
109
  * @class Connection
12
110
  */
@@ -30,7 +128,7 @@ export default class Connection {
30
128
  */
31
129
  constructor(storage, models) {
32
130
  this.#storage = storage;
33
- this.#models = Object.fromEntries((models ?? []).map(model => [model.name, model]));
131
+ this.#models = resolveModels(Object.fromEntries((models ?? []).map(model => [model.name, model])));
34
132
 
35
133
  if (!this.#storage) throw new MissingArgumentsConnectionError('No storage engine provided');
36
134
  }
@@ -58,11 +156,10 @@ export default class Connection {
58
156
  *
59
157
  * and fetches all data for a model.
60
158
  * @param {Object} dryModel
159
+ * @param {Map?} modelCache
61
160
  * @return {Promise<Model>}
62
161
  */
63
- async hydrate(dryModel) {
64
- const hydratedModels = {};
65
-
162
+ async hydrate(dryModel, modelCache = new Map()) {
66
163
  /**
67
164
  * Recursively hydrates a single model and its nested properties.
68
165
  *
@@ -70,18 +167,20 @@ export default class Connection {
70
167
  * @returns {Promise<Model>} The hydrated model instance.
71
168
  */
72
169
  const hydrateModel = async (modelToProcess) => {
73
- hydratedModels[modelToProcess.id] = modelToProcess;
170
+ modelCache.set(modelToProcess.id, modelToProcess);
74
171
 
75
172
  for (const [name, property] of Object.entries(modelToProcess)) {
76
173
  if (Model.isDryModel(property)) {
77
174
  // skipcq: JS-0129
78
175
  modelToProcess[name] = await hydrateSubModel(property);
79
- } else if (Array.isArray(property) && Model.isDryModel(property[0])) {
176
+ } else if (Array.isArray(property) && property.length && Model.isDryModel(property[0])) {
80
177
  // skipcq: JS-0129
81
178
  modelToProcess[name] = await hydrateModelList(property);
82
179
  }
83
180
  }
84
181
 
182
+ modelCache.set(modelToProcess.id, modelToProcess);
183
+
85
184
  return modelToProcess;
86
185
  };
87
186
 
@@ -92,14 +191,12 @@ export default class Connection {
92
191
  * @returns {Promise<Model>} The fully hydrated sub-model.
93
192
  */
94
193
  const hydrateSubModel = async (property) => {
95
- if (hydratedModels[property.id]) {
96
- return hydratedModels[property.id];
97
- }
194
+ if (modelCache.has(property.id)) return modelCache.get(property.id);
98
195
 
99
196
  const subModel = await this.get(property.id);
100
197
 
101
198
  const hydratedSubModel = await hydrateModel(subModel);
102
- hydratedModels[property.id] = hydratedSubModel;
199
+ modelCache.set(property.id, hydratedSubModel);
103
200
  return hydratedSubModel;
104
201
  };
105
202
 
@@ -111,20 +208,18 @@ export default class Connection {
111
208
  */
112
209
  const hydrateModelList = async (property) => {
113
210
  const newModelList = await Promise.all(property.map(subModel => {
114
- if (hydratedModels[subModel.id]) {
115
- return hydratedModels[subModel.id];
116
- }
211
+ if (modelCache.has(subModel.id)) return modelCache.get(subModel.id);
117
212
 
118
213
  return this.get(subModel.id);
119
214
  }));
120
215
 
121
216
  return Promise.all(newModelList.map(async subModel => {
122
- if (hydratedModels[subModel.id]) {
123
- return hydratedModels[subModel.id];
124
- }
217
+ if (modelCache.has(subModel.id)) return modelCache.get(subModel.id);
125
218
 
126
219
  const hydratedSubModel = await hydrateModel(subModel);
127
- hydratedModels[hydratedSubModel.id] = hydratedSubModel;
220
+
221
+ modelCache.set(hydratedSubModel.id, hydratedSubModel);
222
+
128
223
  return hydratedSubModel;
129
224
  }));
130
225
  };
@@ -136,6 +231,7 @@ export default class Connection {
136
231
  * Persists a model if it has changed, and updates all related models and their indexes
137
232
  * @param {Model} model
138
233
  * @return {Promise<void>}
234
+ * @throws {ValidationError|ModelNotRegisteredConnectionError}
139
235
  */
140
236
  async put(model) {
141
237
  const processedModels = [];
@@ -153,7 +249,7 @@ export default class Connection {
153
249
 
154
250
  processedModels.push(modelToProcess.id);
155
251
 
156
- if (!Object.keys(this.#models).includes(modelToProcess.constructor.name))
252
+ if (!this.#models.has(modelToProcess.constructor.name))
157
253
  throw new ModelNotRegisteredConnectionError(modelToProcess, this.#storage);
158
254
 
159
255
  modelToProcess.validate();
@@ -197,7 +293,7 @@ export default class Connection {
197
293
  await Promise.all([
198
294
  Promise.all(modelsToPut.map(m => this.#storage.putModel(m.toData()))),
199
295
  Promise.all(Object.entries(modelsToReindex).map(async ([constructorName, models]) => {
200
- const modelConstructor = this.#models[constructorName];
296
+ const modelConstructor = this.#models.get(constructorName);
201
297
  const index = await this.#storage.getIndex(modelConstructor);
202
298
 
203
299
  await this.#storage.putIndex(modelConstructor, {
@@ -206,7 +302,7 @@ export default class Connection {
206
302
  });
207
303
  })),
208
304
  Promise.all(Object.entries(modelsToReindexSearch).map(async ([constructorName, models]) => {
209
- const modelConstructor = this.#models[constructorName];
305
+ const modelConstructor = this.#models.get(constructorName);
210
306
  const index = await this.#storage.getSearchIndex(modelConstructor);
211
307
 
212
308
  await this.#storage.putSearchIndex(modelConstructor, {
@@ -219,157 +315,153 @@ export default class Connection {
219
315
 
220
316
  /**
221
317
  * Delete a model and update indexes that reference it
222
- * @param {Model} model
223
- * @param {Array<string>} propagateTo - List of model ids that are expected to be deleted
318
+ * @param {Model} subject
319
+ * @param {Array<string>} propagateTo - List of model ids that are expected to be deleted or updated.
224
320
  * @throws {ModelNotRegisteredConnectionError}
225
321
  * @throws {ModelNotFoundStorageEngineError}
226
322
  */
227
- async delete(model, propagateTo = []) {
228
- const processedModels = [];
229
- const modelsToDelete = [];
230
- const modelsToPut = [];
231
- const indexCache = {};
232
- const indexActions = {};
233
- const searchIndexCache = {};
234
- const searchIndexActions = {};
235
- const modelCache = {};
236
-
237
- propagateTo.push(model.id);
323
+ async delete(subject, propagateTo = []) {
324
+ const state = new State(this.#storage);
325
+ const modelsToCheck = this.#findLinkedModelClasses(subject);
326
+ const modelsToDelete = new Set([subject.id]);
327
+ const modelsToUpdate = new Set();
328
+ const indexesToUpdate = new Set();
329
+ const searchIndexesToUpdate = new Set();
330
+
331
+ subject = await this.hydrate(subject, state.modelCache);
332
+
333
+ if (!propagateTo.includes(subject.id)) {
334
+ propagateTo.push(subject.id);
335
+ }
238
336
 
239
- /**
240
- * Process a model for deletion
241
- * @param {Model} modelToProcess
242
- * @return {Promise<void>}
243
- */
244
- const processModel = async (modelToProcess) => {
245
- if (processedModels.includes(modelToProcess.id)) return;
337
+ // Populate model cache
338
+ for (const [[modelName, propertyName, type, direction], _modelConstructor] of modelsToCheck) {
339
+ const query = {};
246
340
 
247
- processedModels.push(modelToProcess.id);
341
+ if (direction === 'up') {
342
+ if (type === 'one') {
343
+ query[propertyName] = {id: {$is: subject.id}};
344
+ }
248
345
 
249
- const modelsToProcess = [];
250
- if (!Object.keys(this.#models).includes(modelToProcess.constructor.name))
251
- throw new ModelNotRegisteredConnectionError(modelToProcess, this.#storage);
346
+ if (type === 'many') {
347
+ query[propertyName] = {
348
+ $contains: {
349
+ id: {$is: subject.id},
350
+ },
351
+ };
352
+ }
353
+ }
252
354
 
253
- const currentModel = modelCache[modelToProcess.id] ?? await this.get(modelToProcess.id);
254
- modelCache[currentModel.id] = currentModel;
355
+ const foundModels =
356
+ _.isEqual(query, {}) ?
357
+ (
358
+ Array.isArray(subject[propertyName]) ?
359
+ subject[propertyName] : [subject[propertyName]]
360
+ ) : new FindIndex(this.#models.get(modelName), await state.getIndex(this.#models.get(modelName))).query(query);
255
361
 
256
- if (!modelsToDelete.includes(currentModel.id)) modelsToDelete.push(currentModel.id);
362
+ for (const foundModel of foundModels) {
363
+ if (!state.modelCache.has(foundModel.id)) {
364
+ state.modelCache.set(foundModel.id, await this.hydrate(foundModel, state.modelCache));
365
+ }
366
+ }
257
367
 
258
- const modelToProcessConstructor = this.#getModelConstructorFromId(modelToProcess.id);
368
+ // for deletes, update models that link to the subject
369
+ if (direction === 'up') {
370
+ if (type === 'one') {
371
+ for (const foundModel of foundModels) {
372
+ const cachedModel = state.modelCache.get(foundModel.id);
259
373
 
260
- indexActions[modelToProcessConstructor.name] = indexActions[modelToProcessConstructor.name] ?? [];
261
- searchIndexActions[modelToProcessConstructor.name] = searchIndexActions[modelToProcessConstructor.name] ?? [];
374
+ if (foundModel.constructor[propertyName]._required) {
375
+ modelsToDelete.add(foundModel.id);
376
+ continue;
377
+ }
262
378
 
263
- indexActions[modelToProcessConstructor.name].push(['delete', modelToProcess]);
379
+ cachedModel[propertyName] = undefined;
380
+ state.modelCache.set(foundModel.id, cachedModel);
381
+ modelsToUpdate.add(foundModel.id);
382
+ }
383
+ }
264
384
 
265
- if (currentModel.constructor.searchProperties().length) {
266
- searchIndexActions[modelToProcessConstructor.name].push(['delete', modelToProcess]);
267
- }
385
+ if (type === 'many') {
386
+ for (const foundModel of foundModels) {
387
+ const cachedModel = state.modelCache.get(foundModel.id);
268
388
 
269
- const linkedModels = await this.#getInstancesLinkedTo(modelToProcess, indexCache);
270
- const links = this.#getLinksFor(modelToProcess.constructor);
271
- Object.values(Object.fromEntries(await Promise.all(
272
- Object.entries(linkedModels)
273
- .map(async ([modelConstructor, updatableModels]) => [
274
- modelConstructor,
275
- await Promise.all(updatableModels.map(async m => {
276
- const upToDateModel = modelCache[m.id] ?? await this.get(m.id);
277
- modelCache[upToDateModel.id] = upToDateModel;
278
- return upToDateModel;
279
- })),
280
- ]),
281
- ))).flat(1)
282
- .forEach(m =>
283
- Object.entries(links[m.constructor.name])
284
- .forEach(([linkName, modelConstructor]) => {
285
- if ((
286
- typeof modelConstructor[linkName] === 'function' &&
287
- !/^class/.test(Function.prototype.toString.call(modelConstructor[linkName])) &&
288
- !Model.isModel(modelConstructor[linkName]) ?
289
- modelConstructor[linkName]() : modelConstructor
290
- )._required) {
291
- if (!modelsToDelete.includes(m.id)) modelsToDelete.push(m.id);
292
- modelsToProcess.push(m);
293
- } else {
294
- const foundModelConstructor = this.#getModelConstructorFromId(m.id);
295
- m[linkName] = undefined;
296
- modelsToPut.push(m);
297
-
298
- indexActions[foundModelConstructor.name] = indexActions[foundModelConstructor.name] ?? [];
299
- indexActions[foundModelConstructor.name].push(['reindex', m]);
300
-
301
- if (m.constructor.searchProperties().length) {
302
- searchIndexActions[foundModelConstructor.name] = searchIndexActions[foundModelConstructor.name] ?? [];
303
- searchIndexActions[foundModelConstructor.name].push(['reindex', m]);
304
- }
305
- }
306
- }),
307
- );
308
-
309
- for (const modelToBeProcessed of modelsToProcess) {
310
- await processModel(modelToBeProcessed);
389
+ cachedModel[propertyName] = cachedModel[propertyName].filter(m => m.id !== subject.id);
390
+ state.modelCache.set(foundModel.id, cachedModel);
391
+ modelsToUpdate.add(foundModel.id);
392
+ }
393
+ }
311
394
  }
312
- };
313
-
314
- await processModel(model);
395
+ }
315
396
 
316
- const unrequestedDeletions = modelsToDelete.filter(m => !propagateTo.includes(m));
397
+ const unrequestedDeletions = [...modelsToDelete].filter(id => !propagateTo.includes(id));
398
+ const unrequestedUpdates = [...modelsToUpdate].filter(id => !propagateTo.includes(id));
317
399
 
318
- if (unrequestedDeletions.length) {
319
- throw new DeleteHasUnintendedConsequencesStorageEngineError(model.id, {
320
- willDelete: unrequestedDeletions,
400
+ if (unrequestedDeletions.length || unrequestedUpdates.length) {
401
+ throw new DeleteHasUnintendedConsequencesStorageEngineError(subject.id, {
402
+ willDelete: unrequestedDeletions.map(id => state.modelCache.get(id)),
403
+ willUpdate: unrequestedUpdates.map(id => state.modelCache.get(id)),
321
404
  });
322
405
  }
323
406
 
324
- await Promise.all([
325
- Promise.all(Object.entries(indexActions).map(async ([constructorName, actions]) => {
326
- const modelConstructor = this.#models[constructorName];
327
- indexCache[constructorName] = indexCache[constructorName] ?? await this.#storage.getIndex(modelConstructor);
407
+ await Promise.all(
408
+ new Set([...modelsToDelete, ...modelsToUpdate].map(this.#getModelConstructorFromId.bind(this)))
409
+ .values()
410
+ .map(async modelConstructor => {
411
+ await state.getIndex(modelConstructor);
412
+ indexesToUpdate.add(modelConstructor.name);
328
413
 
329
- actions.forEach(([action, actionModel]) => {
330
- if (action === 'delete') {
331
- indexCache[constructorName] = _.omit(indexCache[constructorName], [actionModel.id]);
332
- }
333
- if (action === 'reindex') {
334
- indexCache[constructorName] = {
335
- ...indexCache[constructorName],
336
- [actionModel.id]: actionModel.toIndexData(),
337
- };
414
+ if (modelConstructor.searchProperties().length) {
415
+ await state.getSearchIndex(modelConstructor);
416
+ searchIndexesToUpdate.add(modelConstructor.name);
338
417
  }
339
- });
340
- })),
341
- Promise.all(Object.entries(searchIndexActions).map(async ([constructorName, actions]) => {
342
- const modelConstructor = this.#models[constructorName];
343
- searchIndexCache[constructorName] = searchIndexCache[constructorName] ?? await this.#storage.getSearchIndex(modelConstructor);
418
+ }),
419
+ );
344
420
 
345
- actions.forEach(([action, actionModel]) => {
346
- if (action === 'delete') {
347
- searchIndexCache[constructorName] = _.omit(searchIndexCache[constructorName], [actionModel.id]);
348
- }
349
- if (action === 'reindex') {
350
- searchIndexCache[constructorName] = {
351
- ...searchIndexCache[constructorName],
352
- [actionModel.id]: actionModel.toSearchData(),
353
- };
354
- }
355
- });
356
- })),
357
- ]);
421
+ for (const indexName of searchIndexesToUpdate) {
422
+ const index = await state.getSearchIndex(this.#models.get(indexName));
423
+
424
+ for (const model of [...modelsToUpdate].filter(i => i.startsWith(indexName))) {
425
+ index[model] = state.modelCache.get(model).toSearchData();
426
+ }
427
+
428
+ for (const model of [...modelsToDelete].filter(i => i.startsWith(indexName))) {
429
+ delete index[model];
430
+ }
431
+
432
+ state.updateSearchIndex(indexName, index);
433
+ }
434
+
435
+ for (const indexName of indexesToUpdate) {
436
+ const index = await state.getIndex(this.#models.get(indexName));
437
+
438
+ for (const model of [...modelsToUpdate].filter(i => i.startsWith(indexName))) {
439
+ index[model] = state.modelCache.get(model).toIndexData();
440
+ }
441
+
442
+ for (const model of [...modelsToDelete].filter(i => i.startsWith(indexName))) {
443
+ delete index[model];
444
+ }
445
+
446
+ state.updateIndex(indexName, index);
447
+ }
358
448
 
359
449
  await Promise.all([
360
- Promise.all(modelsToPut.map(m => this.#storage.putModel(m.toData()))),
361
- Promise.all(modelsToDelete.map(m => this.#storage.deleteModel(m))),
362
- Promise.all(
363
- Object.entries(indexCache)
364
- .map(([constructorName, index]) => this.#storage.putIndex(this.#models[constructorName], index)),
450
+ Promise.all([...modelsToUpdate].map(id => this.#storage.putModel(state.modelCache.get(id).toData()))),
451
+ Promise.all([...modelsToDelete].map(id => this.#storage.deleteModel(id))),
452
+ Promise.all(state
453
+ .getTaintedIndexes()
454
+ .entries()
455
+ .map(([modelConstructorName, index]) =>
456
+ this.#storage.putIndex(this.#models.get(modelConstructorName), index),
457
+ ),
365
458
  ),
366
- Promise.all(
367
- Object.entries(searchIndexCache)
368
- .map(([constructorName, index]) =>
369
- this.#models[constructorName].searchProperties().length > 0 ?
370
- this.#storage.putSearchIndex(this.#models[constructorName], index) :
371
- Promise.resolve(),
372
- ),
459
+ Promise.all(state
460
+ .getTaintedSearchIndexes()
461
+ .entries()
462
+ .map(([modelConstructorName, index]) =>
463
+ this.#storage.putSearchIndex(this.#models.get(modelConstructorName), index),
464
+ ),
373
465
  ),
374
466
  ]);
375
467
  }
@@ -418,7 +510,7 @@ export default class Connection {
418
510
 
419
511
  const engine = CreateTransactionalStorageEngine(operations, this.#storage);
420
512
 
421
- const transaction = new this.constructor(engine, Object.values(this.#models));
513
+ const transaction = new this.constructor(engine, [...this.#models.values()]);
422
514
 
423
515
  transaction.commit = async () => {
424
516
  try {
@@ -456,6 +548,10 @@ export default class Connection {
456
548
  if (operation.method === 'putSearchIndex')
457
549
  await this.#storage.putSearchIndex(operation.args[0], operation.original);
458
550
  }
551
+
552
+ if (operation.method === 'putModel' && operation.committed && !operation.original) {
553
+ await this.#storage.deleteModel(operation.args[0].id);
554
+ }
459
555
  }
460
556
 
461
557
  throw new CommitFailedTransactionError(operations, error);
@@ -473,7 +569,7 @@ export default class Connection {
473
569
  */
474
570
  #getModelConstructorFromId(modelId) {
475
571
  const modelName = modelId.split('/')[0];
476
- const modelConstructor = this.#models[modelName];
572
+ const modelConstructor = this.#models.get(modelName);
477
573
 
478
574
  if (!modelConstructor) throw new ModelNotRegisteredConnectionError(modelName, this.#storage);
479
575
 
@@ -481,80 +577,111 @@ export default class Connection {
481
577
  }
482
578
 
483
579
  /**
484
- * Get model classes that are directly linked to the given model in either direction
485
- * @param {Model.constructor} model
486
- * @return {Record<string, Record<string, Model.constructor>>}
580
+ * Finds all model classes that are linked to the specified subject model.
581
+ *
582
+ * @private
583
+ * @param {Object} subject - The subject model instance to find linked model classes for.
584
+ * @param {string} subject.id - The ID of the subject model, used to identify its constructor.
585
+ *
586
+ * @returns {Map<Array<string|'one'|'many'|'up'|'down'>, Function>} A map where:
587
+ * - Keys are arrays with the format [modelName, propertyName, cardinality, direction]
588
+ * - modelName: The name of the linked model class
589
+ * - propertyName: The property name where the link is defined
590
+ * - cardinality: Either 'one' (one-to-one) or 'many' (one-to-many)
591
+ * - direction: 'down' for links defined in the subject model pointing to other models
592
+ * 'up' for links defined in other models pointing to the subject
593
+ * - Values are the model constructor functions for the linked classes
594
+ *
595
+ * @description
596
+ * This method identifies four types of relationships:
597
+ * 1. One-to-one links from subject to other models ('one', 'down')
598
+ * 2. One-to-many links from subject to other models ('many', 'down')
599
+ * 3. One-to-one links from other models to subject ('one', 'up')
600
+ * 4. One-to-many links from other models to subject ('many', 'up')
487
601
  */
488
- #getLinksFor(model) {
489
- return Object.fromEntries(
490
- Object.entries(this.#getAllModelLinks())
491
- .filter(([modelName, links]) =>
492
- model.name === modelName ||
493
- Object.values(links).some((link) => link.name === model.name),
494
- ),
495
- );
496
- }
602
+ #findLinkedModelClasses(subject) {
603
+ const subjectModelConstructor = this.#getModelConstructorFromId(subject.id);
604
+ const modelsThatLinkToThisSubject = new Map();
605
+
606
+ for (const [propertyName, propertyType] of Object.entries(subjectModelConstructor)) {
607
+ // The model is a one to one link
608
+ if (propertyType.prototype instanceof Model) {
609
+ modelsThatLinkToThisSubject.set([propertyType.name, propertyName, 'one', 'down'], propertyType);
610
+ }
611
+ // The model is a one to many link
497
612
 
498
- /**
499
- * Get all model links
500
- * @return {Record<string, Record<string, Model.constructor>>}
501
- */
502
- #getAllModelLinks() {
503
- return Object.entries(this.#models)
504
- .map(([registeredModelName, registeredModelClass]) =>
505
- Object.entries(registeredModelClass)
506
- .map(([propertyName, propertyProperty]) => [
507
- registeredModelName,
508
- propertyName,
509
- typeof propertyProperty === 'function' &&
510
- !/^class/.test(Function.prototype.toString.call(propertyProperty)) &&
511
- !Model.isModel(propertyProperty) ?
512
- propertyProperty() : propertyProperty,
513
- ])
514
- .filter(([_m, _p, type]) => Model.isModel(type))
515
- .map(([containingModel, propertyName, propertyProperty]) => ({
516
- containingModel,
517
- propertyName,
518
- propertyProperty,
519
- })),
520
- )
521
- .flat()
522
- .reduce((accumulator, {containingModel, propertyName, propertyProperty}) => {
523
- accumulator[containingModel] = accumulator[containingModel] ?? {};
524
- accumulator[containingModel][propertyName] = propertyProperty;
525
- return accumulator;
526
- }, {});
613
+ if (propertyType._items?.prototype instanceof Model) {
614
+ modelsThatLinkToThisSubject.set([propertyType._items.name, propertyName, 'many', 'down'], propertyType);
615
+ }
616
+ }
617
+
618
+ for (const [modelName, modelConstructor] of this.#models) {
619
+ for (const [propertyName, propertyType] of Object.entries(modelConstructor.properties)) {
620
+ // The model is a one to one link
621
+ if (propertyType === subjectModelConstructor || propertyType.prototype instanceof subjectModelConstructor) {
622
+ modelsThatLinkToThisSubject.set([modelName, propertyName, 'one', 'up'], propertyType);
623
+ }
624
+
625
+ // The model is a one to many link
626
+ if (propertyType._items === subjectModelConstructor || propertyType._items?.prototype instanceof subjectModelConstructor) {
627
+ modelsThatLinkToThisSubject.set([modelName, propertyName, 'many', 'up'], propertyType);
628
+ }
629
+ }
630
+ }
631
+
632
+ return modelsThatLinkToThisSubject;
527
633
  }
634
+ }
528
635
 
529
- /**
530
- * Get model instance that are directly linked to the given model in either direction
531
- * @param {Model} model
532
- * @param {object} cache
533
- * @return {Record<string, Record<string, Model>>}
534
- */
535
- async #getInstancesLinkedTo(model, cache) {
536
- return Object.fromEntries(
537
- Object.entries(
538
- await Promise.all(
539
- Object.entries(this.#getLinksFor(model.constructor))
540
- .map(([name, _index]) =>
541
- cache[name] ? Promise.resolve([name, Object.values(cache[name])]) :
542
- this.#storage.getIndex(this.#models[name])
543
- .then(i => {
544
- cache[name] = i;
545
- return [name, Object.values(i)];
546
- }),
547
- ),
548
- ).then(Object.fromEntries),
549
- ).map(([name, index]) => [
550
- name,
551
- index.map(item => Object.fromEntries(
552
- Object.entries(item)
553
- .filter(([propertyName, property]) => propertyName === 'id' || property?.id === model.id),
554
- )).filter(item => Object.keys(item).length > 1),
555
- ]),
556
- );
636
+ /**
637
+ * Resolves model properties that are factory functions to their class values.
638
+ *
639
+ * @private
640
+ * @param {Object<string, Function>} models - An object mapping model names to model constructor functions
641
+ * @returns {Map<string, Function>} A map of model names to model constructors with all factory
642
+ * function properties class to their bare model instances
643
+ *
644
+ * @description
645
+ * This method processes each property of each model constructor to resolve any factory functions.
646
+ * It skips:
647
+ * - Special properties like 'indexedProperties', 'searchProperties', and '_required'
648
+ * - Properties that are already bare model instances (have a prototype inheriting from Model)
649
+ * - Properties with a defined '_type' (basic types)
650
+ *
651
+ * For all other properties (assumed to be factory functions), it calls the function to get
652
+ * the class value and updates the model constructor.
653
+ */
654
+ function resolveModels(models) {
655
+ const resolvedToBareModels = new Map();
656
+
657
+ for (const [modelName, modelConstructor] of Object.entries(models)) {
658
+ for (const [propertyName, propertyType] of Object.entries(modelConstructor)) {
659
+ // The property is a builtin
660
+ if ([
661
+ 'indexedProperties',
662
+ 'searchProperties',
663
+ '_required',
664
+ ].includes(propertyName)) {
665
+ continue;
666
+ }
667
+
668
+ // The property is a bare model
669
+ if (propertyType.prototype instanceof Model) {
670
+ continue;
671
+ }
672
+
673
+ // The property is a basic type
674
+ if (propertyType._type) {
675
+ continue;
676
+ }
677
+
678
+ modelConstructor[propertyName] = propertyType();
679
+ }
680
+
681
+ resolvedToBareModels.set(modelName, modelConstructor);
557
682
  }
683
+
684
+ return resolvedToBareModels;
558
685
  }
559
686
 
560
687
  /**