@acodeninja/persist 3.0.0-next.27 → 3.0.0-next.29

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
@@ -41,10 +41,10 @@ import Persist from '@acodeninja/persist';
41
41
 
42
42
  class Person extends Persist.Model {
43
43
  static {
44
- this.name = Persist.Property.String.required;
45
- this.dateOfBirth = Persist.Property.Date.required;
46
- this.height = Persist.Property.Number.required;
47
- this.isStudent = Persist.Property.Boolean.required;
44
+ Person.name = Persist.Property.String.required;
45
+ Person.dateOfBirth = Persist.Property.Date.required;
46
+ Person.height = Persist.Property.Number.required;
47
+ Person.isStudent = Persist.Property.Boolean.required;
48
48
  }
49
49
  }
50
50
  ```
@@ -14,10 +14,10 @@ To avoid this problem, you have two options:
14
14
  ```javascript
15
15
  import Persist from "@acodeninja/persist";
16
16
 
17
- export class Person extends Persist.Type.Model {
17
+ export class Person extends Persist.Model {
18
18
  static {
19
- this.withName('Person');
20
- this.name = Persist.Type.String.required;
19
+ Person.withName('Person');
20
+ Person.name = Persist.Property.String.required;
21
21
  }
22
22
  }
23
23
  ```
@@ -37,17 +37,17 @@ To avoid these errors, always define model relationships using arrow functions.
37
37
  ```javascript
38
38
  import Persist from "@acodeninja/persist";
39
39
 
40
- export class Person extends Persist.Type.Model {
40
+ export class Person extends Persist.Model {
41
41
  static {
42
- this.address = () => Address;
42
+ Person.address = () => Address;
43
43
  }
44
44
  }
45
45
 
46
- export class Address extends Persist.Type.Model {
46
+ export class Address extends Persist.Model {
47
47
  static {
48
- this.person = () => Person;
49
- this.address = Persist.Type.String.required;
50
- this.postcode = Persist.Type.String.required;
48
+ Address.person = () => Person;
49
+ Address.address = Persist.Property.String.required;
50
+ Address.postcode = Persist.Property.String.required;
51
51
  }
52
52
  }
53
53
  ```
@@ -28,10 +28,10 @@ import Persist from '@acodeninja/persist';
28
28
 
29
29
  class Person extends Persist.Model {
30
30
  static {
31
- this.name = Persist.Property.String.required;
32
- this.dateOfBirth = Persist.Property.Date.required;
33
- this.height = Persist.Property.Number.required;
34
- this.isStudent = Persist.Property.Boolean.required;
31
+ Person.name = Persist.Property.String.required;
32
+ Person.dateOfBirth = Persist.Property.Date.required;
33
+ Person.height = Persist.Property.Number.required;
34
+ Person.isStudent = Persist.Property.Boolean.required;
35
35
  }
36
36
  }
37
37
  ```
@@ -45,15 +45,15 @@ Models can be linked to other models by declaring them as properties.
45
45
  ```javascript
46
46
  class Address extends Persist.Model {
47
47
  static {
48
- this.address = Persist.Property.String.required;
49
- this.postcode = Persist.Property.String.required;
48
+ Address.address = Persist.Property.String.required;
49
+ Address.postcode = Persist.Property.String.required;
50
50
  }
51
51
  }
52
52
 
53
53
  class Person extends Persist.Model {
54
54
  static {
55
- this.name = Persist.Property.String.required;
56
- this.address = Address;
55
+ Person.name = Persist.Property.String.required;
56
+ Person.address = Address;
57
57
  }
58
58
  }
59
59
  ```
@@ -13,8 +13,8 @@ import Persist from '@acodeninja/persist';
13
13
 
14
14
  class Person extends Persist.Model {
15
15
  static {
16
- this.firstName = Persist.Property.String;
17
- this.lastName = Persist.Property.String;
16
+ Person.firstName = Persist.Property.String;
17
+ Person.lastName = Persist.Property.String;
18
18
  }
19
19
  }
20
20
  ```
@@ -30,8 +30,8 @@ import Persist from '@acodeninja/persist';
30
30
 
31
31
  class Person extends Persist.Model {
32
32
  static {
33
- this.firstName = Persist.Property.String;
34
- this.lastName = Persist.Property.String.required;
33
+ Person.firstName = Persist.Property.String;
34
+ Person.lastName = Persist.Property.String.required;
35
35
  }
36
36
  }
37
37
  ```
@@ -45,8 +45,8 @@ import Persist from '@acodeninja/persist';
45
45
 
46
46
  class Person extends Persist.Model {
47
47
  static {
48
- this.markettingEmailsActive = Persist.Property.Boolean;
49
- this.accountActive = Persist.Property.Boolean.required;
48
+ Person.marketingEmailsActive = Persist.Property.Boolean;
49
+ Person.accountActive = Persist.Property.Boolean.required;
50
50
  }
51
51
  }
52
52
  ```
@@ -60,8 +60,8 @@ import Persist from '@acodeninja/persist';
60
60
 
61
61
  class Person extends Persist.Model {
62
62
  static {
63
- this.loginToken = Persist.Property.Number;
64
- this.accountId = Persist.Property.Number.required;
63
+ Person.loginToken = Persist.Property.Number;
64
+ Person.accountId = Persist.Property.Number.required;
65
65
  }
66
66
  }
67
67
  ```
@@ -77,8 +77,8 @@ import Persist from '@acodeninja/persist';
77
77
 
78
78
  class Person extends Persist.Model {
79
79
  static {
80
- this.lastLogin = Persist.Property.Date;
81
- this.createdAt = Persist.Property.Date.required;
80
+ Person.lastLogin = Persist.Property.Date;
81
+ Person.createdAt = Persist.Property.Date.required;
82
82
  }
83
83
  }
84
84
  ```
@@ -94,8 +94,8 @@ import Persist from '@acodeninja/persist';
94
94
 
95
95
  class Person extends Persist.Model {
96
96
  static {
97
- this.failedLoginAttempts = Persist.Property.Array.of(Persist.Property.Date);
98
- this.fullName = Persist.Property.Array.of(Persist.Property.String).required;
97
+ Person.failedLoginAttempts = Persist.Property.Array.of(Persist.Property.Date);
98
+ Person.fullName = Persist.Property.Array.of(Persist.Property.String).required;
99
99
  }
100
100
  }
101
101
  ```
@@ -109,7 +109,7 @@ import Persist from '@acodeninja/persist';
109
109
 
110
110
  class Person extends Persist.Model {
111
111
  static {
112
- this.address = Persist.Property.Custom.of({
112
+ Person.address = Persist.Property.Custom.of({
113
113
  type: 'object',
114
114
  additionalProperties: false,
115
115
  required: ['line1', 'city', 'postcode'],
@@ -140,8 +140,8 @@ import Persist from '@acodeninja/persist';
140
140
 
141
141
  class Page extends Persist.Model {
142
142
  static {
143
- this.title = Persist.Property.String;
144
- this.slug = Persist.Property.Resolved.Slug.of('title');
143
+ Page.title = Persist.Property.String;
144
+ Page.slug = Persist.Property.Resolved.Slug.of('title');
145
145
  }
146
146
  }
147
147
 
@@ -162,14 +162,14 @@ Most types support the `.required` modifier, which will alter validation to enfo
162
162
  ```javascript
163
163
  class RequiredStringModel extends Persist.Model {
164
164
  static {
165
- this.requiredString = Type.String.required;
166
- this.requiredNumber = Type.Number.required;
167
- this.requiredBoolean = Type.Boolean.required;
168
- this.requiredDate = Type.Date.required;
169
- this.requiredArrayOfString = Type.Array.of(Type.String).required;
170
- this.requiredArrayOfNumber = Type.Array.of(Type.Number).required;
171
- this.requiredArrayOfBoolean = Type.Array.of(Type.Boolean).required;
172
- this.requiredArrayOfDate = Type.Array.of(Type.Date).required;
165
+ RequiredStringModel.requiredString = Persist.Property.String.required;
166
+ RequiredStringModel.requiredNumber = Persist.Property.Number.required;
167
+ RequiredStringModel.requiredBoolean = Persist.Property.Boolean.required;
168
+ RequiredStringModel.requiredDate = Persist.Property.Date.required;
169
+ RequiredStringModel.requiredArrayOfString = Persist.Property.Array.of(Persist.Property.String).required;
170
+ RequiredStringModel.requiredArrayOfNumber = Persist.Property.Array.of(Persist.Property.Number).required;
171
+ RequiredStringModel.requiredArrayOfBoolean = Persist.Property.Array.of(Persist.Property.Boolean).required;
172
+ RequiredStringModel.requiredArrayOfDate = Persist.Property.Array.of(Persist.Property.Date).required;
173
173
  }
174
174
  }
175
175
  ```
@@ -186,11 +186,11 @@ import Persist from '@acodeninja/persist';
186
186
  class IPv4Type extends Persist.Property.Type {
187
187
  static {
188
188
  // Set the type of the property to string
189
- this._type = 'string';
189
+ IPv4Type._type = 'string';
190
190
  // Use the ajv extended format "ipv4"
191
- this._format = 'ipv4';
191
+ IPv4Type._format = 'ipv4';
192
192
  // Ensure that even when minified, the name of the constructor is IPv4
193
- Object.defineProperty(this, 'name', {value: 'IPv4'});
193
+ Object.defineProperty(IPv4Type, 'name', {value: 'IPv4'});
194
194
  }
195
195
  }
196
196
  ```
@@ -9,14 +9,14 @@ import Persist from "@acodeninja/persist";
9
9
 
10
10
  export class Person extends Persist.Model {
11
11
  static {
12
- this.name = Persist.Property.String.required;
12
+ Person.name = Persist.Property.String.required;
13
13
  }
14
14
  }
15
15
 
16
16
  export class Address extends Persist.Model {
17
17
  static {
18
- this.address = Persist.Property.String.required;
19
- this.postcode = Persist.Property.String.required;
18
+ Address.address = Persist.Property.String.required;
19
+ Address.postcode = Persist.Property.String.required;
20
20
  }
21
21
  }
22
22
  ```
@@ -30,15 +30,15 @@ import Persist from "@acodeninja/persist";
30
30
 
31
31
  export class Person extends Persist.Model {
32
32
  static {
33
- this.name = Persist.Property.String.required;
34
- this.address = () => Address;
33
+ Person.name = Persist.Property.String.required;
34
+ Person.address = () => Address;
35
35
  }
36
36
  }
37
37
 
38
38
  export class Address extends Persist.Model {
39
39
  static {
40
- this.address = Persist.Property.String.required;
41
- this.postcode = Persist.Property.String.required;
40
+ Address.address = Persist.Property.String.required;
41
+ Address.postcode = Persist.Property.String.required;
42
42
  }
43
43
  }
44
44
  ```
@@ -57,16 +57,16 @@ import Persist from "@acodeninja/persist";
57
57
 
58
58
  export class Person extends Persist.Model {
59
59
  static {
60
- this.name = Persist.Property.String.required;
61
- this.address = () => Address;
60
+ Person.name = Persist.Property.String.required;
61
+ Person.address = () => Address;
62
62
  }
63
63
  }
64
64
 
65
65
  export class Address extends Persist.Model {
66
66
  static {
67
- this.person = () => Person;
68
- this.address = Persist.Property.String.required;
69
- this.postcode = Persist.Property.String.required;
67
+ Address.person = () => Person;
68
+ Address.address = Persist.Property.String.required;
69
+ Address.postcode = Persist.Property.String.required;
70
70
  }
71
71
  }
72
72
  ```
@@ -80,16 +80,16 @@ import Persist from "@acodeninja/persist";
80
80
 
81
81
  export class Person extends Persist.Model {
82
82
  static {
83
- this.name = Persist.Property.String.required;
84
- this.addresses = () => Persist.Property.Array.of(Address);
83
+ Person.name = Persist.Property.String.required;
84
+ Person.addresses = () => Persist.Property.Array.of(Address);
85
85
  }
86
86
  }
87
87
 
88
88
  export class Address extends Persist.Model {
89
89
  static {
90
- this.person = () => Person;
91
- this.address = Persist.Property.String.required;
92
- this.postcode = Persist.Property.String.required;
90
+ Address.person = () => Person;
91
+ Address.address = Persist.Property.String.required;
92
+ Address.postcode = Persist.Property.String.required;
93
93
  }
94
94
  }
95
95
  ```
@@ -105,16 +105,16 @@ import Persist from "@acodeninja/persist";
105
105
 
106
106
  export class Person extends Persist.Model {
107
107
  static {
108
- this.name = Persist.Property.String.required;
109
- this.addresses = () => Persist.Property.Array.of(Address);
108
+ Person.name = Persist.Property.String.required;
109
+ Person.addresses = () => Persist.Property.Array.of(Address);
110
110
  }
111
111
  }
112
112
 
113
113
  export class Address extends Persist.Model {
114
114
  static {
115
- this.people = () => Persist.Property.Array.of(Person);
116
- this.address = Persist.Property.String.required;
117
- this.postcode = Persist.Property.String.required;
115
+ Address.people = () => Persist.Property.Array.of(Person);
116
+ Address.address = Persist.Property.String.required;
117
+ Address.postcode = Persist.Property.String.required;
118
118
  }
119
119
  }
120
120
  ```
@@ -130,24 +130,24 @@ import Persist from "@acodeninja/persist";
130
130
 
131
131
  export class Person extends Persist.Model {
132
132
  static {
133
- this.name = Persist.Property.String.required;
134
- this.addresses = () => Persist.Property.Array.of(Abode);
133
+ Person.name = Persist.Property.String.required;
134
+ Person.addresses = () => Persist.Property.Array.of(Abode);
135
135
  }
136
136
  }
137
137
 
138
138
  export class Abode extends Persist.Model {
139
139
  static {
140
- this.moveInDate = Persist.Property.Date.required;
141
- this.address = () => Address;
142
- this.person = () => Person;
140
+ Abode.moveInDate = Persist.Property.Date.required;
141
+ Abode.address = () => Address;
142
+ Abode.person = () => Person;
143
143
  }
144
144
  }
145
145
 
146
146
  export class Address extends Persist.Model {
147
147
  static {
148
- this.people = () => Persist.Property.Array.of(Person);
149
- this.address = Persist.Property.String.required;
150
- this.postcode = Persist.Property.String.required;
148
+ Address.people = () => Persist.Property.Array.of(Person);
149
+ Address.address = Persist.Property.String.required;
150
+ Address.postcode = Persist.Property.String.required;
151
151
  }
152
152
  }
153
153
  ```
@@ -11,19 +11,19 @@ Let's consider the following models:
11
11
  ```javascript
12
12
  import Persist from "@acodeninja/persist";
13
13
 
14
- export class Person extends Persist.Type.Model {
14
+ export class Person extends Persist.Model {
15
15
  static {
16
- this.name = Persist.Type.String.required;
17
- this.address = () => Address;
18
- this.searchProperties = () => ['name', 'address.address'];
16
+ Person.name = Persist.Property.String.required;
17
+ Person.address = () => Address;
18
+ Person.searchProperties = () => ['name', 'address.address'];
19
19
  }
20
20
  }
21
21
 
22
- export class Address extends Persist.Type.Model {
22
+ export class Address extends Persist.Model {
23
23
  static {
24
- this.address = Persist.Type.String.required;
25
- this.postcode = Persist.Type.String.required;
26
- this.searchProperties = () => ['address', 'postcode'];
24
+ Address.address = Persist.Property.String.required;
25
+ Address.postcode = Persist.Property.String.required;
26
+ Address.searchProperties = () => ['address', 'postcode'];
27
27
  }
28
28
  }
29
29
  ```
@@ -4,7 +4,7 @@ Use structured queries when you need to filter a collection of models using a se
4
4
 
5
5
  ## Indexing Data
6
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.
7
+ To set index properties on a model, define the static function `indexedProperties` as an arrow function that returns an array of fields that should be indexed for querying.
8
8
 
9
9
  Let's consider the following models:
10
10
 
@@ -13,18 +13,18 @@ import Persist from "@acodeninja/persist";
13
13
 
14
14
  export class Person extends Persist.Model {
15
15
  static {
16
- this.name = Persist.Property.String.required;
17
- this.address = () => Address;
18
- this.indexProperties = () => ['name', 'address.postcode'];
16
+ Person.name = Persist.Property.String.required;
17
+ Person.address = () => Address;
18
+ Person.indexedProperties = () => ['name', 'address.postcode'];
19
19
  }
20
20
  }
21
21
 
22
22
  export class Address extends Persist.Model {
23
23
  static {
24
- this.address = Persist.Property.String.required;
25
- this.postcode = Persist.Property.String.required;
26
- this.people = () => Persist.Property.Array.of(Person)
27
- this.indexProperties = () => ['postcode', 'people.[*].name'];
24
+ Address.address = Persist.Property.String.required;
25
+ Address.postcode = Persist.Property.String.required;
26
+ Address.people = () => Persist.Property.Array.of(Person)
27
+ Address.indexedProperties = () => ['postcode', 'people.[*].name'];
28
28
  }
29
29
  }
30
30
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acodeninja/persist",
3
- "version": "3.0.0-next.27",
3
+ "version": "3.0.0-next.29",
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
@@ -7,6 +7,104 @@ import Model from './data/Model.js';
7
7
  import SearchIndex from './data/SearchIndex.js';
8
8
  import _ from 'lodash';
9
9
 
10
+ /**
11
+ * @class State
12
+ */
13
+ class State {
14
+ /**
15
+ * @private
16
+ * @property {StorageEngine}
17
+ */
18
+ #storage;
19
+
20
+ constructor(storage) {
21
+ this.modelCache = new Map();
22
+ this.indexCache = new Map();
23
+ this.searchIndexCache = new Map();
24
+ this.#storage = storage;
25
+ }
26
+
27
+ /**
28
+ * Get a given index
29
+ * @param {Model.constructor} modelConstructor
30
+ * @return {Object}
31
+ */
32
+ async getIndex(modelConstructor) {
33
+ const modelConstructorName = modelConstructor.name;
34
+
35
+ if (!this.indexCache.has(modelConstructorName)) {
36
+ this.indexCache.set(modelConstructorName, {
37
+ changed: false,
38
+ index: await this.#storage.getIndex(modelConstructor),
39
+ });
40
+ }
41
+
42
+ return this.indexCache.get(modelConstructorName)?.index;
43
+ }
44
+
45
+ /**
46
+ * Get a Map of indexes that have been tainted
47
+ * @return {Map<String, Object>}
48
+ */
49
+ getTaintedIndexes() {
50
+ return new Map(
51
+ this.indexCache
52
+ .entries()
53
+ .filter(([_name, cache]) => cache.changed)
54
+ .map(([name, {index}]) => [name, index]),
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Update a given index
60
+ * @param {string} modelConstructorName
61
+ * @return {Object}
62
+ */
63
+ updateIndex(modelConstructorName, index) {
64
+ this.indexCache.set(modelConstructorName, {index, changed: true});
65
+ }
66
+
67
+ /**
68
+ * Get a given search index
69
+ * @param {Model.constructor} modelConstructor
70
+ * @return {Object}
71
+ */
72
+ async getSearchIndex(modelConstructor) {
73
+ const modelConstructorName = modelConstructor.name;
74
+
75
+ if (!this.searchIndexCache.has(modelConstructorName)) {
76
+ this.searchIndexCache.set(modelConstructorName, {
77
+ changed: false,
78
+ index: await this.#storage.getSearchIndex(modelConstructor),
79
+ });
80
+ }
81
+
82
+ return this.searchIndexCache.get(modelConstructorName)?.index;
83
+ }
84
+
85
+ /**
86
+ * Get a Map of search indexes that have been tainted
87
+ * @return {Map<String, Object>}
88
+ */
89
+ getTaintedSearchIndexes() {
90
+ return new Map(
91
+ this.searchIndexCache
92
+ .entries()
93
+ .filter(([_name, cache]) => cache.changed)
94
+ .map(([name, {index}]) => [name, index]),
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Update a given search index
100
+ * @param {string} modelConstructorName
101
+ * @return {Object}
102
+ */
103
+ updateSearchIndex(modelConstructorName, index) {
104
+ this.searchIndexCache.set(modelConstructorName, {index, changed: true});
105
+ }
106
+ }
107
+
10
108
  /**
11
109
  * @class Connection
12
110
  */
@@ -75,7 +173,7 @@ export default class Connection {
75
173
  if (Model.isDryModel(property)) {
76
174
  // skipcq: JS-0129
77
175
  modelToProcess[name] = await hydrateSubModel(property);
78
- } else if (Array.isArray(property) && Model.isDryModel(property[0])) {
176
+ } else if (Array.isArray(property) && property.length && Model.isDryModel(property[0])) {
79
177
  // skipcq: JS-0129
80
178
  modelToProcess[name] = await hydrateModelList(property);
81
179
  }
@@ -133,6 +231,7 @@ export default class Connection {
133
231
  * Persists a model if it has changed, and updates all related models and their indexes
134
232
  * @param {Model} model
135
233
  * @return {Promise<void>}
234
+ * @throws {ValidationError|ModelNotRegisteredConnectionError}
136
235
  */
137
236
  async put(model) {
138
237
  const processedModels = [];
@@ -222,31 +321,19 @@ export default class Connection {
222
321
  * @throws {ModelNotFoundStorageEngineError}
223
322
  */
224
323
  async delete(subject, propagateTo = []) {
225
- const modelCache = new Map();
324
+ const state = new State(this.#storage);
226
325
  const modelsToCheck = this.#findLinkedModelClasses(subject);
227
326
  const modelsToDelete = new Set([subject.id]);
228
327
  const modelsToUpdate = new Set();
229
328
  const indexesToUpdate = new Set();
230
329
  const searchIndexesToUpdate = new Set();
231
330
 
232
- subject = await this.hydrate(subject, modelCache);
331
+ subject = await this.hydrate(subject, state.modelCache);
233
332
 
234
333
  if (!propagateTo.includes(subject.id)) {
235
334
  propagateTo.push(subject.id);
236
335
  }
237
336
 
238
- // Populate index and search index cache.
239
- const [indexCache, searchIndexCache] = await this.#getModelIndexes(modelsToCheck);
240
-
241
- // Add model to be removed to index caches.
242
- if (!indexCache.has(subject.constructor.name)) {
243
- indexCache.set(subject.constructor.name, (await this.#storage.getIndex(subject.constructor)));
244
- }
245
-
246
- if (!searchIndexCache.has(subject.constructor.name)) {
247
- searchIndexCache.set(subject.constructor.name, (await this.#storage.getSearchIndex(subject.constructor)));
248
- }
249
-
250
337
  // Populate model cache
251
338
  for (const [[modelName, propertyName, type, direction], _modelConstructor] of modelsToCheck) {
252
339
  const query = {};
@@ -270,11 +357,11 @@ export default class Connection {
270
357
  (
271
358
  Array.isArray(subject[propertyName]) ?
272
359
  subject[propertyName] : [subject[propertyName]]
273
- ) : new FindIndex(this.#models.get(modelName), indexCache.get(modelName)).query(query);
360
+ ) : new FindIndex(this.#models.get(modelName), await state.getIndex(this.#models.get(modelName))).query(query);
274
361
 
275
362
  for (const foundModel of foundModels) {
276
- if (!modelCache.has(foundModel.id)) {
277
- modelCache.set(foundModel.id, await this.hydrate(foundModel, modelCache));
363
+ if (!state.modelCache.has(foundModel.id)) {
364
+ state.modelCache.set(foundModel.id, await this.hydrate(foundModel, state.modelCache));
278
365
  }
279
366
  }
280
367
 
@@ -282,7 +369,7 @@ export default class Connection {
282
369
  if (direction === 'up') {
283
370
  if (type === 'one') {
284
371
  for (const foundModel of foundModels) {
285
- const cachedModel = modelCache.get(foundModel.id);
372
+ const cachedModel = state.modelCache.get(foundModel.id);
286
373
 
287
374
  if (foundModel.constructor[propertyName]._required) {
288
375
  modelsToDelete.add(foundModel.id);
@@ -290,17 +377,17 @@ export default class Connection {
290
377
  }
291
378
 
292
379
  cachedModel[propertyName] = undefined;
293
- modelCache.set(foundModel.id, cachedModel);
380
+ state.modelCache.set(foundModel.id, cachedModel);
294
381
  modelsToUpdate.add(foundModel.id);
295
382
  }
296
383
  }
297
384
 
298
385
  if (type === 'many') {
299
386
  for (const foundModel of foundModels) {
300
- const cachedModel = modelCache.get(foundModel.id);
387
+ const cachedModel = state.modelCache.get(foundModel.id);
301
388
 
302
389
  cachedModel[propertyName] = cachedModel[propertyName].filter(m => m.id !== subject.id);
303
- modelCache.set(foundModel.id, cachedModel);
390
+ state.modelCache.set(foundModel.id, cachedModel);
304
391
  modelsToUpdate.add(foundModel.id);
305
392
  }
306
393
  }
@@ -312,52 +399,70 @@ export default class Connection {
312
399
 
313
400
  if (unrequestedDeletions.length || unrequestedUpdates.length) {
314
401
  throw new DeleteHasUnintendedConsequencesStorageEngineError(subject.id, {
315
- willDelete: unrequestedDeletions.map(id => modelCache.get(id)),
316
- willUpdate: unrequestedUpdates.map(id => modelCache.get(id)),
402
+ willDelete: unrequestedDeletions.map(id => state.modelCache.get(id)),
403
+ willUpdate: unrequestedUpdates.map(id => state.modelCache.get(id)),
317
404
  });
318
405
  }
319
406
 
320
- for (const modelId of [...modelsToDelete, ...modelsToUpdate]) {
321
- const modelConstructorName = modelId.split('/')[0];
322
- indexesToUpdate.add(modelConstructorName);
323
- if (this.#models.get(modelConstructorName)?.searchProperties().length) {
324
- searchIndexesToUpdate.add(modelConstructorName);
325
- }
326
- }
407
+ await Promise.all(
408
+ new Set([...modelsToDelete, ...modelsToUpdate].map(this.#getModelConstructorFromId.bind(this)))
409
+ .values()
410
+ .map(async modelConstructor => {
411
+ await state.getIndex(modelConstructor);
412
+ indexesToUpdate.add(modelConstructor.name);
413
+
414
+ if (modelConstructor.searchProperties().length) {
415
+ await state.getSearchIndex(modelConstructor);
416
+ searchIndexesToUpdate.add(modelConstructor.name);
417
+ }
418
+ }),
419
+ );
327
420
 
328
421
  for (const indexName of searchIndexesToUpdate) {
329
- const index = searchIndexCache.get(indexName);
422
+ const index = await state.getSearchIndex(this.#models.get(indexName));
330
423
 
331
424
  for (const model of [...modelsToUpdate].filter(i => i.startsWith(indexName))) {
332
- index[model] = modelCache.get(model).toSearchData();
425
+ index[model] = state.modelCache.get(model).toSearchData();
333
426
  }
334
427
 
335
428
  for (const model of [...modelsToDelete].filter(i => i.startsWith(indexName))) {
336
429
  delete index[model];
337
430
  }
338
431
 
339
- searchIndexCache.set(indexName, index);
432
+ state.updateSearchIndex(indexName, index);
340
433
  }
341
434
 
342
435
  for (const indexName of indexesToUpdate) {
343
- const index = indexCache.get(indexName);
436
+ const index = await state.getIndex(this.#models.get(indexName));
344
437
 
345
438
  for (const model of [...modelsToUpdate].filter(i => i.startsWith(indexName))) {
346
- index[model] = modelCache.get(model).toIndexData();
439
+ index[model] = state.modelCache.get(model).toIndexData();
347
440
  }
348
441
 
349
442
  for (const model of [...modelsToDelete].filter(i => i.startsWith(indexName))) {
350
443
  delete index[model];
351
444
  }
352
445
 
353
- indexCache.set(indexName, index);
446
+ state.updateIndex(indexName, index);
354
447
  }
355
448
 
356
449
  await Promise.all([
357
- Promise.all([...modelsToUpdate].map(id => this.#storage.putModel(modelCache.get(id).toData()))),
450
+ Promise.all([...modelsToUpdate].map(id => this.#storage.putModel(state.modelCache.get(id).toData()))),
358
451
  Promise.all([...modelsToDelete].map(id => this.#storage.deleteModel(id))),
359
- Promise.all([...indexesToUpdate].map(index => this.#storage.putIndex(this.#models.get(index), indexCache.get(index)))),
360
- Promise.all([...searchIndexesToUpdate].map(index => this.#storage.putSearchIndex(this.#models.get(index), searchIndexCache.get(index)))),
452
+ Promise.all(state
453
+ .getTaintedIndexes()
454
+ .entries()
455
+ .map(([modelConstructorName, index]) =>
456
+ this.#storage.putIndex(this.#models.get(modelConstructorName), index),
457
+ ),
458
+ ),
459
+ Promise.all(state
460
+ .getTaintedSearchIndexes()
461
+ .entries()
462
+ .map(([modelConstructorName, index]) =>
463
+ this.#storage.putSearchIndex(this.#models.get(modelConstructorName), index),
464
+ ),
465
+ ),
361
466
  ]);
362
467
  }
363
468
 
@@ -405,7 +510,7 @@ export default class Connection {
405
510
 
406
511
  const engine = CreateTransactionalStorageEngine(operations, this.#storage);
407
512
 
408
- const transaction = new this.constructor(engine, this.#models.values());
513
+ const transaction = new this.constructor(engine, [...this.#models.values()]);
409
514
 
410
515
  transaction.commit = async () => {
411
516
  try {
@@ -432,7 +537,7 @@ export default class Connection {
432
537
  }
433
538
  }
434
539
  } catch (error) {
435
- for (const operation of operations) {
540
+ for (const operation of operations.slice().reverse()) {
436
541
  if (operation.committed && operation.original) {
437
542
  if (['putModel', 'deleteModel'].includes(operation.method))
438
543
  await this.#storage.putModel(operation.original);
@@ -443,6 +548,10 @@ export default class Connection {
443
548
  if (operation.method === 'putSearchIndex')
444
549
  await this.#storage.putSearchIndex(operation.args[0], operation.original);
445
550
  }
551
+
552
+ if (operation.method === 'putModel' && operation.committed && !operation.original) {
553
+ await this.#storage.deleteModel(operation.args[0].id);
554
+ }
446
555
  }
447
556
 
448
557
  throw new CommitFailedTransactionError(operations, error);
@@ -467,42 +576,6 @@ export default class Connection {
467
576
  return modelConstructor;
468
577
  }
469
578
 
470
- /**
471
- * Retrieves and caches index and search index information for specified models.
472
- *
473
- * @private
474
- * @async
475
- * @param {Array<Array<string, *>>} modelsToCheck - An array of arrays where the first element
476
- * of each inner array is the model name to retrieve indexes for
477
- * @returns {Promise<[Map<string, *>, Map<string, *>]>} A promise that resolves to a tuple containing:
478
- * - indexCache: A Map of model names to their corresponding indexes
479
- * - searchIndexCache: A Map of model names to their corresponding search indexes
480
- *
481
- * @description
482
- * This method populates two caches for the specified models:
483
- * 1. A regular index cache retrieved via storage.getIndex()
484
- * 2. A search index cache retrieved via storage.getSearchIndex()
485
- *
486
- * If a model's indexes are already cached, they won't be fetched again.
487
- * The method uses the internal storage interface to retrieve the indexes.
488
- */
489
- async #getModelIndexes(modelsToCheck) {
490
- const indexCache = new Map();
491
- const searchIndexCache = new Map();
492
-
493
- for (const [[modelName]] of modelsToCheck) {
494
- if (!indexCache.has(modelName)) {
495
- indexCache.set(modelName, await this.#storage.getIndex(this.#models.get(modelName)));
496
- }
497
-
498
- if (!searchIndexCache.has(modelName)) {
499
- searchIndexCache.set(modelName, await this.#storage.getSearchIndex(this.#models.get(modelName)));
500
- }
501
- }
502
-
503
- return [indexCache, searchIndexCache];
504
- }
505
-
506
579
  /**
507
580
  * Finds all model classes that are linked to the specified subject model.
508
581
  *
package/src/Schema.js CHANGED
@@ -14,7 +14,7 @@ class Schema {
14
14
  * It uses AJV for the validation process and integrates with model types and their specific validation rules.
15
15
  *
16
16
  * @param {Object|Model} rawSchema - The raw schema or model definition to be compiled.
17
- * @returns {CompiledSchema} - A class that extends `CompiledSchema`, with the compiled schema and validator attached.
17
+ * @returns {CompiledSchema} - A compiled schema, ready to validate instances of the model.
18
18
  *
19
19
  * @example
20
20
  * const schemaClass = Schema.compile(MyModelSchema);
package/src/data/Model.js CHANGED
@@ -151,6 +151,8 @@ class Model {
151
151
  * @static
152
152
  */
153
153
  static get required() {
154
+ const ModelClass = this;
155
+
154
156
  /**
155
157
  * A subclass of the current model with the `_required` flag set to `true`.
156
158
  * Used to indicate that the property is required during validation or schema generation.
@@ -159,11 +161,11 @@ class Model {
159
161
  * @extends {Model}
160
162
  * @private
161
163
  */
162
- class Required extends this {
164
+ class Required extends ModelClass {
163
165
  static _required = true;
164
166
  }
165
167
 
166
- Object.defineProperty(Required, 'name', {value: this.name});
168
+ Object.defineProperty(Required, 'name', {value: ModelClass.name});
167
169
 
168
170
  return Required;
169
171
  }
@@ -191,20 +193,21 @@ class Model {
191
193
  * @static
192
194
  */
193
195
  static indexedPropertiesResolved() {
196
+ const ModelClass = this;
194
197
  return [
195
- ...Object.entries(this.properties)
196
- .filter(([name, type]) => !['indexedProperties', 'searchProperties'].includes(name) && !type._type && (this.isModel(type) || (typeof type === 'function' && this.isModel(type()))))
198
+ ...Object.entries(ModelClass.properties)
199
+ .filter(([name, type]) => !['indexedProperties', 'searchProperties'].includes(name) && !type._type && (ModelClass.isModel(type) || (typeof type === 'function' && ModelClass.isModel(type()))))
197
200
  .map(([name, _type]) => `${name}.id`),
198
- ...Object.entries(this.properties)
201
+ ...Object.entries(ModelClass.properties)
199
202
  .filter(([_name, type]) => {
200
203
  return !Model.isModel(type) && (
201
- (type._type === 'array' && this.isModel(type._items))
204
+ (type._type === 'array' && ModelClass.isModel(type._items))
202
205
  ||
203
- (!type._type && typeof type === 'function' && this.isModel(type()._items))
206
+ (!type._type && typeof type === 'function' && ModelClass.isModel(type()._items))
204
207
  );
205
208
  })
206
209
  .map(([name, _type]) => `${name}.[*].id`),
207
- ...this.indexedProperties(),
210
+ ...ModelClass.indexedProperties(),
208
211
  'id',
209
212
  ];
210
213
  }
@@ -228,17 +231,18 @@ class Model {
228
231
  * @static
229
232
  */
230
233
  static fromData(data) {
231
- const model = new this();
234
+ const ModelClass = this;
235
+ const model = new ModelClass();
232
236
 
233
237
  for (const [name, value] of Object.entries(data)) {
234
- if (this[name]?._resolved) continue;
238
+ if (ModelClass[name]?._resolved) continue;
235
239
 
236
- if (this[name]?.name.endsWith('Date')) {
240
+ if (ModelClass[name]?.name.endsWith('Date')) {
237
241
  model[name] = new Date(value);
238
242
  continue;
239
243
  }
240
244
 
241
- if (this[name]?.name.endsWith('ArrayOf(Date)')) {
245
+ if (ModelClass[name]?.name.endsWith('ArrayOf(Date)')) {
242
246
  model[name] = data[name].map(d => new Date(d));
243
247
  continue;
244
248
  }
@@ -273,7 +277,7 @@ class Model {
273
277
  static isDryModel(possibleDryModel) {
274
278
  try {
275
279
  return (
276
- !this.isModel(possibleDryModel) &&
280
+ !Model.isModel(possibleDryModel) &&
277
281
  Object.keys(possibleDryModel).includes('id') &&
278
282
  new RegExp(/[A-Za-z]+\/[A-Z0-9]+/).test(possibleDryModel.id)
279
283
  );
@@ -291,15 +295,16 @@ class Model {
291
295
  * @static
292
296
  *
293
297
  * @example
294
- * export default class TestModel {
298
+ * export default class TestModel extends Model {
295
299
  * static {
296
- * this.withName('TestModel');
297
- * this.string = Persist.Type.String;
300
+ * TestModel.withName('TestModel');
301
+ * TestModel.string = Persist.Property.String;
298
302
  * }
299
303
  * }
300
304
  */
301
305
  static withName(name) {
302
- Object.defineProperty(this, 'name', {value: name});
306
+ const ModelClass = this;
307
+ Object.defineProperty(ModelClass, 'name', {value: name});
303
308
  }
304
309
 
305
310
  /**
@@ -308,10 +313,11 @@ class Model {
308
313
  * @return {Model}
309
314
  */
310
315
  static get properties() {
316
+ const ModelClass = this;
311
317
  const props = {};
312
318
  const chain = [];
313
319
 
314
- let current = this;
320
+ let current = ModelClass;
315
321
  while (current !== Function.prototype) {
316
322
  chain.push(current);
317
323
  current = Object.getPrototypeOf(current);
@@ -344,7 +350,7 @@ class Model {
344
350
  }
345
351
  }
346
352
 
347
- return Object.assign(this, props);
353
+ return Object.assign(ModelClass, props);
348
354
  }
349
355
  }
350
356
 
@@ -51,12 +51,14 @@ class ArrayType {
51
51
  * const requiredArrayOfStrings = ArrayType.of(StringType).required;
52
52
  */
53
53
  static get required() {
54
+ const ThisType = this;
55
+
54
56
  /**
55
57
  * @class RequiredArrayOf
56
58
  * @extends ArrayOf
57
59
  * Represents a required array of a specific type.
58
60
  */
59
- class Required extends this {
61
+ class Required extends ThisType {
60
62
  /** @type {boolean} Indicates that the array is required */
61
63
  static _required = true;
62
64
 
@@ -70,7 +72,7 @@ class ArrayType {
70
72
  }
71
73
  }
72
74
 
73
- Object.defineProperty(Required, 'name', {value: `Required${this.toString()}`});
75
+ Object.defineProperty(Required, 'name', {value: `Required${ThisType.name}`});
74
76
 
75
77
  return Required;
76
78
  }
@@ -15,9 +15,9 @@ class BooleanType extends Type {
15
15
  * @static
16
16
  * @property {string} _type - The type identifier for BooleanType, set to `'boolean'`.
17
17
  */
18
- this._type = 'boolean';
18
+ BooleanType._type = 'boolean';
19
19
 
20
- Object.defineProperty(this, 'name', {value: 'Boolean'});
20
+ Object.defineProperty(BooleanType, 'name', {value: 'Boolean'});
21
21
  }
22
22
  }
23
23
 
@@ -37,12 +37,12 @@ class CustomType {
37
37
  class Custom extends Type {
38
38
  static {
39
39
  /** @type {string} The data type, which is 'object' */
40
- this._type = 'object';
40
+ Custom._type = 'object';
41
41
 
42
42
  /** @type {Object} The JSON schema that defines the structure and validation rules */
43
- this._schema = schema;
43
+ Custom._schema = schema;
44
44
 
45
- Object.defineProperty(this, 'name', {value: 'Custom'});
45
+ Object.defineProperty(Custom, 'name', {value: 'Custom'});
46
46
  }
47
47
  }
48
48
 
@@ -50,7 +50,7 @@ class CustomType {
50
50
  }
51
51
 
52
52
  static {
53
- Object.defineProperty(this, 'name', {value: 'Custom'});
53
+ Object.defineProperty(CustomType, 'name', {value: 'Custom'});
54
54
  }
55
55
  }
56
56
 
@@ -15,15 +15,15 @@ class DateType extends Type {
15
15
  * @static
16
16
  * @property {string} _type - The type identifier for DateType, set to `'string'`.
17
17
  */
18
- this._type = 'string';
18
+ DateType._type = 'string';
19
19
 
20
20
  /**
21
21
  * @static
22
22
  * @property {string} _format - The format for DateType, set to `'iso-date-time'`.
23
23
  */
24
- this._format = 'iso-date-time';
24
+ DateType._format = 'iso-date-time';
25
25
 
26
- Object.defineProperty(this, 'name', {value: 'Date'});
26
+ Object.defineProperty(DateType, 'name', {value: 'Date'});
27
27
  }
28
28
 
29
29
  /**
@@ -15,9 +15,9 @@ class NumberType extends Type {
15
15
  * @static
16
16
  * @property {string} _type - The type identifier for NumberType, set to `'number'`.
17
17
  */
18
- this._type = 'number';
18
+ NumberType._type = 'number';
19
19
 
20
- Object.defineProperty(this, 'name', {value: 'Number'});
20
+ Object.defineProperty(NumberType, 'name', {value: 'Number'});
21
21
  }
22
22
  }
23
23
 
@@ -65,7 +65,7 @@ class SlugType extends ResolvedType {
65
65
  }
66
66
 
67
67
  static {
68
- Object.defineProperty(this, 'name', {value: 'Slug'});
68
+ Object.defineProperty(SlugType, 'name', {value: 'Slug'});
69
69
  }
70
70
  }
71
71
 
@@ -15,9 +15,9 @@ class StringType extends Type {
15
15
  * @static
16
16
  * @property {string} _type - The type identifier for the string type.
17
17
  */
18
- this._type = 'string';
18
+ StringType._type = 'string';
19
19
 
20
- Object.defineProperty(this, 'name', {value: 'String'});
20
+ Object.defineProperty(StringType, 'name', {value: 'String'});
21
21
  }
22
22
  }
23
23
 
@@ -53,6 +53,8 @@ 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
+ const ThisType = this;
57
+
56
58
  /**
57
59
  * A subclass of the current type with the `_required` flag set to `true`.
58
60
  * Used to indicate that the property is required during validation or schema generation.
@@ -61,18 +63,18 @@ class Type {
61
63
  * @extends {Type}
62
64
  * @private
63
65
  */
64
- class Required extends this {
66
+ class Required extends ThisType {
65
67
  static _required = true;
66
68
  }
67
69
 
68
70
  // Define the class name as "Required<OriginalTypeName>"
69
- Object.defineProperty(Required, 'name', {value: `Required${this.name}`});
71
+ Object.defineProperty(Required, 'name', {value: `Required${ThisType.name}`});
70
72
 
71
73
  return Required;
72
74
  }
73
75
 
74
76
  static {
75
- Object.defineProperty(this, 'name', {value: 'Type'});
77
+ Object.defineProperty(Type, 'name', {value: 'Type'});
76
78
  }
77
79
  }
78
80