@acodeninja/persist 3.0.0-next.2 → 3.0.0-next.21

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 +6 -6
  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} +37 -35
  6. package/docs/models-as-properties.md +40 -40
  7. package/docs/search-queries.md +3 -5
  8. package/docs/storage-engines.md +19 -35
  9. package/docs/structured-queries.md +56 -45
  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 +631 -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 +41 -26
  20. package/src/data/Property.js +19 -0
  21. package/src/data/SearchIndex.js +106 -0
  22. package/src/{type/complex → data/properties}/ArrayType.js +1 -1
  23. package/src/{type/simple → data/properties}/BooleanType.js +1 -1
  24. package/src/{type/complex → data/properties}/CustomType.js +1 -1
  25. package/src/{type/simple → data/properties}/DateType.js +1 -1
  26. package/src/{type/simple → data/properties}/NumberType.js +1 -1
  27. package/src/{type/resolved → data/properties}/ResolvedType.js +3 -2
  28. package/src/{type/simple → data/properties}/StringType.js +1 -1
  29. package/src/{type → data/properties}/Type.js +8 -0
  30. package/src/engine/storage/HTTPStorageEngine.js +149 -253
  31. package/src/engine/storage/S3StorageEngine.js +108 -195
  32. package/src/engine/storage/StorageEngine.js +114 -550
  33. package/exports/engine/storage/file.js +0 -3
  34. package/exports/engine/storage/http.js +0 -3
  35. package/exports/engine/storage/s3.js +0 -3
  36. package/src/SchemaCompiler.js +0 -192
  37. package/src/Transactions.js +0 -145
  38. package/src/engine/StorageEngine.js +0 -250
  39. package/src/engine/storage/FileStorageEngine.js +0 -213
  40. package/src/type/index.js +0 -32
  41. /package/src/{type/resolved → data/properties}/SlugType.js +0 -0
