@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 +4 -4
- package/docs/code-quirks.md +9 -9
- package/docs/defining-models.md +8 -8
- package/docs/model-properties.md +26 -26
- package/docs/models-as-properties.md +30 -30
- package/docs/search-queries.md +8 -8
- package/docs/structured-queries.md +8 -8
- package/package.json +1 -1
- package/src/Connection.js +151 -78
- package/src/Schema.js +1 -1
- package/src/data/Model.js +25 -19
- package/src/data/properties/ArrayType.js +4 -2
- package/src/data/properties/BooleanType.js +2 -2
- package/src/data/properties/CustomType.js +4 -4
- package/src/data/properties/DateType.js +3 -3
- package/src/data/properties/NumberType.js +2 -2
- package/src/data/properties/SlugType.js +1 -1
- package/src/data/properties/StringType.js +2 -2
- package/src/data/properties/Type.js +5 -3
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
```
|
package/docs/code-quirks.md
CHANGED
@@ -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.
|
17
|
+
export class Person extends Persist.Model {
|
18
18
|
static {
|
19
|
-
|
20
|
-
|
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.
|
40
|
+
export class Person extends Persist.Model {
|
41
41
|
static {
|
42
|
-
|
42
|
+
Person.address = () => Address;
|
43
43
|
}
|
44
44
|
}
|
45
45
|
|
46
|
-
export class Address extends Persist.
|
46
|
+
export class Address extends Persist.Model {
|
47
47
|
static {
|
48
|
-
|
49
|
-
|
50
|
-
|
48
|
+
Address.person = () => Person;
|
49
|
+
Address.address = Persist.Property.String.required;
|
50
|
+
Address.postcode = Persist.Property.String.required;
|
51
51
|
}
|
52
52
|
}
|
53
53
|
```
|
package/docs/defining-models.md
CHANGED
@@ -28,10 +28,10 @@ import Persist from '@acodeninja/persist';
|
|
28
28
|
|
29
29
|
class Person extends Persist.Model {
|
30
30
|
static {
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
49
|
-
|
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
|
-
|
56
|
-
|
55
|
+
Person.name = Persist.Property.String.required;
|
56
|
+
Person.address = Address;
|
57
57
|
}
|
58
58
|
}
|
59
59
|
```
|
package/docs/model-properties.md
CHANGED
@@ -13,8 +13,8 @@ import Persist from '@acodeninja/persist';
|
|
13
13
|
|
14
14
|
class Person extends Persist.Model {
|
15
15
|
static {
|
16
|
-
|
17
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
49
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
81
|
-
|
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
|
-
|
98
|
-
|
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
|
-
|
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
|
-
|
144
|
-
|
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
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
189
|
+
IPv4Type._type = 'string';
|
190
190
|
// Use the ajv extended format "ipv4"
|
191
|
-
|
191
|
+
IPv4Type._format = 'ipv4';
|
192
192
|
// Ensure that even when minified, the name of the constructor is IPv4
|
193
|
-
Object.defineProperty(
|
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
|
-
|
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
|
-
|
19
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
61
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
84
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
109
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
134
|
-
|
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
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
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
|
```
|
package/docs/search-queries.md
CHANGED
@@ -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.
|
14
|
+
export class Person extends Persist.Model {
|
15
15
|
static {
|
16
|
-
|
17
|
-
|
18
|
-
|
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.
|
22
|
+
export class Address extends Persist.Model {
|
23
23
|
static {
|
24
|
-
|
25
|
-
|
26
|
-
|
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 `
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
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
|
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),
|
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
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
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 =
|
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
|
-
|
432
|
+
state.updateSearchIndex(indexName, index);
|
340
433
|
}
|
341
434
|
|
342
435
|
for (const indexName of indexesToUpdate) {
|
343
|
-
const index =
|
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
|
-
|
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(
|
360
|
-
|
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
|
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
|
164
|
+
class Required extends ModelClass {
|
163
165
|
static _required = true;
|
164
166
|
}
|
165
167
|
|
166
|
-
Object.defineProperty(Required, 'name', {value:
|
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(
|
196
|
-
.filter(([name, type]) => !['indexedProperties', 'searchProperties'].includes(name) && !type._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(
|
201
|
+
...Object.entries(ModelClass.properties)
|
199
202
|
.filter(([_name, type]) => {
|
200
203
|
return !Model.isModel(type) && (
|
201
|
-
(type._type === 'array' &&
|
204
|
+
(type._type === 'array' && ModelClass.isModel(type._items))
|
202
205
|
||
|
203
|
-
(!type._type && typeof type === 'function' &&
|
206
|
+
(!type._type && typeof type === 'function' && ModelClass.isModel(type()._items))
|
204
207
|
);
|
205
208
|
})
|
206
209
|
.map(([name, _type]) => `${name}.[*].id`),
|
207
|
-
...
|
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
|
234
|
+
const ModelClass = this;
|
235
|
+
const model = new ModelClass();
|
232
236
|
|
233
237
|
for (const [name, value] of Object.entries(data)) {
|
234
|
-
if (
|
238
|
+
if (ModelClass[name]?._resolved) continue;
|
235
239
|
|
236
|
-
if (
|
240
|
+
if (ModelClass[name]?.name.endsWith('Date')) {
|
237
241
|
model[name] = new Date(value);
|
238
242
|
continue;
|
239
243
|
}
|
240
244
|
|
241
|
-
if (
|
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
|
-
!
|
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
|
-
*
|
297
|
-
*
|
300
|
+
* TestModel.withName('TestModel');
|
301
|
+
* TestModel.string = Persist.Property.String;
|
298
302
|
* }
|
299
303
|
* }
|
300
304
|
*/
|
301
305
|
static withName(name) {
|
302
|
-
|
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 =
|
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(
|
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
|
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${
|
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
|
-
|
18
|
+
BooleanType._type = 'boolean';
|
19
19
|
|
20
|
-
Object.defineProperty(
|
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
|
-
|
40
|
+
Custom._type = 'object';
|
41
41
|
|
42
42
|
/** @type {Object} The JSON schema that defines the structure and validation rules */
|
43
|
-
|
43
|
+
Custom._schema = schema;
|
44
44
|
|
45
|
-
Object.defineProperty(
|
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(
|
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
|
-
|
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
|
-
|
24
|
+
DateType._format = 'iso-date-time';
|
25
25
|
|
26
|
-
Object.defineProperty(
|
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
|
-
|
18
|
+
NumberType._type = 'number';
|
19
19
|
|
20
|
-
Object.defineProperty(
|
20
|
+
Object.defineProperty(NumberType, 'name', {value: 'Number'});
|
21
21
|
}
|
22
22
|
}
|
23
23
|
|
@@ -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
|
-
|
18
|
+
StringType._type = 'string';
|
19
19
|
|
20
|
-
Object.defineProperty(
|
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
|
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${
|
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(
|
77
|
+
Object.defineProperty(Type, 'name', {value: 'Type'});
|
76
78
|
}
|
77
79
|
}
|
78
80
|
|