@acodeninja/persist 1.1.0 → 2.0.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/README.md CHANGED
@@ -144,10 +144,10 @@ export class Tag extends Persist.Type.Model {
144
144
 
145
145
  const tag = new Tag({tag: 'documentation', description: 'How to use the persist library'});
146
146
 
147
- FileEngine.find(Tag, {tag: 'documentation'});
147
+ await FileEngine.find(Tag, {tag: 'documentation'});
148
148
  // [Tag {tag: 'documentation', description: 'How to use the persist library'}]
149
149
 
150
- FileEngine.search(Tag, 'how to');
150
+ await FileEngine.search(Tag, 'how to');
151
151
  // [Tag {tag: 'documentation', description: 'How to use the persist library'}]
152
152
  ```
153
153
 
@@ -169,7 +169,26 @@ export class Tag extends Persist.Type.Model {
169
169
  static tag = Persist.Type.String.required;
170
170
  }
171
171
 
172
- Persist.getEngine('local', FileEngine).put(new Tag({tag: 'documentation'}));
172
+ await Persist.getEngine('local', FileEngine).put(new Tag({tag: 'documentation'}));
173
+ ```
174
+
175
+ ### HTTP Storage Engine
176
+
177
+ To store models using an S3 Bucket, use the `S3` storage engine.
178
+
179
+ ```javascript
180
+ import Persist from "@acodeninja/persist";
181
+ import HTTPEngine from "@acodeninja/persist/engine/http";
182
+
183
+ Persist.addEngine('remote', HTTPEngine, {
184
+ host: 'https://api.example.com',
185
+ });
186
+
187
+ export class Tag extends Persist.Type.Model {
188
+ static tag = Persist.Type.String.required;
189
+ }
190
+
191
+ await Persist.getEngine('remote', HTTPEngine).put(new Tag({tag: 'documentation'}));
173
192
  ```
174
193
 
175
194
  ### S3 Storage Engine
@@ -189,5 +208,29 @@ export class Tag extends Persist.Type.Model {
189
208
  static tag = Persist.Type.String.required;
190
209
  }
191
210
 
192
- Persist.getEngine('remote', S3Engine).put(new Tag({tag: 'documentation'}));
211
+ await Persist.getEngine('remote', S3Engine).put(new Tag({tag: 'documentation'}));
212
+ ```
213
+
214
+ ## Transactions
215
+
216
+ Create transactions to automatically roll back on failure to update.
217
+
218
+ ```javascript
219
+ import Persist from "@acodeninja/persist";
220
+ import S3Engine from "@acodeninja/persist/engine/s3";
221
+
222
+ Persist.addEngine('remote', S3Engine, {
223
+ bucket: 'test-bucket',
224
+ client: new S3Client(),
225
+ transactions: true,
226
+ });
227
+
228
+ export class Tag extends Persist.Type.Model {
229
+ static tag = Persist.Type.String.required;
230
+ }
231
+
232
+ const transaction = Persist.getEngine('remote', S3Engine).start();
233
+
234
+ await transaction.put(new Tag({tag: 'documentation'}));
235
+ await transaction.commit();
193
236
  ```
@@ -0,0 +1,3 @@
1
+ import HTTPEngine from '../../src/engine/HTTPEngine.js';
2
+
3
+ export default HTTPEngine;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acodeninja/persist",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "A JSON based data modelling and persistence module with alternate storage mechanisms.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -43,7 +43,7 @@
43
43
  "globals": "^15.8.0",
44
44
  "husky": "^9.1.1",
45
45
  "lodash": "^4.17.21",
46
- "monocart-coverage-reports": "^2.9.2",
46
+ "monocart-coverage-reports": "^2.10.2",
47
47
  "semantic-release": "^24.0.0",
48
48
  "sinon": "^18.0.0"
49
49
  }
package/src/Persist.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import Type from '../src/type/index.js';
2
+ import enableTransactions from './Transactions.js';
2
3
 
3
4
  /**
4
5
  * @class Persist
@@ -35,6 +36,9 @@ export default class Persist {
35
36
  static addEngine(group, engine, configuration) {
36
37
  if (!this._engine[group]) this._engine[group] = {};
37
38
 
38
- this._engine[group][engine.name] = engine.configure(configuration);
39
+ this._engine[group][engine.name] =
40
+ configuration.transactions ?
41
+ enableTransactions(engine.configure(configuration)) :
42
+ engine.configure(configuration);
39
43
  }
40
44
  }
@@ -0,0 +1,67 @@
1
+ class TransactionError extends Error {
2
+ }
3
+
4
+ export class TransactionCommittedError extends TransactionError {
5
+ message = 'Transaction was already committed.';
6
+ }
7
+
8
+ export default function enableTransactions(engine) {
9
+ class TransactionalEngine extends engine {
10
+ }
11
+
12
+ TransactionalEngine.start = () => {
13
+ class Transaction extends TransactionalEngine {
14
+ static transactions = [];
15
+ static committed = false;
16
+ static failed = false;
17
+
18
+ static async put(model) {
19
+ this.transactions.push({
20
+ hasRun: false,
21
+ hasRolledBack: false,
22
+ model,
23
+ });
24
+ }
25
+
26
+ static _checkCommitted() {
27
+ if (this.committed) throw new TransactionCommittedError();
28
+ }
29
+
30
+ static async commit() {
31
+ this._checkCommitted();
32
+
33
+ try {
34
+ for (const [index, {model}] of this.transactions.entries()) {
35
+ try {
36
+ this.transactions[index].original = await engine.get(model.constructor, model.id);
37
+ } catch (_) {
38
+ this.transactions[index].original = null;
39
+ }
40
+
41
+ await engine.put(model);
42
+ this.transactions[index].hasRun = true;
43
+ }
44
+ } catch (e) {
45
+ this.committed = true;
46
+ this.failed = true;
47
+ for (const [index, {original}] of this.transactions.entries()) {
48
+ if (original) {
49
+ await engine.put(this.transactions[index].original);
50
+ }
51
+ this.transactions[index].hasRolledBack = true;
52
+ }
53
+ throw e;
54
+ }
55
+
56
+ this.committed = true;
57
+ this.failed = false;
58
+ }
59
+ }
60
+
61
+ return Transaction;
62
+ };
63
+
64
+ Object.defineProperty(TransactionalEngine, 'name', {value: `${engine.toString()}`});
65
+
66
+ return TransactionalEngine;
67
+ }
@@ -40,36 +40,35 @@ export default class Engine {
40
40
  }
41
41
 
42
42
  static async search(model, query) {
43
- this._checkConfiguration?.();
44
- const index = await this.getSearchIndexCompiled(model);
43
+ this.checkConfiguration();
45
44
 
46
- try {
47
- const searchIndex = lunr.Index.load(index);
48
-
49
- const results = searchIndex.search(query);
50
- const output = [];
51
- for (const result of results) {
52
- output.push({
53
- ...result,
54
- model: await this.get(model, result.ref),
55
- });
56
- }
45
+ const index = await this.getSearchIndexCompiled(model).catch(() => {
46
+ throw new EngineError(`The model ${model.toString()} does not have a search index available.`);
47
+ });
48
+
49
+ const searchIndex = lunr.Index.load(index);
57
50
 
58
- return output;
59
- } catch (_) {
60
- throw new NotImplementedError(`The model ${model.name} does not have a search index available.`);
51
+ const results = searchIndex.search(`*${query}*`);
52
+
53
+ const output = [];
54
+ for (const result of results) {
55
+ output.push({
56
+ ...result,
57
+ model: await this.get(model, result.ref),
58
+ });
61
59
  }
60
+
61
+ return output;
62
62
  }
63
63
 
64
64
  static async find(model, parameters) {
65
- this._checkConfiguration?.();
65
+ this.checkConfiguration();
66
66
  const response = await this.findByValue(model, parameters);
67
-
68
67
  return response.map(m => model.fromData(m));
69
68
  }
70
69
 
71
70
  static async put(model) {
72
- this._checkConfiguration?.();
71
+ this.checkConfiguration();
73
72
  const uploadedModels = [];
74
73
  const indexUpdates = {};
75
74
 
@@ -77,9 +76,9 @@ export default class Engine {
77
76
  if (uploadedModels.includes(model.id)) return false;
78
77
  model.validate();
79
78
 
80
- uploadedModels.push(model.id);
81
-
82
79
  await this.putModel(model);
80
+
81
+ uploadedModels.push(model.id);
83
82
  indexUpdates[model.constructor.name] = (indexUpdates[model.constructor.name] ?? []).concat([model]);
84
83
 
85
84
  if (model.constructor.searchProperties().length > 0) {
@@ -87,6 +86,7 @@ export default class Engine {
87
86
  ...await this.getSearchIndexRaw(model.constructor),
88
87
  [model.id]: model.toSearchData(),
89
88
  };
89
+
90
90
  await this.putSearchIndexRaw(model.constructor, rawSearchIndex);
91
91
 
92
92
  const compiledIndex = lunr(function () {
@@ -121,18 +121,19 @@ export default class Engine {
121
121
  }
122
122
 
123
123
  static async get(model, id) {
124
- this._checkConfiguration?.();
125
- const found = await this.getById(id);
124
+ this.checkConfiguration();
126
125
 
127
126
  try {
127
+ const found = await this.getById(id);
128
128
  return model.fromData(found);
129
- } catch (_error) {
130
- throw new NotFoundEngineError(`${this.name}.get(${id}) model not found`);
129
+ } catch (error) {
130
+ if (error.constructor === NotImplementedError) throw error;
131
+ throw new NotFoundEngineError(`${this.name}.get(${id}) model not found`, error);
131
132
  }
132
133
  }
133
134
 
134
135
  static async hydrate(model) {
135
- this._checkConfiguration?.();
136
+ this.checkConfiguration();
136
137
  const hydratedModels = {};
137
138
 
138
139
  const hydrateModel = async (modelToProcess) => {
@@ -205,12 +206,21 @@ export default class Engine {
205
206
  return ConfiguredStore;
206
207
  }
207
208
 
209
+ static checkConfiguration() {
210
+
211
+ }
212
+
208
213
  static toString() {
209
214
  return this.name;
210
215
  }
211
216
  };
212
217
 
213
218
  export class EngineError extends Error {
219
+ underlyingError;
220
+ constructor(message, error = undefined) {
221
+ super(message);
222
+ this.underlyingError = error;
223
+ }
214
224
  }
215
225
 
216
226
  export class NotFoundEngineError extends EngineError {
@@ -1,7 +1,11 @@
1
+ import Engine, {EngineError, MissConfiguredError} from './Engine.js';
1
2
  import {dirname, join} from 'node:path';
2
- import Engine from './Engine.js';
3
3
  import fs from 'node:fs/promises';
4
4
 
5
+ class FileEngineError extends EngineError {}
6
+
7
+ class FailedWriteFileEngineError extends FileEngineError {}
8
+
5
9
  /**
6
10
  * @class FileEngine
7
11
  * @extends Engine
@@ -14,18 +18,22 @@ export default class FileEngine extends Engine {
14
18
  return super.configure(configuration);
15
19
  }
16
20
 
21
+ static checkConfiguration() {
22
+ if (
23
+ !this._configuration?.path ||
24
+ !this._configuration?.filesystem
25
+ ) throw new MissConfiguredError(this._configuration);
26
+ }
27
+
17
28
  static async getById(id) {
18
29
  const filePath = join(this._configuration.path, `${id}.json`);
19
30
 
20
- try {
21
- return JSON.parse(await this._configuration.filesystem.readFile(filePath).then(f => f.toString()));
22
- } catch (_) {
23
- return null;
24
- }
31
+ return JSON.parse(await this._configuration.filesystem.readFile(filePath).then(f => f.toString()));
25
32
  }
26
33
 
27
34
  static async findByValue(model, parameters) {
28
35
  const index = JSON.parse((await this._configuration.filesystem.readFile(join(this._configuration.path, model.name, '_index.json')).catch(() => '{}')).toString());
36
+
29
37
  return Object.values(index)
30
38
  .filter((model) =>
31
39
  Object.entries(parameters)
@@ -36,8 +44,12 @@ export default class FileEngine extends Engine {
36
44
  static async putModel(model) {
37
45
  const filePath = join(this._configuration.path, `${model.id}.json`);
38
46
 
39
- await this._configuration.filesystem.mkdir(dirname(filePath), {recursive: true});
40
- await this._configuration.filesystem.writeFile(filePath, JSON.stringify(model.toData()));
47
+ try {
48
+ await this._configuration.filesystem.mkdir(dirname(filePath), {recursive: true});
49
+ await this._configuration.filesystem.writeFile(filePath, JSON.stringify(model.toData()));
50
+ } catch (error) {
51
+ throw new FailedWriteFileEngineError(`Failed to put file://${filePath}`, error);
52
+ }
41
53
  }