@@ -0,0 +1,631 @@
1
+ import {
2
+ CreateTransactionalStorageEngine,
3
+ DeleteHasUnintendedConsequencesStorageEngineError,
4
+ } from './engine/storage/StorageEngine.js';
5
+ import FindIndex from './data/FindIndex.js';
6
+ import Model from './data/Model.js';
7
+ import SearchIndex from './data/SearchIndex.js';
8
+ import _ from 'lodash';
9
+
10
+ /**
11
+ * Represents a transactional operation to be executed, typically queued and later committed.
12
+ *
13
+ * Stores the method to invoke, the arguments to apply, and tracks the result or error state
14
+ * of the transaction once it's processed.
15
+ *
16
+ * @class Transaction
17
+ */
18
+ export class Transaction {
19
+ constructor(method, ...args) {
20
+ this.method = method;
21
+ this.args = args;
22
+ this.original = undefined;
23
+ this.error = undefined;
24
+ this.committed = false;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * @class Connection
30
+ */
31
+ export default class Connection {
32
+ /**
33
+ * @private
34
+ * @property {StorageEngine}
35
+ */
36
+ #storage;
37
+
38
+ /**
39
+ * @private
40
+ * @property {CacheEngine|undefined}
41
+ */
42
+ #cache;
43
+
44
+ /**
45
+ * @private
46
+ * @property {Record<String, Model.constructor>}
47
+ */
48
+ #models;
49
+
50
+ /**
51
+ * Create a new connection
52
+ * @param {StorageEngine} storage
53
+ * @param {CacheEngine|undefined} cache
54
+ * @param {Array<Model.constructor>} models
55
+ */
56
+ constructor(storage, cache, models) {
57
+ this.#storage = storage;
58
+ this.#cache = cache;
59
+ this.#models = Object.fromEntries((models ?? []).map(model => [model.name, model]));
60
+
61
+ if (!this.#storage) throw new MissingArgumentsConnectionError('No storage engine provided');
62
+ }
63
+
64
+ /**
65
+ * Get a model by its id
66
+ * @param {String} modelId
67
+ * @throws {ModelNotRegisteredConnectionError}
68
+ * @return {Promise<Model>}
69
+ */
70
+ async get(modelId) {
71
+ const constructor = this.#getModelConstructorFromId(modelId);
72
+
73
+ const data = await this.#storage.getModel(modelId);
74
+
75
+ return constructor.fromData(data);
76
+ }
77
+
78
+ /**
79
+ * Accepts a dry model, for example:
80
+ *
81
+ * - an object with only an id property
82
+ * - a model missing linked model fields
83
+ * - a model missing non-indexed properties
84
+ *
85
+ * and fetches all data for a model.
86
+ * @param {Object} dryModel
87
+ * @return {Promise<Model>}
88
+ */
89
+ async hydrate(dryModel) {
90
+ const hydratedModels = {};
91
+
92
+ /**
93
+ * Recursively hydrates a single model and its nested properties.
94
+ *
95
+ * @param {Object|Model} modelToProcess - The model instance to hydrate.
96
+ * @returns {Promise<Model>} The hydrated model instance.
97
+ */
98
+ const hydrateModel = async (modelToProcess) => {
99
+ hydratedModels[modelToProcess.id] = modelToProcess;
100
+
101
+ for (const [name, property] of Object.entries(modelToProcess)) {
102
+ if (Model.isDryModel(property)) {
103
+ // skipcq: JS-0129
104
+ modelToProcess[name] = await hydrateSubModel(property);
105
+ } else if (Array.isArray(property) && Model.isDryModel(property[0])) {
106
+ // skipcq: JS-0129
107
+ modelToProcess[name] = await hydrateModelList(property);
108
+ }
109
+ }
110
+
111
+ return modelToProcess;
112
+ };
113
+
114
+ /**
115
+ * Hydrates a nested sub-model if it hasn't already been hydrated.
116
+ *
117
+ * @param {Object} property - The sub-model with a known ID but incomplete data.
118
+ * @returns {Promise<Model>} The fully hydrated sub-model.
119
+ */
120
+ const hydrateSubModel = async (property) => {
121
+ if (hydratedModels[property.id]) {
122
+ return hydratedModels[property.id];
123
+ }
124
+
125
+ const subModel = await this.get(property.id);
126
+
127
+ const hydratedSubModel = await hydrateModel(subModel);
128
+ hydratedModels[property.id] = hydratedSubModel;
129
+ return hydratedSubModel;
130
+ };
131
+
132
+ /**
133
+ * Hydrates a list of related sub-models.
134
+ *
135
+ * @param {Array<Object>} property - Array of dry sub-models.
136
+ * @returns {Promise<Array<Model>>} Array of hydrated sub-models.
137
+ */
138
+ const hydrateModelList = async (property) => {
139
+ const newModelList = await Promise.all(property.map(subModel => {
140
+ if (hydratedModels[subModel.id]) {
141
+ return hydratedModels[subModel.id];
142
+ }
143
+
144
+ return this.get(subModel.id);
145
+ }));
146
+
147
+ return Promise.all(newModelList.map(async subModel => {
148
+ if (hydratedModels[subModel.id]) {
149
+ return hydratedModels[subModel.id];
150
+ }
151
+
152
+ const hydratedSubModel = await hydrateModel(subModel);
153
+ hydratedModels[hydratedSubModel.id] = hydratedSubModel;
154
+ return hydratedSubModel;
155
+ }));
156
+ };
157
+
158
+ return hydrateModel(await this.get(dryModel.id));
159
+ }
160
+
161
+ /**
162
+ * Persists a model if it has changed, and updates all related models and their indexes
163
+ * @param {Model} model
164
+ * @return {Promise<void>}
165
+ */
166
+ async put(model) {
167
+ const processedModels = [];
168
+ const modelsToPut = [];
169
+ const modelsToReindex = {};
170
+ const modelsToReindexSearch = {};
171
+
172
+ /**
173
+ * @param {Model} modelToProcess
174
+ * @return {Promise<void>}
175
+ */
176
+ const processModel = async (modelToProcess) => {
177
+ if (processedModels.includes(modelToProcess.id))
178
+ return;
179
+
180
+ processedModels.push(modelToProcess.id);
181
+
182
+ if (!Object.keys(this.#models).includes(modelToProcess.constructor.name))
183
+ throw new ModelNotRegisteredConnectionError(modelToProcess, this.#storage);
184
+
185
+ modelToProcess.validate();
186
+ const currentModel = await this.get(modelToProcess.id).catch(() => null);
187
+
188
+ const modelToProcessHasChanged = !_.isEqual(currentModel?.toData() || {}, modelToProcess.toData());
189
+
190
+ if (modelToProcessHasChanged) modelsToPut.push(modelToProcess);
191
+
192
+ if (
193
+ Boolean(modelToProcess.constructor.indexedProperties().length) &&
194
+ (!currentModel || !_.isEqual(currentModel.toIndexData(), modelToProcess.toIndexData()))
195
+ ) {
196
+ const modelToProcessConstructor = this.#getModelConstructorFromId(modelToProcess.id).name;
197
+ modelsToReindex[modelToProcessConstructor] = modelsToReindex[modelToProcessConstructor] || [];
198
+ modelsToReindex[modelToProcessConstructor].push(modelToProcess);
199
+ }
200
+
201
+ if (
202
+ Boolean(modelToProcess.constructor.searchProperties().length) &&
203
+ (!currentModel || !_.isEqual(currentModel.toSearchData(), modelToProcess.toSearchData()))
204
+ ) {
205
+ const modelToProcessConstructor = this.#getModelConstructorFromId(modelToProcess.id).name;
206
+ modelsToReindexSearch[modelToProcessConstructor] = modelsToReindexSearch[modelToProcessConstructor] || [];
207
+ modelsToReindexSearch[modelToProcessConstructor].push(modelToProcess);
208
+ }
209
+
210
+ for (const [field, value] of Object.entries(modelToProcess)) {
211
+ if (Model.isModel(value)) {
212
+ await processModel(modelToProcess[field]);
213
+ }
214
+ }
215
+ };
216
+
217
+ await processModel(model);
218
+
219
+ await Promise.all([
220
+ Promise.all(modelsToPut.map(m => this.#storage.putModel(m.toData()))),
221
+ Promise.all(Object.entries(modelsToReindex).map(async ([constructorName, models]) => {
222
+ const modelConstructor = this.#models[constructorName];
223
+ const index = await this.#storage.getIndex(modelConstructor);
224
+
225
+ await this.#storage.putIndex(modelConstructor, {
226
+ ...index || {},
227
+ ...Object.fromEntries(models.map(m => [m.id, m.toIndexData()])),
228
+ });
229
+ })),
230
+ Promise.all(Object.entries(modelsToReindexSearch).map(async ([constructorName, models]) => {
231
+ const modelConstructor = this.#models[constructorName];
232
+ const index = await this.#storage.getSearchIndex(modelConstructor);
233
+
234
+ await this.#storage.putSearchIndex(modelConstructor, {
235
+ ...index || {},
236
+ ...Object.fromEntries(models.map(m => [m.id, m.toSearchData()])),
237
+ });
238
+ })),
239
+ ]);
240
+ }
241
+
242
+ /**
243
+ * Delete a model and update indexes that reference it
244
+ * @param {Model} model
245
+ * @param {Array<string>} propagateTo - List of model ids that are expected to be deleted
246
+ * @throws {ModelNotRegisteredConnectionError}
247
+ * @throws {ModelNotFoundStorageEngineError}
248
+ */
249
+ async delete(model, propagateTo = []) {
250
+ const processedModels = [];
251
+ const modelsToDelete = [];
252
+ const modelsToPut = [];
253
+ const indexCache = {};
254
+ const indexActions = {};
255
+ const searchIndexCache = {};
256
+ const searchIndexActions = {};
257
+ const modelCache = {};
258
+
259
+ propagateTo.push(model.id);
260
+
261
+ /**
262
+ * Process a model for deletion
263
+ * @param {Model} modelToProcess
264
+ * @return {Promise<void>}
265
+ */
266
+ const processModel = async (modelToProcess) => {
267
+ if (processedModels.includes(modelToProcess.id)) return;
268
+
269
+ processedModels.push(modelToProcess.id);
270
+
271
+ const modelsToProcess = [];
272
+ if (!Object.keys(this.#models).includes(modelToProcess.constructor.name))
273
+ throw new ModelNotRegisteredConnectionError(modelToProcess, this.#storage);
274
+
275
+ const currentModel = modelCache[modelToProcess.id] ?? await this.get(modelToProcess.id);
276
+ modelCache[currentModel.id] = currentModel;
277
+
278
+ if (!modelsToDelete.includes(currentModel.id)) modelsToDelete.push(currentModel.id);
279
+
280
+ const modelToProcessConstructor = this.#getModelConstructorFromId(modelToProcess.id);
281
+ indexActions[modelToProcessConstructor] = indexActions[modelToProcessConstructor] ?? [];
282
+ searchIndexActions[modelToProcessConstructor] = searchIndexActions[modelToProcessConstructor] ?? [];
283
+
284
+ if (currentModel.constructor.indexedPropertiesResolved().length) {
285
+ indexActions[modelToProcessConstructor].push(['delete', modelToProcess]);
286
+ }
287
+
288
+ if (currentModel.constructor.searchProperties().length) {
289
+ searchIndexActions[modelToProcessConstructor].push(['delete', modelToProcess]);
290
+ }
291
+
292
+ const linkedModels = await this.#getInstancesLinkedTo(modelToProcess, indexCache);
293
+ const links = this.#getLinksFor(modelToProcess.constructor);
294
+ Object.values(Object.fromEntries(await Promise.all(
295
+ Object.entries(linkedModels)
296
+ .map(async ([constructor, updatableModels]) => [
297
+ constructor,
298
+ await Promise.all(updatableModels.map(async m => {
299
+ const upToDateModel = modelCache[m.id] ?? await this.get(m.id);
300
+ modelCache[upToDateModel.id] = upToDateModel;
301
+ return upToDateModel;
302
+ })),
303
+ ]),
304
+ ))).flat(1)
305
+ .forEach(m =>
306
+ Object.entries(links[m.constructor.name])
307
+ .forEach(([linkName, modelConstructor]) => {
308
+ if ((
309
+ typeof modelConstructor[linkName] === 'function' &&
310
+ !/^class/.test(Function.prototype.toString.call(modelConstructor[linkName])) &&
311
+ !Model.isModel(modelConstructor[linkName]) ?
312
+ modelConstructor[linkName]() : modelConstructor
313
+ )._required) {
314
+ if (!modelsToDelete.includes(m.id)) modelsToDelete.push(m.id);
315
+ modelsToProcess.push(m);
316
+ } else {
317
+ m[linkName] = undefined;
318
+ modelsToPut.push(m);
319
+
320
+ indexActions[this.#getModelConstructorFromId(m.id)].push(['reindex', m]);
321
+
322
+ if (m.constructor.searchProperties().length) {
323
+ searchIndexActions[this.#getModelConstructorFromId(m.id)].push(['reindex', m]);
324
+ }
325
+ }
326
+ }),
327
+ );
328
+
329
+ for (const modelToBeProcessed of modelsToProcess) {
330
+ await processModel(modelToBeProcessed);
331
+ }
332
+ };
333
+
334
+ await processModel(model);
335
+
336
+ const unrequestedDeletions = modelsToDelete.filter(m => !propagateTo.includes(m));
337
+ if (unrequestedDeletions.length) {
338
+ throw new DeleteHasUnintendedConsequencesStorageEngineError(model.id, {
339
+ willDelete: unrequestedDeletions,
340
+ });
341
+ }
342
+
343
+ await Promise.all([
344
+ Promise.all(Object.entries(indexActions).map(async ([constructorName, actions]) => {
345
+ const modelConstructor = this.#models[constructorName];
346
+ indexCache[constructorName] = indexCache[constructorName] ?? await this.#storage.getIndex(modelConstructor);
347
+
348
+ actions.forEach(([action, actionModel]) => {
349
+ if (action === 'delete') {
350
+ indexCache[constructorName] = _.omit(indexCache[constructorName], [actionModel.id]);
351
+ }
352
+ if (action === 'reindex') {
353
+ indexCache[constructorName] = {
354
+ ...indexCache[constructorName],
355
+ [actionModel.id]: actionModel.toIndexData(),
356
+ };
357
+ }
358
+ });
359
+ })),
360
+ Promise.all(Object.entries(searchIndexActions).map(async ([constructorName, actions]) => {
361
+ const modelConstructor = this.#models[constructorName];
362
+ searchIndexCache[constructorName] = searchIndexCache[constructorName] ?? await this.#storage.getSearchIndex(modelConstructor);
363
+
364
+ actions.forEach(([action, actionModel]) => {
365
+ if (action === 'delete') {
366
+ searchIndexCache[constructorName] = _.omit(searchIndexCache[constructorName], [actionModel.id]);
367
+ }
368
+ if (action === 'reindex') {
369
+ searchIndexCache[constructorName] = {
370
+ ...searchIndexCache[constructorName],
371
+ [actionModel.id]: actionModel.toSearchData(),
372
+ };
373
+ }
374
+ });
375
+ })),
376
+ ]);
377
+
378
+ await Promise.all([
379
+ Promise.all(modelsToDelete.map(m => this.#storage.deleteModel(m))),
380
+ Promise.all(modelsToPut.map(m => this.#storage.putModel(m.toData()))),
381
+ Promise.all(
382
+ Object.entries(indexCache)
383
+ .map(([constructorName, index]) => this.#storage.putIndex(this.#models[constructorName], index)),
384
+ ),
385
+ Promise.all(
386
+ Object.entries(searchIndexCache)
387
+ .map(([constructorName, index]) =>
388
+ this.#models[constructorName].searchProperties().length > 0 ?
389
+ this.#storage.putSearchIndex(this.#models[constructorName], index) :
390
+ Promise.resolve(),
391
+ ),
392
+ ),
393
+ ]);
394
+ }
395
+
396
+ /**
397
+ * Search the given model's search properties for matching results.
398
+ * Wildcards: character '*' can be placed at any location in a query
399
+ * Fields: search for a specific field's value with 'field:value'
400
+ * Boosting: if foo is important try 'foo^10 bar'
401
+ * Fuzzy: 'foo~1' will match 'boo' but not 'bao', 'foo~2' would match 'bao'
402
+ * Must include: '+foo bar' must include 'foo' and may include 'bar'
403
+ * Must not include: '-foo bar' must not include 'foo' and may include 'bar'
404
+ * Mixed include: '+foo -bar' must include 'foo' must not include 'bar'
405
+ * @param {Model.constructor} constructor
406
+ * @param {string} query
407
+ * @return {Promise<Array<SearchResult>>}
408
+ */
409
+ async search(constructor, query) {
410
+ const searchIndex = await this.#storage.getSearchIndex(constructor)
411
+ .then(index => new SearchIndex(constructor, index));
412
+
413
+ return searchIndex.search(query);
414
+ }
415
+
416
+ /**
417
+ * Find using a structured query and indexed fields.
418
+ *
419
+ * @param {Model.constructor} constructor
420
+ * @param {Object} query
421
+ * @return {Promise<Array<SearchResult>>}
422
+ */
423
+ async find(constructor, query) {
424
+ const findIndex = await this.#storage.getIndex(constructor)
425
+ .then(index => new FindIndex(constructor, index));
426
+
427
+ return findIndex.query(query);
428
+ }
429
+
430
+ /**
431
+ * Start a transaction, allowing multiple queries to be queued up and committed in one go.
432
+ * Should an error occur, any committed operations will be reverted.
433
+ * @return {Connection}
434
+ */
435
+ transaction() {
436
+ const transactions = [];
437
+
438
+ const engine = CreateTransactionalStorageEngine(transactions, this.#storage);
439
+
440
+ const connection = new this.constructor(engine, this.#cache, Object.values(this.#models));
441
+
442
+ connection.commit = async () => {
443
+ try {
444
+ for (const [index, transaction] of transactions.entries()) {
445
+ try {
446
+ if (transaction.method === 'putModel')
447
+ transactions[index].original = await this.#storage.getModel(transaction.args[0].id).catch(() => undefined);
448
+
449
+ if (transaction.method === 'deleteModel')
450
+ transactions[index].original = await this.#storage.getModel(transaction.args[0]);
451
+
452
+ await this.#storage[transaction.method](...transaction.args);
453
+
454
+ transactions[index].committed = true;
455
+ } catch (error) {
456
+ transactions[index].error = error;
457
+ throw error;
458
+ }
459
+ }
460
+ } catch (error) {
461
+ await Promise.all(
462
+ transactions
463
+ .filter(t => t.committed && t.original)
464
+ .map(t => this.#storage.putModel(t.original)),
465
+ );
466
+
467
+ throw new CommitFailedTransactionError(transactions, error);
468
+ }
469
+ };
470
+
471
+ return connection;
472
+ }
473
+
474
+ /**
475
+ * Get the model constructor from a model id
476
+ * @param {String} modelId
477
+ * @throws ModelNotRegisteredConnectionError
478
+ * @return Model.constructor
479
+ */
480
+ #getModelConstructorFromId(modelId) {
481
+ const modelName = modelId.split('/')[0];
482
+ const constructor = this.#models[modelName];
483
+
484
+ if (!constructor) throw new ModelNotRegisteredConnectionError(modelName, this.#storage);
485
+
486
+ return constructor;
487
+ }
488
+
489
+ /**
490
+ * Get model classes that are directly linked to the given model in either direction
491
+ * @param {Model.constructor} model
492
+ * @return {Record<string, Record<string, Model.constructor>>}
493
+ */
494
+ #getLinksFor(model) {
495
+ return Object.fromEntries(
496
+ Object.entries(this.#getAllModelLinks())
497
+ .filter(([modelName, links]) =>
498
+ model.name === modelName ||
499
+ Object.values(links).some((link) => link.name === model.name),
500
+ ),
501
+ );
502
+ }
503
+
504
+ /**
505
+ * Get all model links
506
+ * @return {Record<string, Record<string, Model.constructor>>}
507
+ */
508
+ #getAllModelLinks() {
509
+ return Object.entries(this.#models)
510
+ .map(([registeredModelName, registeredModelClass]) =>
511
+ Object.entries(registeredModelClass)
512
+ .map(([propertyName, propertyProperty]) => [
513
+ registeredModelName,
514
+ propertyName,
515
+ typeof propertyProperty === 'function' &&
516
+ !/^class/.test(Function.prototype.toString.call(propertyProperty)) &&
517
+ !Model.isModel(propertyProperty) ?
518
+ propertyProperty() : propertyProperty,
519
+ ])
520
+ .filter(([_m, _p, type]) => Model.isModel(type))
521
+ .map(([containingModel, propertyName, propertyProperty]) => ({
522
+ containingModel,
523
+ propertyName,
524
+ propertyProperty,
525
+ })),
526
+ )
527
+ .flat()
528
+ .reduce((accumulator, {containingModel, propertyName, propertyProperty}) => ({
529
+ ...accumulator,
530
+ [containingModel]: {
531
+ ...accumulator[containingModel] || {},
532
+ [propertyName]: propertyProperty,
533
+ },
534
+ }), {});
535
+ }
536
+
537
+ /**
538
+ * Get model instance that are directly linked to the given model in either direction
539
+ * @param {Model} model
540
+ * @param {object} cache
541
+ * @return {Record<string, Record<string, Model>>}
542
+ */
543
+ async #getInstancesLinkedTo(model, cache) {
544
+ return Object.fromEntries(
545
+ Object.entries(
546
+ await Promise.all(
547
+ Object.entries(this.#getLinksFor(model.constructor))
548
+ .map(([name, _index]) =>
549
+ cache[name] ? Promise.resolve([name, Object.values(cache[name])]) :
550
+ this.#storage.getIndex(this.#models[name])
551
+ .then(i => {
552
+ cache[name] = i;
553
+ return [name, Object.values(i)];
554
+ }),
555
+ ),
556
+ ).then(Object.fromEntries),
557
+ ).map(([name, index]) => [
558
+ name,
559
+ index.map(item => Object.fromEntries(
560
+ Object.entries(item)
561
+ .filter(([propertyName, property]) => propertyName === 'id' || property?.id === model.id),
562
+ )).filter(item => Object.keys(item).length > 1),
563
+ ]),
564
+ );
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Base class for errors that occur during connection operations.
570
+ *
571
+ * @class ConnectionError
572
+ * @extends Error
573
+ */
574
+ export class ConnectionError extends Error {
575
+ }
576
+
577
+ /**
578
+ * Thrown when a connection is created with missing arguments.
579
+ *
580
+ * @class MissingArgumentsConnectionError
581
+ * @extends ConnectionError
582
+ */
583
+ export class MissingArgumentsConnectionError extends ConnectionError {
584
+ }
585
+
586
+ /**
587
+ * Thrown when a model class is not registered.
588
+ *
589
+ * @class ModelNotRegisteredConnectionError
590
+ * @extends ConnectionError
591
+ */
592
+ export class ModelNotRegisteredConnectionError extends ConnectionError {
593
+ /**
594
+ * @param {Model|String} constructor
595
+ * @param {Connection} connection
596
+ */
597
+ constructor(constructor, connection) {
598
+ const modelName = typeof constructor === 'string' ? constructor : constructor.constructor.name;
599
+ super(`The model ${modelName} is not registered in the storage engine ${connection.constructor.name}`);
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Base class for errors that occur during transactions.
605
+ *
606
+ * @class TransactionError
607
+ * @extends {Error}
608
+ */
609
+ class TransactionError extends Error {
610
+ }
611
+
612
+ /**
613
+ * Thrown when a transaction fails to commit.
614
+ *
615
+ * Contains the original error and the list of transactions involved.
616
+ *
617
+ * @class CommitFailedTransactionError
618
+ * @extends {TransactionError}
619
+ */
620
+ export class CommitFailedTransactionError extends TransactionError {
621
+ /**
622
+ *
623
+ * @param {Array<Transaction>} transactions
624
+ * @param {Error} error
625
+ */
626
+ constructor(transactions, error) {
627
+ super('Transaction failed to commit.');
628
+ this.transactions = transactions;
629
+ this.error = error;
630
+ }
631
+ }