@acodeninja/persist 3.1.0 → 3.2.0-next.1
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/docs/storage-engines.md +35 -0
- package/package.json +1 -1
- package/src/Connection.js +13 -1
- package/src/Schema.js +21 -2
- package/src/data/FindIndex.js +1 -1
- package/src/data/Model.js +30 -5
- package/src/data/Property.js +2 -0
- package/src/data/properties/AnyType.js +87 -0
package/docs/storage-engines.md
CHANGED
@@ -23,6 +23,41 @@ export class Tag extends Persist.Model {
|
|
23
23
|
await connection.put(new Tag({tag: 'documentation'}));
|
24
24
|
```
|
25
25
|
|
26
|
+
### Versioning with S3 Buckets
|
27
|
+
|
28
|
+
When you use versioning with an S3 Bucket, you may have to set `pragma` header and `ResponseCacheControl` metadata on all requests. This can be done by adding middleware for both the `build` and `serialize` steps for each request:
|
29
|
+
|
30
|
+
```javascript
|
31
|
+
import Persist from "@acodeninja/persist";
|
32
|
+
import {S3Client} from "@aws-sdk/client-s3";
|
33
|
+
import S3StorageEngine from "@acodeninja/persist/storage/s3";
|
34
|
+
|
35
|
+
const client = new S3Client();
|
36
|
+
|
37
|
+
client.middlewareStack.add(
|
38
|
+
(next, context) => (args) => {
|
39
|
+
args.request.headers['pragma'] = 'no-cache';
|
40
|
+
return next(args);
|
41
|
+
},
|
42
|
+
{step: 'build'},
|
43
|
+
);
|
44
|
+
|
45
|
+
client.middlewareStack.add(
|
46
|
+
(next, context) => (args) => {
|
47
|
+
args.input.ResponseCacheControl = 'no-cache';
|
48
|
+
return next(args);
|
49
|
+
},
|
50
|
+
{step: 'serialize'},
|
51
|
+
);
|
52
|
+
|
53
|
+
const connection = Persist.registerConnection('remote', new S3StorageEngine({
|
54
|
+
bucket: 'test-bucket',
|
55
|
+
client,
|
56
|
+
}));
|
57
|
+
```
|
58
|
+
|
59
|
+
These changes will ensure that all requests are made with a `no-cache` header set for getting and putting objects to the S3 bucket.
|
60
|
+
|
26
61
|
## HTTP Storage StorageEngine
|
27
62
|
|
28
63
|
To store models using an HTTP server, use the `HTTP` storage engine. When using the `HTTP` engine in the browser, refer to [code quirks](./code-quirks.md#using-http-engine-in-browser).
|
package/package.json
CHANGED
package/src/Connection.js
CHANGED
@@ -622,8 +622,14 @@ export default class Connection {
|
|
622
622
|
if (propertyType.prototype instanceof Model) {
|
623
623
|
modelsThatLinkToThisSubject.set([propertyType.name, propertyName, 'one', 'down'], propertyType);
|
624
624
|
}
|
625
|
-
// The model is a one to many link
|
626
625
|
|
626
|
+
propertyType._items?.forEach?.(i => {
|
627
|
+
if (i.prototype instanceof Model) {
|
628
|
+
modelsThatLinkToThisSubject.set([i.name, propertyName, 'one', 'down'], i);
|
629
|
+
}
|
630
|
+
});
|
631
|
+
|
632
|
+
// The model is a one to many link
|
627
633
|
if (propertyType._items?.prototype instanceof Model) {
|
628
634
|
modelsThatLinkToThisSubject.set([propertyType._items.name, propertyName, 'many', 'down'], propertyType);
|
629
635
|
}
|
@@ -636,6 +642,12 @@ export default class Connection {
|
|
636
642
|
modelsThatLinkToThisSubject.set([modelName, propertyName, 'one', 'up'], propertyType);
|
637
643
|
}
|
638
644
|
|
645
|
+
propertyType._items?.forEach?.(i => {
|
646
|
+
if (i === subjectModelConstructor) {
|
647
|
+
modelsThatLinkToThisSubject.set([modelName, propertyName, 'one', 'up'], i);
|
648
|
+
}
|
649
|
+
});
|
650
|
+
|
639
651
|
// The model is a one to many link
|
640
652
|
if (propertyType._items === subjectModelConstructor || propertyType._items?.prototype instanceof subjectModelConstructor) {
|
641
653
|
modelsThatLinkToThisSubject.set([modelName, propertyName, 'many', 'up'], propertyType);
|
package/src/Schema.js
CHANGED
@@ -38,8 +38,26 @@ class Schema {
|
|
38
38
|
function BuildSchema(schemaSegment) {
|
39
39
|
const thisSchema = {};
|
40
40
|
|
41
|
+
if (schemaSegment?._type === 'anyOf') {
|
42
|
+
thisSchema.anyOf = schemaSegment._items.map(item => Model.isModel(item) ?
|
43
|
+
{
|
44
|
+
type: 'object',
|
45
|
+
additionalProperties: false,
|
46
|
+
required: schemaSegment._required ? ['id'] : [],
|
47
|
+
properties: {
|
48
|
+
id: {
|
49
|
+
type: 'string',
|
50
|
+
pattern: `^${item.toString()}/[A-Z0-9]+$`,
|
51
|
+
},
|
52
|
+
},
|
53
|
+
} : BuildSchema(item),
|
54
|
+
);
|
55
|
+
|
56
|
+
return thisSchema;
|
57
|
+
}
|
58
|
+
|
41
59
|
if (Model.isModel(schemaSegment)) {
|
42
|
-
thisSchema.required = [];
|
60
|
+
thisSchema.required = ['id'];
|
43
61
|
thisSchema.type = 'object';
|
44
62
|
thisSchema.additionalProperties = false;
|
45
63
|
thisSchema.properties = {
|
@@ -50,7 +68,7 @@ class Schema {
|
|
50
68
|
};
|
51
69
|
|
52
70
|
for (const [name, type] of Object.entries(schemaSegment)) {
|
53
|
-
if (['indexedProperties', 'searchProperties'].includes(name)) continue;
|
71
|
+
if (['indexedProperties', 'searchProperties', 'id'].includes(name)) continue;
|
54
72
|
|
55
73
|
const property = type instanceof Function && !type.prototype ? type() : type;
|
56
74
|
|
@@ -70,6 +88,7 @@ class Schema {
|
|
70
88
|
},
|
71
89
|
},
|
72
90
|
};
|
91
|
+
|
73
92
|
continue;
|
74
93
|
}
|
75
94
|
|
package/src/data/FindIndex.js
CHANGED
@@ -102,7 +102,7 @@ class FindIndex {
|
|
102
102
|
|
103
103
|
for (const key of Object.keys(inputQuery)) {
|
104
104
|
if (!['$is', '$contains'].includes(key))
|
105
|
-
if (this.#matchesQuery(subject[key], inputQuery[key]))
|
105
|
+
if (this.#matchesQuery(subject?.[key], inputQuery[key]))
|
106
106
|
return true;
|
107
107
|
}
|
108
108
|
|
package/src/data/Model.js
CHANGED
@@ -88,7 +88,7 @@ class Model {
|
|
88
88
|
* @returns {Object} - A representation of the model's indexed data.
|
89
89
|
*/
|
90
90
|
toIndexData() {
|
91
|
-
return this
|
91
|
+
return this.#extractData(this.constructor.indexedPropertiesResolved());
|
92
92
|
}
|
93
93
|
|
94
94
|
/**
|
@@ -97,7 +97,7 @@ class Model {
|
|
97
97
|
* @returns {Object} - A representation of the model's search data.
|
98
98
|
*/
|
99
99
|
toSearchData() {
|
100
|
-
return this
|
100
|
+
return this.#extractData(this.constructor.searchProperties());
|
101
101
|
}
|
102
102
|
|
103
103
|
/**
|
@@ -107,11 +107,11 @@ class Model {
|
|
107
107
|
* @returns {Object} - The extracted data.
|
108
108
|
* @private
|
109
109
|
*/
|
110
|
-
|
110
|
+
#extractData(keys) {
|
111
111
|
const output = {id: this.id};
|
112
112
|
|
113
113
|
for (const key of keys) {
|
114
|
-
if (_.
|
114
|
+
if (_.get(this, key)) {
|
115
115
|
_.set(output, key, _.get(this, key));
|
116
116
|
}
|
117
117
|
|
@@ -196,7 +196,32 @@ class Model {
|
|
196
196
|
const ModelClass = this;
|
197
197
|
return [
|
198
198
|
...Object.entries(ModelClass.properties)
|
199
|
-
.filter(([name, type]) =>
|
199
|
+
.filter(([name, type]) => {
|
200
|
+
if (['indexedProperties', 'searchProperties'].includes(name)) return false;
|
201
|
+
|
202
|
+
const includesSingleModel = (maybeIncludesSingleModel) => {
|
203
|
+
if (maybeIncludesSingleModel._type === 'array') return false;
|
204
|
+
|
205
|
+
if (Model.isModel(maybeIncludesSingleModel)) return true;
|
206
|
+
|
207
|
+
if (maybeIncludesSingleModel._items?.some?.(i => ModelClass.isModel(i))) return true;
|
208
|
+
|
209
|
+
return false;
|
210
|
+
};
|
211
|
+
|
212
|
+
if (includesSingleModel(type)) return true;
|
213
|
+
|
214
|
+
if (
|
215
|
+
typeof type === 'function' &&
|
216
|
+
// This differentiates between a function and a class.
|
217
|
+
// A class prototype is non-writable.
|
218
|
+
!Object.getOwnPropertyDescriptor(type, 'prototype')
|
219
|
+
) {
|
220
|
+
if (includesSingleModel(type())) return true;
|
221
|
+
}
|
222
|
+
|
223
|
+
return false;
|
224
|
+
})
|
200
225
|
.map(([name, _type]) => `${name}.id`),
|
201
226
|
...Object.entries(ModelClass.properties)
|
202
227
|
.filter(([_name, type]) => {
|
package/src/data/Property.js
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import AnyType from './properties/AnyType.js';
|
1
2
|
import ArrayType from './properties/ArrayType.js';
|
2
3
|
import BooleanType from './properties/BooleanType.js';
|
3
4
|
import CustomType from './properties/CustomType.js';
|
@@ -8,6 +9,7 @@ import StringType from './properties/StringType.js';
|
|
8
9
|
import Type from './properties/Type.js';
|
9
10
|
|
10
11
|
const Property = {
|
12
|
+
Any: AnyType,
|
11
13
|
Array: ArrayType,
|
12
14
|
Boolean: BooleanType,
|
13
15
|
Custom: CustomType,
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import Type from './Type.js';
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Represents a union type definition, allowing the specification of a value that can be one of several types.
|
5
|
+
* This class is used to create type definitions for union types that can be validated and used in schemas.
|
6
|
+
*
|
7
|
+
* @class AnyType
|
8
|
+
*/
|
9
|
+
class AnyType {
|
10
|
+
/**
|
11
|
+
* Creates a new type definition for a union of the specified types.
|
12
|
+
*
|
13
|
+
* The `of` method defines a union type where the value can be any one of the specified types. It returns a
|
14
|
+
* class representing this union type, which can further be marked as required using the `required` getter.
|
15
|
+
*
|
16
|
+
* @param {...(Type|Model)} types - The types or models that the union can contain. A value must match at least one of these types.
|
17
|
+
* @returns {Type} A new class representing a union of the specified types.
|
18
|
+
*
|
19
|
+
* @example
|
20
|
+
* const stringOrNumber = AnyType.of(StringType, NumberType);
|
21
|
+
* const requiredStringOrBoolean = AnyType.of(StringType, BooleanType).required;
|
22
|
+
*/
|
23
|
+
static of(...types) {
|
24
|
+
/**
|
25
|
+
* @class AnyOf
|
26
|
+
* @extends Type
|
27
|
+
* Represents a union of specific types.
|
28
|
+
*/
|
29
|
+
class AnyOf extends Type {
|
30
|
+
/** @type {string} The data type, which is 'anyOf' */
|
31
|
+
static _type = 'anyOf';
|
32
|
+
|
33
|
+
/** @type {Type[]} The array of types that are allowed in the union */
|
34
|
+
static _items = types;
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Returns the string representation of the union type.
|
38
|
+
*
|
39
|
+
* @returns {string} The string representation of the union type.
|
40
|
+
*/
|
41
|
+
static toString() {
|
42
|
+
return `AnyOf(${types.map(t => t.toString()).join('|')})`;
|
43
|
+
}
|
44
|
+
|
45
|
+
/**
|
46
|
+
* Marks the union type as required.
|
47
|
+
*
|
48
|
+
* @returns {Type} A new class representing a required union of the specified types.
|
49
|
+
*
|
50
|
+
* @example
|
51
|
+
* const requiredStringOrNumber = AnyType.of(StringType, NumberType).required;
|
52
|
+
*/
|
53
|
+
static get required() {
|
54
|
+
const ThisType = this;
|
55
|
+
|
56
|
+
/**
|
57
|
+
* @class RequiredAnyOf
|
58
|
+
* @extends AnyOf
|
59
|
+
* Represents a required union of specific types.
|
60
|
+
*/
|
61
|
+
class Required extends ThisType {
|
62
|
+
/** @type {boolean} Indicates that the union type is required */
|
63
|
+
static _required = true;
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Returns the string representation of the required union type.
|
67
|
+
*
|
68
|
+
* @returns {string} The string representation of the required union type.
|
69
|
+
*/
|
70
|
+
static toString() {
|
71
|
+
return `RequiredAnyOf(${types.map(t => t.toString()).join('|')})`;
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
Object.defineProperty(Required, 'name', {value: `Required${ThisType.name}`});
|
76
|
+
|
77
|
+
return Required;
|
78
|
+
}
|
79
|
+
}
|
80
|
+
|
81
|
+
Object.defineProperty(AnyOf, 'name', {value: AnyOf.toString()});
|
82
|
+
|
83
|
+
return AnyOf;
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
export default AnyType;
|