@acodeninja/persist 1.0.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.0.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,33 +40,35 @@ export default class Engine {
40
40
  }
41
41
 
42
42
  static async search(model, query) {
43
- const index = await this.getSearchIndexCompiled(model);
43
+ this.checkConfiguration();
44
44
 
45
- try {
46
- const searchIndex = lunr.Index.load(index);
47
-
48
- const results = searchIndex.search(query);
49
- const output = [];
50
- for (const result of results) {
51
- output.push({
52
- ...result,
53
- model: await this.get(model, result.ref),
54
- });
55
- }
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);
56
50
 
57
- return output;
58
- } catch (_) {
59
- 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
+ });
60
59
  }
60
+
61
+ return output;
61
62
  }
62
63
 
63
64
  static async find(model, parameters) {
65
+ this.checkConfiguration();
64
66
  const response = await this.findByValue(model, parameters);
65
-
66
67
  return response.map(m => model.fromData(m));
67
68
  }
68
69
 
69
70
  static async put(model) {
71
+ this.checkConfiguration();
70
72
  const uploadedModels = [];
71
73
  const indexUpdates = {};
72
74
 
@@ -74,9 +76,9 @@ export default class Engine {
74
76
  if (uploadedModels.includes(model.id)) return false;
75
77
  model.validate();
76
78
 
77
- uploadedModels.push(model.id);
78
-
79
79
  await this.putModel(model);
80
+
81
+ uploadedModels.push(model.id);
80
82
  indexUpdates[model.constructor.name] = (indexUpdates[model.constructor.name] ?? []).concat([model]);
81
83
 
82
84
  if (model.constructor.searchProperties().length > 0) {
@@ -84,10 +86,11 @@ export default class Engine {
84
86
  ...await this.getSearchIndexRaw(model.constructor),
85
87
  [model.id]: model.toSearchData(),
86
88
  };
89
+
87
90
  await this.putSearchIndexRaw(model.constructor, rawSearchIndex);
88
91
 
89
92
  const compiledIndex = lunr(function () {
90
- this.ref('id')
93
+ this.ref('id');
91
94
 
92
95
  for (const field of model.constructor.searchProperties()) {
93
96
  this.field(field);
@@ -95,7 +98,7 @@ export default class Engine {
95
98
 
96
99
  Object.values(rawSearchIndex).forEach(function (doc) {
97
100
  this.add(doc);
98
- }, this)
101
+ }, this);
99
102
  });
100
103
 
101
104
  await this.putSearchIndexCompiled(model.constructor, compiledIndex);
@@ -118,16 +121,19 @@ export default class Engine {
118
121
  }
119
122
 
120
123
  static async get(model, id) {
121
- const found = await this.getById(id);
124
+ this.checkConfiguration();
122
125
 
123
126
  try {
127
+ const found = await this.getById(id);
124
128
  return model.fromData(found);
125
- } catch (_error) {
126
- 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);
127
132
  }
128
133
  }
129
134
 
130
135
  static async hydrate(model) {
136
+ this.checkConfiguration();
131
137
  const hydratedModels = {};
132
138
 
133
139
  const hydrateModel = async (modelToProcess) => {
@@ -142,7 +148,7 @@ export default class Engine {
142
148
  }
143
149
 
144
150
  return modelToProcess;
145
- }
151
+ };
146
152
 
147
153
  const hydrateSubModel = async (property, modelToProcess, name) => {
148
154
  if (hydratedModels[property.id]) {
@@ -155,7 +161,7 @@ export default class Engine {
155
161
  const hydratedSubModel = await hydrateModel(subModel);
156
162
  hydratedModels[property.id] = hydratedSubModel;
157
163
  return hydratedSubModel;
158
- }
164
+ };
159
165
 
160
166
  const hydrateModelList = async (property, modelToProcess, name) => {
161
167
  const subModelClass = getSubModelClass(modelToProcess, name, true);
@@ -177,7 +183,7 @@ export default class Engine {
177
183
  hydratedModels[hydratedSubModel.id] = hydratedSubModel;
178
184
  return hydratedSubModel;
179
185
  }));
180
- }
186
+ };
181
187
 
