@acodeninja/persist 1.1.0 → 2.1.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 +54 -4
- package/exports/engine/http.js +3 -0
- package/package.json +3 -2
- package/src/Persist.js +5 -1
- package/src/SchemaCompiler.js +14 -1
- package/src/Transactions.js +67 -0
- package/src/engine/Engine.js +36 -26
- package/src/engine/FileEngine.js +46 -16
- package/src/engine/HTTPEngine.js +69 -63
- package/src/engine/S3Engine.js +76 -60
- package/src/type/Model.js +11 -0
- package/src/type/complex/ArrayType.js +3 -1
- package/src/type/index.js +2 -0
- package/src/type/simple/DateType.js +10 -0
package/README.md
CHANGED
@@ -17,6 +17,7 @@ export class SimpleModel extends Persist.Type.Model {
|
|
17
17
|
static boolean = Persist.Type.Boolean;
|
18
18
|
static string = Persist.Type.String;
|
19
19
|
static number = Persist.Type.Number;
|
20
|
+
static date = Persist.Type.Date;
|
20
21
|
}
|
21
22
|
```
|
22
23
|
|
@@ -29,6 +30,7 @@ export class SimpleModel extends Persist.Type.Model {
|
|
29
30
|
static requiredBoolean = Persist.Type.Boolean.required;
|
30
31
|
static requiredString = Persist.Type.String.required;
|
31
32
|
static requiredNumber = Persist.Type.Number.required;
|
33
|
+
static requiredDate = Persist.Type.Date.required;
|
32
34
|
}
|
33
35
|
```
|
34
36
|
|
@@ -41,6 +43,11 @@ export class SimpleModel extends Persist.Type.Model {
|
|
41
43
|
static arrayOfBooleans = Persist.Type.Array.of(Type.Boolean);
|
42
44
|
static arrayOfStrings = Persist.Type.Array.of(Type.String);
|
43
45
|
static arrayOfNumbers = Persist.Type.Array.of(Type.Number);
|
46
|
+
static arrayOfDates = Persist.Type.Array.of(Type.Date);
|
47
|
+
static requiredArrayOfBooleans = Persist.Type.Array.of(Type.Boolean).required;
|
48
|
+
static requiredArrayOfStrings = Persist.Type.Array.of(Type.String).required;
|
49
|
+
static requiredArrayOfNumbers = Persist.Type.Array.of(Type.Number).required;
|
50
|
+
static requiredArrayOfDates = Persist.Type.Array.of(Type.Date).required;
|
44
51
|
}
|
45
52
|
```
|
46
53
|
|
@@ -144,10 +151,10 @@ export class Tag extends Persist.Type.Model {
|
|
144
151
|
|
145
152
|
const tag = new Tag({tag: 'documentation', description: 'How to use the persist library'});
|
146
153
|
|
147
|
-
FileEngine.find(Tag, {tag: 'documentation'});
|
154
|
+
await FileEngine.find(Tag, {tag: 'documentation'});
|
148
155
|
// [Tag {tag: 'documentation', description: 'How to use the persist library'}]
|
149
156
|
|
150
|
-
FileEngine.search(Tag, 'how to');
|
157
|
+
await FileEngine.search(Tag, 'how to');
|
151
158
|
// [Tag {tag: 'documentation', description: 'How to use the persist library'}]
|
152
159
|
```
|
153
160
|
|
@@ -169,7 +176,26 @@ export class Tag extends Persist.Type.Model {
|
|
169
176
|
static tag = Persist.Type.String.required;
|
170
177
|
}
|
171
178
|
|
172
|
-
Persist.getEngine('local', FileEngine).put(new Tag({tag: 'documentation'}));
|
179
|
+
await Persist.getEngine('local', FileEngine).put(new Tag({tag: 'documentation'}));
|
180
|
+
```
|
181
|
+
|
182
|
+
### HTTP Storage Engine
|
183
|
+
|
184
|
+
To store models using an S3 Bucket, use the `S3` storage engine.
|
185
|
+
|
186
|
+
```javascript
|
187
|
+
import Persist from "@acodeninja/persist";
|
188
|
+
import HTTPEngine from "@acodeninja/persist/engine/http";
|
189
|
+
|
190
|
+
Persist.addEngine('remote', HTTPEngine, {
|
191
|
+
host: 'https://api.example.com',
|
192
|
+
});
|
193
|
+
|
194
|
+
export class Tag extends Persist.Type.Model {
|
195
|
+
static tag = Persist.Type.String.required;
|
196
|
+
}
|
197
|
+
|
198
|
+
await Persist.getEngine('remote', HTTPEngine).put(new Tag({tag: 'documentation'}));
|
173
199
|
```
|
174
200
|
|
175
201
|
### S3 Storage Engine
|
@@ -189,5 +215,29 @@ export class Tag extends Persist.Type.Model {
|
|
189
215
|
static tag = Persist.Type.String.required;
|
190
216
|
}
|
191
217
|
|
192
|
-
Persist.getEngine('remote', S3Engine).put(new Tag({tag: 'documentation'}));
|
218
|
+
await Persist.getEngine('remote', S3Engine).put(new Tag({tag: 'documentation'}));
|
219
|
+
```
|
220
|
+
|
221
|
+
## Transactions
|
222
|
+
|
223
|
+
Create transactions to automatically roll back on failure to update.
|
224
|
+
|
225
|
+
```javascript
|
226
|
+
import Persist from "@acodeninja/persist";
|
227
|
+
import S3Engine from "@acodeninja/persist/engine/s3";
|
228
|
+
|
229
|
+
Persist.addEngine('remote', S3Engine, {
|
230
|
+
bucket: 'test-bucket',
|
231
|
+
client: new S3Client(),
|
232
|
+
transactions: true,
|
233
|
+
});
|
234
|
+
|
235
|
+
export class Tag extends Persist.Type.Model {
|
236
|
+
static tag = Persist.Type.String.required;
|
237
|
+
}
|
238
|
+
|
239
|
+
const transaction = Persist.getEngine('remote', S3Engine).start();
|
240
|
+
|
241
|
+
await transaction.put(new Tag({tag: 'documentation'}));
|
242
|
+
await transaction.commit();
|
193
243
|
```
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@acodeninja/persist",
|
3
|
-
"version": "
|
3
|
+
"version": "2.1.0",
|
4
4
|
"description": "A JSON based data modelling and persistence module with alternate storage mechanisms.",
|
5
5
|
"type": "module",
|
6
6
|
"scripts": {
|
@@ -25,6 +25,7 @@
|
|
25
25
|
"dependencies": {
|
26
26
|
"ajv": "^8.16.0",
|
27
27
|
"ajv-errors": "^3.0.0",
|
28
|
+
"ajv-formats": "^3.0.1",
|
28
29
|
"lunr": "^2.3.9",
|
29
30
|
"slugify": "^1.6.6",
|
30
31
|
"ulid": "^2.3.0"
|
@@ -43,7 +44,7 @@
|
|
43
44
|
"globals": "^15.8.0",
|
44
45
|
"husky": "^9.1.1",
|
45
46
|
"lodash": "^4.17.21",
|
46
|
-
"monocart-coverage-reports": "^2.
|
47
|
+
"monocart-coverage-reports": "^2.10.2",
|
47
48
|
"semantic-release": "^24.0.0",
|
48
49
|
"sinon": "^18.0.0"
|
49
50
|
}
|
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
|
}
|
package/src/SchemaCompiler.js
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import Type from './type/index.js';
|
2
2
|
import ajv from 'ajv';
|
3
3
|
import ajvErrors from 'ajv-errors';
|
4
|
+
import ajvFormats from 'ajv-formats';
|
4
5
|
|
5
6
|
/**
|
6
7
|
* @class SchemaCompiler
|
@@ -15,6 +16,7 @@ export default class SchemaCompiler {
|
|
15
16
|
const validation = new ajv({allErrors: true});
|
16
17
|
|
17
18
|
ajvErrors(validation);
|
19
|
+
ajvFormats(validation);
|
18
20
|
|
19
21
|
const schema = {
|
20
22
|
type: 'object',
|
@@ -58,9 +60,17 @@ export default class SchemaCompiler {
|
|
58
60
|
|
59
61
|
schema.properties[name] = {type: property?._type};
|
60
62
|
|
63
|
+
if (property?._format) {
|
64
|
+
schema.properties[name].format = property._format;
|
65
|
+
}
|
66
|
+
|
61
67
|
if (property?._type === 'array') {
|
62
68
|
schema.properties[name].items = {type: property?._items._type};
|
63
69
|
|
70
|
+
if (property?._items?._format) {
|
71
|
+
schema.properties[name].items.format = property?._items._format;
|
72
|
+
}
|
73
|
+
|
64
74
|
if (Type.Model.isModel(property?._items)) {
|
65
75
|
schema.properties[name].items = {
|
66
76
|
type: 'object',
|
@@ -102,11 +112,14 @@ export class CompiledSchema {
|
|
102
112
|
* @throws {ValidationError}
|
103
113
|
*/
|
104
114
|
static validate(data) {
|
105
|
-
let inputData = data;
|
115
|
+
let inputData = Object.assign({}, data);
|
116
|
+
|
106
117
|
if (Type.Model.isModel(data)) {
|
107
118
|
inputData = data.toData();
|
108
119
|
}
|
120
|
+
|
109
121
|
const valid = this._validator?.(inputData);
|
122
|
+
|
110
123
|
if (valid) return valid;
|
111
124
|
|
112
125
|
throw new ValidationError(inputData, this._validator.errors);
|
@@ -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,36 +40,35 @@ export default class Engine {
|
|
40
40
|
}
|
41
41
|
|
42
42
|
static async search(model, query) {
|
43
|
-
this.
|
44
|
-
const index = await this.getSearchIndexCompiled(model);
|
43
|
+
this.checkConfiguration();
|
45
44
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
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.
|
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.
|
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.
|
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 (
|
130
|
-
|
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.
|
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 {
|
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
|
}
|
package/src/engine/HTTPEngine.js
CHANGED
@@ -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
|
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.
|
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
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
...
|
93
|
-
|
94
|
-
|
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
|
127
|
+
const url = new URL([this._configuration.host, this._configuration.prefix, location, '_index.json'].filter(e => !!e).join('/'));
|
112
128
|
|
113
|
-
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
}
|
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
@@ -93,6 +93,17 @@ export default class Model {
|
|
93
93
|
|
94
94
|
for (const [name, value] of Object.entries(data)) {
|
95
95
|
if (this[name]?._resolved) continue;
|
96
|
+
|
97
|
+
if (this[name].name.endsWith('DateType')) {
|
98
|
+
model[name] = new Date(value);
|
99
|
+
continue;
|
100
|
+
}
|
101
|
+
|
102
|
+
if (this[name].name.endsWith('ArrayOf(Date)Type')) {
|
103
|
+
model[name] = data[name].map(d => new Date(d));
|
104
|
+
continue;
|
105
|
+
}
|
106
|
+
|
96
107
|
model[name] = value;
|
97
108
|
}
|
98
109
|
|
@@ -7,7 +7,7 @@ export default class ArrayType {
|
|
7
7
|
static _items = type;
|
8
8
|
|
9
9
|
static toString() {
|
10
|
-
return `ArrayOf(${type})`;
|
10
|
+
return `ArrayOf(${type.toString()})`;
|
11
11
|
}
|
12
12
|
|
13
13
|
static get required() {
|
@@ -25,6 +25,8 @@ export default class ArrayType {
|
|
25
25
|
}
|
26
26
|
}
|
27
27
|
|
28
|
+
Object.defineProperty(ArrayOf, 'name', {value: `${ArrayOf.toString()}Type`});
|
29
|
+
|
28
30
|
return ArrayOf;
|
29
31
|
}
|
30
32
|
}
|
package/src/type/index.js
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import ArrayType from './complex/ArrayType.js';
|
2
2
|
import BooleanType from './simple/BooleanType.js';
|
3
3
|
import CustomType from './complex/CustomType.js';
|
4
|
+
import DateType from './simple/DateType.js';
|
4
5
|
import Model from './Model.js';
|
5
6
|
import NumberType from './simple/NumberType.js';
|
6
7
|
import SlugType from './resolved/SlugType.js';
|
@@ -11,6 +12,7 @@ const Type = {};
|
|
11
12
|
Type.String = StringType;
|
12
13
|
Type.Number = NumberType;
|
13
14
|
Type.Boolean = BooleanType;
|
15
|
+
Type.Date = DateType;
|
14
16
|
Type.Array = ArrayType;
|
15
17
|
Type.Custom = CustomType;
|
16
18
|
Type.Resolved = {Slug: SlugType};
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import SimpleType from './SimpleType.js';
|
2
|
+
|
3
|
+
export default class DateType extends SimpleType {
|
4
|
+
static _type = 'string';
|
5
|
+
static _format = 'iso-date-time';
|
6
|
+
|
7
|
+
static isDate(possibleDate) {
|
8
|
+
return possibleDate instanceof Date || !isNaN(new Date(possibleDate));
|
9
|
+
}
|
10
|
+
}
|