@acodeninja/persist 3.0.0-next.9 → 3.0.0
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 +60 -4
- package/docs/code-quirks.md +14 -14
- package/docs/defining-models.md +61 -0
- package/docs/http.openapi.yml +138 -0
- package/docs/{model-property-types.md → model-properties.md} +76 -43
- package/docs/models-as-properties.md +46 -46
- package/docs/search-queries.md +11 -13
- package/docs/storage-engines.md +19 -35
- package/docs/structured-queries.md +59 -48
- package/docs/transactions.md +6 -7
- package/exports/storage/http.js +3 -0
- package/exports/storage/s3.js +3 -0
- package/jest.config.cjs +8 -12
- package/package.json +2 -2
- package/src/Connection.js +750 -0
- package/src/Persist.js +29 -30
- package/src/Schema.js +175 -0
- package/src/{Query.js → data/FindIndex.js} +40 -24
- package/src/{type → data}/Model.js +95 -55
- package/src/data/Property.js +21 -0
- package/src/data/SearchIndex.js +106 -0
- package/src/{type/complex → data/properties}/ArrayType.js +5 -3
- package/src/{type/simple → data/properties}/BooleanType.js +3 -3
- package/src/{type/complex → data/properties}/CustomType.js +5 -5
- package/src/{type/simple → data/properties}/DateType.js +4 -4
- package/src/{type/simple → data/properties}/NumberType.js +3 -3
- package/src/{type/resolved → data/properties}/ResolvedType.js +3 -2
- package/src/{type/resolved → data/properties}/SlugType.js +1 -1
- package/src/{type/simple → data/properties}/StringType.js +3 -3
- package/src/{type → data/properties}/Type.js +13 -3
- package/src/engine/storage/HTTPStorageEngine.js +149 -253
- package/src/engine/storage/S3StorageEngine.js +108 -195
- package/src/engine/storage/StorageEngine.js +131 -549
- package/exports/engine/storage/file.js +0 -3
- package/exports/engine/storage/http.js +0 -3
- package/exports/engine/storage/s3.js +0 -3
- package/src/SchemaCompiler.js +0 -196
- package/src/Transactions.js +0 -145
- package/src/engine/StorageEngine.js +0 -472
- package/src/engine/storage/FileStorageEngine.js +0 -213
- package/src/type/index.js +0 -32
package/src/Persist.js
CHANGED
@@ -1,45 +1,44 @@
|
|
1
|
-
import
|
2
|
-
import
|
1
|
+
import Connection from './Connection.js';
|
2
|
+
import Model from './data/Model.js';
|
3
|
+
import Property from './data/Property.js';
|
4
|
+
import {ValidationError} from './Schema.js';
|
3
5
|
|
4
6
|
/**
|
5
7
|
* @class Persist
|
6
8
|
*/
|
7
9
|
class Persist {
|
8
|
-
static
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
static Property = Property;
|
11
|
+
static Model = Model;
|
12
|
+
static Connection = Connection;
|
13
|
+
|
14
|
+
static Errors = {
|
15
|
+
ValidationError,
|
16
|
+
};
|
17
|
+
|
18
|
+
static #connections = new Map();
|
15
19
|
|
16
20
|
/**
|
17
|
-
*
|
18
|
-
* @
|
19
|
-
* @
|
20
|
-
* @param {
|
21
|
-
* @
|
22
|
-
* @return {Engine|null}
|
21
|
+
* Register a new connection.
|
22
|
+
* @param {string} name
|
23
|
+
* @param {StorageEngine} storage
|
24
|
+
* @param {Array<Model.constructor>} models
|
25
|
+
* @return {Connection}
|
23
26
|
*/
|
24
|
-
static
|
25
|
-
|
27
|
+
static registerConnection(name, storage, models) {
|
28
|
+
const connection = new Connection(storage, models);
|
29
|
+
|
30
|
+
Persist.#connections.set(name, connection);
|
31
|
+
|
32
|
+
return connection;
|
26
33
|
}
|
27
34
|
|
28
35
|
/**
|
29
|
-
*
|
30
|
-
* @
|
31
|
-
* @
|
32
|
-
* @param {string} group - Name of the group containing the engine
|
33
|
-
* @param {Engine} engine - The engine class you wish to configure and add to the group
|
34
|
-
* @param {object?} configuration - The configuration to use with the engine
|
36
|
+
* Get a persist connection by its name.
|
37
|
+
* @param {string} name
|
38
|
+
* @return {Connection|undefined}
|
35
39
|
*/
|
36
|
-
static
|
37
|
-
|
38
|
-
|
39
|
-
this._engine[group][engine.name] =
|
40
|
-
configuration.transactions ?
|
41
|
-
enableTransactions(engine.configure(configuration)) :
|
42
|
-
engine.configure(configuration);
|
40
|
+
static getConnection(name) {
|
41
|
+
return Persist.#connections.get(name);
|
43
42
|
}
|
44
43
|
}
|
45
44
|
|
package/src/Schema.js
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
import Model from './data/Model.js';
|
2
|
+
import ajv from 'ajv';
|
3
|
+
import ajvErrors from 'ajv-errors';
|
4
|
+
import ajvFormats from 'ajv-formats';
|
5
|
+
|
6
|
+
/**
|
7
|
+
* A class responsible for compiling raw schema definitions into a format that can be validated using the AJV (Another JSON Validator) library.
|
8
|
+
*/
|
9
|
+
class Schema {
|
10
|
+
/**
|
11
|
+
* Compiles a raw schema into a validation-ready schema, and returns a class that extends `CompiledSchema`.
|
12
|
+
*
|
13
|
+
* This method converts a given schema into a JSON schema-like format, setting up properties, types, formats, and validation rules.
|
14
|
+
* It uses AJV for the validation process and integrates with model types and their specific validation rules.
|
15
|
+
*
|
16
|
+
* @param {Object|Model} rawSchema - The raw schema or model definition to be compiled.
|
17
|
+
* @returns {CompiledSchema} - A compiled schema, ready to validate instances of the model.
|
18
|
+
*
|
19
|
+
* @example
|
20
|
+
* const schemaClass = Schema.compile(MyModelSchema);
|
21
|
+
* const isValid = schemaClass.validate(data); // Throws ValidationError if data is invalid.
|
22
|
+
*/
|
23
|
+
static compile(rawSchema) {
|
24
|
+
const validation = new ajv({allErrors: true});
|
25
|
+
|
26
|
+
ajvErrors(validation);
|
27
|
+
ajvFormats(validation);
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Recursively builds a JSON-schema-like object from a model or schema segment.
|
31
|
+
*
|
32
|
+
* Handles both `Model` instances and schema property definitions,
|
33
|
+
* including nested models and required property rules.
|
34
|
+
*
|
35
|
+
* @param {Object|Model|Type} schemaSegment - A model or a property descriptor.
|
36
|
+
* @returns {Object} A JSON schema representation of the input segment.
|
37
|
+
*/
|
38
|
+
function BuildSchema(schemaSegment) {
|
39
|
+
const thisSchema = {};
|
40
|
+
|
41
|
+
if (Model.isModel(schemaSegment)) {
|
42
|
+
thisSchema.required = [];
|
43
|
+
thisSchema.type = 'object';
|
44
|
+
thisSchema.additionalProperties = false;
|
45
|
+
thisSchema.properties = {
|
46
|
+
id: {
|
47
|
+
type: 'string',
|
48
|
+
pattern: `^${schemaSegment.toString()}/[A-Z0-9]+$`,
|
49
|
+
},
|
50
|
+
};
|
51
|
+
|
52
|
+
for (const [name, type] of Object.entries(schemaSegment)) {
|
53
|
+
if (['indexedProperties', 'searchProperties'].includes(name)) continue;
|
54
|
+
|
55
|
+
const property = type instanceof Function && !type.prototype ? type() : type;
|
56
|
+
|
57
|
+
if (property?._required || property?._items?._type?._required) {
|
58
|
+
thisSchema.required.push(name);
|
59
|
+
}
|
60
|
+
|
61
|
+
if (Model.isModel(property)) {
|
62
|
+
thisSchema.properties[name] = {
|
63
|
+
type: 'object',
|
64
|
+
additionalProperties: false,
|
65
|
+
required: [],
|
66
|
+
properties: {
|
67
|
+
id: {
|
68
|
+
type: 'string',
|
69
|
+
pattern: `^${property.toString()}/[A-Z0-9]+$`,
|
70
|
+
},
|
71
|
+
},
|
72
|
+
};
|
73
|
+
continue;
|
74
|
+
}
|
75
|
+
|
76
|
+
thisSchema.properties[name] = BuildSchema(property);
|
77
|
+
}
|
78
|
+
|
79
|
+
return thisSchema;
|
80
|
+
}
|
81
|
+
|
82
|
+
if (schemaSegment._schema) {
|
83
|
+
return schemaSegment._schema;
|
84
|
+
}
|
85
|
+
|
86
|
+
thisSchema.type = schemaSegment?._type;
|
87
|
+
|
88
|
+
if (schemaSegment?._format) {
|
89
|
+
thisSchema.format = schemaSegment._format;
|
90
|
+
}
|
91
|
+
|
92
|
+
if (schemaSegment?._items) {
|
93
|
+
thisSchema.items = {};
|
94
|
+
thisSchema.items.type = schemaSegment._items._type;
|
95
|
+
if (schemaSegment._items._format)
|
96
|
+
thisSchema.items.format = schemaSegment._items._format;
|
97
|
+
}
|
98
|
+
|
99
|
+
return thisSchema;
|
100
|
+
}
|
101
|
+
|
102
|
+
const builtSchema = BuildSchema(rawSchema);
|
103
|
+
|
104
|
+
return new CompiledSchema(validation.compile(builtSchema));
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
|
109
|
+
/**
|
110
|
+
* Represents a compiled schema used for validating data models.
|
111
|
+
* This class provides a mechanism to validate data using a precompiled schema and a validator function.
|
112
|
+
*/
|
113
|
+
export class CompiledSchema {
|
114
|
+
/**
|
115
|
+
* The validator function used to validate data against the schema.
|
116
|
+
* @type {?Function}
|
117
|
+
* @private
|
118
|
+
*/
|
119
|
+
#validator = null;
|
120
|
+
|
121
|
+
constructor(validator) {
|
122
|
+
this.#validator = validator;
|
123
|
+
}
|
124
|
+
|
125
|
+
/**
|
126
|
+
* Validates the given data against the compiled schema.
|
127
|
+
*
|
128
|
+
* If the data is an instance of a model, it will be converted to a plain object via `toData()` before validation.
|
129
|
+
*
|
130
|
+
* @param {Object|Model} data - The data or model instance to be validated.
|
131
|
+
* @returns {boolean} - Returns `true` if the data is valid according to the schema.
|
132
|
+
* @throws {ValidationError} - Throws a `ValidationError` if the data is invalid.
|
133
|
+
*/
|
134
|
+
validate(data) {
|
135
|
+
let inputData = structuredClone(data);
|
136
|
+
|
137
|
+
if (Model.isModel(data)) {
|
138
|
+
inputData = data.toData();
|
139
|
+
}
|
140
|
+
|
141
|
+
const valid = this.#validator?.(inputData);
|
142
|
+
|
143
|
+
if (valid) return valid;
|
144
|
+
|
145
|
+
throw new ValidationError(inputData, this.#validator.errors);
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Represents a validation error that occurs when a model or data fails validation.
|
151
|
+
* Extends the built-in JavaScript `Error` class.
|
152
|
+
*/
|
153
|
+
export class ValidationError extends Error {
|
154
|
+
/**
|
155
|
+
* Creates an instance of `ValidationError`.
|
156
|
+
*
|
157
|
+
* @param {Object} data - The data that failed validation.
|
158
|
+
* @param {Array<Object>} errors - A list of validation errors, each typically containing details about what failed.
|
159
|
+
*/
|
160
|
+
constructor(data, errors) {
|
161
|
+
super('Validation failed');
|
162
|
+
/**
|
163
|
+
* An array of validation errors, containing details about each failed validation.
|
164
|
+
* @type {Array<Object>}
|
165
|
+
*/
|
166
|
+
this.errors = errors;
|
167
|
+
/**
|
168
|
+
* The data that caused the validation error.
|
169
|
+
* @type {Object}
|
170
|
+
*/
|
171
|
+
this.data = data;
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
export default Schema;
|
@@ -26,37 +26,47 @@
|
|
26
26
|
* }
|
27
27
|
* });
|
28
28
|
*/
|
29
|
-
class
|
29
|
+
class FindIndex {
|
30
30
|
/**
|
31
31
|
* The query object that defines the search criteria.
|
32
32
|
* @type {Object}
|
33
|
+
* @private
|
33
34
|
*/
|
34
|
-
|
35
|
+
#index;
|
36
|
+
|
37
|
+
/**
|
38
|
+
* @type {Model.constructor} - The model class.
|
39
|
+
* @private
|
40
|
+
*/
|
41
|
+
#modelConstructor;
|
35
42
|
|
36
43
|
/**
|
37
44
|
* Constructs a new `Query` instance with the provided query object.
|
38
45
|
*
|
39
|
-
* @param {
|
46
|
+
* @param {Model.constructor} modelConstructor - The model class.
|
47
|
+
* @param {Record<string, Model>} index - The index dataset to search through.
|
40
48
|
*/
|
41
|
-
constructor(
|
42
|
-
this
|
49
|
+
constructor(modelConstructor, index) {
|
50
|
+
this.#index = index;
|
51
|
+
this.#modelConstructor = modelConstructor;
|
43
52
|
}
|
44
53
|
|
45
54
|
/**
|
46
55
|
* Executes the query against a model's index and returns the matching results.
|
47
56
|
*
|
48
|
-
* @param {
|
49
|
-
* @param {Object<string, Model>} index - The index dataset to search through.
|
57
|
+
* @param {Object} query The structured query.
|
50
58
|
* @returns {Array<Model>} The models that match the query.
|
51
59
|
*/
|
52
|
-
|
53
|
-
|
60
|
+
query(query) {
|
61
|
+
const splitQuery = this.#splitQuery(query);
|
62
|
+
|
63
|
+
return Object.values(this.#index)
|
54
64
|
.filter(m =>
|
55
|
-
|
56
|
-
.map(
|
65
|
+
splitQuery
|
66
|
+
.map(q => Boolean(this.#matchesQuery(m, q)))
|
57
67
|
.every(c => c),
|
58
68
|
)
|
59
|
-
.map(m =>
|
69
|
+
.map(m => this.#modelConstructor.fromData(m));
|
60
70
|
}
|
61
71
|
|
62
72
|
/**
|
@@ -72,26 +82,32 @@ class Query {
|
|
72
82
|
* @param {Object} inputQuery - The query to match against.
|
73
83
|
* @returns {boolean} True if the subject matches the query, otherwise false.
|
74
84
|
*/
|
75
|
-
|
76
|
-
if (['string', 'number', 'boolean'].includes(typeof inputQuery))
|
85
|
+
#matchesQuery(subject, inputQuery) {
|
86
|
+
if (['string', 'number', 'boolean'].includes(typeof inputQuery))
|
87
|
+
return subject === inputQuery;
|
77
88
|
|
78
|
-
if (inputQuery?.$is !== undefined && subject === inputQuery.$is)
|
89
|
+
if (inputQuery?.$is !== undefined && subject === inputQuery.$is)
|
90
|
+
return true;
|
79
91
|
|
80
92
|
if (inputQuery?.$contains !== undefined) {
|
81
|
-
if (subject.includes?.(inputQuery.$contains))
|
93
|
+
if (subject.includes?.(inputQuery.$contains))
|
94
|
+
return true;
|
82
95
|
|
83
|
-
|
84
|
-
|
85
|
-
|
96
|
+
if (typeof subject[Symbol.iterator] === 'function')
|
97
|
+
for (const value of subject) {
|
98
|
+
if (this.#matchesQuery(value, inputQuery.$contains))
|
99
|
+
return true;
|
100
|
+
}
|
86
101
|
}
|
87
102
|
|
88
103
|
for (const key of Object.keys(inputQuery)) {
|
89
104
|
if (!['$is', '$contains'].includes(key))
|
90
|
-
if (this
|
105
|
+
if (this.#matchesQuery(subject[key], inputQuery[key]))
|
106
|
+
return true;
|
91
107
|
}
|
92
108
|
|
93
109
|
return false;
|
94
|
-
}
|
110
|
+
}
|
95
111
|
|
96
112
|
/**
|
97
113
|
* Recursively splits an object into an array of objects,
|
@@ -105,14 +121,14 @@ class Query {
|
|
105
121
|
* @returns {Array<Object>} An array of objects, where each object contains a single key-value pair
|
106
122
|
* from the original query or its nested objects.
|
107
123
|
*/
|
108
|
-
|
124
|
+
#splitQuery(query) {
|
109
125
|
return Object.entries(query)
|
110
126
|
.flatMap(([key, value]) =>
|
111
127
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
112
|
-
? this
|
128
|
+
? this.#splitQuery(value).map(nestedObj => ({[key]: nestedObj}))
|
113
129
|
: {[key]: value},
|
114
130
|
);
|
115
131
|
}
|
116
132
|
}
|
117
133
|
|
118
|
-
export default
|
134
|
+
export default FindIndex;
|
@@ -1,5 +1,5 @@
|
|
1
|
-
import
|
2
|
-
import StringType from './
|
1
|
+
import Schema from '../Schema.js';
|
2
|
+
import StringType from './properties/StringType.js';
|
3
3
|
import _ from 'lodash';
|
4
4
|
import {monotonicFactory} from 'ulid';
|
5
5
|
|
@@ -51,10 +51,9 @@ class Model {
|
|
51
51
|
/**
|
52
52
|
* Serializes the model instance into an object, optionally retaining complex types.
|
53
53
|
*
|
54
|
-
* @param {boolean} [simple=true] - Determines whether to format the output using only JSON serialisable types.
|
55
54
|
* @returns {Object} - A serialized representation of the model.
|
56
55
|
*/
|
57
|
-
toData(
|
56
|
+
toData() {
|
58
57
|
const model = {...this};
|
59
58
|
|
60
59
|
for (const [name, property] of Object.entries(this.constructor)) {
|
@@ -70,21 +69,6 @@ class Model {
|
|
70
69
|
}
|
71
70
|
return value;
|
72
71
|
}),
|
73
|
-
(key, value) => {
|
74
|
-
if (!simple) {
|
75
|
-
if (this.constructor[key]) {
|
76
|
-
if (this.constructor[key].name.endsWith('Date')) {
|
77
|
-
return new Date(value);
|
78
|
-
}
|
79
|
-
|
80
|
-
if (this.constructor[key].name.endsWith('ArrayOf(Date)')) {
|
81
|
-
return value.map(d => new Date(d));
|
82
|
-
}
|
83
|
-
}
|
84
|
-
}
|
85
|
-
|
86
|
-
return value;
|
87
|
-
},
|
88
72
|
);
|
89
73
|
}
|
90
74
|
|
@@ -95,7 +79,7 @@ class Model {
|
|
95
79
|
* @throws {ValidationError} - Throws this error if validation fails.
|
96
80
|
*/
|
97
81
|
validate() {
|
98
|
-
return
|
82
|
+
return Schema.compile(this.constructor).validate(this);
|
99
83
|
}
|
100
84
|
|
101
85
|
/**
|
@@ -167,11 +151,21 @@ class Model {
|
|
167
151
|
* @static
|
168
152
|
*/
|
169
153
|
static get required() {
|
170
|
-
|
154
|
+
const ModelClass = this;
|
155
|
+
|
156
|
+
/**
|
157
|
+
* A subclass of the current model with the `_required` flag set to `true`.
|
158
|
+
* Used to indicate that the property is required during validation or schema generation.
|
159
|
+
*
|
160
|
+
* @class
|
161
|
+
* @extends {Model}
|
162
|
+
* @private
|
163
|
+
*/
|
164
|
+
class Required extends ModelClass {
|
171
165
|
static _required = true;
|
172
166
|
}
|
173
167
|
|
174
|
-
Object.defineProperty(Required, 'name', {value:
|
168
|
+
Object.defineProperty(Required, 'name', {value: ModelClass.name});
|
175
169
|
|
176
170
|
return Required;
|
177
171
|
}
|
@@ -179,6 +173,10 @@ class Model {
|
|
179
173
|
/**
|
180
174
|
* Returns a list of properties that are indexed.
|
181
175
|
*
|
176
|
+
* - To link to properties of a model use `<name>`
|
177
|
+
* - To link to properties of linked models use `<model>.<name>`
|
178
|
+
* - To link to properties of many linked models use `<model>.[*].<name>`
|
179
|
+
*
|
182
180
|
* @returns {Array<string>} - The indexed properties.
|
183
181
|
* @abstract
|
184
182
|
* @static
|
@@ -195,29 +193,23 @@ class Model {
|
|
195
193
|
* @static
|
196
194
|
*/
|
197
195
|
static indexedPropertiesResolved() {
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
)
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
!/^class/.test(Function.prototype.toString.call(type._items)) ?
|
216
|
-
type._items() : type._items,
|
217
|
-
),
|
218
|
-
).map(([name, _]) => `${name}.[*].id`),
|
219
|
-
)
|
220
|
-
.concat(this.indexedProperties());
|
196
|
+
const ModelClass = this;
|
197
|
+
return [
|
198
|
+
...Object.entries(ModelClass.properties)
|
199
|
+
.filter(([name, type]) => !['indexedProperties', 'searchProperties'].includes(name) && !type._type && (ModelClass.isModel(type) || (typeof type === 'function' && ModelClass.isModel(type()))))
|
200
|
+
.map(([name, _type]) => `${name}.id`),
|
201
|
+
...Object.entries(ModelClass.properties)
|
202
|
+
.filter(([_name, type]) => {
|
203
|
+
return !Model.isModel(type) && (
|
204
|
+
(type._type === 'array' && ModelClass.isModel(type._items))
|
205
|
+
||
|
206
|
+
(!type._type && typeof type === 'function' && ModelClass.isModel(type()._items))
|
207
|
+
);
|
208
|
+
})
|
209
|
+
.map(([name, _type]) => `${name}.[*].id`),
|
210
|
+
...ModelClass.indexedProperties(),
|
211
|
+
'id',
|
212
|
+
];
|
221
213
|
}
|
222
214
|
|
223
215
|
/**
|
@@ -239,17 +231,18 @@ class Model {
|
|
239
231
|
* @static
|
240
232
|
*/
|
241
233
|
static fromData(data) {
|
242
|
-
const
|
234
|
+
const ModelClass = this;
|
235
|
+
const model = new ModelClass();
|
243
236
|
|
244
237
|
for (const [name, value] of Object.entries(data)) {
|
245
|
-
if (
|
238
|
+
if (ModelClass[name]?._resolved) continue;
|
246
239
|
|
247
|
-
if (
|
240
|
+
if (ModelClass[name]?.name.endsWith('Date')) {
|
248
241
|
model[name] = new Date(value);
|
249
242
|
continue;
|
250
243
|
}
|
251
244
|
|
252
|
-
if (
|
245
|
+
if (ModelClass[name]?.name.endsWith('ArrayOf(Date)')) {
|
253
246
|
model[name] = data[name].map(d => new Date(d));
|
254
247
|
continue;
|
255
248
|
}
|
@@ -284,7 +277,7 @@ class Model {
|
|
284
277
|
static isDryModel(possibleDryModel) {
|
285
278
|
try {
|
286
279
|
return (
|
287
|
-
!
|
280
|
+
!Model.isModel(possibleDryModel) &&
|
288
281
|
Object.keys(possibleDryModel).includes('id') &&
|
289
282
|
new RegExp(/[A-Za-z]+\/[A-Z0-9]+/).test(possibleDryModel.id)
|
290
283
|
);
|
@@ -302,15 +295,62 @@ class Model {
|
|
302
295
|
* @static
|
303
296
|
*
|
304
297
|
* @example
|
305
|
-
* export default class TestModel {
|
298
|
+
* export default class TestModel extends Model {
|
306
299
|
* static {
|
307
|
-
*
|
308
|
-
*
|
300
|
+
* TestModel.withName('TestModel');
|
301
|
+
* TestModel.string = Persist.Property.String;
|
309
302
|
* }
|
310
303
|
* }
|
311
304
|
*/
|
312
|
-
static
|
313
|
-
|
305
|
+
static withName(name) {
|
306
|
+
const ModelClass = this;
|
307
|
+
Object.defineProperty(ModelClass, 'name', {value: name});
|
308
|
+
}
|
309
|
+
|
310
|
+
/**
|
311
|
+
* Discover model properties all the way up the prototype chain.
|
312
|
+
*
|
313
|
+
* @return {Model}
|
314
|
+
*/
|
315
|
+
static get properties() {
|
316
|
+
const ModelClass = this;
|
317
|
+
const props = {};
|
318
|
+
const chain = [];
|
319
|
+
|
320
|
+
let current = ModelClass;
|
321
|
+
while (current !== Function.prototype) {
|
322
|
+
chain.push(current);
|
323
|
+
current = Object.getPrototypeOf(current);
|
324
|
+
}
|
325
|
+
|
326
|
+
for (const item of chain) {
|
327
|
+
for (const property of Object.getOwnPropertyNames(item)) {
|
328
|
+
if (
|
329
|
+
[
|
330
|
+
'_required',
|
331
|
+
'fromData',
|
332
|
+
'indexedProperties',
|
333
|
+
'indexedPropertiesResolved',
|
334
|
+
'isDryModel',
|
335
|
+
'isModel',
|
336
|
+
'length',
|
337
|
+
'name',
|
338
|
+
'properties',
|
339
|
+
'prototype',
|
340
|
+
'required',
|
341
|
+
'searchProperties',
|
342
|
+
'toString',
|
343
|
+
'withName',
|
344
|
+
].includes(property)
|
345
|
+
) continue;
|
346
|
+
|
347
|
+
if (Object.keys(props).includes(property)) continue;
|
348
|
+
|
349
|
+
props[property] = item[property];
|
350
|
+
}
|
351
|
+
}
|
352
|
+
|
353
|
+
return Object.assign(ModelClass, props);
|
314
354
|
}
|
315
355
|
}
|
316
356
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import ArrayType from './properties/ArrayType.js';
|
2
|
+
import BooleanType from './properties/BooleanType.js';
|
3
|
+
import CustomType from './properties/CustomType.js';
|
4
|
+
import DateType from './properties/DateType.js';
|
5
|
+
import NumberType from './properties/NumberType.js';
|
6
|
+
import SlugType from './properties/SlugType.js';
|
7
|
+
import StringType from './properties/StringType.js';
|
8
|
+
import Type from './properties/Type.js';
|
9
|
+
|
10
|
+
const Property = {
|
11
|
+
Array: ArrayType,
|
12
|
+
Boolean: BooleanType,
|
13
|
+
Custom: CustomType,
|
14
|
+
Date: DateType,
|
15
|
+
Number: NumberType,
|
16
|
+
Slug: SlugType,
|
17
|
+
String: StringType,
|
18
|
+
Type,
|
19
|
+
};
|
20
|
+
|
21
|
+
export default Property;
|