42
54
 
43
55
  static async putIndex(index) {
@@ -46,10 +58,14 @@ export default class FileEngine extends Engine {
46
58
  const filePath = join(this._configuration.path, location, '_index.json');
47
59
  const currentIndex = JSON.parse((await this._configuration.filesystem.readFile(filePath).catch(() => '{}')).toString());
48
60
 
49
- await this._configuration.filesystem.writeFile(filePath, JSON.stringify({
50
- ...currentIndex,
51
- ...modelIndex,
52
- }));
61
+ try {
62
+ await this._configuration.filesystem.writeFile(filePath, JSON.stringify({
63
+ ...currentIndex,
64
+ ...modelIndex,
65
+ }));
66
+ } catch (error) {
67
+ throw new FailedWriteFileEngineError(`Failed to put file://${filePath}`, error);
68
+ }
53
69
  };
54
70
 
55
71
  for (const [location, models] of Object.entries(index)) {
@@ -60,20 +76,34 @@ export default class FileEngine extends Engine {
60
76
  }
61
77
 
62
78
  static async getSearchIndexCompiled(model) {
63
- return JSON.parse((await this._configuration.filesystem.readFile(join(this._configuration.path, model.name, '_search_index.json')).catch(() => '{}')).toString());
79
+ return await this._configuration.filesystem.readFile(join(this._configuration.path, model.name, '_search_index.json'))
80
+ .then(b => b.toString())
81
+ .then(JSON.parse);
64
82
  }
65
83
 
66
84
  static async getSearchIndexRaw(model) {
67
- return JSON.parse((await this._configuration.filesystem.readFile(join(this._configuration.path, model.name, '_search_index_raw.json')).catch(() => '{}')).toString());
85
+ return await this._configuration.filesystem.readFile(join(this._configuration.path, model.name, '_search_index_raw.json'))
86
+ .then(b => b.toString())
87
+ .then(JSON.parse)
88
+ .catch(() => ({}));
68
89
  }
69
90
 
70
91
  static async putSearchIndexCompiled(model, compiledIndex) {
71
92
  const filePath = join(this._configuration.path, model.name, '_search_index.json');
72
- await this._configuration.filesystem.writeFile(filePath, JSON.stringify(compiledIndex));
93
+
94
+ try {
95
+ await this._configuration.filesystem.writeFile(filePath, JSON.stringify(compiledIndex));
96
+ } catch (error) {
97
+ throw new FailedWriteFileEngineError(`Failed to put file://${filePath}`, error);
98
+ }
73
99
  }
74
100
 
75
101
  static async putSearchIndexRaw(model, rawIndex) {
76
102
  const filePath = join(this._configuration.path, model.name, '_search_index_raw.json');
77
- await this._configuration.filesystem.writeFile(filePath, JSON.stringify(rawIndex));
103
+ try {
104
+ await this._configuration.filesystem.writeFile(filePath, JSON.stringify(rawIndex));
105
+ } catch (error) {
106
+ throw new FailedWriteFileEngineError(`Failed to put file://${filePath}`, error);
107
+ }
78
108
  }
79
109
  }
@@ -1,4 +1,17 @@
1
- import Engine, {MissConfiguredError} from './Engine.js';
1
+ import Engine, {EngineError, MissConfiguredError} from './Engine.js';
2
+
3
+ export class HTTPEngineError extends EngineError {
4
+ }
5
+
6
+ export class HTTPRequestFailedError extends HTTPEngineError {
7
+ constructor(url, options, response) {
8
+ const method = options.method?.toLowerCase() || 'get';
9
+ super(`Failed to ${method} ${url}`);
10
+ this.response = response;
11
+ this.url = url;
12
+ this.options = options;
13
+ }
14
+ }
2
15
 
3
16
  export default class HTTPEngine extends Engine {
4
17
  static configure(configuration = {}) {
@@ -13,7 +26,7 @@ export default class HTTPEngine extends Engine {
13
26
  return super.configure(configuration);
14
27
  }
15
28
 
16
- static _checkConfiguration() {
29
+ static checkConfiguration() {
17
30
  if (
18
31
  !this._configuration?.host
19
32
  ) throw new MissConfiguredError(this._configuration);
@@ -34,19 +47,32 @@ export default class HTTPEngine extends Engine {
34
47
  };
35
48
  }
36
49
 
50
+ static async _processFetch(url, options, defaultValue = undefined) {
51
+ return this._configuration.fetch(url, options)
52
+ .then(response => {
53
+ if (!response.ok) {
54
+ if (defaultValue !== undefined) {
55
+ return {json: () => Promise.resolve(defaultValue)};
56
+ }
57
+
58
+ throw new HTTPRequestFailedError(url, options, response);
59
+ }
60
+
61
+ return response;
62
+ })
63
+ .then(r => r.json());
64
+ }
65
+
37
66
  static async getById(id) {
38
- this._checkConfiguration();
67
+ this.checkConfiguration();
68
+
39
69
  const url = new URL([
40
70
  this._configuration.host,
41
71
  this._configuration.prefix,
42
72
  `${id}.json`,
43
73
  ].filter(e => !!e).join('/'));
44
74
 
45
- try {
46
- return await this._configuration.fetch(url, this._getReadOptions()).then(r => r.json());
47
- } catch (_error) {
48
- return undefined;
49
- }
75
+ return await this._processFetch(url, this._getReadOptions());
50
76
  }
51
77
 
52
78
  static async findByValue(model, parameters) {
@@ -65,14 +91,10 @@ export default class HTTPEngine extends Engine {
65
91
  `${model.id}.json`,
66
92
  ].filter(e => !!e).join('/'));
67
93
 
68
- try {
69
- return await this._configuration.fetch(url, {
70
- ...this._getWriteOptions(),
71
- body: JSON.stringify(model.toData()),
72
- }).then(r => r.json());
73
- } catch (_error) {
74
- return undefined;
75
- }
94
+ return await this._processFetch(url, {
95
+ ...this._getWriteOptions(),
96
+ body: JSON.stringify(model.toData()),
97
+ });
76
98
  }
77
99
 
78
100
  static async putIndex(index) {
@@ -85,19 +107,13 @@ export default class HTTPEngine extends Engine {
85
107
  '_index.json',
86
108
  ].filter(e => !!e).join('/'));
87
109
 
88
- const currentIndex = await this.getIndex(location);
89
-
90
- try {
91
- return await this._configuration.fetch(url, {
92
- ...this._getWriteOptions(),
93
- body: JSON.stringify({
94
- ...currentIndex,
95
- ...modelIndex,
96
- }),
97
- }).then(r => r.json());
98
- } catch (_error) {
99
- return undefined;
100
- }
110
+ return await this._processFetch(url, {
111
+ ...this._getWriteOptions(),
112
+ body: JSON.stringify({
113
+ ...await this.getIndex(location),
114
+ ...modelIndex,
115
+ }),
116
+ });
101
117
  };
102
118
 
103
119
  for (const [location, models] of Object.entries(index)) {
@@ -108,33 +124,31 @@ export default class HTTPEngine extends Engine {
108
124
  }
109
125
 
110
126
  static async getIndex(location) {
111
- const url = new URL(this._configuration.host + '/' + [this._configuration.prefix, location, '_index.json'].filter(e => !!e).join('/'));
127
+ const url = new URL([this._configuration.host, this._configuration.prefix, location, '_index.json'].filter(e => !!e).join('/'));
112
128
 
113
- try {
114
- return await this._configuration.fetch(url, this._getReadOptions()).then(r => r.json());
115
- } catch (_error) {
116
- return {};
117
- }
129
+ return await this._processFetch(url, this._getReadOptions(), {});
118
130
  }
119
131
 
120
132
  static async getSearchIndexCompiled(model) {
121
- const url = new URL(this._configuration.host + '/' + [this._configuration.prefix].concat([model.name]).concat(['_search_index.json']).join('/'));
133
+ const url = new URL([
134
+ this._configuration.host,
135
+ this._configuration.prefix,
136
+ model.toString(),
137
+ '_search_index.json',
138
+ ].join('/'));
122
139
 
123
- try {
124
- return await this._configuration.fetch(url, this._getReadOptions()).then(r => r.json());
125
- } catch (_error) {
126
- return {};
127
- }
140
+ return await this._processFetch(url, this._getReadOptions());
128
141
  }
129
142
 
130
143
  static async getSearchIndexRaw(model) {
131
- const url = new URL(this._configuration.host + '/' + [this._configuration.prefix].concat([model.name]).concat(['_search_index_raw.json']).join('/'));
144
+ const url = new URL([
145
+ this._configuration.host,
146
+ this._configuration.prefix,
147
+ model.toString(),
148
+ '_search_index_raw.json',
149
+ ].join('/'));
132
150
 
133
- try {
134
- return await this._configuration.fetch(url, this._getReadOptions()).then(r => r.json());
135
- } catch (_error) {
136
- return {};
137
- }
151
+ return await this._processFetch(url, this._getReadOptions()).catch(() => ({}));
138
152
  }
139
153
 
140
154
  static async putSearchIndexCompiled(model, compiledIndex) {
@@ -145,14 +159,10 @@ export default class HTTPEngine extends Engine {
145
159
  '_search_index.json',
146
160
  ].filter(e => !!e).join('/'));
147
161
 
148
- try {
149
- return await this._configuration.fetch(url, {
150
- ...this._getWriteOptions(),
151
- body: JSON.stringify(compiledIndex),
152
- }).then(r => r.json());
153
- } catch (_error) {
154
- return undefined;
155
- }
162
+ return this._processFetch(url, {
163
+ ...this._getWriteOptions(),
164
+ body: JSON.stringify(compiledIndex),
165
+ });
156
166
  }
157
167
 
158
168
  static async putSearchIndexRaw(model, rawIndex) {
@@ -163,13 +173,9 @@ export default class HTTPEngine extends Engine {
163
173
  '_search_index_raw.json',
164
174
  ].filter(e => !!e).join('/'));
165
175
 
166
- try {
167
- return await this._configuration.fetch(url, {
168
- ...this._getWriteOptions(),
169
- body: JSON.stringify(rawIndex),
170
- }).then(r => r.json());
171
- } catch (_error) {
172
- return undefined;
173
- }
176
+ return await this._processFetch(url, {
177
+ ...this._getWriteOptions(),
178
+ body: JSON.stringify(rawIndex),
179
+ });
174
180
  }
175
181
  }
