functional-models 1.0.24 → 1.0.28
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/package.json +2 -1
- package/src/constants.js +19 -0
- package/src/errors.js +16 -0
- package/src/index.js +3 -0
- package/src/lazy.js +14 -7
- package/src/models.js +18 -6
- package/src/properties.js +121 -61
- package/src/serialization.js +4 -1
- package/src/utils.js +29 -19
- package/src/validation.js +18 -7
- package/test/src/models.test.js +67 -12
- package/test/src/properties.test.js +6 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "functional-models",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"description": "A library for creating JavaScript function based models.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"sinon": "^11.1.2"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
+
"async-lock": "^1.3.0",
|
|
52
53
|
"get-random-values": "^1.2.2",
|
|
53
54
|
"lodash": "^4.17.21"
|
|
54
55
|
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { getObjToArray } = require('./utils')
|
|
2
|
+
|
|
3
|
+
const PROPERTY_TYPES = getObjToArray([
|
|
4
|
+
'UniqueId',
|
|
5
|
+
'DateProperty',
|
|
6
|
+
'ArrayProperty',
|
|
7
|
+
'ReferenceProperty',
|
|
8
|
+
'IntegerProperty',
|
|
9
|
+
'TextProperty',
|
|
10
|
+
'ConstantValueProperty',
|
|
11
|
+
'NumberProperty',
|
|
12
|
+
'ObjectProperty',
|
|
13
|
+
'EmailProperty',
|
|
14
|
+
'BooleanProperty',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
PROPERTY_TYPES,
|
|
19
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* eslint-disable functional/no-this-expression */
|
|
2
|
+
/* eslint-disable functional/no-class */
|
|
3
|
+
class ValidationError extends Error {
|
|
4
|
+
constructor(modelName, keysToErrors) {
|
|
5
|
+
super(`${modelName} did not pass validation`)
|
|
6
|
+
this.name = 'ValidationError'
|
|
7
|
+
this.modelName = modelName
|
|
8
|
+
this.keysToErrors = keysToErrors
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/* eslint-enable functional/no-this-expression */
|
|
12
|
+
/* eslint-enable functional/no-class */
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
ValidationError,
|
|
16
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
+
constants: require('./constants'),
|
|
2
3
|
...require('./properties'),
|
|
3
4
|
...require('./models'),
|
|
4
5
|
...require('./functions'),
|
|
6
|
+
errors: require('./errors'),
|
|
5
7
|
validation: require('./validation'),
|
|
6
8
|
serialization: require('./serialization'),
|
|
9
|
+
utils: require('./utils'),
|
|
7
10
|
}
|
package/src/lazy.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
const AsyncLock = require('async-lock')
|
|
2
|
+
const { createUuid } = require('./utils')
|
|
3
|
+
|
|
1
4
|
const lazyValue = method => {
|
|
5
|
+
const key = createUuid()
|
|
6
|
+
const lock = new AsyncLock()
|
|
2
7
|
/* eslint-disable functional/no-let */
|
|
3
8
|
let value = undefined
|
|
4
9
|
let called = false
|
|
5
10
|
return async (...args) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
return lock.acquire(key, async () => {
|
|
12
|
+
if (!called) {
|
|
13
|
+
called = true
|
|
14
|
+
value = await method(...args)
|
|
15
|
+
// eslint-disable-next-line require-atomic-updates
|
|
16
|
+
}
|
|
17
|
+
}).then(() => {
|
|
18
|
+
return value
|
|
19
|
+
})
|
|
13
20
|
}
|
|
14
21
|
/* eslint-enable functional/no-let */
|
|
15
22
|
}
|
package/src/models.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const merge = require('lodash/merge')
|
|
2
|
+
const get = require('lodash/get')
|
|
2
3
|
const { toObj } = require('./serialization')
|
|
3
|
-
const { createPropertyTitle } = require('./utils')
|
|
4
4
|
const { createModelValidator } = require('./validation')
|
|
5
|
-
const { UniqueId } = require('./properties')
|
|
5
|
+
const { UniqueId, createPropertyTitle } = require('./properties')
|
|
6
6
|
|
|
7
7
|
const MODEL_DEF_KEYS = ['meta', 'functions']
|
|
8
8
|
|
|
@@ -65,12 +65,24 @@ const Model = (
|
|
|
65
65
|
const fleshedOutInstanceProperties = {
|
|
66
66
|
[getPropertyKey]: propertyGetter,
|
|
67
67
|
functions: {
|
|
68
|
+
getters: {
|
|
69
|
+
[key]: propertyGetter,
|
|
70
|
+
},
|
|
68
71
|
validators: {
|
|
69
72
|
[key]: propertyValidator,
|
|
70
73
|
},
|
|
71
74
|
},
|
|
72
75
|
}
|
|
73
|
-
|
|
76
|
+
const referenceProperties = get(property, 'meta.getReferencedId')
|
|
77
|
+
? {
|
|
78
|
+
meta: {
|
|
79
|
+
references: {
|
|
80
|
+
[createPropertyTitle(`${key}Id`)]: () => property.meta.getReferencedId(instanceValues[key])
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
: {}
|
|
85
|
+
return merge(acc, fleshedOutInstanceProperties, referenceProperties)
|
|
74
86
|
},
|
|
75
87
|
{}
|
|
76
88
|
)
|
|
@@ -81,11 +93,11 @@ const Model = (
|
|
|
81
93
|
functions: {
|
|
82
94
|
toObj: toObj(loadedInternals),
|
|
83
95
|
getPrimaryKey: loadedInternals[createPropertyTitle(primaryKey)],
|
|
84
|
-
validate: () => {
|
|
96
|
+
validate: (options={}) => {
|
|
85
97
|
return createModelValidator(
|
|
86
98
|
loadedInternals,
|
|
87
99
|
modelValidators
|
|
88
|
-
)(instance)
|
|
100
|
+
)(instance, options)
|
|
89
101
|
},
|
|
90
102
|
},
|
|
91
103
|
}
|
|
@@ -95,7 +107,7 @@ const Model = (
|
|
|
95
107
|
return merge(acc, {
|
|
96
108
|
functions: {
|
|
97
109
|
[key]: (...args) => {
|
|
98
|
-
return func(
|
|
110
|
+
return func(...args, instance)
|
|
99
111
|
},
|
|
100
112
|
},
|
|
101
113
|
})
|
package/src/properties.js
CHANGED
|
@@ -13,8 +13,14 @@ const {
|
|
|
13
13
|
referenceTypeMatch,
|
|
14
14
|
meetsRegex,
|
|
15
15
|
} = require('./validation')
|
|
16
|
-
const {
|
|
16
|
+
const { PROPERTY_TYPES } = require('./constants')
|
|
17
17
|
const { lazyValue } = require('./lazy')
|
|
18
|
+
const { toTitleCase, createUuid } = require('./utils')
|
|
19
|
+
|
|
20
|
+
const createPropertyTitle = key => {
|
|
21
|
+
const goodName = toTitleCase(key)
|
|
22
|
+
return `get${goodName}`
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
const EMAIL_REGEX =
|
|
20
26
|
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/u
|
|
@@ -30,21 +36,36 @@ const _mergeValidators = (config, validators) => {
|
|
|
30
36
|
return [...validators, ...(config.validators ? config.validators : [])]
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
const Property = (config = {}, additionalMetadata = {}) => {
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
const Property = (type, config = {}, additionalMetadata = {}) => {
|
|
40
|
+
if (!type && !config.type) {
|
|
41
|
+
throw new Error(`Property type must be provided.`)
|
|
42
|
+
}
|
|
43
|
+
if (config.type) {
|
|
44
|
+
type = config.type
|
|
45
|
+
}
|
|
46
|
+
const getConstantValue = () => config.value !== undefined ? config.value : undefined
|
|
47
|
+
const getDefaultValue = () => config.defaultValue !== undefined ? config.defaultValue : undefined
|
|
48
|
+
const getChoices = () => config.choices ? config.choices : []
|
|
36
49
|
const lazyLoadMethod = config.lazyLoadMethod || false
|
|
37
50
|
const valueSelector = config.valueSelector || identity
|
|
38
51
|
if (typeof valueSelector !== 'function') {
|
|
39
52
|
throw new Error(`valueSelector must be a function`)
|
|
40
53
|
}
|
|
41
54
|
|
|
55
|
+
|
|
42
56
|
return {
|
|
43
57
|
...additionalMetadata,
|
|
58
|
+
getConfig: () => config,
|
|
59
|
+
getChoices,
|
|
60
|
+
getDefaultValue,
|
|
61
|
+
getConstantValue,
|
|
62
|
+
getPropertyType: () => type,
|
|
44
63
|
createGetter: instanceValue => {
|
|
64
|
+
const value = getConstantValue()
|
|
45
65
|
if (value !== undefined) {
|
|
46
66
|
return () => value
|
|
47
67
|
}
|
|
68
|
+
const defaultValue = getDefaultValue()
|
|
48
69
|
if (
|
|
49
70
|
defaultValue !== undefined &&
|
|
50
71
|
(instanceValue === null || instanceValue === undefined)
|
|
@@ -62,37 +83,25 @@ const Property = (config = {}, additionalMetadata = {}) => {
|
|
|
62
83
|
},
|
|
63
84
|
getValidator: valueGetter => {
|
|
64
85
|
const validator = createPropertyValidator(config)
|
|
65
|
-
const _propertyValidatorWrapper = async (instance, instanceData) => {
|
|
66
|
-
return validator(await valueGetter(), instance, instanceData)
|
|
86
|
+
const _propertyValidatorWrapper = async (instance, instanceData, options={}) => {
|
|
87
|
+
return validator(await valueGetter(), instance, instanceData, options)
|
|
67
88
|
}
|
|
68
89
|
return _propertyValidatorWrapper
|
|
69
90
|
},
|
|
70
91
|
}
|
|
71
92
|
}
|
|
72
93
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
const DateProperty = (config = {}) =>
|
|
85
|
-
Property({
|
|
86
|
-
...config,
|
|
87
|
-
lazyLoadMethod: value => {
|
|
88
|
-
if (!value && config.autoNow) {
|
|
89
|
-
return new Date()
|
|
90
|
-
}
|
|
91
|
-
return value
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
+
const DateProperty = (config = {}, additionalMetadata={}) => Property(PROPERTY_TYPES.DateProperty, {
|
|
95
|
+
...config,
|
|
96
|
+
lazyLoadMethod: value => {
|
|
97
|
+
if (!value && config.autoNow) {
|
|
98
|
+
return new Date()
|
|
99
|
+
}
|
|
100
|
+
return value
|
|
101
|
+
},
|
|
102
|
+
}, additionalMetadata)
|
|
94
103
|
|
|
95
|
-
const ReferenceProperty = (model, config = {}) => {
|
|
104
|
+
const ReferenceProperty = (model, config = {}, additionalMetadata={}) => {
|
|
96
105
|
if (!model) {
|
|
97
106
|
throw new Error('Must include the referenced model')
|
|
98
107
|
}
|
|
@@ -106,22 +115,23 @@ const ReferenceProperty = (model, config = {}) => {
|
|
|
106
115
|
|
|
107
116
|
const validators = _mergeValidators(config, [referenceTypeMatch(model)])
|
|
108
117
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return instanceValues[primaryKey]
|
|
118
|
-
}
|
|
119
|
-
const primaryKeyFunc = get(instanceValues, 'functions.getPrimaryKey')
|
|
120
|
-
if (primaryKeyFunc) {
|
|
121
|
-
return primaryKeyFunc()
|
|
122
|
-
}
|
|
123
|
-
return instanceValues
|
|
118
|
+
const _getId = (instanceValues) => () => {
|
|
119
|
+
if (!instanceValues) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
const theModel = _getModel()
|
|
123
|
+
const primaryKey = theModel.getPrimaryKeyName()
|
|
124
|
+
if (instanceValues[primaryKey]) {
|
|
125
|
+
return instanceValues[primaryKey]
|
|
124
126
|
}
|
|
127
|
+
const primaryKeyFunc = get(instanceValues, 'functions.getPrimaryKey')
|
|
128
|
+
if (primaryKeyFunc) {
|
|
129
|
+
return primaryKeyFunc()
|
|
130
|
+
}
|
|
131
|
+
return instanceValues
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const lazyLoadMethod = async instanceValues => {
|
|
125
135
|
|
|
126
136
|
const valueIsModelInstance =
|
|
127
137
|
Boolean(instanceValues) && Boolean(instanceValues.functions)
|
|
@@ -135,7 +145,7 @@ const ReferenceProperty = (model, config = {}) => {
|
|
|
135
145
|
: _getModel().create(objToUse)
|
|
136
146
|
return merge({}, instance, {
|
|
137
147
|
functions: {
|
|
138
|
-
toObj: _getId,
|
|
148
|
+
toObj: _getId(instanceValues),
|
|
139
149
|
},
|
|
140
150
|
})
|
|
141
151
|
}
|
|
@@ -144,43 +154,51 @@ const ReferenceProperty = (model, config = {}) => {
|
|
|
144
154
|
return _getInstanceReturn(instanceValues)
|
|
145
155
|
}
|
|
146
156
|
if (config.fetcher) {
|
|
147
|
-
const id = await _getId()
|
|
157
|
+
const id = await _getId(instanceValues)()
|
|
148
158
|
const model = _getModel()
|
|
149
159
|
const obj = await config.fetcher(model, id)
|
|
150
160
|
return _getInstanceReturn(obj)
|
|
151
161
|
}
|
|
152
|
-
return _getId(instanceValues)
|
|
162
|
+
return _getId(instanceValues)()
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
return Property(
|
|
166
|
+
PROPERTY_TYPES.ReferenceProperty,
|
|
156
167
|
merge({}, config, {
|
|
157
168
|
validators,
|
|
158
169
|
lazyLoadMethod,
|
|
159
170
|
}),
|
|
160
171
|
{
|
|
172
|
+
...additionalMetadata,
|
|
161
173
|
meta: {
|
|
174
|
+
getReferencedId: (instanceValues) => _getId(instanceValues)(),
|
|
162
175
|
getReferencedModel: _getModel,
|
|
163
176
|
},
|
|
164
177
|
}
|
|
165
178
|
)
|
|
166
179
|
}
|
|
167
180
|
|
|
168
|
-
const ArrayProperty = (config = {}) =>
|
|
169
|
-
Property(
|
|
181
|
+
const ArrayProperty = (config = {}, additionalMetadata={}) =>
|
|
182
|
+
Property(
|
|
183
|
+
PROPERTY_TYPES.ArrayProperty,
|
|
184
|
+
{
|
|
170
185
|
defaultValue: [],
|
|
171
186
|
...config,
|
|
172
187
|
isArray: true,
|
|
173
|
-
})
|
|
188
|
+
}, additionalMetadata)
|
|
174
189
|
|
|
175
|
-
const ObjectProperty = (config = {}) =>
|
|
190
|
+
const ObjectProperty = (config = {}, additionalMetadata={}) =>
|
|
176
191
|
Property(
|
|
192
|
+
PROPERTY_TYPES.ObjectProperty,
|
|
177
193
|
merge(config, {
|
|
178
194
|
validators: _mergeValidators(config, [isType('object')]),
|
|
179
|
-
})
|
|
195
|
+
}),
|
|
196
|
+
additionalMetadata
|
|
180
197
|
)
|
|
181
198
|
|
|
182
|
-
const TextProperty = (config = {}) =>
|
|
199
|
+
const TextProperty = (config = {}, additionalMetadata={} ) =>
|
|
183
200
|
Property(
|
|
201
|
+
PROPERTY_TYPES.TextProperty,
|
|
184
202
|
merge(config, {
|
|
185
203
|
isString: true,
|
|
186
204
|
validators: _mergeValidators(config, [
|
|
@@ -191,11 +209,13 @@ const TextProperty = (config = {}) =>
|
|
|
191
209
|
minTextLength(value)
|
|
192
210
|
),
|
|
193
211
|
]),
|
|
194
|
-
})
|
|
212
|
+
}),
|
|
213
|
+
additionalMetadata
|
|
195
214
|
)
|
|
196
215
|
|
|
197
|
-
const IntegerProperty = (config = {}) =>
|
|
216
|
+
const IntegerProperty = (config = {}, additionalMetadata={}) =>
|
|
198
217
|
Property(
|
|
218
|
+
PROPERTY_TYPES.IntegerProperty,
|
|
199
219
|
merge(config, {
|
|
200
220
|
isInteger: true,
|
|
201
221
|
validators: _mergeValidators(config, [
|
|
@@ -206,11 +226,13 @@ const IntegerProperty = (config = {}) =>
|
|
|
206
226
|
maxNumber(value)
|
|
207
227
|
),
|
|
208
228
|
]),
|
|
209
|
-
})
|
|
229
|
+
}),
|
|
230
|
+
additionalMetadata
|
|
210
231
|
)
|
|
211
232
|
|
|
212
|
-
const NumberProperty = (config = {}) =>
|
|
233
|
+
const NumberProperty = (config = {}, additionalMetadata={}) =>
|
|
213
234
|
Property(
|
|
235
|
+
PROPERTY_TYPES.NumberProperty,
|
|
214
236
|
merge(config, {
|
|
215
237
|
isNumber: true,
|
|
216
238
|
validators: _mergeValidators(config, [
|
|
@@ -221,23 +243,59 @@ const NumberProperty = (config = {}) =>
|
|
|
221
243
|
maxNumber(value)
|
|
222
244
|
),
|
|
223
245
|
]),
|
|
224
|
-
})
|
|
246
|
+
}),
|
|
247
|
+
additionalMetadata
|
|
225
248
|
)
|
|
226
249
|
|
|
227
|
-
const ConstantValueProperty = (value, config = {}) =>
|
|
250
|
+
const ConstantValueProperty = (value, config = {}, additionalMetadata={}) =>
|
|
228
251
|
TextProperty(
|
|
229
252
|
merge(config, {
|
|
253
|
+
type: PROPERTY_TYPES.ConstantValueProperty,
|
|
230
254
|
value,
|
|
231
|
-
})
|
|
255
|
+
}),
|
|
256
|
+
additionalMetadata
|
|
232
257
|
)
|
|
233
258
|
|
|
234
|
-
const EmailProperty = (config = {}) =>
|
|
259
|
+
const EmailProperty = (config = {}, additionalMetadata={}) =>
|
|
235
260
|
TextProperty(
|
|
236
261
|
merge(config, {
|
|
262
|
+
type: PROPERTY_TYPES.EmailProperty,
|
|
237
263
|
validators: _mergeValidators(config, [meetsRegex(EMAIL_REGEX)]),
|
|
238
|
-
})
|
|
264
|
+
}),
|
|
265
|
+
additionalMetadata
|
|
239
266
|
)
|
|
240
267
|
|
|
268
|
+
const BooleanProperty = (config = {}, additionalMetadata={}) => Property(
|
|
269
|
+
PROPERTY_TYPES.BooleanProperty,
|
|
270
|
+
merge(config, {
|
|
271
|
+
isBoolean: true,
|
|
272
|
+
validators: _mergeValidators(config, [
|
|
273
|
+
_getValidatorFromConfigElseEmpty(config, 'minValue', value =>
|
|
274
|
+
minNumber(value)
|
|
275
|
+
),
|
|
276
|
+
_getValidatorFromConfigElseEmpty(config, 'maxValue', value =>
|
|
277
|
+
maxNumber(value)
|
|
278
|
+
),
|
|
279
|
+
]),
|
|
280
|
+
}),
|
|
281
|
+
additionalMetadata
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
const UniqueId = (config = {}, additionalMetadata={}) =>
|
|
285
|
+
Property(
|
|
286
|
+
PROPERTY_TYPES.UniqueId,
|
|
287
|
+
{
|
|
288
|
+
...config,
|
|
289
|
+
lazyLoadMethod: value => {
|
|
290
|
+
if (!value) {
|
|
291
|
+
return createUuid()
|
|
292
|
+
}
|
|
293
|
+
return value
|
|
294
|
+
},
|
|
295
|
+
}, additionalMetadata)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
241
299
|
module.exports = {
|
|
242
300
|
Property,
|
|
243
301
|
UniqueId,
|
|
@@ -250,4 +308,6 @@ module.exports = {
|
|
|
250
308
|
NumberProperty,
|
|
251
309
|
ObjectProperty,
|
|
252
310
|
EmailProperty,
|
|
311
|
+
BooleanProperty,
|
|
312
|
+
createPropertyTitle,
|
|
253
313
|
}
|
package/src/serialization.js
CHANGED
package/src/utils.js
CHANGED
|
@@ -1,42 +1,52 @@
|
|
|
1
|
+
const keyBy = require('lodash/keyBy')
|
|
2
|
+
|
|
1
3
|
const HEX = 16
|
|
2
4
|
const FOUR = 4
|
|
3
5
|
const FIFTEEN = 15
|
|
4
6
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const createPropertyTitle = key => {
|
|
10
|
-
const goodName = toTitleCase(key)
|
|
11
|
-
return `get${goodName}`
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const getCryptoRandomValues = () => {
|
|
7
|
+
const getRandomValues = () => {
|
|
8
|
+
const array = new Uint8Array(1)
|
|
15
9
|
if (typeof window !== 'undefined') {
|
|
10
|
+
if (window.crypto) {
|
|
11
|
+
return window.crypto.getRandomValues(array)
|
|
12
|
+
}
|
|
13
|
+
if (window.msCrypto) {
|
|
14
|
+
window.msCrypto.getRandomValues(array)
|
|
15
|
+
}
|
|
16
16
|
return (window.crypto || window.msCrypto).getRandomValues
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
return require('get-random-values')
|
|
19
|
+
return require('get-random-values')(array)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const createUuid = () => {
|
|
23
|
-
const getRandomValues = getCryptoRandomValues()
|
|
24
23
|
// eslint-disable-next-line no-magic-numbers,require-unicode-regexp
|
|
25
|
-
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
|
|
26
|
-
(
|
|
27
|
-
c ^
|
|
28
|
-
|
|
29
|
-
).toString(HEX)
|
|
24
|
+
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => {
|
|
25
|
+
const value = getRandomValues()[0] & (FIFTEEN >> (c / FOUR))
|
|
26
|
+
return (c ^ value).toString(HEX)
|
|
27
|
+
}
|
|
30
28
|
)
|
|
31
29
|
}
|
|
32
30
|
|
|
31
|
+
const toTitleCase = string => {
|
|
32
|
+
return `${string.slice(0, 1).toUpperCase()}${string.slice(1)}`
|
|
33
|
+
}
|
|
34
|
+
|
|
33
35
|
const loweredTitleCase = string => {
|
|
34
36
|
return `${string.slice(0, 1).toLowerCase()}${string.slice(1)}`
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
const getObjToArray = array => {
|
|
40
|
+
const obj = keyBy(array)
|
|
41
|
+
return {
|
|
42
|
+
...obj,
|
|
43
|
+
toArray: () => array,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
module.exports = {
|
|
38
|
-
createUuid,
|
|
39
48
|
loweredTitleCase,
|
|
40
|
-
createPropertyTitle,
|
|
41
49
|
toTitleCase,
|
|
50
|
+
createUuid,
|
|
51
|
+
getObjToArray,
|
|
42
52
|
}
|
package/src/validation.js
CHANGED
|
@@ -57,6 +57,15 @@ const arrayType = type => value => {
|
|
|
57
57
|
}, undefined)
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
const multiplePropertiesMustMatch = (getKeyA, getKeyB, errorMessage='Properties do not match') => async (_, instance) => {
|
|
61
|
+
const keyA = await getKeyA(instance)
|
|
62
|
+
const keyB = await getKeyB(instance)
|
|
63
|
+
if (keyA !== keyB) {
|
|
64
|
+
return errorMessage
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
const meetsRegex =
|
|
61
70
|
(regex, flags, errorMessage = 'Format was invalid') =>
|
|
62
71
|
value => {
|
|
@@ -193,6 +202,7 @@ const CONFIG_TO_VALIDATE_METHOD = {
|
|
|
193
202
|
isNumber: _boolChoice(isNumber),
|
|
194
203
|
isString: _boolChoice(isString),
|
|
195
204
|
isArray: _boolChoice(isArray),
|
|
205
|
+
isBoolean: _boolChoice(isBoolean),
|
|
196
206
|
choices,
|
|
197
207
|
}
|
|
198
208
|
|
|
@@ -208,18 +218,18 @@ const createPropertyValidator = config => {
|
|
|
208
218
|
: validators.includes(isRequired)
|
|
209
219
|
const validator =
|
|
210
220
|
validators.length > 0 ? aggregateValidator(validators) : emptyValidator
|
|
211
|
-
const _propertyValidator = async (value, instance, instanceData) => {
|
|
221
|
+
const _propertyValidator = async (value, instance, instanceData={}, options) => {
|
|
212
222
|
if (!value && !isRequiredValue) {
|
|
213
223
|
return []
|
|
214
224
|
}
|
|
215
|
-
const errors = await validator(value, instance, instanceData)
|
|
225
|
+
const errors = await validator(value, instance, instanceData, options)
|
|
216
226
|
return [...new Set(flatMap(errors))]
|
|
217
227
|
}
|
|
218
228
|
return _propertyValidator
|
|
219
229
|
}
|
|
220
230
|
|
|
221
231
|
const createModelValidator = (properties, modelValidators = []) => {
|
|
222
|
-
const _modelValidator = async instance => {
|
|
232
|
+
const _modelValidator = async (instance, options) => {
|
|
223
233
|
if (!instance) {
|
|
224
234
|
throw new Error(`Instance cannot be empty`)
|
|
225
235
|
}
|
|
@@ -227,17 +237,17 @@ const createModelValidator = (properties, modelValidators = []) => {
|
|
|
227
237
|
get(properties, 'functions.validators', {})
|
|
228
238
|
)
|
|
229
239
|
const instanceData = await instance.functions.toObj()
|
|
230
|
-
const
|
|
240
|
+
const propertyValidationErrors = await Promise.all(
|
|
231
241
|
keysAndFunctions.map(async ([key, validator]) => {
|
|
232
|
-
return [key, await validator(instance, instanceData)]
|
|
242
|
+
return [key, await validator(instance, instanceData, options)]
|
|
233
243
|
})
|
|
234
244
|
)
|
|
235
245
|
const modelValidationErrors = (
|
|
236
246
|
await Promise.all(
|
|
237
|
-
modelValidators.map(validator => validator(instance, instanceData))
|
|
247
|
+
modelValidators.map(validator => validator(instance, instanceData, options))
|
|
238
248
|
)
|
|
239
249
|
).filter(x => x)
|
|
240
|
-
const propertyErrors =
|
|
250
|
+
const propertyErrors = propertyValidationErrors
|
|
241
251
|
.filter(([_, errors]) => Boolean(errors) && errors.length > 0)
|
|
242
252
|
.reduce((acc, [key, errors]) => {
|
|
243
253
|
return { ...acc, [key]: errors }
|
|
@@ -270,5 +280,6 @@ module.exports = {
|
|
|
270
280
|
createModelValidator,
|
|
271
281
|
arrayType,
|
|
272
282
|
referenceTypeMatch,
|
|
283
|
+
multiplePropertiesMustMatch,
|
|
273
284
|
TYPE_PRIMATIVES,
|
|
274
285
|
}
|
package/test/src/models.test.js
CHANGED
|
@@ -2,7 +2,9 @@ const _ = require('lodash')
|
|
|
2
2
|
const sinon = require('sinon')
|
|
3
3
|
const assert = require('chai').assert
|
|
4
4
|
const { Model } = require('../../src/models')
|
|
5
|
-
const { Property } = require('../../src/properties')
|
|
5
|
+
const { Property, TextProperty, ReferenceProperty } = require('../../src/properties')
|
|
6
|
+
|
|
7
|
+
const TEST_MODEL_1 = Model('MyModel', )
|
|
6
8
|
|
|
7
9
|
describe('/src/models.js', () => {
|
|
8
10
|
describe('#Model()', () => {
|
|
@@ -12,11 +14,10 @@ describe('/src/models.js', () => {
|
|
|
12
14
|
{},
|
|
13
15
|
{
|
|
14
16
|
instanceFunctions: {
|
|
15
|
-
func1: instance =>
|
|
16
|
-
console.log(instance)
|
|
17
|
+
func1: instance => {
|
|
17
18
|
return instance.functions.func2()
|
|
18
19
|
},
|
|
19
|
-
func2: instance =>
|
|
20
|
+
func2: instance => {
|
|
20
21
|
return 'from instance func2'
|
|
21
22
|
},
|
|
22
23
|
},
|
|
@@ -27,6 +28,25 @@ describe('/src/models.js', () => {
|
|
|
27
28
|
const expected = 'from instance func2'
|
|
28
29
|
assert.deepEqual(actual, expected)
|
|
29
30
|
})
|
|
31
|
+
it('should the clients arguments before the model is passed', () => {
|
|
32
|
+
const model = Model(
|
|
33
|
+
'ModelName',
|
|
34
|
+
{},
|
|
35
|
+
{
|
|
36
|
+
modelFunctions: {
|
|
37
|
+
func1: (model) => (input) => {
|
|
38
|
+
return `${input} ${model.func2()}`
|
|
39
|
+
},
|
|
40
|
+
func2: model => () => {
|
|
41
|
+
return 'from func2'
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
const actual = model.func1('hello')
|
|
47
|
+
const expected = 'hello from func2'
|
|
48
|
+
assert.deepEqual(actual, expected)
|
|
49
|
+
})
|
|
30
50
|
it('should pass a functional model to the modelFunction by the time the function is called by a client', () => {
|
|
31
51
|
const model = Model(
|
|
32
52
|
'ModelName',
|
|
@@ -80,6 +100,16 @@ describe('/src/models.js', () => {
|
|
|
80
100
|
})
|
|
81
101
|
})
|
|
82
102
|
describe('#create()', () => {
|
|
103
|
+
it('should have a meta.references.getTheReferenceId when the property has meta.getReferencedId and the key is theReference', () => {
|
|
104
|
+
const model = Model(
|
|
105
|
+
'ModelName',
|
|
106
|
+
{
|
|
107
|
+
theReference: ReferenceProperty(TEST_MODEL_1),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
const instance = model.create({})
|
|
111
|
+
assert.isFunction(instance.meta.references.getTheReferenceId)
|
|
112
|
+
})
|
|
83
113
|
it('should have an "getId" field when no primaryKey is passed', () => {
|
|
84
114
|
const model = Model(
|
|
85
115
|
'ModelName',
|
|
@@ -212,7 +242,7 @@ describe('/src/models.js', () => {
|
|
|
212
242
|
})
|
|
213
243
|
it('should use the value for Property.value when even if Property.defaultValue is set and a value is passed in', async () => {
|
|
214
244
|
const input = {
|
|
215
|
-
myProperty: Property({
|
|
245
|
+
myProperty: Property('MyProperty', {
|
|
216
246
|
value: 'value',
|
|
217
247
|
defaultValue: 'default-value',
|
|
218
248
|
}),
|
|
@@ -225,7 +255,7 @@ describe('/src/models.js', () => {
|
|
|
225
255
|
})
|
|
226
256
|
it('should use the value for Property.value when even if Property.defaultValue is not set and a value is passed in', async () => {
|
|
227
257
|
const input = {
|
|
228
|
-
myProperty: Property({ value: 'value' }),
|
|
258
|
+
myProperty: Property('MyProperty', { value: 'value' }),
|
|
229
259
|
}
|
|
230
260
|
const model = Model('name', input)
|
|
231
261
|
const instance = model.create({ myProperty: 'passed-in' })
|
|
@@ -235,7 +265,7 @@ describe('/src/models.js', () => {
|
|
|
235
265
|
})
|
|
236
266
|
it('should use the value for Property.defaultValue when Property.value is not set and no value is passed in', async () => {
|
|
237
267
|
const input = {
|
|
238
|
-
myProperty: Property({ defaultValue: 'defaultValue' }),
|
|
268
|
+
myProperty: Property('MyProperty', { defaultValue: 'defaultValue' }),
|
|
239
269
|
}
|
|
240
270
|
const model = Model('name', input)
|
|
241
271
|
const instance = model.create({})
|
|
@@ -245,7 +275,7 @@ describe('/src/models.js', () => {
|
|
|
245
275
|
})
|
|
246
276
|
it('should use the value for Property.defaultValue when Property.value is not set and null is passed as a value', async () => {
|
|
247
277
|
const input = {
|
|
248
|
-
myProperty: Property({ defaultValue: 'defaultValue' }),
|
|
278
|
+
myProperty: Property('MyProperty', { defaultValue: 'defaultValue' }),
|
|
249
279
|
}
|
|
250
280
|
const model = Model('name', input)
|
|
251
281
|
const instance = model.create({ myProperty: null })
|
|
@@ -255,8 +285,8 @@ describe('/src/models.js', () => {
|
|
|
255
285
|
})
|
|
256
286
|
it('should return a model with getId and getType for the provided valid keyToProperty', () => {
|
|
257
287
|
const input = {
|
|
258
|
-
id: Property({ required: true }),
|
|
259
|
-
type: Property(),
|
|
288
|
+
id: Property('MyProperty', { required: true }),
|
|
289
|
+
type: Property('MyProperty'),
|
|
260
290
|
}
|
|
261
291
|
const model = Model('name', input)
|
|
262
292
|
const actual = model.create({ id: 'my-id', type: 'my-type' })
|
|
@@ -265,8 +295,8 @@ describe('/src/models.js', () => {
|
|
|
265
295
|
})
|
|
266
296
|
it('should return a model where validate returns one error for id', async () => {
|
|
267
297
|
const input = {
|
|
268
|
-
id: Property({ required: true }),
|
|
269
|
-
type: Property(),
|
|
298
|
+
id: Property('MyProperty', { required: true }),
|
|
299
|
+
type: Property('MyProperty'),
|
|
270
300
|
}
|
|
271
301
|
const model = Model('name', input)
|
|
272
302
|
const instance = model.create({ type: 'my-type' })
|
|
@@ -274,6 +304,17 @@ describe('/src/models.js', () => {
|
|
|
274
304
|
const expected = 1
|
|
275
305
|
assert.equal(Object.values(actual).length, expected)
|
|
276
306
|
})
|
|
307
|
+
it('should return a model where validate returns one error for the missing text property', async () => {
|
|
308
|
+
const input = {
|
|
309
|
+
id: Property('MyProperty', { required: true }),
|
|
310
|
+
text: TextProperty({required: true}),
|
|
311
|
+
}
|
|
312
|
+
const model = Model('name', input)
|
|
313
|
+
const instance = model.create({ id: 'my-id' })
|
|
314
|
+
const actual = await instance.functions.validate()
|
|
315
|
+
const expected = 1
|
|
316
|
+
assert.equal(Object.values(actual).length, expected)
|
|
317
|
+
})
|
|
277
318
|
})
|
|
278
319
|
it('should return an object with a function "create" when called once with valid data', () => {
|
|
279
320
|
const actual = Model('name', {})
|
|
@@ -284,6 +325,20 @@ describe('/src/models.js', () => {
|
|
|
284
325
|
Model('name', { model: 'weeee' }).create()
|
|
285
326
|
})
|
|
286
327
|
})
|
|
328
|
+
describe('#meta.references.getMyReferencedId()', () => {
|
|
329
|
+
it('should return the id from the ReferenceProperty', () => {
|
|
330
|
+
const model = Model(
|
|
331
|
+
'ModelName',
|
|
332
|
+
{
|
|
333
|
+
myReference: ReferenceProperty(TEST_MODEL_1),
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
const instance = model.create({ myReference: 'unit-test-id' })
|
|
337
|
+
const actual = instance.meta.references.getMyReferenceId()
|
|
338
|
+
const expected = 'unit-test-id'
|
|
339
|
+
assert.deepEqual(actual, expected)
|
|
340
|
+
})
|
|
341
|
+
})
|
|
287
342
|
describe('#functions.getPrimaryKey()', () => {
|
|
288
343
|
it('should return the id field when no primaryKey is passed', async () => {
|
|
289
344
|
const model = Model(
|
|
@@ -373,34 +373,34 @@ describe('/src/properties.js', () => {
|
|
|
373
373
|
describe('#Property()', () => {
|
|
374
374
|
it('should throw an exception if config.valueSelector is not a function but is set', () => {
|
|
375
375
|
assert.throws(() => {
|
|
376
|
-
Property({ valueSelector: 'blah' })
|
|
376
|
+
Property('MyProperty', { valueSelector: 'blah' })
|
|
377
377
|
})
|
|
378
378
|
})
|
|
379
379
|
it('should not throw an exception if config.valueSelector is a function', () => {
|
|
380
380
|
assert.doesNotThrow(() => {
|
|
381
|
-
Property({ valueSelector: () => ({}) })
|
|
381
|
+
Property('MyProperty', { valueSelector: () => ({}) })
|
|
382
382
|
})
|
|
383
383
|
})
|
|
384
384
|
describe('#createGetter()', () => {
|
|
385
385
|
it('should return a function even if config.value is set to a value', () => {
|
|
386
|
-
const instance = Property({ value: 'my-value' })
|
|
386
|
+
const instance = Property('MyProperty', { value: 'my-value' })
|
|
387
387
|
const actual = instance.createGetter('not-my-value')
|
|
388
388
|
assert.isFunction(actual)
|
|
389
389
|
})
|
|
390
390
|
it('should return the value passed into config.value regardless of what is passed into the createGetter', async () => {
|
|
391
|
-
const instance = Property({ value: 'my-value' })
|
|
391
|
+
const instance = Property('MyProperty', { value: 'my-value' })
|
|
392
392
|
const actual = await instance.createGetter('not-my-value')()
|
|
393
393
|
const expected = 'my-value'
|
|
394
394
|
assert.deepEqual(actual, expected)
|
|
395
395
|
})
|
|
396
396
|
it('should return the value passed into createGetter when config.value is not set', async () => {
|
|
397
|
-
const instance = Property()
|
|
397
|
+
const instance = Property('MyProperty')
|
|
398
398
|
const actual = await instance.createGetter('my-value')()
|
|
399
399
|
const expected = 'my-value'
|
|
400
400
|
assert.deepEqual(actual, expected)
|
|
401
401
|
})
|
|
402
402
|
it('should return the value of the function passed into createGetter when config.value is not set', async () => {
|
|
403
|
-
const instance = Property()
|
|
403
|
+
const instance = Property('MyProperty')
|
|
404
404
|
const actual = await instance.createGetter(() => 'my-value')()
|
|
405
405
|
const expected = 'my-value'
|
|
406
406
|
assert.deepEqual(actual, expected)
|