@acodeninja/persist 3.1.0-next.2 → 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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acodeninja/persist",
3
- "version": "3.1.0-next.2",
3
+ "version": "3.2.0-next.1",
4
4
  "description": "A JSON based data modelling and persistence module with alternate storage mechanisms.",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/Connection.js CHANGED
@@ -483,10 +483,10 @@ export default class Connection {
483
483
  * Retrieve a search index to query at your leisure.
484
484
  *
485
485
  * @param {Model.constructor} modelConstructor
486
- * @return {SearchIndex}
486
+ * @return {Promise<SearchIndex>}
487
487
  */
488
- async getSearchIndex(modelConstructor) {
489
- return await this.#storage.getSearchIndex(modelConstructor)
488
+ getSearchIndex(modelConstructor) {
489
+ return this.#storage.getSearchIndex(modelConstructor)
490
490
  .then(index => new SearchIndex(modelConstructor, index));
491
491
  }
492
492
 
@@ -504,13 +504,13 @@ export default class Connection {
504
504
  }
505
505
 
506
506
  /**
507
- * Retrieve a index to query at your leisure.
507
+ * Retrieve an index to query at your leisure.
508
508
  *
509
509
  * @param {Model.constructor} modelConstructor
510
- * @return {FindIndex}
510
+ * @return {Promise<FindIndex>}
511
511
  */
512
- async getIndex(modelConstructor) {
513
- return await this.#storage.getIndex(modelConstructor)
512
+ getIndex(modelConstructor) {
513
+ return this.#storage.getIndex(modelConstructor)
514
514
  .then(index => new FindIndex(modelConstructor, index));
515
515
  }
516
516
 
@@ -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
 
@@ -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._extractData(this.constructor.indexedPropertiesResolved());
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._extractData(this.constructor.searchProperties());
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
- _extractData(keys) {
110
+ #extractData(keys) {
111
111
  const output = {id: this.id};
112
112
 
113
113
  for (const key of keys) {
114
- if (_.has(this, key)) {
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]) => !['indexedProperties', 'searchProperties'].includes(name) && !type._type && (ModelClass.isModel(type) || (typeof type === 'function' && ModelClass.isModel(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]) => {
@@ -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,
@@ -13,7 +13,7 @@ export class SearchResult {
13
13
  }
14
14
 
15
15
  /**
16
- * A full-text search index wrapper using Lunr.js for a given model.
16
+ * A full-text search index wrapper using Fuse.js for a given model.
17
17
  * Supports indexing and querying model data.
18
18
  *
19
19
  * @class SearchIndex
@@ -39,7 +39,7 @@ export default class SearchIndex {
39
39
  }
40
40
 
41
41
  /**
42
- * Performs a search query on the compiled Lunr index.
42
+ * Performs a search query on the compiled index.
43
43
  *
44
44
  * @param {string} query - The search string.
45
45
  * @return {Array<SearchResult>} An array of search results with model instances and scores.
@@ -51,18 +51,18 @@ export default class SearchIndex {
51
51
  }
52
52
 
53
53
  /**
54
- * Lazily compiles and returns the Lunr index instance.
54
+ * Lazily compiles and returns the index instance.
55
55
  *
56
- * @return {Fuse} The compiled Lunr index.
56
+ * @return {Fuse} The compiled index.
57
57
  */
58
58
  get searchIndex() {
59
59
  return this.#compiledIndex ?? this.#compileIndex();
60
60
  }
61
61
 
62
62
  /**
63
- * Compiles the Lunr index using the model's search properties.
63
+ * Compiles the index using the model's search properties.
64
64
  *
65
- * @return {Fuse} The compiled Lunr index.
65
+ * @return {Fuse} The compiled index.
66
66
  * @private
67
67
  */
68
68
  #compileIndex() {
@@ -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;