@acodeninja/persist 3.0.0-next.1 → 3.0.0-next.10

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.
@@ -0,0 +1,26 @@
1
+ /** @type {import('jest').Config} */
2
+ const config = {
3
+ coveragePathIgnorePatterns: [
4
+ 'node_modules',
5
+ 'test/fixtures',
6
+ 'test/mocks',
7
+ 'test/scripts',
8
+ ],
9
+ coverageThreshold: {
10
+ global: {
11
+ branches: 100,
12
+ functions: 100,
13
+ lines: 100,
14
+ statements: 100,
15
+ },
16
+ },
17
+ testMatch: [
18
+ '**/*.test.js',
19
+ ],
20
+ watchPathIgnorePatterns: [
21
+ 'coverage/',
22
+ 'test/fixtures/minified',
23
+ ],
24
+ };
25
+
26
+ module.exports = config;
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@acodeninja/persist",
3
- "version": "3.0.0-next.1",
3
+ "version": "3.0.0-next.10",
4
4
  "description": "A JSON based data modelling and persistence module with alternate storage mechanisms.",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "ava",
8
- "test:watch": "ava --watch",
9
- "test:coverage": "c8 --experimental-monocart --100 --reporter=console-details ava",
10
- "test:coverage:report": "c8 --experimental-monocart --100 --lcov --reporter=console-details --reporter=v8 ava",
7
+ "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest",
8
+ "test:watch": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest --watch",
9
+ "test:coverage": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest --collect-coverage",
11
10
  "lint": "eslint ./",
12
11
  "prepare": "husky"
13
12
  },
@@ -38,14 +37,12 @@
38
37
  "@commitlint/cli": "^19.6.1",
39
38
  "@commitlint/config-conventional": "^19.6.0",
40
39
  "@eslint/js": "^9.19.0",
40
+ "@jest/globals": "^29.7.0",
41
41
  "@semantic-release/commit-analyzer": "^13.0.1",
42
- "ava": "^6.2.0",
43
- "c8": "^10.1.3",
44
42
  "eslint": "^9.19.0",
45
43
  "globals": "^15.14.0",
46
44
  "husky": "^9.1.7",
47
- "monocart-coverage-reports": "^2.12.0",
48
- "semantic-release": "^24.2.1",
49
- "sinon": "^19.0.2"
45
+ "jest": "^29.7.0",
46
+ "semantic-release": "^24.2.1"
50
47
  }
51
48
  }
