@acodeninja/persist 3.0.0-next.9 → 3.0.1-next.1

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.
Files changed (41) hide show
  1. package/README.md +60 -4
  2. package/docs/code-quirks.md +14 -14
  3. package/docs/defining-models.md +61 -0
  4. package/docs/http.openapi.yml +138 -0
  5. package/docs/{model-property-types.md → model-properties.md} +76 -43
  6. package/docs/models-as-properties.md +46 -46
  7. package/docs/search-queries.md +11 -13
  8. package/docs/storage-engines.md +19 -35
  9. package/docs/structured-queries.md +59 -48
  10. package/docs/transactions.md +6 -7
  11. package/exports/storage/http.js +3 -0
  12. package/exports/storage/s3.js +3 -0
  13. package/jest.config.cjs +8 -12
  14. package/package.json +2 -2
  15. package/src/Connection.js +750 -0
  16. package/src/Persist.js +29 -30
  17. package/src/Schema.js +175 -0
  18. package/src/{Query.js → data/FindIndex.js} +40 -24
  19. package/src/{type → data}/Model.js +95 -55
  20. package/src/data/Property.js +21 -0
  21. package/src/data/SearchIndex.js +106 -0
  22. package/src/{type/complex → data/properties}/ArrayType.js +5 -3
  23. package/src/{type/simple → data/properties}/BooleanType.js +3 -3
  24. package/src/{type/complex → data/properties}/CustomType.js +5 -5
  25. package/src/{type/simple → data/properties}/DateType.js +4 -4
  26. package/src/{type/simple → data/properties}/NumberType.js +3 -3
  27. package/src/{type/resolved → data/properties}/ResolvedType.js +3 -2
  28. package/src/{type/resolved → data/properties}/SlugType.js +1 -1
  29. package/src/{type/simple → data/properties}/StringType.js +3 -3
  30. package/src/{type → data/properties}/Type.js +13 -3
  31. package/src/engine/storage/HTTPStorageEngine.js +149 -253
  32. package/src/engine/storage/S3StorageEngine.js +108 -195
  33. package/src/engine/storage/StorageEngine.js +131 -549
  34. package/exports/engine/storage/file.js +0 -3
  35. package/exports/engine/storage/http.js +0 -3
  36. package/exports/engine/storage/s3.js +0 -3
  37. package/src/SchemaCompiler.js +0 -196
  38. package/src/Transactions.js +0 -145
  39. package/src/engine/StorageEngine.js +0 -472
  40. package/src/engine/storage/FileStorageEngine.js +0 -213
  41. package/src/type/index.js +0 -32
