@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.
Files changed (41) hide show
  1. package/README.md +60 -4
  2. package/docs/code-quirks.md +6 -6
  3. package/docs/defining-models.md +61 -0
  4. package/docs/http.openapi.yml +138 -0
  5. package/docs/{model-property-types.md → model-properties.md} +37 -35
  6. package/docs/models-as-properties.md +40 -40
  7. package/docs/search-queries.md +3 -5
  8. package/docs/storage-engines.md +19 -35
  9. package/docs/structured-queries.md +56 -45
  10. package/docs/transactions.md +6 -7
  11. package/exports/storage/http.js +3 -0
  12. package/exports/storage/s3.js +3 -0
  13. package/jest.config.cjs +8 -12
  14. package/package.json +2 -2
  15. package/src/Connection.js +631 -0
  16. package/src/Persist.js +29 -30
  17. package/src/Schema.js +175 -0
  18. package/src/{Query.js → data/FindIndex.js} +40 -24
  19. package/src/{type → data}/Model.js +41 -26
  20. package/src/data/Property.js +19 -0
  21. package/src/data/SearchIndex.js +106 -0
  22. package/src/{type/complex → data/properties}/ArrayType.js +1 -1
  23. package/src/{type/simple → data/properties}/BooleanType.js +1 -1
  24. package/src/{type/complex → data/properties}/CustomType.js +1 -1
  25. package/src/{type/simple → data/properties}/DateType.js +1 -1
  26. package/src/{type/simple → data/properties}/NumberType.js +1 -1
  27. package/src/{type/resolved → data/properties}/ResolvedType.js +3 -2
  28. package/src/{type/simple → data/properties}/StringType.js +1 -1
  29. package/src/{type → data/properties}/Type.js +8 -0
  30. package/src/engine/storage/HTTPStorageEngine.js +149 -253
  31. package/src/engine/storage/S3StorageEngine.js +108 -195
  32. package/src/engine/storage/StorageEngine.js +114 -550
  33. package/exports/engine/storage/file.js +0 -3
  34. package/exports/engine/storage/http.js +0 -3
  35. package/exports/engine/storage/s3.js +0 -3
  36. package/src/SchemaCompiler.js +0 -192
  37. package/src/Transactions.js +0 -145
  38. package/src/engine/StorageEngine.js +0 -250
  39. package/src/engine/storage/FileStorageEngine.js +0 -213
  40. package/src/type/index.js +0 -32
  41. /package/src/{type/resolved → data/properties}/SlugType.js +0 -0
@@ -1,4 +1,4 @@
1
- import Type from '../Type.js';
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 `ResolvedTypeOf(${property})`;
50
+ return `${that.toString()}Of(${property})`;
50
51
  }
51
52
  }
52
53
 
@@ -1,4 +1,4 @@
1
- import Type from '../Type.js';
1
+ import Type from './Type.js';
2
2
 
3
3
  /**
4
4
  * Class representing a string type.
@@ -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, {EngineError, MissConfiguredError} from './StorageEngine.js';
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.host] - Hostname and protocol of the HTTP service to use (ie: https://example.com).
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
- * @returns {Object} The configured settings for the HTTP engine.
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
- static configure(configuration = {}) {
49
- configuration.fetchOptions = {
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
- return super.configure(configuration);
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
- * Validates the engine's configuration, ensuring that the host is defined.
62
- *
63
- * @throws {MissConfiguredError} Thrown if the configuration is missing a host.
45
+ * Update a model
46
+ * @param {object} model
47
+ * @throws HTTPRequestFailedError
48
+ * @return Promise<void>
64
49
  */
65
- static checkConfiguration() {
66
- if (!this.configuration?.host) throw new MissConfiguredError(this.configuration);
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
- static _getReadOptions() {
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
- static _getWriteOptions() {
145
+ #getWriteOptions() {
85
146
  return {
86
- ...this._getReadOptions(),
147
+ ...this.#getReadOptions(),
87
148
  headers: {
88
- ...this._getReadOptions().headers,
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
- static _processFetch(url, options, defaultValue = undefined) {
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
- * Retrieves an object by its ID from the server.
123
- *
124
- * @param {string} id - The ID of the object to retrieve.
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
- static deleteById(id) {
149
- this.checkConfiguration();
150
-
151
- const url = new URL([
152
- this.configuration.host,
153
- this.configuration.prefix,
154
- `${id}.json`,
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
- * Uploads (puts) a raw search index for a model to the server.
302
- *
303
- * @param {Model.constructor} model - The model whose raw search index to upload.
304
- * @param {Object} rawIndex - The raw search index data.
305
- * @returns {Promise<Object>} The response from the server.
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
- return this._processFetch(url, {
318
- ...this._getWriteOptions(),
319
- body: JSON.stringify(rawIndex),
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;