@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/README.md +4 -4
- package/docs/code-quirks.md +9 -9
- package/docs/defining-models.md +8 -8
- package/docs/model-properties.md +54 -23
- package/docs/models-as-properties.md +30 -30
- package/docs/search-queries.md +8 -8
- package/docs/structured-queries.md +8 -8
- package/package.json +1 -1
- package/src/Connection.js +344 -217
- package/src/Schema.js +3 -3
- package/src/data/Model.js +63 -9
- package/src/data/Property.js +2 -0
- package/src/data/properties/ArrayType.js +4 -2
- package/src/data/properties/BooleanType.js +2 -2
- package/src/data/properties/CustomType.js +4 -4
- package/src/data/properties/DateType.js +3 -3
- package/src/data/properties/NumberType.js +2 -2
- package/src/data/properties/SlugType.js +1 -1
- package/src/data/properties/StringType.js +2 -2
- package/src/data/properties/Type.js +5 -3
- package/src/engine/storage/StorageEngine.js +3 -1
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
|
-
|
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 (
|
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
|
-
|
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 (
|
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 (
|
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
|
-
|
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 (!
|
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
|
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
|
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}
|
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(
|
228
|
-
const
|
229
|
-
const
|
230
|
-
const
|
231
|
-
const
|
232
|
-
const
|
233
|
-
const
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
propagateTo.
|
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
|
-
|
241
|
-
|
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
|
-
|
341
|
+
if (direction === 'up') {
|
342
|
+
if (type === 'one') {
|
343
|
+
query[propertyName] = {id: {$is: subject.id}};
|
344
|
+
}
|
248
345
|
|
249
|
-
|
250
|
-
|
251
|
-
|
346
|
+
if (type === 'many') {
|
347
|
+
query[propertyName] = {
|
348
|
+
$contains: {
|
349
|
+
id: {$is: subject.id},
|
350
|
+
},
|
351
|
+
};
|
352
|
+
}
|
353
|
+
}
|
252
354
|
|
253
|
-
const
|
254
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
261
|
-
|
374
|
+
if (foundModel.constructor[propertyName]._required) {
|
375
|
+
modelsToDelete.add(foundModel.id);
|
376
|
+
continue;
|
377
|
+
}
|
262
378
|
|
263
|
-
|
379
|
+
cachedModel[propertyName] = undefined;
|
380
|
+
state.modelCache.set(foundModel.id, cachedModel);
|
381
|
+
modelsToUpdate.add(foundModel.id);
|
382
|
+
}
|
383
|
+
}
|
264
384
|
|
265
|
-
|
266
|
-
|
267
|
-
|
385
|
+
if (type === 'many') {
|
386
|
+
for (const foundModel of foundModels) {
|
387
|
+
const cachedModel = state.modelCache.get(foundModel.id);
|
268
388
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
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(
|
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(
|
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
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
330
|
-
|
331
|
-
|
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
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
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(
|
361
|
-
Promise.all(modelsToDelete.map(
|
362
|
-
Promise.all(
|
363
|
-
|
364
|
-
|
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
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
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,
|
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
|
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
|
-
*
|
485
|
-
*
|
486
|
-
* @
|
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
|
-
#
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
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
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
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
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
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
|
/**
|