package/src/Query.js CHANGED
@@ -69,10 +69,10 @@ class Query {
69
69
  *
70
70
  * @private
71
71
  * @param {*} subject - The subject to be matched.
72
- * @param {Object} [inputQuery=this.query] - The query to match against. Defaults to `this.query` if not provided.
72
+ * @param {Object} inputQuery - The query to match against.
73
73
  * @returns {boolean} True if the subject matches the query, otherwise false.
74
74
  */
75
- _matchesQuery(subject, inputQuery = this.query) {
75
+ _matchesQuery(subject, inputQuery) {
76
76
  if (['string', 'number', 'boolean'].includes(typeof inputQuery)) return subject === inputQuery;
77
77
 
78
78
  if (inputQuery?.$is !== undefined && subject === inputQuery.$is) return true;
@@ -79,7 +79,11 @@ class SchemaCompiler {
79
79
  schema.properties[name].items.format = property?._items._format;
80
80
  }
81
81
 
82
- if (Type.Model.isModel(property?._items)) {
82
+ const prop = typeof property?._items === 'function' &&
83
+ !/^class/.test(Function.prototype.toString.call(property?._items)) ?
84
+ property?._items() : property?._items;
85
+
86
+ if (Type.Model.isModel(prop)) {
83
87
  schema.properties[name].items = {
84
88
  type: 'object',
85
89
  additionalProperties: false,
@@ -87,7 +91,7 @@ class SchemaCompiler {
87
91
  properties: {
88
92
  id: {
89
93
  type: 'string',
90
- pattern: `^${property?._items.toString()}/[A-Z0-9]+$`,
94
+ pattern: `^${prop.toString()}/[A-Z0-9]+$`,
91
95
  },
92
96
  },
93
97
  };
@@ -0,0 +1,517 @@
1
+ import Type from '../type/index.js';
2
+ import _ from 'lodash';
3
+
4
+ export default class StorageEngine {
5
+ /**
6
+ * @param {Object} configuration
7
+ * @param {Array<Type.Model.constructor>?} models
8
+ */
9
+ constructor(configuration = {}, models = null) {
10
+ this.configuration = configuration;
11
+ this.models = Object.fromEntries((models ?? []).map(model => [model.name, model]));
12
+ }
13
+
14
+ /**
15
+ * Persists a model if it has changed, and updates all related models and their indexes
16
+ * @param {Type.Model} model
17
+ * @return {Promise<void>}
18
+ */
19
+ async put(model) {
20
+ const processedModels = [];
21
+ const modelsToPut = [];
22
+ const modelsToReindex = {};
23
+ const modelsToReindexSearch = {};
24
+
25
+ /**
26
+ * @param {Type.Model} modelToProcess
27
+ * @return {Promise<void>}
28
+ */
29
+ const processModel = async (modelToProcess) => {
30
+ if (processedModels.includes(modelToProcess.id))
31
+ return;
32
+
33
+ processedModels.push(modelToProcess.id);
34
+
35
+ if (!Object.keys(this.models).includes(modelToProcess.constructor.name))
36
+ throw new ModelNotRegisteredStorageEngineError(modelToProcess, this);
37
+
38
+ modelToProcess.validate();
39
+ const currentModel = await this.get(modelToProcess.id).catch(() => null);
40
+
41
+ const modelToProcessHasChanged = JSON.stringify(currentModel?.toData() || {}) !== JSON.stringify(modelToProcess.toData());
42
+
43
+ if (modelToProcessHasChanged) modelsToPut.push(modelToProcess);
44
+
45
+ if (
46
+ Boolean(modelToProcess.constructor.indexedProperties().length) &&
47
+ indexedFieldsHaveChanged(currentModel, modelToProcess)
48
+ ) {
49
+ const modelToProcessConstructor = this.getModelConstructorFromId(modelToProcess.id);
50
+ modelsToReindex[modelToProcessConstructor] = modelsToReindex[modelToProcessConstructor] || [];
51
+ modelsToReindex[modelToProcessConstructor].push(modelToProcess);
52
+ }
53
+
54
+ if (
55
+ Boolean(modelToProcess.constructor.searchProperties().length) &&
56
+ searchableFieldsHaveChanged(currentModel, modelToProcess)
57
+ ) {
58
+ const modelToProcessConstructor = this.getModelConstructorFromId(modelToProcess.id);
59
+ modelsToReindexSearch[modelToProcessConstructor] = modelsToReindexSearch[modelToProcessConstructor] || [];
60
+ modelsToReindexSearch[modelToProcessConstructor].push(modelToProcess);
61
+ }
62
+
63
+ for (const [field, value] of Object.entries(modelToProcess)) {
64
+ if (Type.Model.isModel(value)) {
65
+ await processModel(modelToProcess[field]);
66
+ }
67
+ }
68
+ };
69
+
70
+ await processModel(model);
71
+
72
+ await Promise.all([
73
+ Promise.all(modelsToPut.map(m => this._putModel(m.toData()))),
74
+ Promise.all(Object.entries(modelsToReindex).map(async ([constructorName, models]) => {
75
+ const modelConstructor = this.models[constructorName];
76
+ const index = await this._getIndex(modelConstructor);
77
+
78
+ await this._putIndex(modelConstructor, {
79
+ ...index || {},
80
+ ...Object.fromEntries(models.map(m => [m.id, m.toIndexData()])),
81
+ });
82
+ })),
83
+ Promise.all(Object.entries(modelsToReindexSearch).map(async ([constructorName, models]) => {
84
+ const modelConstructor = this.models[constructorName];
85
+ const index = await this._getSearchIndex(modelConstructor);
86
+
87
+ await this._putSearchIndex(modelConstructor, {
88
+ ...index || {},
89
+ ...Object.fromEntries(models.map(m => [m.id, m.toSearchData()])),
90
+ });
91
+ })),
92
+ ]);
93
+ }
94
+
95
+ /**
96
+ * Get a model by its id
97
+ * @param {string} modelId
98
+ * @throws {ModelNotFoundStorageEngineError}
99
+ * @return {Promise<Type.Model>}
100
+ */
101
+ get(modelId) {
102
+ try {
103
+ this.getModelConstructorFromId(modelId);
104
+ } catch (e) {
105
+ return Promise.reject(e);
106
+ }
107
+ return this._getModel(modelId);
108
+ }
109
+
110
+ /**
111
+ * Delete a model and update indexes that reference it
112
+ * @param {Type.Model} model
113
+ * @param {Array<string>} propagateTo - List of model ids that are expected to be deleted
114
+ * @throws {ModelNotRegisteredStorageEngineError}
115
+ * @throws {ModelNotFoundStorageEngineError}
116
+ */
117
+ async delete(model, propagateTo = []) {
118
+ const processedModels = [];
119
+ const modelsToDelete = [];
120
+ const modelsToPut = [];
121
+ const indexCache = {};
122
+ const indexActions = {};
123
+ const searchIndexCache = {};
124
+ const searchIndexActions = {};
125
+ const modelCache = {};
126
+
127
+ propagateTo.push(model.id);
128
+
129
+ /**
130
+ * Process a model for deletion
131
+ * @param {Type.Model} modelToProcess
132
+ * @return {Promise<void>}
133
+ */
134
+ const processModel = async (modelToProcess) => {
135
+ if (processedModels.includes(modelToProcess.id)) return;
136
+ processedModels.push(modelToProcess.id);
137
+
138
+ const modelsToProcess = [];
139
+ if (!Object.keys(this.models).includes(modelToProcess.constructor.name))
140
+ throw new ModelNotRegisteredStorageEngineError(modelToProcess, this);
141
+
142
+ const currentModel = modelCache[model.id] ?? await this.get(model.id);
143
+ modelCache[currentModel.id] = currentModel;
144
+
145
+ if (!modelsToDelete.includes(currentModel.id)) modelsToDelete.push(currentModel.id);
146
+
147
+ const modelToProcessConstructor = this.getModelConstructorFromId(modelToProcess.id);
148
+ indexActions[modelToProcessConstructor] = indexActions[modelToProcessConstructor] ?? [];
149
+ searchIndexActions[modelToProcessConstructor] = searchIndexActions[modelToProcessConstructor] ?? [];
150
+
151
+ if (currentModel.constructor.indexedPropertiesResolved().length) {
152
+ indexActions[modelToProcessConstructor].push(['delete', modelToProcess]);
153
+ }
154
+
155
+ if (currentModel.constructor.searchProperties().length) {
156
+ searchIndexActions[modelToProcessConstructor].push(['delete', modelToProcess]);
157
+ }
158
+
159
+ const linkedModels = await this.getInstancesLinkedTo(modelToProcess, indexCache);
160
+ const links = this.getLinksFor(modelToProcess.constructor);
161
+ Object.values(Object.fromEntries(await Promise.all(
162
+ Object.entries(linkedModels)
163
+ .map(async ([constructor, updatableModels]) => [
164
+ constructor,
165
+ await Promise.all(updatableModels.map(async m => {
166
+ const currentModel = modelCache[m.id] ?? await this.get(m.id);
167
+ modelCache[currentModel.id] = currentModel;
168
+ return currentModel;
169
+ })),
170
+ ]),
171
+ ))).flat(1)
172
+ .forEach(m =>
173
+ Object.entries(links[m.constructor.name])
174
+ .forEach(([linkName, modelConstructor]) => {
175
+ if ((
176
+ typeof modelConstructor[linkName] === 'function' &&
177
+ !/^class/.test(Function.prototype.toString.call(modelConstructor[linkName])) &&
178
+ !Type.Model.isModel(modelConstructor[linkName]) ?
179
+ modelConstructor[linkName]() : modelConstructor
180
+ )._required) {
181
+ if (!modelsToDelete.includes(m.id)) modelsToDelete.push(m.id);
182
+ modelsToProcess.push(m);
183
+ } else {
184
+ m[linkName] = undefined;
185
+ modelsToPut.push(m);
186
+
187
+ indexActions[this.getModelConstructorFromId(m.id)].push(['reindex', m]);
188
+
189
+ if (m.constructor.searchProperties().length) {
190
+ searchIndexActions[this.getModelConstructorFromId(m.id)].push(['reindex', m]);
191
+ }
192
+ }
193
+ }),
194
+ );
195
+
196
+ for (const model of modelsToProcess) {
197
+ await processModel(model);
198
+ }
199
+ };
200
+
201
+ await processModel(model);
202
+
203
+ const unrequestedDeletions = modelsToDelete.filter(m => !propagateTo.includes(m));
204
+ if (unrequestedDeletions.length) {
205
+ throw new DeleteHasUnintendedConsequencesStorageEngineError(model.id, {
206
+ willDelete: unrequestedDeletions,
207
+ });
208
+ }
209
+
210
+ await Promise.all([
211
+ Promise.all(Object.entries(indexActions).map(async ([constructorName, actions]) => {
212
+ const modelConstructor = this.models[constructorName];
213
+ indexCache[modelConstructor] = indexCache[modelConstructor] ?? await this._getIndex(modelConstructor);
214
+
215
+ actions.forEach(([action, actionModel]) => {
216
+ if (action === 'delete') {
217
+ indexCache[modelConstructor] = _.omit(indexCache[modelConstructor], [actionModel.id]);
218
+ }
219
+ if (action === 'reindex') {
220
+ indexCache[modelConstructor] = {
221
+ ...indexCache[modelConstructor],
222
+ [actionModel.id]: actionModel.toIndexData(),
223
+ };
224
+ }
225
+ });
226
+ })),
227
+ Promise.all(Object.entries(searchIndexActions).map(async ([constructorName, actions]) => {
228
+ const modelConstructor = this.models[constructorName];
229
+ searchIndexCache[modelConstructor] = searchIndexCache[modelConstructor] ?? await this._getSearchIndex(modelConstructor);
230
+
231
+ actions.forEach(([action, actionModel]) => {
232
+ if (action === 'delete') {
233
+ searchIndexCache[modelConstructor] = _.omit(searchIndexCache[modelConstructor], [actionModel.id]);
234
+ }
235
+ if (action === 'reindex') {
236
+ searchIndexCache[modelConstructor] = {
237
+ ...searchIndexCache[modelConstructor],
238
+ [actionModel.id]: actionModel.toSearchData(),
239
+ };
240
+ }
241
+ });
242
+ })),
243
+ ]);
244
+
245
+ await Promise.all([
246
+ Promise.all(modelsToDelete.map(m => this._deleteModel(m))),
247
+ Promise.all(modelsToPut.map(m => this._putModel(m))),
248
+ Promise.all(
249
+ Object.entries(indexCache)
250
+ .map(([constructorName, index]) => this._putIndex(this.models[constructorName], index)),
251
+ ),
252
+ Promise.all(
253
+ Object.entries(searchIndexCache)
254
+ .map(([constructorName, index]) => this._putSearchIndex(this.models[constructorName], index)),
255
+ ),
256
+ ]);
257
+ }
258
+
259
+ /**
260
+ * Get the model constructor from a model id
261
+ * @param {string} modelId
262
+ * @return {Model.constructor}
263
+ */
264
+ getModelConstructorFromId(modelId) {
265
+ const modelName = modelId.split('/')[0];
266
+ const constructor = this.models[modelName];
267
+
268
+ if (!constructor) throw new ModelNotRegisteredStorageEngineError(modelName, this);
269
+
270
+ return constructor;
271
+ }
272
+
273
+ /**
274
+ * Get model instance that are directly linked to the given model in either direction
275
+ * @param {Type.Model} model
276
+ * @param {object} cache
277
+ * @return {Record<string, Record<string, Type.Model>>}
278
+ */
279
+ async getInstancesLinkedTo(model, cache = {}) {
280
+ return Object.fromEntries(
281
+ Object.entries(
282
+ await Promise.all(
283
+ Object.entries(this.getLinksFor(model.constructor))
284
+ .map(([name, _index]) =>
285
+ cache[name] ? Promise.resolve([name, Object.values(cache[name])]) :
286
+ this._getIndex(this.models[name])
287
+ .then(i => {
288
+ cache[name] = i;
289
+ return [name, Object.values(i)];
290
+ }),
291
+ ),
292
+ ).then(Object.fromEntries),
293
+ ).map(([name, index]) => [
294
+ name,
295
+ index.map(item => Object.fromEntries(
296
+ Object.entries(item)
297
+ .filter(([propertyName, property]) => propertyName === 'id' || property?.id === model.id),
298
+ )).filter(item => Object.keys(item).length > 1),
299
+ ]),
300
+ );
301
+ }
302
+
303
+ /**
304
+ * Get model classes that are directly linked to the given model in either direction
305
+ * @param {Type.Model.constructor} model
306
+ * @return {Record<string, Record<string, Type.Model.constructor>>}
307
+ */
308
+ getLinksFor(model) {
309
+ return Object.fromEntries(
310
+ Object.entries(this.getAllModelLinks())
311
+ .filter(([modelName, links]) =>
312
+ model.name === modelName ||
313
+ Object.values(links).some((link) => link.name === model.name),
314
+ ),
315
+ );
316
+ }
317
+
318
+ /**
319
+ * Get all model links
320
+ * @return {Record<string, Record<string, Type.Model.constructor>>}
321
+ */
322
+ getAllModelLinks() {
323
+ return Object.entries(this.models)
324
+ .map(([registeredModelName, registeredModelClass]) =>
325
+ Object.entries(registeredModelClass)
326
+ .map(([propertyName, propertyType]) => [
327
+ registeredModelName,
328
+ propertyName,
329
+ typeof propertyType === 'function' &&
330
+ !/^class/.test(Function.prototype.toString.call(propertyType)) &&
331
+ !Type.Model.isModel(propertyType) ?
332
+ propertyType() : propertyType,
333
+ ])
334
+ .filter(([_m, _p, type]) => Type.Model.isModel(type))
335
+ .map(([containingModel, propertyName, propertyType]) => ({
336
+ containingModel,
337
+ propertyName,
338
+ propertyType,
339
+ })),
340
+ )
341
+ .flat()
342
+ .reduce((accumulator, {containingModel, propertyName, propertyType}) => ({
343
+ ...accumulator,
344
+ [containingModel]: {
345
+ ...accumulator[containingModel] || {},
346
+ [propertyName]: propertyType,
347
+ },
348
+ }), {});
349
+ }
350
+
351
+ /**
352
+ * Update a model
353
+ * @param {Model} _model
354
+ * @throws MethodNotImplementedStorageEngineError
355
+ * @return Promise<void>
356
+ */
357
+ _putModel(_model) {
358
+ return Promise.reject(new MethodNotImplementedStorageEngineError('_putModel', this));
359
+ }
360
+
361
+ /**
362
+ * Get a model
363
+ * @param {string} _id
364
+ * @throws MethodNotImplementedStorageEngineError
365
+ * @throws ModelNotFoundStorageEngineError
366
+ * @return Promise<Model>
367
+ */
368
+ _getModel(_id) {
369
+ return Promise.reject(new MethodNotImplementedStorageEngineError('_getModel', this));
370
+ }
371
+
372
+ /**
373
+ * Delete a model
374
+ * @param {string} _id
375
+ * @throws MethodNotImplementedStorageEngineError
376
+ * @throws ModelNotFoundStorageEngineError
377
+ * @return Promise<void>
378
+ */
379
+ _deleteModel(_id) {
380
+ return Promise.reject(new MethodNotImplementedStorageEngineError('_deleteModel', this));
381
+ }
382
+
383
+ /**
384
+ * Get a model's index data
385
+ * @param {Model.constructor} _modelConstructor
386
+ * @throws MethodNotImplementedStorageEngineError
387
+ * @return Promise<void>
388
+ */
389
+ _getIndex(_modelConstructor) {
390
+ return Promise.reject(new MethodNotImplementedStorageEngineError('_getIndex', this));
391
+ }
392
+
393
+ /**
394
+ * Put a model's index data
395
+ * @param {Model.constructor} _modelConstructor
396
+ * @param {object} _data
397
+ * @throws MethodNotImplementedStorageEngineError
398
+ * @return Promise<void>
399
+ */
400
+ _putIndex(_modelConstructor, _data) {
401
+ return Promise.reject(new MethodNotImplementedStorageEngineError('_putIndex', this));
402
+ }
403
+
404
+ /**
405
+ * Get a model's raw search index data
406
+ * @param {Model.constructor} _modelConstructor
407
+ * @throws MethodNotImplementedStorageEngineError
408
+ * @return Promise<void>
409
+ */
410
+ _getSearchIndex(_modelConstructor) {
411
+ return Promise.reject(new MethodNotImplementedStorageEngineError('_getSearchIndex', this));
412
+ }
413
+
414
+ /**
415
+ * Get a model's raw search index data
416
+ * @param {Model.constructor} _modelConstructor
417
+ * @throws MethodNotImplementedStorageEngineError
418
+ * @return Promise<void>
419
+ */
420
+ _getSearchIndexCompiled(_modelConstructor) {
421
+ return Promise.reject(new MethodNotImplementedStorageEngineError('_getSearchIndexCompiled', this));
422
+ }
423
+
424
+ /**
425
+ * Put a model's raw and compiled search index data
426
+ * @param {Model.constructor} _modelConstructor
427
+ * @param {object} _data
428
+ * @throws MethodNotImplementedStorageEngineError
429
+ * @return Promise<void>
430
+ */
431
+ _putSearchIndex(_modelConstructor, _data) {
432
+ return Promise.reject(new MethodNotImplementedStorageEngineError('_putSearchIndex', this));
433
+ }
434
+ }
435
+
436
+
437
+ /**
438
+ * Decide if two models indexable fields are different
439
+ * @param {Type.Model} currentModel
440
+ * @param {Type.Model} modelToProcess
441
+ * @return {boolean}
442
+ * @private
443
+ */
444
+ function indexedFieldsHaveChanged(currentModel, modelToProcess) {
445
+ return !currentModel || JSON.stringify(currentModel.toIndexData()) !== JSON.stringify(modelToProcess.toIndexData());
446
+ }
447
+
448
+ /**
449
+ * Decide if two models searchable fields have changed
450
+ * @param {Type.Model} currentModel
451
+ * @param {Type.Model} modelToProcess
452
+ * @return {boolean}
453
+ * @private
454
+ */
455
+ function searchableFieldsHaveChanged(currentModel, modelToProcess) {
456
+ return !currentModel || JSON.stringify(currentModel.toSearchData()) !== JSON.stringify(modelToProcess.toSearchData());
457
+ }
458
+
459
+ /**
460
+ * @class StorageEngineError
461
+ * @extends Error
462
+ */
463
+ export class StorageEngineError extends Error {
464
+ }
465
+
466
+ /**
467
+ * @class ModelNotRegisteredStorageEngineError
468
+ * @extends StorageEngineError
469
+ */
470
+ export class ModelNotRegisteredStorageEngineError extends StorageEngineError {
471
+ /**
472
+ * @param {Type.Model} model
473
+ * @param {StorageEngine} storageEngine
474
+ */
475
+ constructor(model, storageEngine) {
476
+ const modelName = typeof model === 'string' ? model : model.constructor.name;
477
+ super(`The model ${modelName} is not registered in the storage engine ${storageEngine.constructor.name}`);
478
+ }
479
+ }
480
+
481
+ /**
482
+ * @class MethodNotImplementedStorageEngineError
483
+ * @extends StorageEngineError
484
+ */
485
+ export class MethodNotImplementedStorageEngineError extends StorageEngineError {
486
+ /**
487
+ * @param {string} method
488
+ * @param {StorageEngine} storageEngine
489
+ */
490
+ constructor(method, storageEngine) {
491
+ super(`The method ${method} is not implemented in the storage engine ${storageEngine.constructor.name}`);
492
+ }
493
+ }
494
+
495
+ /**
496
+ * @class ModelNotFoundStorageEngineError
497
+ * @extends StorageEngineError
498
+ */
499
+ export class ModelNotFoundStorageEngineError extends StorageEngineError {
500
+ /**
501
+ * @param {string} modelId
502
+ */
503
+ constructor(modelId) {
504
+ super(`The model ${modelId} was not found`);
505
+ }
506
+ }
507
+
508
+ export class DeleteHasUnintendedConsequencesStorageEngineError extends StorageEngineError {
509
+ /**
510
+ * @param {string} modelId
511
+ * @param {object} consequences
512
+ */
513
+ constructor(modelId, consequences) {
514
+ super(`Deleting ${modelId} has unintended consequences`);
515
+ this.consequences = consequences;
516
+ }
517
+ }
@@ -113,6 +113,13 @@ class FileStorageEngine extends StorageEngine {
113
113
  * @throws {FailedWriteFileStorageEngineError} Throws if the index cannot be written to the file system.
114
114
  */
115
115
  static async putIndex(index) {
116
+ /**
117
+ * Process an index of models
118
+ * @param {string} location
119
+ * @param {Array<Model>} models
120
+ * @throws FailedWriteFileStorageEngineError
121
+ * @return {Promise<void>}
122
+ */
116
123
  const processIndex = async (location, models) => {
117
124
  const filePath = join(this.configuration.path, location, '_index.json');
118
125
  const currentIndex = JSON.parse((await this.configuration.filesystem.readFile(filePath).catch(() => '{}')).toString());
@@ -186,6 +186,12 @@ class HTTPStorageEngine extends StorageEngine {
186
186
  * @throws {HTTPRequestFailedError} Thrown if the PUT request fails.
187
187
  */
188
188
  static async putIndex(index) {
189
+ /**
190
+ * Process an index of models
191
+ * @param {string} location
192
+ * @param {Array<Model>} models
193
+ * @return {Promise<void>}
194
+ */
189
195
  const processIndex = async (location, models) => {
190
196
  const url = new URL([
191
197
  this.configuration.host,
@@ -136,6 +136,13 @@ class S3StorageEngine extends StorageEngine {
136
136
  * @throws {FailedPutS3StorageEngineError} Thrown if there is an error during the S3 PutObject operation.
137
137
  */
138
138
  static async putIndex(index) {
139
+ /**
140
+ * Process an index of models
141
+ * @param {string} location
142
+ * @param {Array<Model>} models
143
+ * @throws FailedPutS3StorageEngineError
144
+ * @return {Promise<void>}
145
+ */
139
146
  const processIndex = async (location, models) => {
140
147
  const Key = [this.configuration.prefix, location, '_index.json'].filter(e => Boolean(e)).join('/');
141
148
  const currentIndex = await this.getIndex(location);
@@ -186,6 +186,11 @@ class StorageEngine {
186
186
  const uploadedModels = [];
187
187
  const indexUpdates = {};
188
188
 
189
+ /**
190
+ * Process a model, putting updates to the model and all linked models.
191
+ * @param {Model} m
192
+ * @return {Promise<void>}
193
+ */
189
194
  const processModel = async (m) => {
190
195
  if (!uploadedModels.includes(m.id)) {
191
196
  m.validate();
@@ -389,6 +394,11 @@ class StorageEngine {
389
394
  this.checkConfiguration();
390
395
  const hydratedModels = {};
391
396
 
397
+ /**
398
+ * Hydrate a model
399
+ * @param {Model} modelToProcess
400
+ * @return {Promise<Model>}
401
+ */
392
402
  const hydrateModel = async (modelToProcess) => {
393
403
  hydratedModels[modelToProcess.id] = modelToProcess;
394
404
 
@@ -405,6 +415,13 @@ class StorageEngine {
405
415
  return modelToProcess;
406
416
  };
407
417
 
418
+ /**
419
+ * Hydrate a dry sub model
420
+ * @param property
421
+ * @param modelToProcess
422
+ * @param name
423
+ * @return {Promise<Model>}
424
+ */
408
425
  const hydrateSubModel = async (property, modelToProcess, name) => {
409
426
  if (hydratedModels[property.id]) {
410
427
  return hydratedModels[property.id];
@@ -418,6 +435,13 @@ class StorageEngine {
418
435
  return hydratedSubModel;
419
436
  };
420
437
 
438
+ /**
439
+ * Hydrate an array of dry models
440
+ * @param property
441
+ * @param modelToProcess
442
+ * @param name
443
+ * @return {Promise<Awaited<*>[]>}
444
+ */
421
445
  const hydrateModelList = async (property, modelToProcess, name) => {
422
446
  const subModelClass = getSubModelClass(modelToProcess, name, true);
423
447
 
@@ -440,6 +464,13 @@ class StorageEngine {
440
464
  }));
441
465
  };
442
466
 
467
+ /**
468
+ * Get the class of a sub model
469
+ * @param modelToProcess
470
+ * @param name
471
+ * @param isArray
472
+ * @return {Model.constructor|Type}
473
+ */
443
474
  function getSubModelClass(modelToProcess, name, isArray = false) {
444
475
  const constructorField = modelToProcess.constructor[name];
445
476
 
@@ -460,6 +491,10 @@ class StorageEngine {
460
491
  * @returns {StorageEngine} A new engine instance with the applied configuration.
461
492
  */
462
493
  static configure(configuration) {
494
+ /**
495
+ * @class ConfiguredStore
496
+ * @extends StorageEngine
497
+ */
463
498
  class ConfiguredStore extends this {
464
499
  static configuration = configuration;
465
500
  }
package/src/type/Model.js CHANGED
@@ -100,11 +100,11 @@ class Model {
100
100
 
101
101
  /**
102
102
  * Extracts data from the model based on the indexed properties defined in the class.
103
- *
103
+ * Includes the ID of any linked models.
104
104
  * @returns {Object} - A representation of the model's indexed data.
105
105
  */
106
106
  toIndexData() {
107
- return this._extractData(this.constructor.indexedProperties());
107
+ return this._extractData(this.constructor.indexedPropertiesResolved());
108
108
  }
109
109
 
110
110
  /**
@@ -187,6 +187,39 @@ class Model {
187
187
  return [];
188
188
  }
189
189
 
190
+ /**
191
+ * Returns a list of properties that are indexed including links to other models.
192
+ *
193
+ * @returns {Array<string>} - The indexed properties.
194
+ * @abstract
195
+ * @static
196
+ */
197
+ static indexedPropertiesResolved() {
198
+ return []
199
+ .concat(
200
+ Object.entries(this)
201
+ .filter(([_name, type]) =>
202
+ this.isModel(
203
+ typeof type === 'function' &&
204
+ !/^class/.test(Function.prototype.toString.call(type)) ?
205
+ type() : type,
206
+ ),
207
+ )
208
+ .map(([name, _type]) => `${name}.id`),
209
+ )
210
+ .concat(
211
+ Object.entries(this)
212
+ .filter(([_name, type]) =>
213
+ type?._type === 'array' && this.isModel(
214
+ typeof type._items === 'function' &&
215
+ !/^class/.test(Function.prototype.toString.call(type._items)) ?
216
+ type._items() : type._items,
217
+ ),
218
+ ).map(([name, _type]) => `${name}.[*].id`),
219
+ )
220
+ .concat(this.indexedProperties());
221
+ }
222
+
190
223
  /**
191
224
  * Returns a list of properties used for search.
192
225
  *
@@ -1,15 +1,15 @@
1
- import SimpleType from './SimpleType.js';
1
+ import Type from '../Type.js';
2
2
 
3
3
  /**
4
4
  * Class representing a boolean type.
5
5
  *
6
6
  * This class is used to define and handle data of the boolean type.
7
- * It extends the {@link SimpleType} class to represent string-specific behavior.
7
+ * It extends the {@link Type} class to represent string-specific behavior.
8
8
  *
9
9
  * @class BooleanType
10
- * @extends SimpleType
10
+ * @extends Type
11
11
  */
12
- class BooleanType extends SimpleType {
12
+ class BooleanType extends Type {
13
13
  static {
14
14
  /**
15
15
  * @static
@@ -1,15 +1,15 @@
1
- import SimpleType from './SimpleType.js';
1
+ import Type from '../Type.js';
2
2
 
3
3
  /**
4
4
  * Class representing a date type with ISO date-time format.
5
5
  *
6
6
  * This class is used to define and handle data of the date type.
7
- * It extends the {@link SimpleType} class to represent string-specific behavior.
7
+ * It extends the {@link Type} class to represent string-specific behavior.
8
8
  *
9
9
  * @class DateType
10
- * @extends SimpleType
10
+ * @extends Type
11
11
  */
12
- class DateType extends SimpleType {
12
+ class DateType extends Type {
13
13
  static {
14
14
  /**
15
15
  * @static
@@ -1,15 +1,15 @@
1
- import SimpleType from './SimpleType.js';
1
+ import Type from '../Type.js';
2
2
 
3
3
  /**
4
4
  * Class representing a number type.
5
5
  *
6
6
  * This class is used to define and handle data of the number type.
7
- * It extends the {@link SimpleType} class to represent string-specific behavior.
7
+ * It extends the {@link Type} class to represent string-specific behavior.
8
8
  *
9
9
  * @class NumberType
10
- * @extends SimpleType
10
+ * @extends Type
11
11
  */
12
- class NumberType extends SimpleType {
12
+ class NumberType extends Type {
13
13
  static {
14
14
  /**
15
15
  * @static
@@ -1,15 +1,15 @@
1
- import SimpleType from './SimpleType.js';
1
+ import Type from '../Type.js';
2
2
 
3
3
  /**
4
4
  * Class representing a string type.
5
5
  *
6
6
  * This class is used to define and handle data of the string type.
7
- * It extends the {@link SimpleType} class to represent string-specific behavior.
7
+ * It extends the {@link Type} class to represent string-specific behavior.
8
8
  *
9
9
  * @class StringType
10
- * @extends SimpleType
10
+ * @extends Type
11
11
  */
12
- class StringType extends SimpleType {
12
+ class StringType extends Type {
13
13
  static {
14
14
  /**
15
15
  * @static
@@ -1,14 +0,0 @@
1
- import Type from '../Type.js';
2
-
3
- /**
4
- * Class representing a simple type.
5
- *
6
- * This serves as a base class for primitive or simple types such as string, number, or boolean.
7
- *
8
- * @class SimpleType
9
- * @extends Type
10
- */
11
- class SimpleType extends Type {
12
- }
13
-
14
- export default SimpleType;