@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 +47 -4
- package/exports/engine/http.js +3 -0
- package/package.json +2 -2
- package/src/Persist.js +5 -1
- package/src/Transactions.js +67 -0
- package/src/engine/Engine.js +51 -27
- package/src/engine/FileEngine.js +46 -16
- package/src/engine/HTTPEngine.js +181 -0
- package/src/engine/S3Engine.js +76 -60
- package/src/type/Model.js +1 -1
- package/src/type/complex/ArrayType.js +2 -2
- package/src/type/resolved/SlugType.js +1 -1
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
|
```
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@acodeninja/persist",
|
3
|
-
"version": "
|
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.
|
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] =
|
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
|
+
}
|
package/src/engine/Engine.js
CHANGED
@@ -40,33 +40,35 @@ export default class Engine {
|
|
40
40
|
}
|
41
41
|
|
42
42
|
static async search(model, query) {
|
43
|
-
|
43
|
+
this.checkConfiguration();
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
124
|
+
this.checkConfiguration();
|
122
125
|
|
123
126
|
try {
|
127
|
+
const found = await this.getById(id);
|
124
128
|
return model.fromData(found);
|
125
|
-
} catch (
|
126
|
-
|
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
|
+
}
|
package/src/engine/FileEngine.js
CHANGED
@@ -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
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
+
}
|
package/src/engine/S3Engine.js
CHANGED
@@ -1,23 +1,32 @@
|
|
1
|
+
import Engine, {EngineError, MissConfiguredError} from './Engine.js';
|
1
2
|
import {GetObjectCommand, PutObjectCommand} from '@aws-sdk/client-s3';
|
2
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
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 (
|
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
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
@@ -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
|
}
|