@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,125 +1,100 @@
1
- import {DeleteObjectCommand, GetObjectCommand, PutObjectCommand} from '@aws-sdk/client-s3';
2
- import StorageEngine, {EngineError, MissConfiguredError} from './StorageEngine.js';
1
+ import {
2
+ DeleteObjectCommand,
3
+ GetObjectCommand,
4
+ NoSuchKey,
5
+ PutObjectCommand,
6
+ } from '@aws-sdk/client-s3';
7
+ import StorageEngine, {
8
+ MisconfiguredStorageEngineError,
9
+ ModelNotFoundStorageEngineError,
10
+ } from './StorageEngine.js';
3
11
 
4
- /**
5
- * Represents an error specific to the S3 engine operations.
6
- * @class S3StorageEngineError
7
- * @extends EngineError
8
- */
9
- class S3StorageEngineError extends EngineError {}
10
-
11
- /**
12
- * Error indicating a failure when putting an object to S3.
13
- * @class FailedPutS3StorageEngineError
14
- * @extends S3StorageEngineError
15
- */
16
- class FailedPutS3StorageEngineError extends S3StorageEngineError {}
17
-
18
- /**
19
- * S3StorageEngine is an extension of the StorageEngine class that provides methods for interacting with AWS S3.
20
- * It allows for storing, retrieving, and managing model data in an S3 bucket.
21
- *
22
- * @class S3StorageEngine
23
- * @extends StorageEngine
24
- */
25
12
  class S3StorageEngine extends StorageEngine {
26
13
  /**
27
- * Configures the S3 engine with additional options.
28
- *
29
- * @param {Object} configuration - Configuration object.
30
- * @param {S3Client} [configuration.client] - An S3 client used to process operations.
31
- * @param {string} [configuration.bucket] - The S3 bucket to perform operations against.
32
- * @param {string?} [configuration.prefix] - The optional prefix in the bucket to perform operations against.
33
- * @returns {Object} The configured settings for the HTTP engine.
34
- */
35
- static configure(configuration = {}) {
36
- return super.configure(configuration);
37
- }
38
-
39
- /**
40
- * Validates the S3 engine configuration to ensure necessary parameters (bucket and client) are present.
41
- * Throws an error if the configuration is invalid.
42
- *
43
- * @throws {MissConfiguredError} Thrown when the configuration is missing required parameters.
14
+ * @param {Object} configuration - Configuration object containing fetch options and other settings.
15
+ * @param {string} [configuration.bucket] - Hostname and protocol of the HTTP service to use (ie: https://example.com).
16
+ * @param {string?} [configuration.prefix] - The prefix on the host to perform operations against.
17
+ * @param {S3Client} [configuration.client] - The http client that implements fetch.
44
18
  */
45
- static checkConfiguration() {
46
- if (
47
- !this.configuration?.bucket ||
48
- !this.configuration?.client
49
- ) throw new MissConfiguredError(this.configuration);
19
+ constructor(configuration) {
20
+ super(configuration);
21
+ if (!configuration?.bucket || !configuration?.client)
22
+ throw new MisconfiguredStorageEngineError('both bucket and client must be provided', this);
50
23
  }
51
24
 
52
25
  /**
53
- * Retrieves an object from S3 by its ID.
54
- *
55
- * @param {string} id - The ID of the object to retrieve.
56
- * @returns {Promise<Object>} The parsed JSON object retrieved from S3.
57
- *
58
- * @throws {Error} Thrown if there is an issue with the S3 client request.
26
+ * Retrieves an object from S3 by its id
27
+ * @param {string} id
28
+ * @returns {Promise<Object>}
29
+ * @throws {ModelNotFoundStorageEngineError|Error} Thrown if there is an issue with the S3 client request.
59
30
  */
60
- static async getById(id) {
61
- const objectPath = [this.configuration.prefix, `${id}.json`].join('/');
31
+ async getModel(id) {
32
+ const objectPath = this.#generatePath([`${id}.json`]);
62
33
 
63
- const data = await this.configuration.client.send(new GetObjectCommand({
64
- Bucket: this.configuration.bucket,
65
- Key: objectPath,
66
- }));
34
+ try {
35
+ const data = await this.configuration.client.send(new GetObjectCommand({
36
+ Bucket: this.configuration.bucket,
37
+ Key: objectPath,
38
+ }));
67
39
 
68
- return JSON.parse(await data.Body.transformToString());
40
+ return JSON.parse(await data.Body.transformToString());
41
+ } catch (error) {
42
+ if (error instanceof NoSuchKey) {
43
+ throw new ModelNotFoundStorageEngineError(id);
44
+ }
45
+ throw error;
46
+ }
69
47
  }
70
48
 
71
49
  /**
72
- * Deletes a model by its ID from theS3 bucket.
73
- *
74
- * @param {string} id - The ID of the model to delete.
75
- * @returns {Promise<void>} Resolves when the model has been deleted.
76
- * @throws {Error} Throws if the model cannot be deleted.
50
+ * Upload an object to S3
51
+ * @param {object} model - The model object to upload.
52
+ * @returns {Promise<void>}
77
53
  */
78
- static async deleteById(id) {
79
- const objectPath = [this.configuration.prefix, `${id}.json`].join('/');
54
+ async putModel(model) {
55
+ const Key = this.#generatePath([`${model.id}.json`]);
80
56
 
81
- await this.configuration.client.send(new DeleteObjectCommand({
57
+ await this.configuration.client.send(new PutObjectCommand({
58
+ Key,
59
+ Body: JSON.stringify(model),
82
60
  Bucket: this.configuration.bucket,
83
- Key: objectPath,
61
+ ContentType: 'application/json',
84
62
  }));
85
-
86
- return undefined;
87
63
  }
88
64
 
89
65
  /**
90
- * Puts (uploads) a model object to S3.
91
- *
92
- * @param {Model} model - The model object to upload.
93
- * @returns {Promise<void>}
94
- *
95
- * @throws {FailedPutS3StorageEngineError} Thrown if there is an error during the S3 PutObject operation.
66
+ * Delete a model by its id
67
+ * @param {string} id
68
+ * @throws {ModelNotFoundStorageEngineError|Error}
69
+ * @return Promise<void>
96
70
  */
97
- static async putModel(model) {
98
- const Key = [this.configuration.prefix, `${model.id}.json`].join('/');
99
-
71
+ async deleteModel(id) {
72
+ const Key = this.#generatePath([`${id}.json`]);
100
73
  try {
101
- await this.configuration.client.send(new PutObjectCommand({
102
- Key,
103
- Body: JSON.stringify(model.toData()),
74
+ await this.configuration.client.send(new DeleteObjectCommand({
104
75
  Bucket: this.configuration.bucket,
105
- ContentType: 'application/json',
76
+ Key,
106
77
  }));
107
78
  } catch (error) {
108
- throw new FailedPutS3StorageEngineError(`Failed to put s3://${this.configuration.bucket}/${Key}`, error);
79
+ if (error instanceof NoSuchKey) {
80
+ throw new ModelNotFoundStorageEngineError(id);
81
+ }
82
+ throw error;
109
83
  }
110
84
  }
111
85
 
112
86
  /**
113
- * Retrieves the index object from S3 at the specified location.
114
- *
115
- * @param {Model.constructor?} model - The model in the bucket where the index is stored.
116
- * @returns {Promise<Object>} The parsed index object.
87
+ * Get a model's index data
88
+ * @param {Model.constructor} modelConstructor
89
+ * @throws MethodNotImplementedStorageEngineError
90
+ * @return Promise<object>
117
91
  */
118
- static async getIndex(model) {
92
+ async getIndex(modelConstructor) {
93
+ const Key = this.#generatePath([modelConstructor.name, '_index.json']);
119
94
  try {
120
95
  const data = await this.configuration.client.send(new GetObjectCommand({
121
- Key: [this.configuration.prefix, model?.toString(), '_index.json'].filter(e => Boolean(e)).join('/'),
122
96
  Bucket: this.configuration.bucket,
97
+ Key,
123
98
  }));
124
99
 
125
100
  return JSON.parse(await data.Body.transformToString());
@@ -129,128 +104,66 @@ class S3StorageEngine extends StorageEngine {
129
104
  }
130
105
 
131
106
  /**
132
- * Puts (uploads) an index object to S3.
133
- *
134
- * @param {Object} index - An object where keys are locations and values are key value pairs of models and their ids.
135
- * @returns {Promise<void>}
136
- * @throws {FailedPutS3StorageEngineError} Thrown if there is an error during the S3 PutObject operation.
137
- */
138
- static async putIndex(index) {
139
- /**
140
- * Process an index of models
141
- * @param {string} location
142
- * @param {Array<Model>} models
143
- * @throws FailedPutS3StorageEngineError
144
- * @return {Promise<void>}
145
- */
146
- const processIndex = async (location, models) => {
147
- const Key = [this.configuration.prefix, location, '_index.json'].filter(e => Boolean(e)).join('/');
148
- const currentIndex = await this.getIndex(location);
149
-
150
- try {
151
- await this.configuration.client.send(new PutObjectCommand({
152
- Key,
153
- Bucket: this.configuration.bucket,
154
- ContentType: 'application/json',
155
- Body: JSON.stringify({
156
- ...currentIndex,
157
- ...Object.fromEntries(
158
- Object.entries(models).map(([k, v]) => [k, v?.toIndexData?.() || v]),
159
- ),
160
- }),
161
- }));
162
- } catch (error) {
163
- throw new FailedPutS3StorageEngineError(`Failed to put s3://${this.configuration.bucket}/${Key}`, error);
164
- }
165
- };
166
-
167
- for (const [location, models] of Object.entries(index)) {
168
- await processIndex(location, models);
169
- }
170
-
171
- await processIndex(null, Object.values(index).reduce((accumulator, currentValue) => {
172
- Object.keys(currentValue).forEach(key => {
173
- accumulator[key] = currentValue[key];
174
- });
175
- return accumulator;
176
- }, {}));
177
- }
178
-
179
- /**
180
- * Retrieves the compiled search index for a specific model from S3.
181
- *
182
- * @param {Model.constructor} model - The model whose search index to retrieve.
183
- * @returns {Promise<Object>} The compiled search index.
184
- */
185
- static getSearchIndexCompiled(model) {
186
- return this.configuration.client.send(new GetObjectCommand({
187
- Key: [this.configuration.prefix, model.toString(), '_search_index.json'].join('/'),
188
- Bucket: this.configuration.bucket,
189
- })).then(data => data.Body.transformToString())
190
- .then(JSON.parse);
191
- }
192
-
193
- /**
194
- * Retrieves the raw (uncompiled) search index for a specific model from S3.
195
- *
196
- * @param {Model.constructor} model - The model whose raw search index to retrieve.
197
- * @returns {Promise<Object>} The raw search index, or an empty object if not found.
107
+ * Put a model's index data
108
+ * @param {Model.constructor} modelConstructor
109
+ * @param {object} index
110
+ * @throws MethodNotImplementedStorageEngineError
111
+ * @return Promise<void>
198
112
  */
199
- static getSearchIndexRaw(model) {
200
- return this.configuration.client.send(new GetObjectCommand({
201
- Key: [this.configuration.prefix, model.toString(), '_search_index_raw.json'].join('/'),
113
+ async putIndex(modelConstructor, index) {
114
+ const Key = this.#generatePath([modelConstructor.name, '_index.json']);
115
+ await this.configuration.client.send(new PutObjectCommand({
116
+ Key,
202
117
  Bucket: this.configuration.bucket,
203
- })).then(data => data.Body.transformToString())
204
- .then(JSON.parse)
205
- .catch(() => ({}));
118
+ Body: JSON.stringify(index),
119
+ ContentType: 'application/json',
120
+ }));
206
121
  }
207
122
 
208
123
  /**
209
- * Puts (uploads) a compiled search index for a specific model to S3.
210
- *
211
- * @param {Model.constructor} model - The model whose compiled search index to upload.
212
- * @param {Object} compiledIndex - The compiled search index data.
213
- * @returns {Promise<void>}
214
- *
215
- * @throws {FailedPutS3StorageEngineError} Thrown if there is an error during the S3 PutObject operation.
124
+ * Get a model's raw search index data
125
+ * @param {Model.constructor} modelConstructor
126
+ * @return Promise<Record<string, object>>
216
127
  */
217
- static async putSearchIndexCompiled(model, compiledIndex) {
218
- const Key = [this.configuration.prefix, model.toString(), '_search_index.json'].join('/');
128
+ async getSearchIndex(modelConstructor) {
129
+ const Key = this.#generatePath([modelConstructor.name, '_search_index.json']);
219
130
 
220
131
  try {
221
- await this.configuration.client.send(new PutObjectCommand({
222
- Key,
223
- Body: JSON.stringify(compiledIndex),
132
+ const data = await this.configuration.client.send(new GetObjectCommand({
224
133
  Bucket: this.configuration.bucket,
225
- ContentType: 'application/json',
134
+ Key,
226
135
  }));
227
- } catch (error) {
228
- throw new FailedPutS3StorageEngineError(`Failed to put s3://${this.configuration.bucket}/${Key}`, error);
136
+
137
+ return JSON.parse(await data.Body.transformToString());
138
+ } catch (_error) {
139
+ return {};
229
140
  }
230
141
  }
231
142
 
232
143
  /**
233
- * Puts (uploads) a raw search index for a specific model to S3.
234
- *
235
- * @param {Model.constructor} model - The model whose raw search index to upload.
236
- * @param {Object} rawIndex - The raw search index data.
237
- * @returns {Promise<void>}
238
- *
239
- * @throws {FailedPutS3StorageEngineError} Thrown if there is an error during the S3 PutObject operation.
144
+ * Put a model's search index data
145
+ * @param {Model.constructor} modelConstructor
146
+ * @param {Record<string, object>} index
147
+ * @return Promise<void>
240
148
  */
241
- static async putSearchIndexRaw(model, rawIndex) {
242
- const Key = [this.configuration.prefix, model.toString(), '_search_index_raw.json'].join('/');
149
+ async putSearchIndex(modelConstructor, index) {
150
+ const Key = this.#generatePath([modelConstructor.name, '_search_index.json']);
243
151
 
244
- try {
245
- await this.configuration.client.send(new PutObjectCommand({
246
- Key,
247
- Body: JSON.stringify(rawIndex),
248
- Bucket: this.configuration.bucket,
249
- ContentType: 'application/json',
250
- }));
251
- } catch (error) {
252
- throw new FailedPutS3StorageEngineError(`Failed to put s3://${this.configuration.bucket}/${Key}`, error);
253
- }
152
+ await this.configuration.client.send(new PutObjectCommand({
153
+ Key,
154
+ Bucket: this.configuration.bucket,
155
+ Body: JSON.stringify(index),
156
+ ContentType: 'application/json',
157
+ }));
158
+ }
159
+
160
+ /**
161
+ * Generate an S3 prefix path
162
+ * @param {Array<string>} path
163
+ * @return {string}
164
+ */
165
+ #generatePath(path) {
166
+ return [this.configuration.prefix].concat(path).filter(Boolean).join('/');
254
167
  }
255
168
  }
256
169