182
188
  function getSubModelClass(modelToProcess, name, isArray = false) {
183
189
  const constructorField = modelToProcess.constructor[name];
@@ -195,17 +201,26 @@ export default class Engine {
195
201
  static _configuration = configuration;
196
202
  }
197
203
 
198
- Object.defineProperty(ConfiguredStore, 'name', {value: `${this.toString()}`})
204
+ Object.defineProperty(ConfiguredStore, 'name', {value: `${this.toString()}`});
199
205
 
200
206
  return ConfiguredStore;
201
207
  }
202
208
 
209
+ static checkConfiguration() {
210
+
211
+ }
212
+
203
213
  static toString() {
204
214
  return this.name;
205
215
  }
206
216
  };
207
217
 
208
218
  export class EngineError extends Error {
219
+ underlyingError;
220
+ constructor(message, error = undefined) {
221
+ super(message);
222
+ this.underlyingError = error;
223
+ }
209
224
  }
210
225
 
211
226
  export class NotFoundEngineError extends EngineError {
@@ -213,3 +228,12 @@ export class NotFoundEngineError extends EngineError {
213
228
 
214
229
  export class NotImplementedError extends EngineError {
215
230
  }
231
+
232
+ export class MissConfiguredError extends EngineError {
233
+ configuration;
234
+
235
+ constructor(configuration) {
236
+ super('Engine is miss-configured');
237
+ this.configuration = configuration;
238
+ }
239
+ }
@@ -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
  }
@@ -0,0 +1,181 @@
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
+ }
15
+
16
+ export default class HTTPEngine extends Engine {
17
+ static configure(configuration = {}) {
18
+ configuration.fetchOptions = {
19
+ ...(configuration.fetchOptions ?? {}),
20
+ headers: {
21
+ ...(configuration.fetchOptions?.headers ?? {}),
22
+ Accept: 'application/json',
23
+ },
24
+ };
25
+
26
+ return super.configure(configuration);
27
+ }
28
+
29
+ static checkConfiguration() {
30
+ if (
31
+ !this._configuration?.host
32
+ ) throw new MissConfiguredError(this._configuration);
33
+ }
34
+
35
+ static _getReadOptions() {
36
+ return this._configuration.fetchOptions;
37
+ }
38
+
39
+ static _getWriteOptions() {
40
+ return {
41
+ ...this._getReadOptions(),
42
+ headers: {
43
+ ...this._getReadOptions().headers,
44
+ 'Content-Type': 'application/json',
45
+ },
46
+ method: 'PUT',
47
+ };
48
+ }
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
+
66
+ static async getById(id) {
67
+ this.checkConfiguration();
68
+
69
+ const url = new URL([
70
+ this._configuration.host,
71
+ this._configuration.prefix,
72
+ `${id}.json`,
73
+ ].filter(e => !!e).join('/'));
74
+
75
+ return await this._processFetch(url, this._getReadOptions());
76
+ }
77
+
78
+ static async findByValue(model, parameters) {
79
+ const index = await this.getIndex(model.name);
80
+ return Object.values(index)
81
+ .filter((model) =>
82
+ Object.entries(parameters)
83
+ .some(([name, value]) => model[name] === value),
84
+ );
85
+ }
86
+
87
+ static async putModel(model) {
88
+ const url = new URL([
89
+ this._configuration.host,
90
+ this._configuration.prefix,
91
+ `${model.id}.json`,
92
+ ].filter(e => !!e).join('/'));
93
+
94
+ return await this._processFetch(url, {
95
+ ...this._getWriteOptions(),
96
+ body: JSON.stringify(model.toData()),
97
+ });
98
+ }
99
+
100
+ static async putIndex(index) {
101
+ const processIndex = async (location, models) => {
102
+ const modelIndex = Object.fromEntries(models.map(m => [m.id, m.toIndexData()]));
103
+ const url = new URL([
104
+ this._configuration.host,
105
+ this._configuration.prefix,
106
+ location,
107
+ '_index.json',
108
+ ].filter(e => !!e).join('/'));
109
+
110
+ return await this._processFetch(url, {
111
+ ...this._getWriteOptions(),
112
+ body: JSON.stringify({
113
+ ...await this.getIndex(location),
114
+ ...modelIndex,
115
+ }),
116
+ });
117
+ };
118
+
119
+ for (const [location, models] of Object.entries(index)) {
120
+ await processIndex(location, models);
121
+ }
122
+
123
+ await processIndex(null, Object.values(index).flat());
124
+ }
125
+
126
+ static async getIndex(location) {
127
+ const url = new URL([this._configuration.host, this._configuration.prefix, location, '_index.json'].filter(e => !!e).join('/'));
128
+
129
+ return await this._processFetch(url, this._getReadOptions(), {});
130
+ }
131
+
132
+ static async getSearchIndexCompiled(model) {
133
+ const url = new URL([
134
+ this._configuration.host,
135
+ this._configuration.prefix,
136
+ model.toString(),
137
+ '_search_index.json',
138
+ ].join('/'));
139
+
140
+ return await this._processFetch(url, this._getReadOptions());
141
+ }
142
+
143
+ static async getSearchIndexRaw(model) {
144
+ const url = new URL([
145
+ this._configuration.host,
146
+ this._configuration.prefix,
147
+ model.toString(),
148
+ '_search_index_raw.json',
149
+ ].join('/'));
150
+
151
+ return await this._processFetch(url, this._getReadOptions()).catch(() => ({}));
152
+ }
153
+
154
+ static async putSearchIndexCompiled(model, compiledIndex) {
155
+ const url = new URL([
156
+ this._configuration.host,
157
+ this._configuration.prefix,
158
+ model.name,
159
+ '_search_index.json',
160
+ ].filter(e => !!e).join('/'));
161
+
162
+ return this._processFetch(url, {
163
+ ...this._getWriteOptions(),
164
+ body: JSON.stringify(compiledIndex),
165
+ });
166
+ }
167
+
168
+ static async putSearchIndexRaw(model, rawIndex) {
169
+ const url = new URL([
170
+ this._configuration.host,
171
+ this._configuration.prefix,
172
+ model.name,
173
+ '_search_index_raw.json',
174
+ ].filter(e => !!e).join('/'));
175
+
176
+ return await this._processFetch(url, {
177
+ ...this._getWriteOptions(),
178
+ body: JSON.stringify(rawIndex),
179
+ });
180
+ }
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
  }