@@ -1,23 +1,32 @@
1
+ import Engine, {EngineError, MissConfiguredError} from './Engine.js';
1
2
  import {GetObjectCommand, PutObjectCommand} from '@aws-sdk/client-s3';
2
- import Engine from './Engine.js';
3
+
4
+ class S3EngineError extends EngineError {}
5
+
6
+ class FailedPutS3EngineError extends S3EngineError {}
3
7
 
4
8
  export default class S3Engine extends Engine {
9
+ static checkConfiguration() {
10
+ if (
11
+ !this._configuration?.bucket ||
12
+ !this._configuration?.client
13
+ ) throw new MissConfiguredError(this._configuration);
14
+ }
15
+
5
16
  static async getById(id) {
6
17
  const objectPath = [this._configuration.prefix, `${id}.json`].join('/');
7
18
 
8
- try {
9
- const data = await this._configuration.client.send(new GetObjectCommand({
10
- Bucket: this._configuration.bucket,
11
- Key: objectPath,
12
- }));
13
- return JSON.parse(await data.Body.transformToString());
14
- } catch (_error) {
15
- return undefined;
16
- }
19
+ const data = await this._configuration.client.send(new GetObjectCommand({
20
+ Bucket: this._configuration.bucket,
21
+ Key: objectPath,
22
+ }));
23
+
24
+ return JSON.parse(await data.Body.transformToString());
17
25
  }
18
26
 
19
27
  static async findByValue(model, parameters) {
20
28
  const index = await this.getIndex(model.name);
29
+
21
30
  return Object.values(index)
22
31
  .filter((model) =>
23
32
  Object.entries(parameters)
@@ -28,23 +37,27 @@ export default class S3Engine extends Engine {
28
37
  static async putModel(model) {
29
38
  const Key = [this._configuration.prefix, `${model.id}.json`].join('/');
30
39
 
31
- await this._configuration.client.send(new PutObjectCommand({
32
- Key,
33
- Body: JSON.stringify(model.toData()),
34
- Bucket: this._configuration.bucket,
35
- ContentType: 'application/json',
36
- }));
40
+ try {
41
+ await this._configuration.client.send(new PutObjectCommand({
42
+ Key,
43
+ Body: JSON.stringify(model.toData()),
44
+ Bucket: this._configuration.bucket,
45
+ ContentType: 'application/json',
46
+ }));
47
+ } catch (error) {
48
+ throw new FailedPutS3EngineError(`Failed to put s3://${this._configuration.bucket}/${Key}`, error);
49
+ }
37
50
  }
38
51
 
39
52
  static async getIndex(location) {
40
53
  try {
41
54
  const data = await this._configuration.client.send(new GetObjectCommand({
42
- Key: [this._configuration.prefix].concat([location]).concat(['_index.json']).filter(e => !!e).join('/'),
55
+ Key: [this._configuration.prefix, location, '_index.json'].filter(e => !!e).join('/'),
43
56
  Bucket: this._configuration.bucket,
44
57
  }));
45
58
 
46
59
  return JSON.parse(await data.Body.transformToString());
47
- } catch (_error) {
60
+ } catch (_) {
48
61
  return {};
49
62
  }
50
63
  }
@@ -52,19 +65,23 @@ export default class S3Engine extends Engine {
52
65
  static async putIndex(index) {
53
66
  const processIndex = async (location, models) => {
54
67
  const modelIndex = Object.fromEntries(models.map(m => [m.id, m.toIndexData()]));
55
- const Key = [this._configuration.prefix].concat([location]).concat(['_index.json']).filter(e => !!e).join('/');
68
+ const Key = [this._configuration.prefix, location, '_index.json'].filter(e => !!e).join('/');
56
69
 
57
70
  const currentIndex = await this.getIndex(location);
58
71
 
59
- await this._configuration.client.send(new PutObjectCommand({
60
- Key,
61
- Bucket: this._configuration.bucket,
62
- ContentType: 'application/json',
63
- Body: JSON.stringify({
64
- ...currentIndex,
65
- ...modelIndex,
66
- }),
67
- }));
72
+ try {
73
+ await this._configuration.client.send(new PutObjectCommand({
74
+ Key,
75
+ Bucket: this._configuration.bucket,
76
+ ContentType: 'application/json',
77
+ Body: JSON.stringify({
78
+ ...currentIndex,
79
+ ...modelIndex,
80
+ }),
81
+ }));
82
+ } catch (error) {
83
+ throw new FailedPutS3EngineError(`Failed to put s3://${this._configuration.bucket}/${Key}`, error);
84
+ }
68
85
  };
69
86
 
70
87
  for (const [location, models] of Object.entries(index)) {
@@ -75,50 +92,49 @@ export default class S3Engine extends Engine {
75
92
  }
76
93
 
77
94
  static async getSearchIndexCompiled(model) {
78
- try {
79
- const data = await this._configuration.client.send(new GetObjectCommand({
80
- Key: [this._configuration.prefix].concat([model.name]).concat(['_search_index.json']).join('/'),
81
- Bucket: this._configuration.bucket,
82
- }));
83
-
84
- return JSON.parse(await data.Body.transformToString());
85
- } catch (_error) {
86
- return {};
87
- }
95
+ return await this._configuration.client.send(new GetObjectCommand({
96
+ Key: [this._configuration.prefix, model.name, '_search_index.json'].join('/'),
97
+ Bucket: this._configuration.bucket,
98
+ })).then(data => data.Body.transformToString())
99
+ .then(JSON.parse);
88
100
  }
89
101
 
90
102
  static async getSearchIndexRaw(model) {
91
- try {
92
- const data = await this._configuration.client.send(new GetObjectCommand({
93
- Key: [this._configuration.prefix].concat([model.name]).concat(['_search_index_raw.json']).join('/'),
94
- Bucket: this._configuration.bucket,
95
- }));
96
-
97
- return JSON.parse(await data.Body.transformToString());
98
- } catch (_error) {
99
- return {};
100
- }
103
+ return await this._configuration.client.send(new GetObjectCommand({
104
+ Key: [this._configuration.prefix, model.name, '_search_index_raw.json'].join('/'),
105
+ Bucket: this._configuration.bucket,
106
+ })).then(data => data.Body.transformToString())
107
+ .then(JSON.parse)
108
+ .catch(() => ({}));
101
109
  }
102
110
 
103
111
  static async putSearchIndexCompiled(model, compiledIndex) {
104
112
  const Key = [this._configuration.prefix, model.name, '_search_index.json'].join('/');
105
113
 
106
- await this._configuration.client.send(new PutObjectCommand({
107
- Key,
108
- Body: JSON.stringify(compiledIndex),
109
- Bucket: this._configuration.bucket,
110
- ContentType: 'application/json',
111
- }));
114
+ try {
115
+ await this._configuration.client.send(new PutObjectCommand({
116
+ Key,
117
+ Body: JSON.stringify(compiledIndex),
118
+ Bucket: this._configuration.bucket,
119
+ ContentType: 'application/json',
120
+ }));
121
+ } catch (error) {
122
+ throw new FailedPutS3EngineError(`Failed to put s3://${this._configuration.bucket}/${Key}`, error);
123
+ }
112
124
  }
113
125
 
114
126
  static async putSearchIndexRaw(model, rawIndex) {
115
127
  const Key = [this._configuration.prefix, model.name, '_search_index_raw.json'].join('/');
116
128
 
117
- await this._configuration.client.send(new PutObjectCommand({
118
- Key,
119
- Body: JSON.stringify(rawIndex),
120
- Bucket: this._configuration.bucket,
121
- ContentType: 'application/json',
122
- }));
129
+ try {
130
+ await this._configuration.client.send(new PutObjectCommand({
131
+ Key,
132
+ Body: JSON.stringify(rawIndex),
133
+ Bucket: this._configuration.bucket,
134
+ ContentType: 'application/json',
135
+ }));
136
+ } catch (error) {
137
+ throw new FailedPutS3EngineError(`Failed to put s3://${this._configuration.bucket}/${Key}`, error);
138
+ }
123
139
  }
124
140
  }