@acodeninja/persist 3.0.0-next.26 → 3.0.0-next.27
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/docs/model-properties.md +31 -0
- package/package.json +1 -1
- package/src/Connection.js +268 -214
- package/src/Schema.js +2 -2
- package/src/data/Model.js +56 -5
- package/src/data/Property.js +2 -0
- package/src/data/properties/Type.js +1 -1
- package/src/engine/storage/StorageEngine.js +3 -1
package/docs/model-properties.md
CHANGED
@@ -173,3 +173,34 @@ class RequiredStringModel extends Persist.Model {
|
|
173
173
|
}
|
174
174
|
}
|
175
175
|
```
|
176
|
+
|
177
|
+
## Custom Property Types
|
178
|
+
|
179
|
+
Under the hood, model validation uses the [ajv](https://ajv.js.org/) library with [ajv-formats](https://ajv.js.org/packages/ajv-formats.html) included. Because of this, you can create your own property types.
|
180
|
+
|
181
|
+
Say you want to attach an IPv4 address to your models. The following type class can accomplish this.
|
182
|
+
|
183
|
+
```javascript
|
184
|
+
import Persist from '@acodeninja/persist';
|
185
|
+
|
186
|
+
class IPv4Type extends Persist.Property.Type {
|
187
|
+
static {
|
188
|
+
// Set the type of the property to string
|
189
|
+
this._type = 'string';
|
190
|
+
// Use the ajv extended format "ipv4"
|
191
|
+
this._format = 'ipv4';
|
192
|
+
// Ensure that even when minified, the name of the constructor is IPv4
|
193
|
+
Object.defineProperty(this, 'name', {value: 'IPv4'});
|
194
|
+
}
|
195
|
+
}
|
196
|
+
```
|
197
|
+
|
198
|
+
This type can then be used in any model as needed.
|
199
|
+
|
200
|
+
```javascript
|
201
|
+
import Persist from '@acodeninja/persist';
|
202
|
+
|
203
|
+
class StaticIP extends Persist.Model {
|
204
|
+
static ip = IPv4Type.required;
|
205
|
+
}
|
206
|
+
```
|
package/package.json
CHANGED
package/src/Connection.js
CHANGED
@@ -30,7 +30,7 @@ export default class Connection {
|
|
30
30
|
*/
|
31
31
|
constructor(storage, models) {
|
32
32
|
this.#storage = storage;
|
33
|
-
this.#models = Object.fromEntries((models ?? []).map(model => [model.name, model]));
|
33
|
+
this.#models = resolveModels(Object.fromEntries((models ?? []).map(model => [model.name, model])));
|
34
34
|
|
35
35
|
if (!this.#storage) throw new MissingArgumentsConnectionError('No storage engine provided');
|
36
36
|
}
|
@@ -58,11 +58,10 @@ export default class Connection {
|
|
58
58
|
*
|
59
59
|
* and fetches all data for a model.
|
60
60
|
* @param {Object} dryModel
|
61
|
+
* @param {Map?} modelCache
|
61
62
|
* @return {Promise<Model>}
|
62
63
|
*/
|
63
|
-
async hydrate(dryModel) {
|
64
|
-
const hydratedModels = {};
|
65
|
-
|
64
|
+
async hydrate(dryModel, modelCache = new Map()) {
|
66
65
|
/**
|
67
66
|
* Recursively hydrates a single model and its nested properties.
|
68
67
|
*
|
@@ -70,7 +69,7 @@ export default class Connection {
|
|
70
69
|
* @returns {Promise<Model>} The hydrated model instance.
|
71
70
|
*/
|
72
71
|
const hydrateModel = async (modelToProcess) => {
|
73
|
-
|
72
|
+
modelCache.set(modelToProcess.id, modelToProcess);
|
74
73
|
|
75
74
|
for (const [name, property] of Object.entries(modelToProcess)) {
|
76
75
|
if (Model.isDryModel(property)) {
|
@@ -82,6 +81,8 @@ export default class Connection {
|
|
82
81
|
}
|
83
82
|
}
|
84
83
|
|
84
|
+
modelCache.set(modelToProcess.id, modelToProcess);
|
85
|
+
|
85
86
|
return modelToProcess;
|
86
87
|
};
|
87
88
|
|
@@ -92,14 +93,12 @@ export default class Connection {
|
|
92
93
|
* @returns {Promise<Model>} The fully hydrated sub-model.
|
93
94
|
*/
|
94
95
|
const hydrateSubModel = async (property) => {
|
95
|
-
if (
|
96
|
-
return hydratedModels[property.id];
|
97
|
-
}
|
96
|
+
if (modelCache.has(property.id)) return modelCache.get(property.id);
|
98
97
|
|
99
98
|
const subModel = await this.get(property.id);
|
100
99
|
|
101
100
|
const hydratedSubModel = await hydrateModel(subModel);
|
102
|
-
|
101
|
+
modelCache.set(property.id, hydratedSubModel);
|
103
102
|
return hydratedSubModel;
|
104
103
|
};
|
105
104
|
|
@@ -111,20 +110,18 @@ export default class Connection {
|
|
111
110
|
*/
|
112
111
|
const hydrateModelList = async (property) => {
|
113
112
|
const newModelList = await Promise.all(property.map(subModel => {
|
114
|
-
if (
|
115
|
-
return hydratedModels[subModel.id];
|
116
|
-
}
|
113
|
+
if (modelCache.has(subModel.id)) return modelCache.get(subModel.id);
|
117
114
|
|
118
115
|
return this.get(subModel.id);
|
119
116
|
}));
|
120
117
|
|
121
118
|
return Promise.all(newModelList.map(async subModel => {
|
122
|
-
if (
|
123
|
-
return hydratedModels[subModel.id];
|
124
|
-
}
|
119
|
+
if (modelCache.has(subModel.id)) return modelCache.get(subModel.id);
|
125
120
|
|
126
121
|
const hydratedSubModel = await hydrateModel(subModel);
|
127
|
-
|
122
|
+
|
123
|
+
modelCache.set(hydratedSubModel.id, hydratedSubModel);
|
124
|
+
|
128
125
|
return hydratedSubModel;
|
129
126
|
}));
|
130
127
|
};
|
@@ -153,7 +150,7 @@ export default class Connection {
|
|
153
150
|
|
154
151
|
processedModels.push(modelToProcess.id);
|
155
152
|
|
156
|
-
if (!
|
153
|
+
if (!this.#models.has(modelToProcess.constructor.name))
|
157
154
|
throw new ModelNotRegisteredConnectionError(modelToProcess, this.#storage);
|
158
155
|
|
159
156
|
modelToProcess.validate();
|
@@ -197,7 +194,7 @@ export default class Connection {
|
|
197
194
|
await Promise.all([
|
198
195
|
Promise.all(modelsToPut.map(m => this.#storage.putModel(m.toData()))),
|
199
196
|
Promise.all(Object.entries(modelsToReindex).map(async ([constructorName, models]) => {
|
200
|
-
const modelConstructor = this.#models
|
197
|
+
const modelConstructor = this.#models.get(constructorName);
|
201
198
|
const index = await this.#storage.getIndex(modelConstructor);
|
202
199
|
|
203
200
|
await this.#storage.putIndex(modelConstructor, {
|
@@ -206,7 +203,7 @@ export default class Connection {
|
|
206
203
|
});
|
207
204
|
})),
|
208
205
|
Promise.all(Object.entries(modelsToReindexSearch).map(async ([constructorName, models]) => {
|
209
|
-
const modelConstructor = this.#models
|
206
|
+
const modelConstructor = this.#models.get(constructorName);
|
210
207
|
const index = await this.#storage.getSearchIndex(modelConstructor);
|
211
208
|
|
212
209
|
await this.#storage.putSearchIndex(modelConstructor, {
|
@@ -219,158 +216,148 @@ export default class Connection {
|
|
219
216
|
|
220
217
|
/**
|
221
218
|
* 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
|
219
|
+
* @param {Model} subject
|
220
|
+
* @param {Array<string>} propagateTo - List of model ids that are expected to be deleted or updated.
|
224
221
|
* @throws {ModelNotRegisteredConnectionError}
|
225
222
|
* @throws {ModelNotFoundStorageEngineError}
|
226
223
|
*/
|
227
|
-
async delete(
|
228
|
-
const
|
229
|
-
const
|
230
|
-
const
|
231
|
-
const
|
232
|
-
const
|
233
|
-
const
|
234
|
-
|
235
|
-
|
224
|
+
async delete(subject, propagateTo = []) {
|
225
|
+
const modelCache = new Map();
|
226
|
+
const modelsToCheck = this.#findLinkedModelClasses(subject);
|
227
|
+
const modelsToDelete = new Set([subject.id]);
|
228
|
+
const modelsToUpdate = new Set();
|
229
|
+
const indexesToUpdate = new Set();
|
230
|
+
const searchIndexesToUpdate = new Set();
|
231
|
+
|
232
|
+
subject = await this.hydrate(subject, modelCache);
|
233
|
+
|
234
|
+
if (!propagateTo.includes(subject.id)) {
|
235
|
+
propagateTo.push(subject.id);
|
236
|
+
}
|
236
237
|
|
237
|
-
|
238
|
+
// Populate index and search index cache.
|
239
|
+
const [indexCache, searchIndexCache] = await this.#getModelIndexes(modelsToCheck);
|
238
240
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
*/
|
244
|
-
const processModel = async (modelToProcess) => {
|
245
|
-
if (processedModels.includes(modelToProcess.id)) return;
|
241
|
+
// Add model to be removed to index caches.
|
242
|
+
if (!indexCache.has(subject.constructor.name)) {
|
243
|
+
indexCache.set(subject.constructor.name, (await this.#storage.getIndex(subject.constructor)));
|
244
|
+
}
|
246
245
|
|
247
|
-
|
246
|
+
if (!searchIndexCache.has(subject.constructor.name)) {
|
247
|
+
searchIndexCache.set(subject.constructor.name, (await this.#storage.getSearchIndex(subject.constructor)));
|
248
|
+
}
|
248
249
|
|
249
|
-
|
250
|
-
|
251
|
-
|
250
|
+
// Populate model cache
|
251
|
+
for (const [[modelName, propertyName, type, direction], _modelConstructor] of modelsToCheck) {
|
252
|
+
const query = {};
|
252
253
|
|
253
|
-
|
254
|
-
|
254
|
+
if (direction === 'up') {
|
255
|
+
if (type === 'one') {
|
256
|
+
query[propertyName] = {id: {$is: subject.id}};
|
257
|
+
}
|
258
|
+
|
259
|
+
if (type === 'many') {
|
260
|
+
query[propertyName] = {
|
261
|
+
$contains: {
|
262
|
+
id: {$is: subject.id},
|
263
|
+
},
|
264
|
+
};
|
265
|
+
}
|
266
|
+
}
|
255
267
|
|
256
|
-
|
268
|
+
const foundModels =
|
269
|
+
_.isEqual(query, {}) ?
|
270
|
+
(
|
271
|
+
Array.isArray(subject[propertyName]) ?
|
272
|
+
subject[propertyName] : [subject[propertyName]]
|
273
|
+
) : new FindIndex(this.#models.get(modelName), indexCache.get(modelName)).query(query);
|
257
274
|
|
258
|
-
const
|
275
|
+
for (const foundModel of foundModels) {
|
276
|
+
if (!modelCache.has(foundModel.id)) {
|
277
|
+
modelCache.set(foundModel.id, await this.hydrate(foundModel, modelCache));
|
278
|
+
}
|
279
|
+
}
|
280
|
+
|
281
|
+
// for deletes, update models that link to the subject
|
282
|
+
if (direction === 'up') {
|
283
|
+
if (type === 'one') {
|
284
|
+
for (const foundModel of foundModels) {
|
285
|
+
const cachedModel = modelCache.get(foundModel.id);
|
259
286
|
|
260
|
-
|
261
|
-
|
287
|
+
if (foundModel.constructor[propertyName]._required) {
|
288
|
+
modelsToDelete.add(foundModel.id);
|
289
|
+
continue;
|
290
|
+
}
|
262
291
|
|
263
|
-
|
292
|
+
cachedModel[propertyName] = undefined;
|
293
|
+
modelCache.set(foundModel.id, cachedModel);
|
294
|
+
modelsToUpdate.add(foundModel.id);
|
295
|
+
}
|
296
|
+
}
|
264
297
|
|
265
|
-
|
266
|
-
|
298
|
+
if (type === 'many') {
|
299
|
+
for (const foundModel of foundModels) {
|
300
|
+
const cachedModel = modelCache.get(foundModel.id);
|
301
|
+
|
302
|
+
cachedModel[propertyName] = cachedModel[propertyName].filter(m => m.id !== subject.id);
|
303
|
+
modelCache.set(foundModel.id, cachedModel);
|
304
|
+
modelsToUpdate.add(foundModel.id);
|
305
|
+
}
|
306
|
+
}
|
267
307
|
}
|
308
|
+
}
|
309
|
+
|
310
|
+
const unrequestedDeletions = [...modelsToDelete].filter(id => !propagateTo.includes(id));
|
311
|
+
const unrequestedUpdates = [...modelsToUpdate].filter(id => !propagateTo.includes(id));
|
312
|
+
|
313
|
+
if (unrequestedDeletions.length || unrequestedUpdates.length) {
|
314
|
+
throw new DeleteHasUnintendedConsequencesStorageEngineError(subject.id, {
|
315
|
+
willDelete: unrequestedDeletions.map(id => modelCache.get(id)),
|
316
|
+
willUpdate: unrequestedUpdates.map(id => modelCache.get(id)),
|
317
|
+
});
|
318
|
+
}
|
268
319
|
|
269
|
-
|
270
|
-
const
|
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);
|
320
|
+
for (const modelId of [...modelsToDelete, ...modelsToUpdate]) {
|
321
|
+
const modelConstructorName = modelId.split('/')[0];
|
322
|
+
indexesToUpdate.add(modelConstructorName);
|
323
|
+
if (this.#models.get(modelConstructorName)?.searchProperties().length) {
|
324
|
+
searchIndexesToUpdate.add(modelConstructorName);
|
311
325
|
}
|
312
|
-
}
|
326
|
+
}
|
313
327
|
|
314
|
-
|
328
|
+
for (const indexName of searchIndexesToUpdate) {
|
329
|
+
const index = searchIndexCache.get(indexName);
|
315
330
|
|
316
|
-
|
331
|
+
for (const model of [...modelsToUpdate].filter(i => i.startsWith(indexName))) {
|
332
|
+
index[model] = modelCache.get(model).toSearchData();
|
333
|
+
}
|
317
334
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
335
|
+
for (const model of [...modelsToDelete].filter(i => i.startsWith(indexName))) {
|
336
|
+
delete index[model];
|
337
|
+
}
|
338
|
+
|
339
|
+
searchIndexCache.set(indexName, index);
|
322
340
|
}
|
323
341
|
|
324
|
-
|
325
|
-
|
326
|
-
const modelConstructor = this.#models[constructorName];
|
327
|
-
indexCache[constructorName] = indexCache[constructorName] ?? await this.#storage.getIndex(modelConstructor);
|
342
|
+
for (const indexName of indexesToUpdate) {
|
343
|
+
const index = indexCache.get(indexName);
|
328
344
|
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
}
|
333
|
-
if (action === 'reindex') {
|
334
|
-
indexCache[constructorName] = {
|
335
|
-
...indexCache[constructorName],
|
336
|
-
[actionModel.id]: actionModel.toIndexData(),
|
337
|
-
};
|
338
|
-
}
|
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);
|
345
|
+
for (const model of [...modelsToUpdate].filter(i => i.startsWith(indexName))) {
|
346
|
+
index[model] = modelCache.get(model).toIndexData();
|
347
|
+
}
|
344
348
|
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
...searchIndexCache[constructorName],
|
352
|
-
[actionModel.id]: actionModel.toSearchData(),
|
353
|
-
};
|
354
|
-
}
|
355
|
-
});
|
356
|
-
})),
|
357
|
-
]);
|
349
|
+
for (const model of [...modelsToDelete].filter(i => i.startsWith(indexName))) {
|
350
|
+
delete index[model];
|
351
|
+
}
|
352
|
+
|
353
|
+
indexCache.set(indexName, index);
|
354
|
+
}
|
358
355
|
|
359
356
|
await Promise.all([
|
360
|
-
Promise.all(
|
361
|
-
Promise.all(modelsToDelete.map(
|
362
|
-
Promise.all(
|
363
|
-
|
364
|
-
.map(([constructorName, index]) => this.#storage.putIndex(this.#models[constructorName], index)),
|
365
|
-
),
|
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
|
-
),
|
373
|
-
),
|
357
|
+
Promise.all([...modelsToUpdate].map(id => this.#storage.putModel(modelCache.get(id).toData()))),
|
358
|
+
Promise.all([...modelsToDelete].map(id => this.#storage.deleteModel(id))),
|
359
|
+
Promise.all([...indexesToUpdate].map(index => this.#storage.putIndex(this.#models.get(index), indexCache.get(index)))),
|
360
|
+
Promise.all([...searchIndexesToUpdate].map(index => this.#storage.putSearchIndex(this.#models.get(index), searchIndexCache.get(index)))),
|
374
361
|
]);
|
375
362
|
}
|
376
363
|
|
@@ -418,7 +405,7 @@ export default class Connection {
|
|
418
405
|
|
419
406
|
const engine = CreateTransactionalStorageEngine(operations, this.#storage);
|
420
407
|
|
421
|
-
const transaction = new this.constructor(engine,
|
408
|
+
const transaction = new this.constructor(engine, this.#models.values());
|
422
409
|
|
423
410
|
transaction.commit = async () => {
|
424
411
|
try {
|
@@ -473,7 +460,7 @@ export default class Connection {
|
|
473
460
|
*/
|
474
461
|
#getModelConstructorFromId(modelId) {
|
475
462
|
const modelName = modelId.split('/')[0];
|
476
|
-
const modelConstructor = this.#models
|
463
|
+
const modelConstructor = this.#models.get(modelName);
|
477
464
|
|
478
465
|
if (!modelConstructor) throw new ModelNotRegisteredConnectionError(modelName, this.#storage);
|
479
466
|
|
@@ -481,80 +468,147 @@ export default class Connection {
|
|
481
468
|
}
|
482
469
|
|
483
470
|
/**
|
484
|
-
*
|
485
|
-
*
|
486
|
-
* @
|
471
|
+
* Retrieves and caches index and search index information for specified models.
|
472
|
+
*
|
473
|
+
* @private
|
474
|
+
* @async
|
475
|
+
* @param {Array<Array<string, *>>} modelsToCheck - An array of arrays where the first element
|
476
|
+
* of each inner array is the model name to retrieve indexes for
|
477
|
+
* @returns {Promise<[Map<string, *>, Map<string, *>]>} A promise that resolves to a tuple containing:
|
478
|
+
* - indexCache: A Map of model names to their corresponding indexes
|
479
|
+
* - searchIndexCache: A Map of model names to their corresponding search indexes
|
480
|
+
*
|
481
|
+
* @description
|
482
|
+
* This method populates two caches for the specified models:
|
483
|
+
* 1. A regular index cache retrieved via storage.getIndex()
|
484
|
+
* 2. A search index cache retrieved via storage.getSearchIndex()
|
485
|
+
*
|
486
|
+
* If a model's indexes are already cached, they won't be fetched again.
|
487
|
+
* The method uses the internal storage interface to retrieve the indexes.
|
487
488
|
*/
|
488
|
-
#
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
)
|
495
|
-
|
489
|
+
async #getModelIndexes(modelsToCheck) {
|
490
|
+
const indexCache = new Map();
|
491
|
+
const searchIndexCache = new Map();
|
492
|
+
|
493
|
+
for (const [[modelName]] of modelsToCheck) {
|
494
|
+
if (!indexCache.has(modelName)) {
|
495
|
+
indexCache.set(modelName, await this.#storage.getIndex(this.#models.get(modelName)));
|
496
|
+
}
|
497
|
+
|
498
|
+
if (!searchIndexCache.has(modelName)) {
|
499
|
+
searchIndexCache.set(modelName, await this.#storage.getSearchIndex(this.#models.get(modelName)));
|
500
|
+
}
|
501
|
+
}
|
502
|
+
|
503
|
+
return [indexCache, searchIndexCache];
|
496
504
|
}
|
497
505
|
|
498
506
|
/**
|
499
|
-
*
|
500
|
-
*
|
507
|
+
* Finds all model classes that are linked to the specified subject model.
|
508
|
+
*
|
509
|
+
* @private
|
510
|
+
* @param {Object} subject - The subject model instance to find linked model classes for.
|
511
|
+
* @param {string} subject.id - The ID of the subject model, used to identify its constructor.
|
512
|
+
*
|
513
|
+
* @returns {Map<Array<string|'one'|'many'|'up'|'down'>, Function>} A map where:
|
514
|
+
* - Keys are arrays with the format [modelName, propertyName, cardinality, direction]
|
515
|
+
* - modelName: The name of the linked model class
|
516
|
+
* - propertyName: The property name where the link is defined
|
517
|
+
* - cardinality: Either 'one' (one-to-one) or 'many' (one-to-many)
|
518
|
+
* - direction: 'down' for links defined in the subject model pointing to other models
|
519
|
+
* 'up' for links defined in other models pointing to the subject
|
520
|
+
* - Values are the model constructor functions for the linked classes
|
521
|
+
*
|
522
|
+
* @description
|
523
|
+
* This method identifies four types of relationships:
|
524
|
+
* 1. One-to-one links from subject to other models ('one', 'down')
|
525
|
+
* 2. One-to-many links from subject to other models ('many', 'down')
|
526
|
+
* 3. One-to-one links from other models to subject ('one', 'up')
|
527
|
+
* 4. One-to-many links from other models to subject ('many', 'up')
|
501
528
|
*/
|
502
|
-
#
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
529
|
+
#findLinkedModelClasses(subject) {
|
530
|
+
const subjectModelConstructor = this.#getModelConstructorFromId(subject.id);
|
531
|
+
const modelsThatLinkToThisSubject = new Map();
|
532
|
+
|
533
|
+
for (const [propertyName, propertyType] of Object.entries(subjectModelConstructor)) {
|
534
|
+
// The model is a one to one link
|
535
|
+
if (propertyType.prototype instanceof Model) {
|
536
|
+
modelsThatLinkToThisSubject.set([propertyType.name, propertyName, 'one', 'down'], propertyType);
|
537
|
+
}
|
538
|
+
// The model is a one to many link
|
539
|
+
|
540
|
+
if (propertyType._items?.prototype instanceof Model) {
|
541
|
+
modelsThatLinkToThisSubject.set([propertyType._items.name, propertyName, 'many', 'down'], propertyType);
|
542
|
+
}
|
543
|
+
}
|
544
|
+
|
545
|
+
for (const [modelName, modelConstructor] of this.#models) {
|
546
|
+
for (const [propertyName, propertyType] of Object.entries(modelConstructor.properties)) {
|
547
|
+
// The model is a one to one link
|
548
|
+
if (propertyType === subjectModelConstructor || propertyType.prototype instanceof subjectModelConstructor) {
|
549
|
+
modelsThatLinkToThisSubject.set([modelName, propertyName, 'one', 'up'], propertyType);
|
550
|
+
}
|
551
|
+
|
552
|
+
// The model is a one to many link
|
553
|
+
if (propertyType._items === subjectModelConstructor || propertyType._items?.prototype instanceof subjectModelConstructor) {
|
554
|
+
modelsThatLinkToThisSubject.set([modelName, propertyName, 'many', 'up'], propertyType);
|
555
|
+
}
|
556
|
+
}
|
557
|
+
}
|
558
|
+
|
559
|
+
return modelsThatLinkToThisSubject;
|
527
560
|
}
|
561
|
+
}
|
528
562
|
|
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
|
-
|
563
|
+
/**
|
564
|
+
* Resolves model properties that are factory functions to their class values.
|
565
|
+
*
|
566
|
+
* @private
|
567
|
+
* @param {Object<string, Function>} models - An object mapping model names to model constructor functions
|
568
|
+
* @returns {Map<string, Function>} A map of model names to model constructors with all factory
|
569
|
+
* function properties class to their bare model instances
|
570
|
+
*
|
571
|
+
* @description
|
572
|
+
* This method processes each property of each model constructor to resolve any factory functions.
|
573
|
+
* It skips:
|
574
|
+
* - Special properties like 'indexedProperties', 'searchProperties', and '_required'
|
575
|
+
* - Properties that are already bare model instances (have a prototype inheriting from Model)
|
576
|
+
* - Properties with a defined '_type' (basic types)
|
577
|
+
*
|
578
|
+
* For all other properties (assumed to be factory functions), it calls the function to get
|
579
|
+
* the class value and updates the model constructor.
|
580
|
+
*/
|
581
|
+
function resolveModels(models) {
|
582
|
+
const resolvedToBareModels = new Map();
|
583
|
+
|
584
|
+
for (const [modelName, modelConstructor] of Object.entries(models)) {
|
585
|
+
for (const [propertyName, propertyType] of Object.entries(modelConstructor)) {
|
586
|
+
// The property is a builtin
|
587
|
+
if ([
|
588
|
+
'indexedProperties',
|
589
|
+
'searchProperties',
|
590
|
+
'_required',
|
591
|
+
].includes(propertyName)) {
|
592
|
+
continue;
|
593
|
+
}
|
594
|
+
|
595
|
+
// The property is a bare model
|
596
|
+
if (propertyType.prototype instanceof Model) {
|
597
|
+
continue;
|
598
|
+
}
|
599
|
+
|
600
|
+
// The property is a basic type
|
601
|
+
if (propertyType._type) {
|
602
|
+
continue;
|
603
|
+
}
|
604
|
+
|
605
|
+
modelConstructor[propertyName] = propertyType();
|
606
|
+
}
|
607
|
+
|
608
|
+
resolvedToBareModels.set(modelName, modelConstructor);
|
557
609
|
}
|
610
|
+
|
611
|
+
return resolvedToBareModels;
|
558
612
|
}
|
559
613
|
|
560
614
|
/**
|
package/src/Schema.js
CHANGED
@@ -39,7 +39,7 @@ class Schema {
|
|
39
39
|
const thisSchema = {};
|
40
40
|
|
41
41
|
if (Model.isModel(schemaSegment)) {
|
42
|
-
thisSchema.required = [
|
42
|
+
thisSchema.required = [];
|
43
43
|
thisSchema.type = 'object';
|
44
44
|
thisSchema.additionalProperties = false;
|
45
45
|
thisSchema.properties = {
|
@@ -62,7 +62,7 @@ class Schema {
|
|
62
62
|
thisSchema.properties[name] = {
|
63
63
|
type: 'object',
|
64
64
|
additionalProperties: false,
|
65
|
-
required: [
|
65
|
+
required: [],
|
66
66
|
properties: {
|
67
67
|
id: {
|
68
68
|
type: 'string',
|
package/src/data/Model.js
CHANGED
@@ -163,7 +163,7 @@ class Model {
|
|
163
163
|
static _required = true;
|
164
164
|
}
|
165
165
|
|
166
|
-
Object.defineProperty(Required, 'name', {value:
|
166
|
+
Object.defineProperty(Required, 'name', {value: this.name});
|
167
167
|
|
168
168
|
return Required;
|
169
169
|
}
|
@@ -192,11 +192,17 @@ class Model {
|
|
192
192
|
*/
|
193
193
|
static indexedPropertiesResolved() {
|
194
194
|
return [
|
195
|
-
...Object.entries(this)
|
196
|
-
.filter(([
|
195
|
+
...Object.entries(this.properties)
|
196
|
+
.filter(([name, type]) => !['indexedProperties', 'searchProperties'].includes(name) && !type._type && (this.isModel(type) || (typeof type === 'function' && this.isModel(type()))))
|
197
197
|
.map(([name, _type]) => `${name}.id`),
|
198
|
-
...Object.entries(this)
|
199
|
-
.filter(([_name, type]) =>
|
198
|
+
...Object.entries(this.properties)
|
199
|
+
.filter(([_name, type]) => {
|
200
|
+
return !Model.isModel(type) && (
|
201
|
+
(type._type === 'array' && this.isModel(type._items))
|
202
|
+
||
|
203
|
+
(!type._type && typeof type === 'function' && this.isModel(type()._items))
|
204
|
+
);
|
205
|
+
})
|
200
206
|
.map(([name, _type]) => `${name}.[*].id`),
|
201
207
|
...this.indexedProperties(),
|
202
208
|
'id',
|
@@ -295,6 +301,51 @@ class Model {
|
|
295
301
|
static withName(name) {
|
296
302
|
Object.defineProperty(this, 'name', {value: name});
|
297
303
|
}
|
304
|
+
|
305
|
+
/**
|
306
|
+
* Discover model properties all the way up the prototype chain.
|
307
|
+
*
|
308
|
+
* @return {Model}
|
309
|
+
*/
|
310
|
+
static get properties() {
|
311
|
+
const props = {};
|
312
|
+
const chain = [];
|
313
|
+
|
314
|
+
let current = this;
|
315
|
+
while (current !== Function.prototype) {
|
316
|
+
chain.push(current);
|
317
|
+
current = Object.getPrototypeOf(current);
|
318
|
+
}
|
319
|
+
|
320
|
+
for (const item of chain) {
|
321
|
+
for (const property of Object.getOwnPropertyNames(item)) {
|
322
|
+
if (
|
323
|
+
[
|
324
|
+
'_required',
|
325
|
+
'fromData',
|
326
|
+
'indexedProperties',
|
327
|
+
'indexedPropertiesResolved',
|
328
|
+
'isDryModel',
|
329
|
+
'isModel',
|
330
|
+
'length',
|
331
|
+
'name',
|
332
|
+
'properties',
|
333
|
+
'prototype',
|
334
|
+
'required',
|
335
|
+
'searchProperties',
|
336
|
+
'toString',
|
337
|
+
'withName',
|
338
|
+
].includes(property)
|
339
|
+
) continue;
|
340
|
+
|
341
|
+
if (Object.keys(props).includes(property)) continue;
|
342
|
+
|
343
|
+
props[property] = item[property];
|
344
|
+
}
|
345
|
+
}
|
346
|
+
|
347
|
+
return Object.assign(this, props);
|
348
|
+
}
|
298
349
|
}
|
299
350
|
|
300
351
|
export default Model;
|
package/src/data/Property.js
CHANGED
@@ -5,6 +5,7 @@ import DateType from './properties/DateType.js';
|
|
5
5
|
import NumberType from './properties/NumberType.js';
|
6
6
|
import SlugType from './properties/SlugType.js';
|
7
7
|
import StringType from './properties/StringType.js';
|
8
|
+
import Type from './properties/Type.js';
|
8
9
|
|
9
10
|
const Property = {
|
10
11
|
Array: ArrayType,
|
@@ -14,6 +15,7 @@ const Property = {
|
|
14
15
|
Number: NumberType,
|
15
16
|
Slug: SlugType,
|
16
17
|
String: StringType,
|
18
|
+
Type,
|
17
19
|
};
|
18
20
|
|
19
21
|
export default Property;
|
@@ -66,7 +66,7 @@ class Type {
|
|
66
66
|
}
|
67
67
|
|
68
68
|
// Define the class name as "Required<OriginalTypeName>"
|
69
|
-
Object.defineProperty(Required, 'name', {value: `Required${this.
|
69
|
+
Object.defineProperty(Required, 'name', {value: `Required${this.name}`});
|
70
70
|
|
71
71
|
return Required;
|
72
72
|
}
|
@@ -132,7 +132,9 @@ export class ModelNotFoundStorageEngineError extends StorageEngineError {
|
|
132
132
|
export class DeleteHasUnintendedConsequencesStorageEngineError extends StorageEngineError {
|
133
133
|
/**
|
134
134
|
* @param {string} modelId
|
135
|
-
* @param {
|
135
|
+
* @param {Object} consequences
|
136
|
+
* @param {Array<String>?} consequences.willDelete
|
137
|
+
* @param {Array<String>?} consequences.willUpdate
|
136
138
|
*/
|
137
139
|
constructor(modelId, consequences) {
|
138
140
|
super(`Deleting ${modelId} has unintended consequences`);
|