package/src/type/Model.js CHANGED
@@ -75,7 +75,7 @@ export default class Model {
75
75
  static _required = true;
76
76
  }
77
77
 
78
- Object.defineProperty(Required, 'name', {value: `${this.toString()}`})
78
+ Object.defineProperty(Required, 'name', {value: `${this.toString()}`});
79
79
 
80
80
  return Required;
81
81
  }
@@ -3,7 +3,7 @@ import Type from '../Type.js';
3
3
  export default class ArrayType {
4
4
  static of(type) {
5
5
  class ArrayOf extends Type {
6
- static _type = 'array'
6
+ static _type = 'array';
7
7
  static _items = type;
8
8
 
9
9
  static toString() {
@@ -19,7 +19,7 @@ export default class ArrayType {
19
19
  }
20
20
  }
21
21
 
22
- Object.defineProperty(Required, 'name', {value: `Required${this.toString()}Type`})
22
+ Object.defineProperty(Required, 'name', {value: `Required${this.toString()}Type`});
23
23
 
24
24
  return Required;
25
25
  }
@@ -4,7 +4,7 @@ import slugify from 'slugify';
4
4
  export default class SlugType extends ResolvedType {
5
5
  static of(property) {
6
6
  class SlugOf extends ResolvedType {
7
- static _type = 'string'
7
+ static _type = 'string';
8
8
 
9
9
  static toString() {
10
10
  return `SlugOf(${property})`;