@acodeninja/persist 3.0.0-next.12 → 3.0.0-next.14

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