@acodeninja/persist 3.0.0-next.2 → 3.0.0-next.20
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 +60 -4
- package/docs/code-quirks.md +6 -6
- package/docs/defining-models.md +61 -0
- package/docs/http.openapi.yml +138 -0
- package/docs/{model-property-types.md → model-properties.md} +37 -35
- package/docs/models-as-properties.md +40 -40
- package/docs/search-queries.md +3 -5
- package/docs/storage-engines.md +19 -35
- package/docs/structured-queries.md +56 -45
- package/docs/transactions.md +6 -7
- package/exports/storage/http.js +3 -0
- package/exports/storage/s3.js +3 -0
- package/jest.config.cjs +8 -12
- package/package.json +2 -2
- package/src/Connection.js +631 -0
- package/src/Persist.js +29 -30
- package/src/Schema.js +175 -0
- package/src/{Query.js → data/FindIndex.js} +40 -24
- package/src/{type → data}/Model.js +41 -26
- package/src/data/Property.js +19 -0
- package/src/data/SearchIndex.js +106 -0
- package/src/{type/complex → data/properties}/ArrayType.js +1 -1
- package/src/{type/simple → data/properties}/BooleanType.js +1 -1
- package/src/{type/complex → data/properties}/CustomType.js +1 -1
- package/src/{type/simple → data/properties}/DateType.js +1 -1
- package/src/{type/simple → data/properties}/NumberType.js +1 -1
- package/src/{type/resolved → data/properties}/ResolvedType.js +3 -2
- package/src/{type/simple → data/properties}/StringType.js +1 -1
- package/src/{type → data/properties}/Type.js +8 -0
- package/src/engine/storage/HTTPStorageEngine.js +149 -253
- package/src/engine/storage/S3StorageEngine.js +108 -195
- package/src/engine/storage/StorageEngine.js +114 -550
- package/exports/engine/storage/file.js +0 -3
- package/exports/engine/storage/http.js +0 -3
- package/exports/engine/storage/s3.js +0 -3
- package/src/SchemaCompiler.js +0 -192
- package/src/Transactions.js +0 -145
- package/src/engine/StorageEngine.js +0 -250
- package/src/engine/storage/FileStorageEngine.js +0 -213
- package/src/type/index.js +0 -32
- /package/src/{type/resolved → data/properties}/SlugType.js +0 -0
@@ -1,4 +1,4 @@
|
|
1
|
-
import Type from '
|
1
|
+
import Type from './Type.js';
|
2
2
|
|
3
3
|
/**
|
4
4
|
* Class representing a resolved type.
|
@@ -39,6 +39,7 @@ class ResolvedType extends Type {
|
|
39
39
|
* @returns {ResolvedType} A subclass of `ResolvedType` customized for the provided property.
|
40
40
|
*/
|
41
41
|
static of(property) {
|
42
|
+
const that = this;
|
42
43
|
class ResolvedTypeOf extends ResolvedType {
|
43
44
|
/**
|
44
45
|
* Converts the resolved type to a string, displaying the resolved property.
|
@@ -46,7 +47,7 @@ class ResolvedType extends Type {
|
|
46
47
|
* @returns {string} A string representing the resolved type, including the property.
|
47
48
|
*/
|
48
49
|
static toString() {
|
49
|
-
return
|
50
|
+
return `${that.toString()}Of(${property})`;
|
50
51
|
}
|
51
52
|
}
|
52
53
|
|
@@ -53,6 +53,14 @@ class Type {
|
|
53
53
|
* @returns {Type} A subclass of the current type with `_required` set to `true`.
|
54
54
|
*/
|
55
55
|
static get required() {
|
56
|
+
/**
|
57
|
+
* A subclass of the current type with the `_required` flag set to `true`.
|
58
|
+
* Used to indicate that the property is required during validation or schema generation.
|
59
|
+
*
|
60
|
+
* @class
|
61
|
+
* @extends {Type}
|
62
|
+
* @private
|
63
|
+
*/
|
56
64
|
class Required extends this {
|
57
65
|
static _required = true;
|
58
66
|
}
|
@@ -1,69 +1,130 @@
|
|
1
|
-
import StorageEngine, {
|
2
|
-
|
3
|
-
|
4
|
-
* Represents an error specific to HTTP engine operations.
|
5
|
-
* @class HTTPStorageEngineError
|
6
|
-
* @extends EngineError
|
7
|
-
*/
|
8
|
-
export class HTTPStorageEngineError extends EngineError {}
|
9
|
-
|
10
|
-
/**
|
11
|
-
* Error indicating a failed HTTP request.
|
12
|
-
* @class HTTPRequestFailedError
|
13
|
-
* @extends HTTPStorageEngineError
|
14
|
-
*
|
15
|
-
* @param {string} url - The URL of the failed request.
|
16
|
-
* @param {Object} options - The options used in the fetch request.
|
17
|
-
* @param {Response} response - The HTTP response object.
|
18
|
-
*/
|
19
|
-
export class HTTPRequestFailedError extends HTTPStorageEngineError {
|
20
|
-
constructor(url, options, response) {
|
21
|
-
const method = options.method?.toLowerCase() || 'get';
|
22
|
-
super(`Failed to ${method} ${url}`);
|
23
|
-
this.response = response;
|
24
|
-
this.url = url;
|
25
|
-
this.options = options;
|
26
|
-
}
|
27
|
-
}
|
28
|
-
|
29
|
-
/**
|
30
|
-
* HTTPStorageEngine is an extension of the StorageEngine class that provides methods for interacting with HTTP-based APIs.
|
31
|
-
* It uses the Fetch API for sending and receiving data.
|
32
|
-
*
|
33
|
-
* @class HTTPStorageEngine
|
34
|
-
* @extends StorageEngine
|
35
|
-
*/
|
36
|
-
class HTTPStorageEngine extends StorageEngine {
|
1
|
+
import StorageEngine, {
|
2
|
+
MisconfiguredStorageEngineError, ModelNotFoundStorageEngineError, StorageEngineError,
|
3
|
+
} from './StorageEngine.js';
|
37
4
|
|
5
|
+
export default class HTTPStorageEngine extends StorageEngine {
|
38
6
|
/**
|
39
|
-
* Configures the HTTP engine with additional fetch options.
|
40
|
-
* Sets the Accept header to 'application/json' by default.
|
41
|
-
*
|
42
7
|
* @param {Object} configuration - Configuration object containing fetch options and other settings.
|
43
|
-
* @param {string} [configuration.
|
8
|
+
* @param {string} [configuration.baseURL] - Hostname and protocol of the HTTP service to use (ie: https://example.com).
|
44
9
|
* @param {string?} [configuration.prefix] - The prefix on the host to perform operations against.
|
45
|
-
* @param {Object} [configuration.fetchOptions] - Fetch overrides to attach to all requests sent to the HTTP service.
|
46
|
-
* @
|
10
|
+
* @param {Object?} [configuration.fetchOptions] - Fetch overrides to attach to all requests sent to the HTTP service.
|
11
|
+
* @param {fetch} [configuration.fetch] - The http client that implements fetch.
|
12
|
+
* @param {Array<Model.constructor>} models
|
47
13
|
*/
|
48
|
-
|
49
|
-
configuration
|
14
|
+
constructor(configuration, models) {
|
15
|
+
super(configuration, models);
|
16
|
+
if (!this.configuration?.baseURL || !this.configuration?.fetch)
|
17
|
+
throw new MisconfiguredStorageEngineError('both baseURL and fetch must be provided', this);
|
18
|
+
|
19
|
+
this.configuration.fetchOptions = {
|
50
20
|
...(configuration.fetchOptions ?? {}),
|
51
21
|
headers: {
|
52
22
|
...(configuration.fetchOptions?.headers ?? {}),
|
53
23
|
Accept: 'application/json',
|
54
24
|
},
|
55
25
|
};
|
26
|
+
}
|
56
27
|
|
57
|
-
|
28
|
+
/**
|
29
|
+
* Get a model
|
30
|
+
* @param {string} id
|
31
|
+
* @throws ModelNotFoundStorageEngineError
|
32
|
+
* @return Promise<Model>
|
33
|
+
*/
|
34
|
+
getModel(id) {
|
35
|
+
return this.#processFetch(this.#generateURL([id]), this.#getReadOptions())
|
36
|
+
.catch(error => {
|
37
|
+
if (error.response.status === 404) {
|
38
|
+
throw new ModelNotFoundStorageEngineError(id);
|
39
|
+
}
|
40
|
+
throw error;
|
41
|
+
});
|
58
42
|
}
|
59
43
|
|
60
44
|
/**
|
61
|
-
*
|
62
|
-
*
|
63
|
-
* @throws
|
45
|
+
* Update a model
|
46
|
+
* @param {object} model
|
47
|
+
* @throws HTTPRequestFailedError
|
48
|
+
* @return Promise<void>
|
64
49
|
*/
|
65
|
-
|
66
|
-
|
50
|
+
putModel(model) {
|
51
|
+
return this.#processFetch(this.#generateURL([model.id]), {
|
52
|
+
...this.#getWriteOptions(),
|
53
|
+
body: JSON.stringify(model),
|
54
|
+
});
|
55
|
+
}
|
56
|
+
|
57
|
+
/**
|
58
|
+
* Delete a model
|
59
|
+
* @param {string} id
|
60
|
+
* @throws HTTPRequestFailedError
|
61
|
+
* @return Promise<void>
|
62
|
+
*/
|
63
|
+
deleteModel(id) {
|
64
|
+
return this.#processFetch(this.#generateURL([id]), {
|
65
|
+
...this.#getReadOptions(),
|
66
|
+
method: 'DELETE',
|
67
|
+
}).catch(error => {
|
68
|
+
if (error.response.status === 404) {
|
69
|
+
throw new ModelNotFoundStorageEngineError(id);
|
70
|
+
}
|
71
|
+
throw error;
|
72
|
+
});
|
73
|
+
}
|
74
|
+
|
75
|
+
/**
|
76
|
+
* Get a model's index data
|
77
|
+
* @param {Model.constructor} modelConstructor
|
78
|
+
* @return Promise<void>
|
79
|
+
*/
|
80
|
+
getIndex(modelConstructor) {
|
81
|
+
return this.#processFetch(
|
82
|
+
this.#generateURL([modelConstructor.name]),
|
83
|
+
this.#getReadOptions(),
|
84
|
+
{},
|
85
|
+
);
|
86
|
+
}
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Put a model's index data
|
90
|
+
* @param {Model.constructor} modelConstructor
|
91
|
+
* @param {object} index
|
92
|
+
* @throws MethodNotImplementedStorageEngineError
|
93
|
+
* @return Promise<void>
|
94
|
+
*/
|
95
|
+
putIndex(modelConstructor, index) {
|
96
|
+
return this.#processFetch(this.#generateURL([modelConstructor.name]), {
|
97
|
+
...this.#getWriteOptions(),
|
98
|
+
body: JSON.stringify(index),
|
99
|
+
});
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Get a model's raw search index data
|
104
|
+
* @param {Model.constructor} modelConstructor
|
105
|
+
* @throws MethodNotImplementedStorageEngineError
|
106
|
+
* @return Promise<object>
|
107
|
+
*/
|
108
|
+
getSearchIndex(modelConstructor) {
|
109
|
+
return this.#processFetch(
|
110
|
+
this.#generateURL([modelConstructor.name, 'search']),
|
111
|
+
this.#getReadOptions(),
|
112
|
+
{},
|
113
|
+
);
|
114
|
+
}
|
115
|
+
|
116
|
+
/**
|
117
|
+
* Put a model's raw and compiled search index data
|
118
|
+
* @param {Model.constructor} modelConstructor
|
119
|
+
* @param {Record<string, object>} index
|
120
|
+
* @throws MethodNotImplementedStorageEngineError
|
121
|
+
* @return Promise<void>
|
122
|
+
*/
|
123
|
+
putSearchIndex(modelConstructor, index) {
|
124
|
+
return this.#processFetch(this.#generateURL([modelConstructor.name, 'search']), {
|
125
|
+
...this.#getWriteOptions(),
|
126
|
+
body: JSON.stringify(index),
|
127
|
+
});
|
67
128
|
}
|
68
129
|
|
69
130
|
/**
|
@@ -71,7 +132,7 @@ class HTTPStorageEngine extends StorageEngine {
|
|
71
132
|
*
|
72
133
|
* @returns {Object} The fetch options for reading.
|
73
134
|
*/
|
74
|
-
|
135
|
+
#getReadOptions() {
|
75
136
|
return this.configuration.fetchOptions;
|
76
137
|
}
|
77
138
|
|
@@ -81,11 +142,11 @@ class HTTPStorageEngine extends StorageEngine {
|
|
81
142
|
*
|
82
143
|
* @returns {Object} The fetch options for writing.
|
83
144
|
*/
|
84
|
-
|
145
|
+
#getWriteOptions() {
|
85
146
|
return {
|
86
|
-
...this
|
147
|
+
...this.#getReadOptions(),
|
87
148
|
headers: {
|
88
|
-
...this
|
149
|
+
...this.#getReadOptions().headers,
|
89
150
|
'Content-Type': 'application/json',
|
90
151
|
},
|
91
152
|
method: 'PUT',
|
@@ -99,10 +160,9 @@ class HTTPStorageEngine extends StorageEngine {
|
|
99
160
|
* @param {Object} options - The fetch options.
|
100
161
|
* @param {*} [defaultValue] - A default value to return if the fetch fails.
|
101
162
|
* @returns {Promise<Object>} The parsed JSON response.
|
102
|
-
*
|
103
163
|
* @throws {HTTPRequestFailedError} Thrown if the fetch request fails.
|
104
164
|
*/
|
105
|
-
|
165
|
+
#processFetch(url, options, defaultValue = undefined) {
|
106
166
|
return this.configuration.fetch(url, options)
|
107
167
|
.then(response => {
|
108
168
|
if (!response.ok) {
|
@@ -119,206 +179,42 @@ class HTTPStorageEngine extends StorageEngine {
|
|
119
179
|
}
|
120
180
|
|
121
181
|
/**
|
122
|
-
*
|
123
|
-
*
|
124
|
-
* @
|
125
|
-
* @returns {Promise<Object>} The retrieved object in JSON format.
|
126
|
-
*
|
127
|
-
* @throws {HTTPRequestFailedError} Thrown if the fetch request fails.
|
128
|
-
*/
|
129
|
-
static getById(id) {
|
130
|
-
this.checkConfiguration();
|
131
|
-
|
132
|
-
const url = new URL([
|
133
|
-
this.configuration.host,
|
134
|
-
this.configuration.prefix,
|
135
|
-
`${id}.json`,
|
136
|
-
].filter(e => Boolean(e)).join('/'));
|
137
|
-
|
138
|
-
return this._processFetch(url, this._getReadOptions());
|
139
|
-
}
|
140
|
-
|
141
|
-
/**
|
142
|
-
* Deletes a model by its ID from an HTTP server.
|
143
|
-
*
|
144
|
-
* @param {string} id - The ID of the model to delete.
|
145
|
-
* @returns {Promise<void>} Resolves when the model has been deleted.
|
146
|
-
* @throws {Error} Throws if the file cannot be deleted.
|
182
|
+
* Creates a URL object based on the input path
|
183
|
+
* @param {Array<string>} input
|
184
|
+
* @return {module:url.URL}
|
147
185
|
*/
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
].filter(e => Boolean(e)).join('/'));
|
156
|
-
|
157
|
-
return this._processFetch(url, {...this._getReadOptions(), method: 'DELETE'});
|
158
|
-
}
|
159
|
-
|
160
|
-
/**
|
161
|
-
* Uploads (puts) a model object to the server.
|
162
|
-
*
|
163
|
-
* @param {Model} model - The model object to upload.
|
164
|
-
* @returns {Promise<Object>} The response from the server.
|
165
|
-
*
|
166
|
-
* @throws {HTTPRequestFailedError} Thrown if the PUT request fails.
|
167
|
-
*/
|
168
|
-
static putModel(model) {
|
169
|
-
const url = new URL([
|
170
|
-
this.configuration.host,
|
171
|
-
this.configuration.prefix,
|
172
|
-
`${model.id}.json`,
|
173
|
-
].filter(e => Boolean(e)).join('/'));
|
174
|
-
|
175
|
-
return this._processFetch(url, {
|
176
|
-
...this._getWriteOptions(),
|
177
|
-
body: JSON.stringify(model.toData()),
|
178
|
-
});
|
179
|
-
}
|
180
|
-
|
181
|
-
/**
|
182
|
-
* Uploads (puts) an index object to the server.
|
183
|
-
*
|
184
|
-
* @param {Object} index - An object where keys are locations and values are key value pairs of models and their ids.
|
185
|
-
* @returns {Promise<void>}
|
186
|
-
* @throws {HTTPRequestFailedError} Thrown if the PUT request fails.
|
187
|
-
*/
|
188
|
-
static async putIndex(index) {
|
189
|
-
/**
|
190
|
-
* Process an index of models
|
191
|
-
* @param {string} location
|
192
|
-
* @param {Array<Model>} models
|
193
|
-
* @return {Promise<void>}
|
194
|
-
*/
|
195
|
-
const processIndex = async (location, models) => {
|
196
|
-
const url = new URL([
|
197
|
-
this.configuration.host,
|
198
|
-
this.configuration.prefix,
|
199
|
-
location,
|
200
|
-
'_index.json',
|
201
|
-
].filter(e => Boolean(e)).join('/'));
|
202
|
-
|
203
|
-
return this._processFetch(url, {
|
204
|
-
...this._getWriteOptions(),
|
205
|
-
body: JSON.stringify({
|
206
|
-
...await this.getIndex(location),
|
207
|
-
...Object.fromEntries(
|
208
|
-
Object.entries(models).map(([k, v]) => [k, v?.toIndexData?.() || v]),
|
209
|
-
),
|
210
|
-
}),
|
211
|
-
});
|
212
|
-
};
|
213
|
-
|
214
|
-
for (const [location, models] of Object.entries(index)) {
|
215
|
-
await processIndex(location, models);
|
216
|
-
}
|
217
|
-
|
218
|
-
await processIndex(null, Object.values(index).reduce((accumulator, currentValue) => {
|
219
|
-
Object.keys(currentValue).forEach(key => {
|
220
|
-
accumulator[key] = currentValue[key];
|
221
|
-
});
|
222
|
-
return accumulator;
|
223
|
-
}, {}));
|
224
|
-
}
|
225
|
-
|
226
|
-
/**
|
227
|
-
* Retrieves the index object from the server for the specified location.
|
228
|
-
*
|
229
|
-
* @param {Model.constructor?} model - The model in the host where the index is stored.
|
230
|
-
* @returns {Promise<Object>} The index data in JSON format.
|
231
|
-
*/
|
232
|
-
static getIndex(model) {
|
233
|
-
const url = new URL([
|
234
|
-
this.configuration.host,
|
235
|
-
this.configuration.prefix,
|
236
|
-
model?.toString(),
|
237
|
-
'_index.json',
|
238
|
-
].filter(e => Boolean(e)).join('/'));
|
239
|
-
|
240
|
-
return this._processFetch(url, this._getReadOptions(), {});
|
241
|
-
}
|
242
|
-
|
243
|
-
/**
|
244
|
-
* Retrieves the compiled search index for a model from the server.
|
245
|
-
*
|
246
|
-
* @param {Model.constructor} model - The model whose compiled search index to retrieve.
|
247
|
-
* @returns {Promise<Object>} The compiled search index in JSON format.
|
248
|
-
*/
|
249
|
-
static getSearchIndexCompiled(model) {
|
250
|
-
const url = new URL([
|
251
|
-
this.configuration.host,
|
252
|
-
this.configuration.prefix,
|
253
|
-
model.toString(),
|
254
|
-
'_search_index.json',
|
255
|
-
].join('/'));
|
256
|
-
|
257
|
-
return this._processFetch(url, this._getReadOptions());
|
258
|
-
}
|
259
|
-
|
260
|
-
/**
|
261
|
-
* Retrieves the raw (uncompiled) search index for a model from the server.
|
262
|
-
*
|
263
|
-
* @param {Model.constructor} model - The model whose raw search index to retrieve.
|
264
|
-
* @returns {Promise<Object>} The raw search index in JSON format, or an empty object if not found.
|
265
|
-
*/
|
266
|
-
static getSearchIndexRaw(model) {
|
267
|
-
const url = new URL([
|
268
|
-
this.configuration.host,
|
269
|
-
this.configuration.prefix,
|
270
|
-
model.toString(),
|
271
|
-
'_search_index_raw.json',
|
272
|
-
].join('/'));
|
273
|
-
|
274
|
-
return this._processFetch(url, this._getReadOptions()).catch(() => ({}));
|
275
|
-
}
|
276
|
-
|
277
|
-
/**
|
278
|
-
* Uploads (puts) a compiled search index for a model to the server.
|
279
|
-
*
|
280
|
-
* @param {Model.constructor} model - The model whose compiled search index to upload.
|
281
|
-
* @param {Object} compiledIndex - The compiled search index data.
|
282
|
-
* @returns {Promise<Object>} The response from the server.
|
283
|
-
*
|
284
|
-
* @throws {HTTPRequestFailedError} Thrown if the PUT request fails.
|
285
|
-
*/
|
286
|
-
static putSearchIndexCompiled(model, compiledIndex) {
|
287
|
-
const url = new URL([
|
288
|
-
this.configuration.host,
|
289
|
-
this.configuration.prefix,
|
290
|
-
model.toString(),
|
291
|
-
'_search_index.json',
|
292
|
-
].filter(e => Boolean(e)).join('/'));
|
293
|
-
|
294
|
-
return this._processFetch(url, {
|
295
|
-
...this._getWriteOptions(),
|
296
|
-
body: JSON.stringify(compiledIndex),
|
297
|
-
});
|
186
|
+
#generateURL(input) {
|
187
|
+
return new URL(
|
188
|
+
[this.configuration.baseURL, this.configuration.prefix]
|
189
|
+
.concat(input)
|
190
|
+
.filter(Boolean)
|
191
|
+
.join('/'),
|
192
|
+
);
|
298
193
|
}
|
194
|
+
}
|
299
195
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
*
|
307
|
-
* @throws {HTTPRequestFailedError} Thrown if the PUT request fails.
|
308
|
-
*/
|
309
|
-
static putSearchIndexRaw(model, rawIndex) {
|
310
|
-
const url = new URL([
|
311
|
-
this.configuration.host,
|
312
|
-
this.configuration.prefix,
|
313
|
-
model.toString(),
|
314
|
-
'_search_index_raw.json',
|
315
|
-
].filter(e => Boolean(e)).join('/'));
|
196
|
+
/**
|
197
|
+
* Represents an error specific to HTTP engine operations.
|
198
|
+
* @class HTTPStorageEngineError
|
199
|
+
* @extends StorageEngineError
|
200
|
+
*/
|
201
|
+
export class HTTPStorageEngineError extends StorageEngineError {}
|
316
202
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
203
|
+
/**
|
204
|
+
* Error indicating a failed HTTP request.
|
205
|
+
* @class HTTPRequestFailedError
|
206
|
+
* @extends HTTPStorageEngineError
|
207
|
+
*
|
208
|
+
* @param {string} url - The URL of the failed request.
|
209
|
+
* @param {Object} options - The options used in the fetch request.
|
210
|
+
* @param {Response} response - The HTTP response object.
|
211
|
+
*/
|
212
|
+
export class HTTPRequestFailedError extends HTTPStorageEngineError {
|
213
|
+
constructor(url, options, response) {
|
214
|
+
const method = options.method?.toLowerCase() || 'get';
|
215
|
+
super(`Failed to ${method} ${url}`);
|
216
|
+
this.response = response;
|
217
|
+
this.url = url;
|
218
|
+
this.options = options;
|
321
219
|
}
|
322
220
|
}
|
323
|
-
|
324
|
-
export default HTTPStorageEngine;
|