@@ -1,611 +1,193 @@
1
- import Query from '../../Query.js';
2
- import Type from '../../type/index.js';
3
- import lunr from 'lunr';
4
-
5
- /**
6
- * The `StorageEngine` class provides a base interface for implementing data storage and retrieval engines.
7
- * It includes methods for handling models, indexes, and search functionality.
8
- *
9
- * @class StorageEngine
10
- */
11
- class StorageEngine {
12
- static configuration = undefined;
13
- static _searchCache = undefined;
14
-
15
- /**
16
- * Retrieves a model by its ID. This method must be implemented by subclasses.
17
- *
18
- * @param {string} _id - The ID of the model to retrieve.
19
- * @throws {NotImplementedError} Throws if the method is not implemented.
20
- * @returns {Promise<Object>} - Returns a promise resolving to the raw data of the requested model.
21
- * @abstract
22
- */
23
- static getById(_id) {
24
- return Promise.reject(new NotImplementedError(`${this.name} must implement .getById()`));
25
- }
26
-
27
- /**
28
- * Deletes a model by its ID. This method must be implemented by subclasses.
29
- *
30
- * @param {string} _id - The ID of the model to retrieve.
31
- * @throws {NotImplementedError} Throws if the method is not implemented.
32
- * @returns {Promise<void>} - Returns a promise resolving when the model has been deleted.
33
- * @abstract
34
- */
35
- static deleteById(_id) {
36
- return Promise.reject(new NotImplementedError(`${this.name} must implement .deleteById()`));
37
- }
38
-
1
+ export default class StorageEngine {
39
2
  /**
40
- * Saves a model to the data store. This method must be implemented by subclasses.
41
- *
42
- * @param {Model} _data - The model data to save.
43
- * @throws {NotImplementedError} Throws if the method is not implemented.
44
- * @returns {Promise<void>}
45
- * @abstract
3
+ * @param {Object} configuration
46
4
  */
47
- static putModel(_data) {
48
- return Promise.reject(new NotImplementedError(`${this.name} must implement .putModel()`));
49
- }
50
-
51
- /**
52
- * Retrieves the index for a given model. This method must be implemented by subclasses.
53
- *
54
- * @param {Model.constructor} _model - The model to retrieve the index for.
55
- * @throws {NotImplementedError} Throws if the method is not implemented.
56
- * @returns {Promise<Object>} - Returns a promise resolving to the model index.
57
- * @abstract
58
- */
59
- static getIndex(_model) {
60
- return Promise.reject(new NotImplementedError(`${this.name} does not implement .getIndex()`));
5
+ constructor(configuration = {}) {
6
+ this.configuration = configuration;
61
7
  }
62
8
 
63
9
  /**
64
- * Saves the index for a given model. This method must be implemented by subclasses.
65
- *
66
- * @param {Object} _index - The index data to save.
67
- * @throws {NotImplementedError} Throws if the method is not implemented.
68
- * @returns {Promise<void>}
69
- * @abstract
10
+ * Get a model
11
+ * @param {string} _id
12
+ * @throws MethodNotImplementedStorageEngineError
13
+ * @throws ModelNotFoundStorageEngineError
14
+ * @return Promise<Object>
70
15
  */
71
- static putIndex(_index) {
72
- return Promise.reject(new NotImplementedError(`${this.name} does not implement .putIndex()`));
16
+ getModel(_id) {
17
+ return Promise.reject(new MethodNotImplementedStorageEngineError('getModel', this));
73
18
  }
74
19
 
75
20
  /**
76
- * Retrieves the compiled search index for a model. This method must be implemented by subclasses.
77
- *
78
- * @param {Model.constructor} _model - The model to retrieve the compiled search index for.
79
- * @throws {NotImplementedError} Throws if the method is not implemented.
80
- * @returns {Promise<Object>} - Returns a promise resolving to the compiled search index.
81
- * @abstract
21
+ * Update a model
22
+ * @param {Object} _model
23
+ * @throws MethodNotImplementedStorageEngineError
24
+ * @return Promise<void>
82
25
  */
83
- static getSearchIndexCompiled(_model) {
84
- return Promise.reject(new NotImplementedError(`${this.name} does not implement .getSearchIndexCompiled()`));
26
+ putModel(_model) {
27
+ return Promise.reject(new MethodNotImplementedStorageEngineError('putModel', this));
85
28
  }
86
29
 
87
30
  /**
88
- * Retrieves the raw search index for a model. This method must be implemented by subclasses.
89
- *
90
- * @param {Model.constructor} _model - The model to retrieve the raw search index for.
91
- * @throws {NotImplementedError} Throws if the method is not implemented.
92
- * @returns {Promise<Object>} - Returns a promise resolving to the raw search index.
93
- * @abstract
31
+ * Delete a model
32
+ * @param {string} _id
33
+ * @throws MethodNotImplementedStorageEngineError
34
+ * @throws ModelNotFoundStorageEngineError
35
+ * @return Promise<void>
94
36
  */
95
- static getSearchIndexRaw(_model) {
96
- return Promise.reject(new NotImplementedError(`${this.name} does not implement .getSearchIndexRaw()`));
37
+ deleteModel(_id) {
38
+ return Promise.reject(new MethodNotImplementedStorageEngineError('deleteModel', this));
97
39
  }
98
40
 
99
41
  /**
100
- * Saves the compiled search index for a model. This method must be implemented by subclasses.
101
- *
102
- * @param {Model.constructor} _model - The model for which the compiled search index is saved.
103
- * @param {Object} _compiledIndex - The compiled search index data.
104
- * @throws {NotImplementedError} Throws if the method is not implemented.
105
- * @returns {Promise<void>}
106
- * @abstract
42
+ * Get a model's index data
43
+ * @param {Model.constructor} _modelConstructor
44
+ * @throws MethodNotImplementedStorageEngineError
45
+ * @return Promise<Record<String, Object>>
107
46
  */
108
- static putSearchIndexCompiled(_model, _compiledIndex) {
109
- return Promise.reject(new NotImplementedError(`${this.name} does not implement .putSearchIndexCompiled()`));
47
+ getIndex(_modelConstructor) {
48
+ return Promise.reject(new MethodNotImplementedStorageEngineError('getIndex', this));
110
49
  }
111
50
 
112
51
  /**
113
- * Saves the raw search index for a model. This method must be implemented by subclasses.
114
- *
115
- * @param {Model.constructor} _model - The model for which the raw search index is saved.
116
- * @param {Object} _rawIndex - The raw search index data.
117
- * @throws {NotImplementedError} Throws if the method is not implemented.
118
- * @returns {Promise<void>}
119
- * @abstract
52
+ * Put a model's index data
53
+ * @param {Model.constructor} _modelConstructor
54
+ * @param {Record<String, Object>} _data
55
+ * @throws MethodNotImplementedStorageEngineError
56
+ * @return Promise<void>
120
57
  */
121
- static putSearchIndexRaw(_model, _rawIndex) {
122
- return Promise.reject(new NotImplementedError(`${this.name} does not implement .putSearchIndexRaw()`));
58
+ putIndex(_modelConstructor, _data) {
59
+ return Promise.reject(new MethodNotImplementedStorageEngineError('putIndex', this));
123
60
  }
124
61
 
125
62
  /**
126
- * Performs a search query on a model's index and returns the matching models.
127
- *
128
- * @param {Model.constructor} model - The model class.
129
- * @param {string} query - The search query string.
130
- * @returns {Promise<Array<Model>>} An array of models matching the search query.
131
- * @throws {EngineError} Throws if the search index is not available for the model.
63
+ * Get a model's raw search index data
64
+ * @param {Model.constructor} _modelConstructor
65
+ * @throws MethodNotImplementedStorageEngineError
66
+ * @return Promise<Record<String, Object>>
132
67
  */
133
- static async search(model, query) {
134
- this.checkConfiguration();
135
-
136
- const index =
137
- (this._searchCache && this.configuration?.cache?.search && Date.now() - this._searchCache[0] < this.configuration.cache.search) ?
138
- this._searchCache[1] :
139
- await this.getSearchIndexCompiled(model)
140
- .then(i => {
141
- this._searchCache = [Date.now(), i];
142
- return i;
143
- })
144
- .catch(error => {
145
- throw new EngineError(`The model ${model.toString()} does not have a search index available.`, error);
146
- });
147
-
148
- const searchIndex = lunr.Index.load(index);
149
-
150
- const results = searchIndex.search(`*${query}*`);
151
-
152
- const output = [];
153
- for (const result of results) {
154
- output.push({
155
- ...result,
156
- score: Number(result.score.toFixed(4)),
157
- model: await this.get(model, result.ref),
158
- });
159
- }
160
-
161
- return output;
68
+ getSearchIndex(_modelConstructor) {
69
+ return Promise.reject(new MethodNotImplementedStorageEngineError('getSearchIndex', this));
162
70
  }
163
71
 
164
72
  /**
165
- * Finds models that match a query in the model's index.
166
- *
167
- * @param {Model.constructor} model - The model class to search.
168
- * @param {object} query - The query object containing search criteria.
169
- * @returns {Array<Model>} An array of models matching the query.
73
+ * Put a model's raw and compiled search index data
74
+ * @param {Model.constructor} _constructor
75
+ * @param {Record<string, object>} _index
76
+ * @throws MethodNotImplementedStorageEngineError
77
+ * @return Promise<Record<String, Object>>
170
78
  */
171
- static async find(model, query) {
172
- this.checkConfiguration();
173
- const index = await this.getIndex(model);
174
-
175
- return new Query(query).execute(model, index);
176
- }
177
-
178
- /**
179
- * Stores a model and its associated index data into the system.
180
- *
181
- * @param {Model} model - The model to store.
182
- * @throws {EngineError} Throws if the model fails to validate or index fails to save.
183
- */
184
- static async put(model) {
185
- this.checkConfiguration();
186
- const uploadedModels = [];
187
- const indexUpdates = {};
188
-
189
- /**
190
- * Process a model, putting updates to the model and all linked models.
191
- * @param {Model} m
192
- * @return {Promise<void>}
193
- */
194
- const processModel = async (m) => {
195
- if (!uploadedModels.includes(m.id)) {
196
- m.validate();
197
-
198
- await this.putModel(m);
199
-
200
- uploadedModels.push(m.id);
201
- indexUpdates[m.constructor.name] = {
202
- ...indexUpdates[m.constructor.name] || {},
203
- [m.id]: m,
204
- };
205
-
206
- if (m.constructor.searchProperties().length > 0) {
207
- const rawSearchIndex = {
208
- ...await this.getSearchIndexRaw(m.constructor),
209
- [m.id]: m.toSearchData(),
210
- };
211
-
212
- await this.putSearchIndexRaw(m.constructor, rawSearchIndex);
213
-
214
- const compiledIndex = lunr(function () {
215
- this.ref('id');
216
-
217
- for (const field of m.constructor.searchProperties()) {
218
- this.field(field);
219
- }
220
-
221
- Object.values(rawSearchIndex).forEach(function (doc) {
222
- this.add(doc);
223
- }, this);
224
- });
225
-
226
- await this.putSearchIndexCompiled(m.constructor, compiledIndex);
227
- }
228
-
229
- for (const [_, property] of Object.entries(m)) {
230
- if (Type.Model.isModel(property)) {
231
- await processModel(property);
232
- }
233
- if (Array.isArray(property) && Type.Model.isModel(property[0])) {
234
- for (const subModel of property) {
235
- await processModel(subModel);
236
- }
237
- }
238
- }
239
- }
240
- };
241
-
242
- await processModel(model);
243
- await this.putIndex(indexUpdates);
244
- }
245
-
246
- /**
247
- * Retrieves a model by its ID and converts it to its data representation.
248
- *
249
- * @param {Model.constructor} model - The model class to retrieve.
250
- * @param {string} id - The ID of the model to retrieve.
251
- * @returns {Model} The found model.
252
- * @throws {NotFoundEngineError} Throws if the model is not found.
253
- */
254
- static async get(model, id) {
255
- this.checkConfiguration();
256
-
257
- try {
258
- const found = await this.getById(id);
259
- return model.fromData(found);
260
- } catch (error) {
261
- if (error.constructor === NotImplementedError) throw error;
262
- throw new NotFoundEngineError(`${this.name}.get(${id}) model not found`, error);
263
- }
264
- }
265
-
266
- /**
267
- * Deletes a model
268
- *
269
- * @param {Model} model
270
- * @return {Promise<void>}
271
- * @throws {NotFoundEngineError} Throws if the model is not found.
272
- */
273
- static async delete(model) {
274
- this.checkConfiguration();
275
-
276
- const modelUpdates = [];
277
- const processedModels = [];
278
- const indexUpdates = {};
279
- const additionalDeletions = [];
280
- const deletedModels = [];
281
-
282
- /**
283
- * Delete the given model, updating search indexes as required.
284
- * @param {Model} m - The model to be deleted.
285
- * @return {Promise<void>}
286
- */
287
- const deleteModel = async (m) => {
288
- if (deletedModels.includes(m.id)) return;
289
- if (m.constructor.searchProperties().length > 0) {
290
- const rawSearchIndex = await this.getSearchIndexRaw(m.constructor);
291
-
292
- delete rawSearchIndex[m.id];
293
-
294
- await this.putSearchIndexRaw(m.constructor, rawSearchIndex);
295
-
296
- const compiledIndex = lunr(function () {
297
- this.ref('id');
298
-
299
- for (const field of m.constructor.searchProperties()) {
300
- this.field(field);
301
- }
302
-
303
- Object.values(rawSearchIndex).forEach(function (doc) {
304
- this.add(doc);
305
- }, this);
306
- });
307
-
308
- await this.putSearchIndexCompiled(m.constructor, compiledIndex);
309
- }
310
-
311
- if (m.constructor.indexedProperties().length > 0) {
312
- indexUpdates[m.constructor.name] = {
313
- ...indexUpdates[m.constructor.name] || {},
314
- [m.id]: undefined,
315
- };
316
- }
317
-
318
- await this.deleteById(m.id);
319
- deletedModels.push(m.id);
320
- };
321
-
322
- /**
323
- * Process updates to all sub-models of the given model.
324
- * @param {Model} m - The model to process for updates.
325
- * @return {Promise<void>}
326
- */
327
- const processModelUpdates = async (m) => {
328
- if (!processedModels.includes(m.id)) {
329
- processedModels.push(m.id);
330
-
331
- for (const [key, property] of Object.entries(m)) {
332
- if (Type.Model.isModel(property)) {
333
- if (property.id === model.id) {
334
- m[key] = undefined;
335
- indexUpdates[m.constructor.name] = {
336
- ...indexUpdates[m.constructor.name] || {},
337
- [m.id]: m,
338
- };
339
- modelUpdates.push(m);
340
- }
341
-
342
- if (m.id !== model.id && (Type.Model.isModel(m.constructor[key]) ? m.constructor[key] : m.constructor[key]())._required) {
343
- additionalDeletions.push(m);
344
- }
345
-
346
- await processModelUpdates(property);
347
- }
348
- if (Array.isArray(property) && Type.Model.isModel(property[0])) {
349
- for (const [index, subModel] of property.entries()) {
350
- if (subModel.id === model.id) {
351
- m[key].splice(index, 1);
352
- indexUpdates[m.constructor.name] = {
353
- ...indexUpdates[m.constructor.name] || {},
354
- [m.id]: m,
355
- };
356
- modelUpdates.push(m);
357
- }
358
- await processModelUpdates(subModel);
359
- }
360
- }
361
- }
362
- }
363
- };
364
-
365
- try {
366
- const hydrated = await this.hydrate(model);
367
- await processModelUpdates(hydrated);
368
- await deleteModel(hydrated);
369
-
370
- for (const updatedModel of modelUpdates) {
371
- if (!additionalDeletions.map(m => m.id).includes(updatedModel.id)) {
372
- await this.put(updatedModel);
373
- }
374
- }
375
-
376
- for (const modelToBeDeleted of additionalDeletions) {
377
- await deleteModel(modelToBeDeleted);
378
- }
379
-
380
- await this.putIndex(indexUpdates);
381
- } catch (error) {
382
- if (error.constructor === NotImplementedError) throw error;
383
- throw new CannotDeleteEngineError(`${this.name}.delete(${model.id}) model cannot be deleted`, error);
384
- }
385
- }
386
-
387
- /**
388
- * Hydrates a model by populating its related properties (e.g., submodels) from stored data.
389
- *
390
- * @param {Model} model - The model to hydrate.
391
- * @returns {Model} The hydrated model.
392
- */
393
- static async hydrate(model) {
394
- this.checkConfiguration();
395
- const hydratedModels = {};
396
-
397
- /**
398
- * Hydrate a model
399
- * @param {Model} modelToProcess
400
- * @return {Promise<Model>}
401
- */
402
- const hydrateModel = async (modelToProcess) => {
403
- hydratedModels[modelToProcess.id] = modelToProcess;
404
-
405
- for (const [name, property] of Object.entries(modelToProcess)) {
406
- if (Type.Model.isDryModel(property)) {
407
- // skipcq: JS-0129
408
- modelToProcess[name] = await hydrateSubModel(property, modelToProcess, name);
409
- } else if (Array.isArray(property) && Type.Model.isDryModel(property[0])) {
410
- // skipcq: JS-0129
411
- modelToProcess[name] = await hydrateModelList(property, modelToProcess, name);
412
- }
413
- }
414
-
415
- return modelToProcess;
416
- };
417
-
418
- /**
419
- * Hydrate a dry sub model
420
- * @param property
421
- * @param modelToProcess
422
- * @param name
423
- * @return {Promise<Model>}
424
- */
425
- const hydrateSubModel = async (property, modelToProcess, name) => {
426
- if (hydratedModels[property.id]) {
427
- return hydratedModels[property.id];
428
- }
429
-
430
- const subModelClass = getSubModelClass(modelToProcess, name);
431
- const subModel = await this.get(subModelClass, property.id);
432
-
433
- const hydratedSubModel = await hydrateModel(subModel);
434
- hydratedModels[property.id] = hydratedSubModel;
435
- return hydratedSubModel;
436
- };
437
-
438
- /**
439
- * Hydrate an array of dry models
440
- * @param property
441
- * @param modelToProcess
442
- * @param name
443
- * @return {Promise<Awaited<*>[]>}
444
- */
445
- const hydrateModelList = async (property, modelToProcess, name) => {
446
- const subModelClass = getSubModelClass(modelToProcess, name, true);
447
-
448
- const newModelList = await Promise.all(property.map(subModel => {
449
- if (hydratedModels[subModel.id]) {
450
- return hydratedModels[subModel.id];
451
- }
452
-
453
- return this.get(subModelClass, subModel.id);
454
- }));
455
-
456
- return Promise.all(newModelList.map(async subModel => {
457
- if (hydratedModels[subModel.id]) {
458
- return hydratedModels[subModel.id];
459
- }
460
-
461
- const hydratedSubModel = await hydrateModel(subModel);
462
- hydratedModels[hydratedSubModel.id] = hydratedSubModel;
463
- return hydratedSubModel;
464
- }));
465
- };
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
- */
474
- function getSubModelClass(modelToProcess, name, isArray = false) {
475
- const constructorField = modelToProcess.constructor[name];
476
-
477
- if (constructorField instanceof Function && !constructorField.prototype) {
478
- return isArray ? constructorField()._items : constructorField();
479
- }
480
-
481
- return isArray ? constructorField._items : constructorField;
482
- }
483
-
484
- return hydrateModel(await this.get(model.constructor, model.id));
485
- }
486
-
487
- /**
488
- * Configures the engine with specific settings.
489
- *
490
- * @param {Object} configuration - The configuration settings for the engine.
491
- * @returns {StorageEngine} A new engine instance with the applied configuration.
492
- */
493
- static configure(configuration) {
494
- /**
495
- * @class ConfiguredStore
496
- * @extends StorageEngine
497
- */
498
- class ConfiguredStore extends this {
499
- static configuration = configuration;
500
- }
501
-
502
- Object.defineProperty(ConfiguredStore, 'name', {value: `${this.toString()}`});
503
-
504
- return ConfiguredStore;
505
- }
506
-
507
- /**
508
- * Checks if the engine is properly configured.
509
- *
510
- * @throws {MissConfiguredError} Throws if the engine is misconfigured.
511
- * @abstract
512
- */
513
- static checkConfiguration() {
514
- // Implemented in extending StorageEngine class
515
- }
516
-
517
- /**
518
- * Returns the name of the engine class.
519
- *
520
- * @returns {string} The name of the engine class.
521
- */
522
- static toString() {
523
- return this.name;
79
+ putSearchIndex(_constructor, _index) {
80
+ return Promise.reject(new MethodNotImplementedStorageEngineError('putSearchIndex', this));
524
81
  }
525
82
  }
526
83
 
527
84
  /**
528
- * Represents a general error that occurs within the engine.
529
- * Extends the built-in `Error` class.
85
+ * @class StorageEngineError
86
+ * @extends Error
530
87
  */
531
- export class EngineError extends Error {
532
- /**
533
- * The underlying error that caused this engine error, if available.
534
- * @type {Error|undefined}
535
- */
536
- underlyingError;
88
+ export class StorageEngineError extends Error {
89
+ }
537
90
 
91
+ /**
92
+ * @class MisconfiguredStorageEngineError
93
+ * @extends StorageEngineError
94
+ */
95
+ export class MisconfiguredStorageEngineError extends StorageEngineError {
538
96
  /**
539
- * Creates an instance of `EngineError`.
540
- *
541
- * @param {string} message - The error message.
542
- * @param {Error} [error] - An optional underlying error that caused this error.
97
+ * @param {string} message
98
+ * @param {StorageEngine} storageEngine
543
99
  */
544
- constructor(message, error = undefined) {
545
- super(message);
546
- this.underlyingError = error;
100
+ constructor(message, storageEngine) {
101
+ super(`Incorrect configuration given for storage engine ${storageEngine.constructor.name}: ${message}`);
547
102
  }
548
103
  }
549
104
 
550
105
  /**
551
- * Represents an error that occurs when a requested resource or item is not found in the engine.
552
- * Extends the `EngineError` class.
106
+ * @class MethodNotImplementedStorageEngineError
107
+ * @extends StorageEngineError
553
108
  */
554
- export class NotFoundEngineError extends EngineError {
109
+ export class MethodNotImplementedStorageEngineError extends StorageEngineError {
555
110
  /**
556
- * Creates an instance of `NotFoundEngineError`.
557
- *
558
- * @param {string} message - The error message.
559
- * @param {Error} [error] - An optional underlying error that caused this error.
111
+ * @param {string} method
112
+ * @param {StorageEngine} storageEngine
560
113
  */
114
+ constructor(method, storageEngine) {
115
+ super(`The method ${method} is not implemented in the storage engine ${storageEngine.constructor.name}`);
116
+ }
561
117
  }
562
118
 
563
119
  /**
564
- * Represents an error that occurs when a requested resource or item cannot be deleted by the engine.
565
- * Extends the `EngineError` class.
120
+ * @class ModelNotFoundStorageEngineError
121
+ * @extends StorageEngineError
566
122
  */
567
- export class CannotDeleteEngineError extends EngineError {
123
+ export class ModelNotFoundStorageEngineError extends StorageEngineError {
568
124
  /**
569
- * Creates an instance of `CannotDeleteEngineError`.
570
- *
571
- * @param {string} message - The error message.
572
- * @param {Error} [error] - An optional underlying error that caused this error.
125
+ * @param {string} modelId
573
126
  */
127
+ constructor(modelId) {
128
+ super(`The model ${modelId} was not found`);
129
+ }
574
130
  }
575
131
 
576
- /**
577
- * Represents an error indicating that a certain method or functionality is not implemented in the engine.
578
- * Extends the `EngineError` class.
579
- */
580
- export class NotImplementedError extends EngineError {
132
+ export class DeleteHasUnintendedConsequencesStorageEngineError extends StorageEngineError {
581
133
  /**
582
- * Creates an instance of `NotImplementedError`.
583
- *
584
- * @param {string} message - The error message.
585
- * @param {Error} [error] - An optional underlying error that caused this error.
134
+ * @param {string} modelId
135
+ * @param {Object} consequences
136
+ * @param {Array<String>?} consequences.willDelete
137
+ * @param {Array<String>?} consequences.willUpdate
586
138
  */
139
+ constructor(modelId, consequences) {
140
+ super(`Deleting ${modelId} has unintended consequences`);
141
+ this.consequences = consequences;
142
+ }
587
143
  }
588
144
 
589
145
  /**
590
- * Represents an error indicating that the engine is misconfigured.
591
- * Extends the `EngineError` class.
146
+ * Represents a transactional operation to be executed, typically queued and later committed.
147
+ *
148
+ * Stores the method to invoke, the arguments to apply, and tracks the result or error state
149
+ * of the transaction once it's processed.
150
+ *
151
+ * @class Operation
592
152
  */
593
- export class MissConfiguredError extends EngineError {
594
- /**
595
- * The configuration that led to the misconfiguration error.
596
- * @type {Object}
597
- */
598
- configuration;
599
-
600
- /**
601
- * Creates an instance of `MissConfiguredError`.
602
- *
603
- * @param {Object} configuration - The configuration object that caused the misconfiguration.
604
- */
605
- constructor(configuration) {
606
- super('StorageEngine is miss-configured');
607
- this.configuration = configuration;
153
+ export class Operation {
154
+ constructor(method, ...args) {
155
+ this.method = method;
156
+ this.args = args;
157
+ this.original = undefined;
158
+ this.error = undefined;
159
+ this.committed = false;
608
160
  }
609
161
  }
610
162
 
611
- export default StorageEngine;
163
+ /**
164
+ *
165
+ * @param {Array<Operation>} transactions
166
+ * @param {StorageEngine} engine
167
+ * @return {StorageEngine}
168
+ */
169
+ export function CreateTransactionalStorageEngine(operations, engine) {
170
+ const transactionalEngine = Object.create(engine);
171
+
172
+ transactionalEngine.putModel = (...args) => {
173
+ operations.push(new Operation('putModel', ...args));
174
+ return Promise.resolve();
175
+ };
176
+
177
+ transactionalEngine.deleteModel = (...args) => {
178
+ operations.push(new Operation('deleteModel', ...args));
179
+ return Promise.resolve();
180
+ };
181
+
182
+ transactionalEngine.putIndex = (...args) => {
183
+ operations.push(new Operation('putIndex', ...args));
184
+ return Promise.resolve();
185
+ };
186
+
187
+ transactionalEngine.putSearchIndex = (...args) => {
188
+ operations.push(new Operation('putSearchIndex', ...args));
189
+ return Promise.resolve();
190
+ };
191
+
192
+ return transactionalEngine;
193
+ }