@acodeninja/persist 2.2.3 → 2.3.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/README.md CHANGED
@@ -1,243 +1,29 @@
1
1
  # @acodeninja/persist
2
2
 
3
- A JSON based data modelling and persistence library with alternate storage mechanisms.
3
+ A JSON based data modelling and persistence library with alternate storage mechanisms, designed with static site generation in mind.
4
4
 
5
- ## Models
5
+ ![NPM Version](https://img.shields.io/npm/v/%40acodeninja%2Fpersist)
6
+ ![NPM Unpacked Size](https://img.shields.io/npm/unpacked-size/%40acodeninja%2Fpersist)
7
+ ![GitHub top language](https://img.shields.io/github/languages/top/acodeninja/persist)
8
+ ![NPM Downloads](https://img.shields.io/npm/dw/%40acodeninja%2Fpersist)
6
9
 
7
- The `Model` and `Type` classes allow creating representations of data objects
10
+ [![DeepSource](https://app.deepsource.com/gh/acodeninja/persist.svg/?label=active+issues&show_trend=true&token=Vd8_PJuRwwoq4_uBJ0_ymc06)](https://app.deepsource.com/gh/acodeninja/persist/)
11
+ [![DeepSource](https://app.deepsource.com/gh/acodeninja/persist.svg/?label=code+coverage&show_trend=true&token=Vd8_PJuRwwoq4_uBJ0_ymc06)](https://app.deepsource.com/gh/acodeninja/persist/)
8
12
 
9
- ### Defining Models
13
+ ## Features
10
14
 
11
- ##### A model using all available basic types
15
+ - Data modelling with relationships
16
+ - Data validation
17
+ - Data querying
18
+ - Fuzzy search
19
+ - Storage with: S3, HTTP and Filesystem
12
20
 
13
- ```javascript
14
- import Persist from "@acodeninja/persist";
21
+ ## Find out more
15
22
 
16
- export class SimpleModel extends Persist.Type.Model {
17
- static boolean = Persist.Type.Boolean;
18
- static string = Persist.Type.String;
19
- static number = Persist.Type.Number;
20
- static date = Persist.Type.Date;
21
- }
22
- ```
23
-
24
- ##### A simple model using required types
25
-
26
- ```javascript
27
- import Persist from "@acodeninja/persist";
28
-
29
- export class SimpleModel extends Persist.Type.Model {
30
- static requiredBoolean = Persist.Type.Boolean.required;
31
- static requiredString = Persist.Type.String.required;
32
- static requiredNumber = Persist.Type.Number.required;
33
- static requiredDate = Persist.Type.Date.required;
34
- }
35
- ```
36
-
37
- ##### A simple model using arrays of basic types
38
-
39
- ```javascript
40
- import Persist from "@acodeninja/persist";
41
-
42
- export class SimpleModel extends Persist.Type.Model {
43
- static arrayOfBooleans = Persist.Type.Array.of(Type.Boolean);
44
- static arrayOfStrings = Persist.Type.Array.of(Type.String);
45
- static arrayOfNumbers = Persist.Type.Array.of(Type.Number);
46
- static arrayOfDates = Persist.Type.Array.of(Type.Date);
47
- static requiredArrayOfBooleans = Persist.Type.Array.of(Type.Boolean).required;
48
- static requiredArrayOfStrings = Persist.Type.Array.of(Type.String).required;
49
- static requiredArrayOfNumbers = Persist.Type.Array.of(Type.Number).required;
50
- static requiredArrayOfDates = Persist.Type.Array.of(Type.Date).required;
51
- }
52
- ```
53
-
54
- <details>
55
- <summary>Complex relationships are also supported</summary>
56
-
57
- #### One-to-One Relationships
58
-
59
- ##### A one-to-one relationship
60
-
61
- ```javascript
62
- import Persist from "@acodeninja/persist";
63
-
64
- export class ModelB extends Persist.Type.Model {
65
- }
66
-
67
- export class ModelA extends Persist.Type.Model {
68
- static linked = ModelB;
69
- }
70
- ```
71
-
72
- ##### A circular one-to-one relationship
73
-
74
- ```javascript
75
- import Persist from "@acodeninja/persist";
76
-
77
- export class ModelA extends Persist.Type.Model {
78
- static linked = () => ModelB;
79
- }
80
-
81
- export class ModelB extends Persist.Type.Model {
82
- static linked = ModelA;
83
- }
84
- ```
85
-
86
- #### One-to-Many Relationships
87
-
88
- ##### A one-to-many relationship
89
-
90
- ```javascript
91
- import Persist from "@acodeninja/persist";
92
-
93
- export class ModelB extends Persist.Type.Model {
94
- }
95
-
96
- export class ModelA extends Persist.Type.Model {
97
- static linked = Persist.Type.Array.of(ModelB);
98
- }
99
- ```
100
-
101
- ##### A circular one-to-many relationship
102
-
103
- ```javascript
104
- import Persist from "@acodeninja/persist";
105
-
106
- export class ModelA extends Persist.Type.Model {
107
- static linked = () => Type.Array.of(ModelB);
108
- }
109
-
110
- export class ModelB extends Persist.Type.Model {
111
- static linked = ModelA;
112
- }
113
- ```
114
-
115
- #### Many-to-Many Relationships
116
-
117
- ##### A many-to-many relationship
118
-
119
- ```javascript
120
- import Persist from "@acodeninja/persist";
121
-
122
- export class ModelA extends Persist.Type.Model {
123
- static linked = Persist.Type.Array.of(ModelB);
124
- }
125
-
126
- export class ModelB extends Persist.Type.Model {
127
- static linked = Persist.Type.Array.of(ModelA);
128
- }
129
- ```
130
- </details>
131
-
132
- ## Find and Search
133
-
134
- Models may expose a `searchProperties()` and `indexProperties()` static method to indicate which
135
- fields should be indexed for storage engine `find()` and `search()` methods.
136
-
137
- Use `find()` for a low usage exact string match on any indexed attribute of a model.
138
-
139
- Use `search()` for a medium usage fuzzy string match on any search indexed attribute of a model.
140
-
141
- ```javascript
142
- import Persist from "@acodeninja/persist";
143
- import FileEngine from "@acodeninja/persist/engine/file";
144
-
145
- export class Tag extends Persist.Type.Model {
146
- static tag = Persist.Type.String.required;
147
- static description = Persist.Type.String;
148
- static searchProperties = () => ['tag', 'description'];
149
- static indexProperties = () => ['tag'];
150
- }
151
-
152
- const tag = new Tag({tag: 'documentation', description: 'How to use the persist library'});
153
-
154
- await FileEngine.find(Tag, {tag: 'documentation'});
155
- // [Tag {tag: 'documentation', description: 'How to use the persist library'}]
156
-
157
- await FileEngine.search(Tag, 'how to');
158
- // [Tag {tag: 'documentation', description: 'How to use the persist library'}]
159
- ```
160
-
161
- ## Storage
162
-
163
- ### Filesystem Storage Engine
164
-
165
- To store models using the local file system, use the `File` storage engine.
166
-
167
- ```javascript
168
- import Persist from "@acodeninja/persist";
169
- import FileEngine from "@acodeninja/persist/engine/file";
170
-
171
- Persist.addEngine('local', FileEngine, {
172
- path: '/app/storage',
173
- });
174
-
175
- export class Tag extends Persist.Type.Model {
176
- static tag = Persist.Type.String.required;
177
- }
178
-
179
- await Persist.getEngine('local', FileEngine).put(new Tag({tag: 'documentation'}));
180
- ```
181
-
182
- ### HTTP Storage Engine
183
-
184
- To store models using an S3 Bucket, use the `S3` storage engine.
185
-
186
- ```javascript
187
- import Persist from "@acodeninja/persist";
188
- import HTTPEngine from "@acodeninja/persist/engine/http";
189
-
190
- Persist.addEngine('remote', HTTPEngine, {
191
- host: 'https://api.example.com',
192
- });
193
-
194
- export class Tag extends Persist.Type.Model {
195
- static tag = Persist.Type.String.required;
196
- }
197
-
198
- await Persist.getEngine('remote', HTTPEngine).put(new Tag({tag: 'documentation'}));
199
- ```
200
-
201
- ### S3 Storage Engine
202
-
203
- To store models using an S3 Bucket, use the `S3` storage engine.
204
-
205
- ```javascript
206
- import Persist from "@acodeninja/persist";
207
- import S3Engine from "@acodeninja/persist/engine/s3";
208
-
209
- Persist.addEngine('remote', S3Engine, {
210
- bucket: 'test-bucket',
211
- client: new S3Client(),
212
- });
213
-
214
- export class Tag extends Persist.Type.Model {
215
- static tag = Persist.Type.String.required;
216
- }
217
-
218
- await Persist.getEngine('remote', S3Engine).put(new Tag({tag: 'documentation'}));
219
- ```
220
-
221
- ## Transactions
222
-
223
- Create transactions to automatically roll back on failure to update.
224
-
225
- ```javascript
226
- import Persist from "@acodeninja/persist";
227
- import S3Engine from "@acodeninja/persist/engine/s3";
228
-
229
- Persist.addEngine('remote', S3Engine, {
230
- bucket: 'test-bucket',
231
- client: new S3Client(),
232
- transactions: true,
233
- });
234
-
235
- export class Tag extends Persist.Type.Model {
236
- static tag = Persist.Type.String.required;
237
- }
238
-
239
- const transaction = Persist.getEngine('remote', S3Engine).start();
240
-
241
- await transaction.put(new Tag({tag: 'documentation'}));
242
- await transaction.commit();
243
- ```
23
+ - [Model Property Types](./docs/model-property-types.md)
24
+ - [Models as Properties](./docs/models-as-properties.md)
25
+ - [Structured Queries](./docs/structured-queries.md)
26
+ - [Search Queries](./docs/search-queries.md)
27
+ - [Storage Engines](./docs/storage-engines.md)
28
+ - [Transactions](./docs/transactions.md)
29
+ - [Quirks](./docs/code-quirks.md)
@@ -0,0 +1,71 @@
1
+ # Code Quirks
2
+
3
+ When using Persist in a minified or bundled codebase, it's important to be aware of two key quirks: handling class names during minification and managing reference errors when working with model relationships.
4
+
5
+ ## Class Names and Minification
6
+
7
+ When you bundle or minify JavaScript code for production, class names are often altered, which can cause issues. Specifically, models may lose their original class names, which we rely on for storing data in the correct namespace.
8
+
9
+ To avoid this problem, you have two options:
10
+
11
+ 1. Disable class name mangling in your minifier.
12
+ 2. Use `this.setMinifiedName(name)` to manually specify the model's name.
13
+
14
+ ```javascript
15
+ import Persist from "@acodeninja/persist";
16
+
17
+ export class Person extends Persist.Type.Model {
18
+ static {
19
+ this.setMinifiedName('Person');
20
+ this.name = Persist.Type.String.required;
21
+ }
22
+ }
23
+ ```
24
+
25
+ If you don't set the minified name, the wrong namespace may be used when saving models, leading to unexpected behavior.
26
+
27
+ ## Reference Errors
28
+
29
+ When defining relationships between models, especially circular relationships (e.g., `Person` references `Address`, and `Address` references `Person`), the order of declarations matters. If the models are referenced before they are initialized, you'll encounter `ReferenceError` messages, like:
30
+
31
+ ```console
32
+ ReferenceError: Cannot access 'Address' before initialization
33
+ ```
34
+
35
+ To avoid these errors, always define model relationships using arrow functions. For example:
36
+
37
+ ```javascript
38
+ import Persist from "@acodeninja/persist";
39
+
40
+ export class Person extends Persist.Type.Model {
41
+ static {
42
+ this.address = () => Address;
43
+ }
44
+ }
45
+
46
+ export class Address extends Persist.Type.Model {
47
+ static {
48
+ this.person = () => Person;
49
+ this.address = Persist.Type.String.required;
50
+ this.postcode = Persist.Type.String.required;
51
+ }
52
+ }
53
+ ```
54
+
55
+ By doing this, you ensure that model references are evaluated lazily, after all models have been initialized, preventing `ReferenceError` issues.
56
+
57
+ ## Using `HTTP` Engine in Browser
58
+
59
+ When implementing thee `HTTP` engine for code that runs in the web browser, you must pass `fetch` into the engine configuration and bind it to the `window` object.
60
+
61
+ ```javascript
62
+ import Persist from "@acodeninja/persist";
63
+ import HTTPEngine from "@acodeninja/persist/engine/http";
64
+
65
+ Persist.addEngine('remote', HTTPEngine, {
66
+ host: 'https://api.example.com',
67
+ fetch: fetch.bind(window),
68
+ });
69
+ ```
70
+
71
+ This will ensure that `fetch` can access the window context which is required for it to function.
@@ -0,0 +1,173 @@
1
+ # Model Property Types
2
+
3
+ Persist uses a type definition for the properties of each model, this allows for validation and type coercion when saving and retrieving data.
4
+
5
+ Model properties can be assigned a `Type`, or another `Model`. For more information on see [Models as Properties](./models-as-properties.md).
6
+
7
+ ## Defining Model Properties
8
+
9
+ Properties can be defined on a model by setting static properties to the value of a type on the class that describes the model.
10
+
11
+ ```javascript
12
+ import Persist from '@acodeninja/persist';
13
+
14
+ class Person extends Persist.Type.Model {
15
+ static {
16
+ this.firstName = Persist.Type.String;
17
+ this.lastName = Persist.Type.String;
18
+ }
19
+ }
20
+ ```
21
+
22
+ ## Simple Types
23
+
24
+ ### `Persist.Type.String`
25
+
26
+ Use the `String` type for model properties that should store a [string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String). The `String` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it.
27
+
28
+ ```javascript
29
+ import Persist from '@acodeninja/persist';
30
+
31
+ class Person extends Persist.Type.Model {
32
+ static {
33
+ this.firstName = Persist.Type.String;
34
+ this.lastName = Persist.Type.String.required;
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### `Persist.Type.Boolean`
40
+
41
+ Use the `Boolean` type for model properties that should store a [boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean). The `Boolean` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it.
42
+
43
+ ```javascript
44
+ import Persist from '@acodeninja/persist';
45
+
46
+ class Person extends Persist.Type.Model {
47
+ static {
48
+ this.markettingEmailsActive = Persist.Type.Boolean;
49
+ this.accountActive = Persist.Type.Boolean.required;
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### `Persist.Type.Number`
55
+
56
+ Use the `Number` type for model properties that should store a [number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number). The `Number` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it.
57
+
58
+ ```javascript
59
+ import Persist from '@acodeninja/persist';
60
+
61
+ class Person extends Persist.Type.Model {
62
+ static {
63
+ this.loginToken = Persist.Type.Number;
64
+ this.accountId = Persist.Type.Number.required;
65
+ }
66
+ }
67
+ ```
68
+
69
+ ### `Persist.Type.Date`
70
+
71
+ Use the `Date` type for model properties that should store a [date](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). The `Date` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it.
72
+
73
+ ```javascript
74
+ import Persist from '@acodeninja/persist';
75
+
76
+ class Person extends Persist.Type.Model {
77
+ static {
78
+ this.lastLogin = Persist.Type.Date;
79
+ this.createdAt = Persist.Type.Date.required;
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## Complex Types
85
+
86
+ ### `Persist.Type.Array.of(type)`
87
+
88
+ Use the `Array` type for model properties that should store an array of another type or model. The `Array` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it.
89
+
90
+ ```javascript
91
+ import Persist from '@acodeninja/persist';
92
+
93
+ class Person extends Persist.Type.Model {
94
+ static {
95
+ this.failedLoginAttempts = Persist.Type.Array.of(Persist.Type.Date);
96
+ this.fullName = Persist.Type.Array.of(Persist.Type.String).required;
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### `Persist.Type.Custom.of(schema)`
102
+
103
+ Use the `Custom` type for model properties that should store a custom [json-schema draft-07](https://json-schema.org/draft-07/json-schema-hypermedia) object. You can also use any formats defined by the [`avj-formats`](https://ajv.js.org/packages/ajv-formats.html) library. The `Custom` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it.
104
+
105
+ ```javascript
106
+ import Persist from '@acodeninja/persist';
107
+
108
+ class Person extends Persist.Type.Model {
109
+ static {
110
+ this.address = Persist.Type.Custom.of({
111
+ type: 'object',
112
+ additionalProperties: false,
113
+ required: ['line1', 'city', 'postcode'],
114
+ properties: {
115
+ line1: {type: 'string'},
116
+ line2: {type: 'string'},
117
+ city: {type: 'string'},
118
+ postcode: {
119
+ type: 'string',
120
+ pattern: "^[A-Z]+[0-9]+\s[A-Z]+[0-9]+$",
121
+ },
122
+ },
123
+ }).required;
124
+ }
125
+ }
126
+ ```
127
+
128
+ ## Resolved Types
129
+
130
+ Resolved types are different from other types in that they do not directly store data themselves, rather they perform an action on another property of the model.
131
+
132
+ ### `Persist.Type.Resolved.Slug.of(property)`
133
+
134
+ Use the `Slug` type for model properties that should have a slug version of another properties value. The `Custom` type also supports the `.required` modifier to ensure that when the model is persisted a value must exist for it.
135
+
136
+ ```javascript
137
+ import Persist from '@acodeninja/persist';
138
+
139
+ class Page extends Persist.Type.Model {
140
+ static {
141
+ this.title = Persist.Type.String;
142
+ this.slug = Persist.Type.Resolved.Slug.of('title');
143
+ }
144
+ }
145
+
146
+ const page = new Page({title: 'A really important article!'});
147
+ const {slug} = page.toData();
148
+
149
+ console.log(slug); // a-really-important-article
150
+ ```
151
+
152
+ ## Modifiers
153
+
154
+ Models and most types support a modifier, this will alter the validation and persistence process based on the type of modifier used.
155
+
156
+ ### `.required`
157
+
158
+ Most types support the `.required` modifier, which will alter validation to enforce the presence of the property when saving data.
159
+
160
+ ```javascript
161
+ class RequiredStringModel extends Persist.Type.Model {
162
+ static {
163
+ this.requiredString = Type.String.required;
164
+ this.requiredNumber = Type.Number.required;
165
+ this.requiredBoolean = Type.Boolean.required;
166
+ this.requiredDate = Type.Date.required;
167
+ this.requiredArrayOfString = Type.Array.of(Type.String).required;
168
+ this.requiredArrayOfNumber = Type.Array.of(Type.Number).required;
169
+ this.requiredArrayOfBoolean = Type.Array.of(Type.Boolean).required;
170
+ this.requiredArrayOfDate = Type.Array.of(Type.Date).required;
171
+ }
172
+ }
173
+ ```
@@ -0,0 +1,159 @@
1
+ # Models as Properties
2
+
3
+ In addition to assigning basic types to model properties, you can assign entire models as properties. This allows for the creation of complex relationships between models. For information on using basic types for properties, refer to [model property types](./model-property-types.md).
4
+
5
+ We’ll explore different types of relationships between models using examples of `Person` and `Address` models, evolving the definition step by step.
6
+
7
+ ```javascript
8
+ import Persist from "@acodeninja/persist";
9
+
10
+ export class Person extends Persist.Type.Model {
11
+ static {
12
+ this.name = Persist.Type.String.required;
13
+ }
14
+ }
15
+
16
+ export class Address extends Persist.Type.Model {
17
+ static {
18
+ this.address = Persist.Type.String.required;
19
+ this.postcode = Persist.Type.String.required;
20
+ }
21
+ }
22
+ ```
23
+
24
+ ## One-to-One Relationships
25
+
26
+ To define a **one-to-one** relationship between two models, set a static property in one model as a function that returns the other model. This ensures that the models can be defined in any order, avoiding issues with initialization.
27
+
28
+ ```javascript
29
+ import Persist from "@acodeninja/persist";
30
+
31
+ export class Person extends Persist.Type.Model {
32
+ static {
33
+ this.name = Persist.Type.String.required;
34
+ this.address = () => Address;
35
+ }
36
+ }
37
+
38
+ export class Address extends Persist.Type.Model {
39
+ static {
40
+ this.address = Persist.Type.String.required;
41
+ this.postcode = Persist.Type.String.required;
42
+ }
43
+ }
44
+ ```
45
+
46
+ > [!IMPORTANT]
47
+ > **Why Use an Arrow Function?**
48
+ >
49
+ > The arrow function allows the model to reference another model that may not have been defined yet. Without it, you might encounter an error like `ReferenceError: Cannot access 'Address' before initialization`.
50
+
51
+ ### Circular One-to-One Relationships
52
+
53
+ You can extend the previous example by allowing both models to reference each other. This is useful for circular relationships, where querying one model (e.g., `Address`) should also allow access to the related model (e.g., `Person`).
54
+
55
+ ```javascript
56
+ import Persist from "@acodeninja/persist";
57
+
58
+ export class Person extends Persist.Type.Model {
59
+ static {
60
+ this.name = Persist.Type.String.required;
61
+ this.address = () => Address;
62
+ }
63
+ }
64
+
65
+ export class Address extends Persist.Type.Model {
66
+ static {
67
+ this.person = () => Person;
68
+ this.address = Persist.Type.String.required;
69
+ this.postcode = Persist.Type.String.required;
70
+ }
71
+ }
72
+ ```
73
+
74
+ ## One-to-Many Relationships
75
+
76
+ To model a **one-to-many** relationship, use `Persist.Type.Array` to store an array of related models. For instance, if a `Person` can have multiple addresses, this is how it would be defined:
77
+
78
+ ```javascript
79
+ import Persist from "@acodeninja/persist";
80
+
81
+ export class Person extends Persist.Type.Model {
82
+ static {
83
+ this.name = Persist.Type.String.required;
84
+ this.addresses = () => Persist.Type.Array.of(Address);
85
+ }
86
+ }
87
+
88
+ export class Address extends Persist.Type.Model {
89
+ static {
90
+ this.person = () => Person;
91
+ this.address = Persist.Type.String.required;
92
+ this.postcode = Persist.Type.String.required;
93
+ }
94
+ }
95
+ ```
96
+
97
+ This structure allows for querying both the Person and their multiple Address records, while maintaining the ability to retrieve the related person from any address.
98
+
99
+ ## Many-to-Many Relationships
100
+
101
+ In some cases, you may want to model a many-to-many relationship. For example, if multiple people can live at the same address, this type of relationship is ideal.
102
+
103
+ ```javascript
104
+ import Persist from "@acodeninja/persist";
105
+
106
+ export class Person extends Persist.Type.Model {
107
+ static {
108
+ this.name = Persist.Type.String.required;
109
+ this.addresses = () => Persist.Type.Array.of(Address);
110
+ }
111
+ }
112
+
113
+ export class Address extends Persist.Type.Model {
114
+ static {
115
+ this.people = () => Persist.Type.Array.of(Person);
116
+ this.address = Persist.Type.String.required;
117
+ this.postcode = Persist.Type.String.required;
118
+ }
119
+ }
120
+ ```
121
+
122
+ This allows both `Person` and `Address` models to reference each other as arrays, establishing a many-to-many relationship.
123
+
124
+ ## Combining Relationships
125
+
126
+ In more complex scenarios, you may want to capture additional information about the relationship itself. For example, when tracking when a person moved to a particular address, you can create an intermediary model (e.g., `Abode`) to store this information.
127
+
128
+ ```javascript
129
+ import Persist from "@acodeninja/persist";
130
+
131
+ export class Person extends Persist.Type.Model {
132
+ static {
133
+ this.name = Persist.Type.String.required;
134
+ this.addresses = () => Persist.Type.Array.of(Abode);
135
+ }
136
+ }
137
+
138
+ export class Abode extends Persist.Type.Model {
139
+ static {
140
+ this.moveInDate = Persist.Type.Date.required;
141
+ this.address = () => Address;
142
+ this.person = () => Person;
143
+ }
144
+ }
145
+
146
+ export class Address extends Persist.Type.Model {
147
+ static {
148
+ this.people = () => Persist.Type.Array.of(Person);
149
+ this.address = Persist.Type.String.required;
150
+ this.postcode = Persist.Type.String.required;
151
+ }
152
+ }
153
+ ```
154
+
155
+ In this setup:
156
+
157
+ - A `Person` can have multiple `Abode` entries (i.e., where they lived and when they moved in).
158
+ - Each `Abode` links a `Person` to an `Address`, while also recording the move-in date.
159
+ - An `Address` can still reference multiple people, making this a flexible and more complex relationship model.
@@ -0,0 +1,47 @@
1
+ # Search Queries
2
+
3
+ In addition to [structured queries](./structured-queries.md), persist also supports fuzzy search across fields indexed for search.
4
+
5
+ ## Indexing Data for Search
6
+
7
+ To set index properties on a model for search, define the static function `searchProperties` as an arrow function that returns an array of fields that should be indexed for search.
8
+
9
+ Let's consider the following models:
10
+
11
+ ```javascript
12
+ import Persist from "@acodeninja/persist";
13
+
14
+ export class Person extends Persist.Type.Model {
15
+ static {
16
+ this.name = Persist.Type.String.required;
17
+ this.address = () => Address;
18
+ this.searchProperties = () => ['name', 'address.address'];
19
+ }
20
+ }
21
+
22
+ export class Address extends Persist.Type.Model {
23
+ static {
24
+ this.address = Persist.Type.String.required;
25
+ this.postcode = Persist.Type.String.required;
26
+ this.searchProperties = () => ['address', 'postcode'];
27
+ }
28
+ }
29
+ ```
30
+
31
+ Every time a `Person` model is put to a storage engine, the person's name and address are saved to the search index and can be queried.
32
+
33
+ ## Searching
34
+
35
+ To search for any `Person` who lives on station road, the following search query can be run:
36
+
37
+ ```javascript
38
+ import Persist from "@acodeninja/persist";
39
+ import Person from "./Person";
40
+ import FileEngine from "@acodeninja/persist/engine/file"
41
+
42
+ FileEngine
43
+ .configure(configuration)
44
+ .search(Person, 'station road');
45
+ ```
46
+
47
+ This will find all matches for people who live at any address that includes `station road`.
@@ -0,0 +1,61 @@
1
+ # Storage Engines
2
+
3
+ Persist makes several storage engines available for use with the library
4
+
5
+ ## Filesystem Storage Engine
6
+
7
+ To store models using the local file system, use the `File` storage engine.
8
+
9
+ ```javascript
10
+ import Persist from "@acodeninja/persist";
11
+ import FileEngine from "@acodeninja/persist/engine/file";
12
+
13
+ Persist.addEngine('local', FileEngine, {
14
+ path: '/app/storage',
15
+ });
16
+
17
+ export class Tag extends Persist.Type.Model {
18
+ static tag = Persist.Type.String.required;
19
+ }
20
+
21
+ await Persist.getEngine('local', FileEngine).put(new Tag({tag: 'documentation'}));
22
+ ```
23
+
24
+ ## HTTP Storage Engine
25
+
26
+ 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).
27
+
28
+ ```javascript
29
+ import Persist from "@acodeninja/persist";
30
+ import HTTPEngine from "@acodeninja/persist/engine/http";
31
+
32
+ Persist.addEngine('remote', HTTPEngine, {
33
+ host: 'https://api.example.com',
34
+ });
35
+
36
+ export class Tag extends Persist.Type.Model {
37
+ static tag = Persist.Type.String.required;
38
+ }
39
+
40
+ await Persist.getEngine('remote', HTTPEngine).put(new Tag({tag: 'documentation'}));
41
+ ```
42
+
43
+ ## S3 Storage Engine
44
+
45
+ To store models using an S3 Bucket, use the `S3` storage engine. To use the `S3` engine you must also add the `@aws-sdk/client-s3` dependency to your `package.json` file.
46
+
47
+ ```javascript
48
+ import Persist from "@acodeninja/persist";
49
+ import S3Engine from "@acodeninja/persist/engine/s3";
50
+
51
+ Persist.addEngine('remote', S3Engine, {
52
+ bucket: 'test-bucket',
53
+ client: new S3Client(),
54
+ });
55
+
56
+ export class Tag extends Persist.Type.Model {
57
+ static tag = Persist.Type.String.required;
58
+ }
59
+
60
+ await Persist.getEngine('remote', S3Engine).put(new Tag({tag: 'documentation'}));
61
+ ```
@@ -0,0 +1,107 @@
1
+ # Structured Queries
2
+
3
+ Use structured queries when you need to filter a collection of models using a series of exact and partial matching conditions.
4
+
5
+ ## Indexing Data
6
+
7
+ To set index properties on a model, define the static function `indexProperties` as an arrow function that returns an array of fields that should be indexed for querying.
8
+
9
+ Let's consider the following models:
10
+
11
+ ```javascript
12
+ import Persist from "@acodeninja/persist";
13
+
14
+ export class Person extends Persist.Type.Model {
15
+ static {
16
+ this.name = Persist.Type.String.required;
17
+ this.address = () => Address;
18
+ this.indexProperties = () => ['name', 'address.postcode'];
19
+ }
20
+ }
21
+
22
+ export class Address extends Persist.Type.Model {
23
+ static {
24
+ this.address = Persist.Type.String.required;
25
+ this.postcode = Persist.Type.String.required;
26
+ this.indexProperties = () => ['postcode'];
27
+ }
28
+ }
29
+ ```
30
+
31
+ Every time a `Person` model is put to a storage engine, the person's name and address postcode are saved to the index and can be queried.
32
+
33
+ > [!NOTE]
34
+ > All fields included in the model index will be stored in the same file so be careful not to index fields that contain a lot of data.
35
+
36
+ ## Querying Exact Matches
37
+
38
+ To query for a `Person` called `Joe Bloggs` an exact query can be written:
39
+
40
+ ```javascript
41
+ import Persist from "@acodeninja/persist";
42
+ import Person from "./Person";
43
+ import FileEngine from "@acodeninja/persist/engine/file"
44
+
45
+ FileEngine
46
+ .configure(configuration)
47
+ .find(Person, {
48
+ name: {$is: 'Joe Bloggs'},
49
+ });
50
+ ```
51
+
52
+ ## Querying Partial Matches
53
+
54
+ To query for a `Person` with name `Joe` a contains query can be written:
55
+
56
+ ```javascript
57
+ import Persist from "@acodeninja/persist";
58
+ import Person from "./Person";
59
+ import FileEngine from "@acodeninja/persist/engine/file"
60
+
61
+ FileEngine
62
+ .configure(configuration)
63
+ .find(Person, {
64
+ name: {$contains: 'Joe'},
65
+ });
66
+ ```
67
+
68
+ ## Querying Combination Matches
69
+
70
+ To query for a `Person` who lives at `SW1 1AA` a combination of contains and exact queries can be written:
71
+
72
+ ```javascript
73
+ import Persist from "@acodeninja/persist";
74
+ import Person from "./Person";
75
+ import FileEngine from "@acodeninja/persist/engine/file"
76
+
77
+ FileEngine
78
+ .configure(configuration)
79
+ .find(Person, {
80
+ address: {
81
+ $contains: {
82
+ postcode: {$is: 'SW1 1AA'},
83
+ },
84
+ },
85
+ });
86
+ ```
87
+
88
+ ## Multiple Queries
89
+
90
+ To query for anyone called `Joe Bloggs` who lives in the `SW1` postcode area, we can combine queries:
91
+
92
+ ```javascript
93
+ import Persist from "@acodeninja/persist";
94
+ import Person from "./Person";
95
+ import FileEngine from "@acodeninja/persist/engine/file"
96
+
97
+ FileEngine
98
+ .configure(configuration)
99
+ .find(Person, {
100
+ name: {$is: 'Joe Bloggs'},
101
+ address: {
102
+ $contains: {
103
+ postcode: {$contains: 'SW1'},
104
+ },
105
+ },
106
+ });
107
+ ```
@@ -0,0 +1,23 @@
1
+ ## Transactions
2
+
3
+ Create transactions to automatically roll back on failure.
4
+
5
+ ```javascript
6
+ import Persist from "@acodeninja/persist";
7
+ import S3Engine from "@acodeninja/persist/engine/s3";
8
+
9
+ Persist.addEngine('remote', S3Engine, {
10
+ bucket: 'test-bucket',
11
+ client: new S3Client(),
12
+ transactions: true,
13
+ });
14
+
15
+ export class Tag extends Persist.Type.Model {
16
+ static tag = Persist.Type.String.required;
17
+ }
18
+
19
+ const transaction = Persist.getEngine('remote', S3Engine).start();
20
+
21
+ await transaction.put(new Tag({tag: 'documentation'}));
22
+ await transaction.commit();
23
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acodeninja/persist",
3
- "version": "2.2.3",
3
+ "version": "2.3.1",
4
4
  "description": "A JSON based data modelling and persistence module with alternate storage mechanisms.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -10,6 +10,7 @@ import lunr from 'lunr';
10
10
  */
11
11
  class Engine {
12
12
  static configuration = undefined;
13
+ static _searchCache = undefined;
13
14
 
14
15
  /**
15
16
  * Retrieves a model by its ID. This method must be implemented by subclasses.
@@ -113,16 +114,24 @@ class Engine {
113
114
  * Performs a search query on a model's index and returns the matching models.
114
115
  *
115
116
  * @param {Model.constructor} model - The model class.
116
- * @param {object} query - The search query string.
117
- * @returns {Array<Model>} An array of models matching the search query.
117
+ * @param {string} query - The search query string.
118
+ * @returns {Promise<Array<Model>>} An array of models matching the search query.
118
119
  * @throws {EngineError} Throws if the search index is not available for the model.
119
120
  */
120
121
  static async search(model, query) {
121
122
  this.checkConfiguration();
122
123
 
123
- const index = await this.getSearchIndexCompiled(model).catch(() => {
124
- throw new EngineError(`The model ${model.toString()} does not have a search index available.`);
125
- });
124
+ const index =
125
+ (this._searchCache && this.configuration?.cache?.search && Date.now() - this._searchCache[0] < this.configuration.cache.search) ?
126
+ this._searchCache[1] :
127
+ await this.getSearchIndexCompiled(model)
128
+ .then(i => {
129
+ this._searchCache = [Date.now(), i];
130
+ return i;
131
+ })
132
+ .catch(error => {
133
+ throw new EngineError(`The model ${model.toString()} does not have a search index available.`, error);
134
+ });
126
135
 
127
136
  const searchIndex = lunr.Index.load(index);
128
137
 
@@ -317,7 +326,7 @@ class Engine {
317
326
  static configuration = configuration;
318
327
  }
319
328
 
320
- Object.defineProperty(ConfiguredStore, 'name', { value: `${this.toString()}` });
329
+ Object.defineProperty(ConfiguredStore, 'name', {value: `${this.toString()}`});
321
330
 
322
331
  return ConfiguredStore;
323
332
  }
@@ -329,7 +338,7 @@ class Engine {
329
338
  * @abstract
330
339
  */
331
340
  static checkConfiguration() {
332
- // Implemented in extending Engine class
341
+ // Implemented in extending Engine class
333
342
  }
334
343
 
335
344
  /**
package/src/type/Model.js CHANGED
@@ -73,11 +73,11 @@ class Model {
73
73
  (key, value) => {
74
74
  if (!simple) {
75
75
  if (this.constructor[key]) {
76
- if (this.constructor[key].name.endsWith('DateType')) {
76
+ if (this.constructor[key].name.endsWith('Date')) {
77
77
  return new Date(value);
78
78
  }
79
79
 
80
- if (this.constructor[key].name.endsWith('ArrayOf(Date)Type')) {
80
+ if (this.constructor[key].name.endsWith('ArrayOf(Date)')) {
81
81
  return value.map(d => new Date(d));
82
82
  }
83
83
  }
@@ -211,12 +211,12 @@ class Model {
211
211
  for (const [name, value] of Object.entries(data)) {
212
212
  if (this[name]?._resolved) continue;
213
213
 
214
- if (this[name].name.endsWith('DateType')) {
214
+ if (this[name].name.endsWith('Date')) {
215
215
  model[name] = new Date(value);
216
216
  continue;
217
217
  }
218
218
 
219
- if (this[name].name.endsWith('ArrayOf(Date)Type')) {
219
+ if (this[name].name.endsWith('ArrayOf(Date)')) {
220
220
  model[name] = data[name].map(d => new Date(d));
221
221
  continue;
222
222
  }
@@ -259,6 +259,26 @@ class Model {
259
259
  return false;
260
260
  }
261
261
  }
262
+
263
+ /**
264
+ * Set the name of the Model class
265
+ *
266
+ * Use this when your model might be minified to retain consistent class names.
267
+ *
268
+ * @param {string} name
269
+ * @static
270
+ *
271
+ * @example
272
+ * export default class TestModel {
273
+ * static {
274
+ * this.string = Persist.Type.String;
275
+ * Object.defineProperty(this, 'name', {value: 'TestModel'});
276
+ * }
277
+ * }
278
+ */
279
+ static setMinifiedName(name) {
280
+ Object.defineProperty(this, 'name', {value: name});
281
+ }
262
282
  }
263
283
 
264
284
  export default Model;
package/src/type/Type.js CHANGED
@@ -38,12 +38,12 @@ class Type {
38
38
  static _schema = undefined;
39
39
 
40
40
  /**
41
- * Converts the class name to a string, removing the "Type" suffix.
41
+ * Converts the class name to a string
42
42
  *
43
- * @returns {string} The name of the type without the "Type" suffix.
43
+ * @returns {string} The name of the type.
44
44
  */
45
45
  static toString() {
46
- return this.name?.replace(/Type$/, '');
46
+ return this.name;
47
47
  }
48
48
 
49
49
  /**
@@ -58,10 +58,14 @@ class Type {
58
58
  }
59
59
 
60
60
  // Define the class name as "Required<OriginalTypeName>"
61
- Object.defineProperty(Required, 'name', {value: `Required${this.toString()}Type`});
61
+ Object.defineProperty(Required, 'name', {value: `Required${this.toString()}`});
62
62
 
63
63
  return Required;
64
64
  }
65
+
66
+ static {
67
+ Object.defineProperty(this, 'name', {value: 'Type'});
68
+ }
65
69
  }
66
70
 
67
71
  export default Type;
@@ -66,17 +66,17 @@ class ArrayType {
66
66
  * @returns {string} The string representation of the required array type.
67
67
  */
68
68
  static toString() {
69
- return `RequiredArrayOf(${type})`;
69
+ return `RequiredArrayOf(${type.toString()})`;
70
70
  }
71
71
  }
72
72
 
73
- Object.defineProperty(Required, 'name', {value: `Required${this.toString()}Type`});
73
+ Object.defineProperty(Required, 'name', {value: `Required${this.toString()}`});
74
74
 
75
75
  return Required;
76
76
  }
77
77
  }
78
78
 
79
- Object.defineProperty(ArrayOf, 'name', {value: `${ArrayOf.toString()}Type`});
79
+ Object.defineProperty(ArrayOf, 'name', {value: ArrayOf.toString()});
80
80
 
81
81
  return ArrayOf;
82
82
  }
@@ -35,15 +35,23 @@ class CustomType {
35
35
  * Represents a custom type defined by a JSON schema.
36
36
  */
37
37
  class Custom extends Type {
38
- /** @type {string} The data type, which is 'object' */
39
- static _type = 'object';
38
+ static {
39
+ /** @type {string} The data type, which is 'object' */
40
+ this._type = 'object';
40
41
 
41
- /** @type {Object} The JSON schema that defines the structure and validation rules */
42
- static _schema = schema;
42
+ /** @type {Object} The JSON schema that defines the structure and validation rules */
43
+ this._schema = schema;
44
+
45
+ Object.defineProperty(this, 'name', {value: 'Custom'});
46
+ }
43
47
  }
44
48
 
45
49
  return Custom;
46
50
  }
51
+
52
+ static {
53
+ Object.defineProperty(this, 'name', {value: 'Custom'});
54
+ }
47
55
  }
48
56
 
49
57
  export default CustomType;
@@ -57,8 +57,14 @@ class SlugType extends ResolvedType {
57
57
  }
58
58
  }
59
59
 
60
+ Object.defineProperty(SlugOf, 'name', {value: `SlugOf(${property})`});
61
+
60
62
  return SlugOf;
61
63
  }
64
+
65
+ static {
66
+ Object.defineProperty(this, 'name', {value: 'Slug'});
67
+ }
62
68
  }
63
69
 
64
70
  export default SlugType;
@@ -10,11 +10,15 @@ import SimpleType from './SimpleType.js';
10
10
  * @extends SimpleType
11
11
  */
12
12
  class BooleanType extends SimpleType {
13
- /**
14
- * @static
15
- * @property {string} _type - The type identifier for BooleanType, set to `'boolean'`.
16
- */
17
- static _type = 'boolean';
13
+ static {
14
+ /**
15
+ * @static
16
+ * @property {string} _type - The type identifier for BooleanType, set to `'boolean'`.
17
+ */
18
+ this._type = 'boolean';
19
+
20
+ Object.defineProperty(this, 'name', {value: 'Boolean'});
21
+ }
18
22
  }
19
23
 
20
24
  export default BooleanType;
@@ -10,17 +10,21 @@ import SimpleType from './SimpleType.js';
10
10
  * @extends SimpleType
11
11
  */
12
12
  class DateType extends SimpleType {
13
- /**
14
- * @static
15
- * @property {string} _type - The type identifier for DateType, set to `'string'`.
16
- */
17
- static _type = 'string';
13
+ static {
14
+ /**
15
+ * @static
16
+ * @property {string} _type - The type identifier for DateType, set to `'string'`.
17
+ */
18
+ this._type = 'string';
18
19
 
19
- /**
20
- * @static
21
- * @property {string} _format - The format for DateType, set to `'iso-date-time'`.
22
- */
23
- static _format = 'iso-date-time';
20
+ /**
21
+ * @static
22
+ * @property {string} _format - The format for DateType, set to `'iso-date-time'`.
23
+ */
24
+ this._format = 'iso-date-time';
25
+
26
+ Object.defineProperty(this, 'name', {value: 'Date'});
27
+ }
24
28
 
25
29
  /**
26
30
  * Checks if the given value is a valid date.
@@ -10,11 +10,15 @@ import SimpleType from './SimpleType.js';
10
10
  * @extends SimpleType
11
11
  */
12
12
  class NumberType extends SimpleType {
13
- /**
14
- * @static
15
- * @property {string} _type - The type identifier for NumberType, set to `'number'`.
16
- */
17
- static _type = 'number';
13
+ static {
14
+ /**
15
+ * @static
16
+ * @property {string} _type - The type identifier for NumberType, set to `'number'`.
17
+ */
18
+ this._type = 'number';
19
+
20
+ Object.defineProperty(this, 'name', {value: 'Number'});
21
+ }
18
22
  }
19
23
 
20
24
  export default NumberType;
@@ -10,11 +10,15 @@ import SimpleType from './SimpleType.js';
10
10
  * @extends SimpleType
11
11
  */
12
12
  class StringType extends SimpleType {
13
- /**
14
- * @static
15
- * @property {string} _type - The type identifier for the string type.
16
- */
17
- static _type = 'string';
13
+ static {
14
+ /**
15
+ * @static
16
+ * @property {string} _type - The type identifier for the string type.
17
+ */
18
+ this._type = 'string';
19
+
20
+ Object.defineProperty(this, 'name', {value: 'String'});
21
+ }
18
22
  }
19
23
 
20
24
  export default StringType;
package/.deepsource.toml DELETED
@@ -1,10 +0,0 @@
1
- version = 1
2
-
3
- [[analyzers]]
4
- name = "javascript"
5
-
6
- [analyzers.meta]
7
- environment = [
8
- "nodejs",
9
- "browser"
10
- ]