@acodeninja/persist 2.3.2 → 2.4.0
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.
- package/package.json +1 -1
- package/src/engine/Engine.js +150 -1
- package/src/engine/FileEngine.js +21 -4
- package/src/engine/HTTPEngine.js +29 -5
- package/src/engine/S3Engine.js +29 -7
package/package.json
CHANGED
package/src/engine/Engine.js
CHANGED
@@ -24,6 +24,18 @@ class Engine {
|
|
24
24
|
return Promise.reject(new NotImplementedError(`${this.name} must implement .getById()`));
|
25
25
|
}
|
26
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
|
+
|
27
39
|
/**
|
28
40
|
* Saves a model to the data store. This method must be implemented by subclasses.
|
29
41
|
*
|
@@ -181,7 +193,10 @@ class Engine {
|
|
181
193
|
await this.putModel(m);
|
182
194
|
|
183
195
|
uploadedModels.push(m.id);
|
184
|
-
indexUpdates[m.constructor.name] =
|
196
|
+
indexUpdates[m.constructor.name] = {
|
197
|
+
...indexUpdates[m.constructor.name] || {},
|
198
|
+
[m.id]: m,
|
199
|
+
};
|
185
200
|
|
186
201
|
if (m.constructor.searchProperties().length > 0) {
|
187
202
|
const rawSearchIndex = {
|
@@ -243,6 +258,127 @@ class Engine {
|
|
243
258
|
}
|
244
259
|
}
|
245
260
|
|
261
|
+
/**
|
262
|
+
* Deletes a model
|
263
|
+
*
|
264
|
+
* @param {Model} model
|
265
|
+
* @return {Promise<void>}
|
266
|
+
* @throws {NotFoundEngineError} Throws if the model is not found.
|
267
|
+
*/
|
268
|
+
static async delete(model) {
|
269
|
+
this.checkConfiguration();
|
270
|
+
|
271
|
+
const modelUpdates = [];
|
272
|
+
const processedModels = [];
|
273
|
+
const indexUpdates = {};
|
274
|
+
const additionalDeletions = [];
|
275
|
+
const deletedModels = [];
|
276
|
+
|
277
|
+
/**
|
278
|
+
* Delete the given model, updating search indexes as required.
|
279
|
+
* @param {Model} m - The model to be deleted.
|
280
|
+
* @return {Promise<void>}
|
281
|
+
*/
|
282
|
+
const deleteModel = async (m) => {
|
283
|
+
if (deletedModels.includes(m.id)) return;
|
284
|
+
if (m.constructor.searchProperties().length > 0) {
|
285
|
+
const rawSearchIndex = await this.getSearchIndexRaw(m.constructor);
|
286
|
+
|
287
|
+
delete rawSearchIndex[m.id];
|
288
|
+
|
289
|
+
await this.putSearchIndexRaw(m.constructor, rawSearchIndex);
|
290
|
+
|
291
|
+
const compiledIndex = lunr(function () {
|
292
|
+
this.ref('id');
|
293
|
+
|
294
|
+
for (const field of m.constructor.searchProperties()) {
|
295
|
+
this.field(field);
|
296
|
+
}
|
297
|
+
|
298
|
+
Object.values(rawSearchIndex).forEach(function (doc) {
|
299
|
+
this.add(doc);
|
300
|
+
}, this);
|
301
|
+
});
|
302
|
+
|
303
|
+
await this.putSearchIndexCompiled(m.constructor, compiledIndex);
|
304
|
+
}
|
305
|
+
|
306
|
+
if (m.constructor.indexedProperties().length > 0) {
|
307
|
+
indexUpdates[m.constructor.name] = {
|
308
|
+
...indexUpdates[m.constructor.name] || {},
|
309
|
+
[m.id]: undefined,
|
310
|
+
};
|
311
|
+
}
|
312
|
+
|
313
|
+
await this.deleteById(m.id);
|
314
|
+
deletedModels.push(m.id);
|
315
|
+
};
|
316
|
+
|
317
|
+
/**
|
318
|
+
* Process updates to all sub-models of the given model.
|
319
|
+
* @param {Model} m - The model to process for updates.
|
320
|
+
* @return {Promise<void>}
|
321
|
+
*/
|
322
|
+
const processModelUpdates = async (m) => {
|
323
|
+
if (!processedModels.includes(m.id)) {
|
324
|
+
processedModels.push(m.id);
|
325
|
+
|
326
|
+
for (const [key, property] of Object.entries(m)) {
|
327
|
+
if (Type.Model.isModel(property)) {
|
328
|
+
if (property.id === model.id) {
|
329
|
+
m[key] = undefined;
|
330
|
+
indexUpdates[m.constructor.name] = {
|
331
|
+
...indexUpdates[m.constructor.name] || {},
|
332
|
+
[m.id]: m,
|
333
|
+
};
|
334
|
+
modelUpdates.push(m);
|
335
|
+
}
|
336
|
+
|
337
|
+
if (m.id !== model.id && (Type.Model.isModel(m.constructor[key]) ? m.constructor[key] : m.constructor[key]())._required) {
|
338
|
+
additionalDeletions.push(m);
|
339
|
+
}
|
340
|
+
|
341
|
+
await processModelUpdates(property);
|
342
|
+
}
|
343
|
+
if (Array.isArray(property) && Type.Model.isModel(property[0])) {
|
344
|
+
for (const [index, subModel] of property.entries()) {
|
345
|
+
if (subModel.id === model.id) {
|
346
|
+
m[key].splice(index, 1);
|
347
|
+
indexUpdates[m.constructor.name] = {
|
348
|
+
...indexUpdates[m.constructor.name] || {},
|
349
|
+
[m.id]: m,
|
350
|
+
};
|
351
|
+
modelUpdates.push(m);
|
352
|
+
}
|
353
|
+
await processModelUpdates(subModel);
|
354
|
+
}
|
355
|
+
}
|
356
|
+
}
|
357
|
+
}
|
358
|
+
};
|
359
|
+
|
360
|
+
try {
|
361
|
+
const hydrated = await this.hydrate(model);
|
362
|
+
await processModelUpdates(hydrated);
|
363
|
+
await deleteModel(hydrated);
|
364
|
+
|
365
|
+
for (const updatedModel of modelUpdates) {
|
366
|
+
if (!additionalDeletions.map(m => m.id).includes(updatedModel.id)) {
|
367
|
+
await this.put(updatedModel);
|
368
|
+
}
|
369
|
+
}
|
370
|
+
|
371
|
+
for (const modelToBeDeleted of additionalDeletions) {
|
372
|
+
await deleteModel(modelToBeDeleted);
|
373
|
+
}
|
374
|
+
|
375
|
+
await this.putIndex(indexUpdates);
|
376
|
+
} catch (error) {
|
377
|
+
if (error.constructor === NotImplementedError) throw error;
|
378
|
+
throw new CannotDeleteEngineError(`${this.name}.delete(${model.id}) model cannot be deleted`, error);
|
379
|
+
}
|
380
|
+
}
|
381
|
+
|
246
382
|
/**
|
247
383
|
* Hydrates a model by populating its related properties (e.g., submodels) from stored data.
|
248
384
|
*
|
@@ -389,6 +525,19 @@ export class NotFoundEngineError extends EngineError {
|
|
389
525
|
*/
|
390
526
|
}
|
391
527
|
|
528
|
+
/**
|
529
|
+
* Represents an error that occurs when a requested resource or item cannot be deleted by the engine.
|
530
|
+
* Extends the `EngineError` class.
|
531
|
+
*/
|
532
|
+
export class CannotDeleteEngineError extends EngineError {
|
533
|
+
/**
|
534
|
+
* Creates an instance of `CannotDeleteEngineError`.
|
535
|
+
*
|
536
|
+
* @param {string} message - The error message.
|
537
|
+
* @param {Error} [error] - An optional underlying error that caused this error.
|
538
|
+
*/
|
539
|
+
}
|
540
|
+
|
392
541
|
/**
|
393
542
|
* Represents an error indicating that a certain method or functionality is not implemented in the engine.
|
394
543
|
* Extends the `EngineError` class.
|
package/src/engine/FileEngine.js
CHANGED
@@ -64,6 +64,17 @@ class FileEngine extends Engine {
|
|
64
64
|
.then(JSON.parse);
|
65
65
|
}
|
66
66
|
|
67
|
+
/**
|
68
|
+
* Deletes a model by its ID from the file system.
|
69
|
+
*
|
70
|
+
* @param {string} id - The ID of the model to delete.
|
71
|
+
* @returns {Promise<void>} Resolves when the model has been deleted.
|
72
|
+
* @throws {Error} Throws if the file cannot be deleted.
|
73
|
+
*/
|
74
|
+
static deleteById(id) {
|
75
|
+
return this.configuration.filesystem.rm(join(this.configuration.path, `${id}.json`));
|
76
|
+
}
|
77
|
+
|
67
78
|
/**
|
68
79
|
* Retrieves the index for a given model from the file system.
|
69
80
|
*
|
@@ -98,19 +109,20 @@ class FileEngine extends Engine {
|
|
98
109
|
/**
|
99
110
|
* Saves the index for multiple models to the file system.
|
100
111
|
*
|
101
|
-
* @param {Object} index - An object where keys are locations and values are
|
112
|
+
* @param {Object} index - An object where keys are locations and values are key value pairs of models and their ids.
|
102
113
|
* @throws {FailedWriteFileEngineError} Throws if the index cannot be written to the file system.
|
103
114
|
*/
|
104
115
|
static async putIndex(index) {
|
105
116
|
const processIndex = async (location, models) => {
|
106
|
-
const modelIndex = Object.fromEntries(models.map((m) => [m.id, m.toIndexData()]));
|
107
117
|
const filePath = join(this.configuration.path, location, '_index.json');
|
108
118
|
const currentIndex = JSON.parse((await this.configuration.filesystem.readFile(filePath).catch(() => '{}')).toString());
|
109
119
|
|
110
120
|
try {
|
111
121
|
await this.configuration.filesystem.writeFile(filePath, JSON.stringify({
|
112
122
|
...currentIndex,
|
113
|
-
...
|
123
|
+
...Object.fromEntries(
|
124
|
+
Object.entries(models).map(([k, v]) => [k, v?.toIndexData?.() || v]),
|
125
|
+
),
|
114
126
|
}));
|
115
127
|
} catch (error) {
|
116
128
|
throw new FailedWriteFileEngineError(`Failed to put file://${filePath}`, error);
|
@@ -121,7 +133,12 @@ class FileEngine extends Engine {
|
|
121
133
|
await processIndex(location, models);
|
122
134
|
}
|
123
135
|
|
124
|
-
await processIndex('', Object.values(index).
|
136
|
+
await processIndex('', Object.values(index).reduce((accumulator, currentValue) => {
|
137
|
+
Object.keys(currentValue).forEach(key => {
|
138
|
+
accumulator[key] = currentValue[key];
|
139
|
+
});
|
140
|
+
return accumulator;
|
141
|
+
}, {}));
|
125
142
|
}
|
126
143
|
|
127
144
|
/**
|
package/src/engine/HTTPEngine.js
CHANGED
@@ -138,6 +138,25 @@ class HTTPEngine extends Engine {
|
|
138
138
|
return this._processFetch(url, this._getReadOptions());
|
139
139
|
}
|
140
140
|
|
141
|
+
/**
|
142
|
+
* Deletes a model by its ID from an HTTP server.
|
143
|
+
*
|
144
|
+
* @param {string} id - The ID of the model to delete.
|
145
|
+
* @returns {Promise<void>} Resolves when the model has been deleted.
|
146
|
+
* @throws {Error} Throws if the file cannot be deleted.
|
147
|
+
*/
|
148
|
+
static deleteById(id) {
|
149
|
+
this.checkConfiguration();
|
150
|
+
|
151
|
+
const url = new URL([
|
152
|
+
this.configuration.host,
|
153
|
+
this.configuration.prefix,
|
154
|
+
`${id}.json`,
|
155
|
+
].filter(e => Boolean(e)).join('/'));
|
156
|
+
|
157
|
+
return this._processFetch(url, {...this._getReadOptions(), method: 'DELETE'});
|
158
|
+
}
|
159
|
+
|
141
160
|
/**
|
142
161
|
* Uploads (puts) a model object to the server.
|
143
162
|
*
|
@@ -162,14 +181,12 @@ class HTTPEngine extends Engine {
|
|
162
181
|
/**
|
163
182
|
* Uploads (puts) an index object to the server.
|
164
183
|
*
|
165
|
-
* @param {Object} index -
|
184
|
+
* @param {Object} index - An object where keys are locations and values are key value pairs of models and their ids.
|
166
185
|
* @returns {Promise<void>}
|
167
|
-
*
|
168
186
|
* @throws {HTTPRequestFailedError} Thrown if the PUT request fails.
|
169
187
|
*/
|
170
188
|
static async putIndex(index) {
|
171
189
|
const processIndex = async (location, models) => {
|
172
|
-
const modelIndex = Object.fromEntries(models.map(m => [m.id, m.toIndexData()]));
|
173
190
|
const url = new URL([
|
174
191
|
this.configuration.host,
|
175
192
|
this.configuration.prefix,
|
@@ -181,7 +198,9 @@ class HTTPEngine extends Engine {
|
|
181
198
|
...this._getWriteOptions(),
|
182
199
|
body: JSON.stringify({
|
183
200
|
...await this.getIndex(location),
|
184
|
-
...
|
201
|
+
...Object.fromEntries(
|
202
|
+
Object.entries(models).map(([k, v]) => [k, v?.toIndexData?.() || v]),
|
203
|
+
),
|
185
204
|
}),
|
186
205
|
});
|
187
206
|
};
|
@@ -190,7 +209,12 @@ class HTTPEngine extends Engine {
|
|
190
209
|
await processIndex(location, models);
|
191
210
|
}
|
192
211
|
|
193
|
-
await processIndex(null, Object.values(index).
|
212
|
+
await processIndex(null, Object.values(index).reduce((accumulator, currentValue) => {
|
213
|
+
Object.keys(currentValue).forEach(key => {
|
214
|
+
accumulator[key] = currentValue[key];
|
215
|
+
});
|
216
|
+
return accumulator;
|
217
|
+
}, {}));
|
194
218
|
}
|
195
219
|
|
196
220
|
/**
|
package/src/engine/S3Engine.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
+
import {DeleteObjectCommand, GetObjectCommand, PutObjectCommand} from '@aws-sdk/client-s3';
|
1
2
|
import Engine, {EngineError, MissConfiguredError} from './Engine.js';
|
2
|
-
import {GetObjectCommand, PutObjectCommand} from '@aws-sdk/client-s3';
|
3
3
|
|
4
4
|
/**
|
5
5
|
* Represents an error specific to the S3 engine operations.
|
@@ -68,6 +68,24 @@ class S3Engine extends Engine {
|
|
68
68
|
return JSON.parse(await data.Body.transformToString());
|
69
69
|
}
|
70
70
|
|
71
|
+
/**
|
72
|
+
* Deletes a model by its ID from theS3 bucket.
|
73
|
+
*
|
74
|
+
* @param {string} id - The ID of the model to delete.
|
75
|
+
* @returns {Promise<void>} Resolves when the model has been deleted.
|
76
|
+
* @throws {Error} Throws if the model cannot be deleted.
|
77
|
+
*/
|
78
|
+
static async deleteById(id) {
|
79
|
+
const objectPath = [this.configuration.prefix, `${id}.json`].join('/');
|
80
|
+
|
81
|
+
await this.configuration.client.send(new DeleteObjectCommand({
|
82
|
+
Bucket: this.configuration.bucket,
|
83
|
+
Key: objectPath,
|
84
|
+
}));
|
85
|
+
|
86
|
+
return undefined;
|
87
|
+
}
|
88
|
+
|
71
89
|
/**
|
72
90
|
* Puts (uploads) a model object to S3.
|
73
91
|
*
|
@@ -113,16 +131,13 @@ class S3Engine extends Engine {
|
|
113
131
|
/**
|
114
132
|
* Puts (uploads) an index object to S3.
|
115
133
|
*
|
116
|
-
* @param {Object} index -
|
134
|
+
* @param {Object} index - An object where keys are locations and values are key value pairs of models and their ids.
|
117
135
|
* @returns {Promise<void>}
|
118
|
-
*
|
119
136
|
* @throws {FailedPutS3EngineError} Thrown if there is an error during the S3 PutObject operation.
|
120
137
|
*/
|
121
138
|
static async putIndex(index) {
|
122
139
|
const processIndex = async (location, models) => {
|
123
|
-
const modelIndex = Object.fromEntries(models.map(m => [m.id, m.toIndexData()]));
|
124
140
|
const Key = [this.configuration.prefix, location, '_index.json'].filter(e => Boolean(e)).join('/');
|
125
|
-
|
126
141
|
const currentIndex = await this.getIndex(location);
|
127
142
|
|
128
143
|
try {
|
@@ -132,7 +147,9 @@ class S3Engine extends Engine {
|
|
132
147
|
ContentType: 'application/json',
|
133
148
|
Body: JSON.stringify({
|
134
149
|
...currentIndex,
|
135
|
-
...
|
150
|
+
...Object.fromEntries(
|
151
|
+
Object.entries(models).map(([k, v]) => [k, v?.toIndexData?.() || v]),
|
152
|
+
),
|
136
153
|
}),
|
137
154
|
}));
|
138
155
|
} catch (error) {
|
@@ -144,7 +161,12 @@ class S3Engine extends Engine {
|
|
144
161
|
await processIndex(location, models);
|
145
162
|
}
|
146
163
|
|
147
|
-
await processIndex(null, Object.values(index).
|
164
|
+
await processIndex(null, Object.values(index).reduce((accumulator, currentValue) => {
|
165
|
+
Object.keys(currentValue).forEach(key => {
|
166
|
+
accumulator[key] = currentValue[key];
|
167
|
+
});
|
168
|
+
return accumulator;
|
169
|
+
}, {}));
|
148
170
|
}
|
149
171
|
|
150
172
|
/**
|