@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
@@ -1,517 +0,0 @@
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 upToDateModel = modelCache[m.id] ?? await this.get(m.id);
167
- modelCache[upToDateModel.id] = upToDateModel;
168
- return upToDateModel;
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 modelToBeProcessed of modelsToProcess) {
197
- await processModel(modelToBeProcessed);
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
- }