functional-models 1.1.20 → 1.2.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/.eslintignore +4 -0
- package/.eslintrc +186 -0
- package/.github/workflows/feature.yml +26 -0
- package/.github/workflows/ut.yml +32 -0
- package/.prettierignore +7 -0
- package/.prettierrc.json +14 -0
- package/LICENSE +674 -0
- package/features/arrayFields.feature +52 -0
- package/features/functions.feature +11 -0
- package/features/model.feature +7 -0
- package/features/stepDefinitions/steps.js +193 -0
- package/features/validation.feature +12 -0
- package/index.js +1 -41
- package/package.json +10 -35
- package/src/constants.js +19 -0
- package/src/errors.js +16 -0
- package/src/functions.js +7 -0
- package/src/index.js +10 -0
- package/src/lazy.js +26 -0
- package/src/models.js +152 -0
- package/src/properties.js +313 -0
- package/src/serialization.js +50 -0
- package/src/utils.js +52 -0
- package/src/validation.js +285 -0
- package/test/base/index.test.js +5 -0
- package/test/src/functions.test.js +45 -0
- package/test/src/index.test.js +5 -0
- package/test/src/lazy.test.js +15 -0
- package/test/src/models.test.js +380 -0
- package/test/src/properties.test.js +554 -0
- package/test/src/serialization.test.js +127 -0
- package/test/src/utils.test.js +54 -0
- package/test/src/validation.test.js +614 -0
- package/constants.d.ts +0 -14
- package/constants.js +0 -19
- package/constants.js.map +0 -1
- package/errors.d.ts +0 -9
- package/errors.js +0 -15
- package/errors.js.map +0 -1
- package/index.d.ts +0 -10
- package/index.js.map +0 -1
- package/interfaces.d.ts +0 -160
- package/interfaces.js +0 -4
- package/interfaces.js.map +0 -1
- package/lazy.d.ts +0 -2
- package/lazy.js +0 -37
- package/lazy.js.map +0 -1
- package/methods.d.ts +0 -4
- package/methods.js +0 -18
- package/methods.js.map +0 -1
- package/models.d.ts +0 -3
- package/models.js +0 -129
- package/models.js.map +0 -1
- package/properties.d.ts +0 -16
- package/properties.js +0 -214
- package/properties.js.map +0 -1
- package/serialization.d.ts +0 -3
- package/serialization.js +0 -49
- package/serialization.js.map +0 -1
- package/utils.d.ts +0 -4
- package/utils.js +0 -44
- package/utils.js.map +0 -1
- package/validation.d.ts +0 -29
- package/validation.js +0 -292
- package/validation.js.map +0 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
const identity = require('lodash/identity')
|
|
2
|
+
const get = require('lodash/get')
|
|
3
|
+
const isFunction = require('lodash/isFunction')
|
|
4
|
+
const merge = require('lodash/merge')
|
|
5
|
+
const {
|
|
6
|
+
createPropertyValidator,
|
|
7
|
+
emptyValidator,
|
|
8
|
+
maxTextLength,
|
|
9
|
+
minTextLength,
|
|
10
|
+
minNumber,
|
|
11
|
+
maxNumber,
|
|
12
|
+
isType,
|
|
13
|
+
referenceTypeMatch,
|
|
14
|
+
meetsRegex,
|
|
15
|
+
} = require('./validation')
|
|
16
|
+
const { PROPERTY_TYPES } = require('./constants')
|
|
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
|
+
}
|
|
24
|
+
|
|
25
|
+
const EMAIL_REGEX =
|
|
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
|
|
27
|
+
|
|
28
|
+
const _getValidatorFromConfigElseEmpty = (config, key, validatorGetter) => {
|
|
29
|
+
if (key in config) {
|
|
30
|
+
return validatorGetter(config[key])
|
|
31
|
+
}
|
|
32
|
+
return emptyValidator
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const _mergeValidators = (config, validators) => {
|
|
36
|
+
return [...validators, ...(config.validators ? config.validators : [])]
|
|
37
|
+
}
|
|
38
|
+
|
|
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 : []
|
|
49
|
+
const lazyLoadMethod = config.lazyLoadMethod || false
|
|
50
|
+
const valueSelector = config.valueSelector || identity
|
|
51
|
+
if (typeof valueSelector !== 'function') {
|
|
52
|
+
throw new Error(`valueSelector must be a function`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
...additionalMetadata,
|
|
58
|
+
getConfig: () => config,
|
|
59
|
+
getChoices,
|
|
60
|
+
getDefaultValue,
|
|
61
|
+
getConstantValue,
|
|
62
|
+
getPropertyType: () => type,
|
|
63
|
+
createGetter: instanceValue => {
|
|
64
|
+
const value = getConstantValue()
|
|
65
|
+
if (value !== undefined) {
|
|
66
|
+
return () => value
|
|
67
|
+
}
|
|
68
|
+
const defaultValue = getDefaultValue()
|
|
69
|
+
if (
|
|
70
|
+
defaultValue !== undefined &&
|
|
71
|
+
(instanceValue === null || instanceValue === undefined)
|
|
72
|
+
) {
|
|
73
|
+
return () => defaultValue
|
|
74
|
+
}
|
|
75
|
+
const method = lazyLoadMethod
|
|
76
|
+
? lazyValue(lazyLoadMethod)
|
|
77
|
+
: typeof instanceValue === 'function'
|
|
78
|
+
? instanceValue
|
|
79
|
+
: () => instanceValue
|
|
80
|
+
return async () => {
|
|
81
|
+
return valueSelector(await method(instanceValue))
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
getValidator: valueGetter => {
|
|
85
|
+
const validator = createPropertyValidator(config)
|
|
86
|
+
const _propertyValidatorWrapper = async (instance, instanceData, options={}) => {
|
|
87
|
+
return validator(await valueGetter(), instance, instanceData, options)
|
|
88
|
+
}
|
|
89
|
+
return _propertyValidatorWrapper
|
|
90
|
+
},
|
|
91
|
+
}
|
|
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)
|
|
103
|
+
|
|
104
|
+
const ReferenceProperty = (model, config = {}, additionalMetadata={}) => {
|
|
105
|
+
if (!model) {
|
|
106
|
+
throw new Error('Must include the referenced model')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const _getModel = () => {
|
|
110
|
+
if (isFunction(model)) {
|
|
111
|
+
return model()
|
|
112
|
+
}
|
|
113
|
+
return model
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const validators = _mergeValidators(config, [referenceTypeMatch(model)])
|
|
117
|
+
|
|
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]
|
|
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 => {
|
|
135
|
+
|
|
136
|
+
const valueIsModelInstance =
|
|
137
|
+
Boolean(instanceValues) && Boolean(instanceValues.functions)
|
|
138
|
+
|
|
139
|
+
const _getInstanceReturn = objToUse => {
|
|
140
|
+
// We need to determine if the object we just go is an actual model instance to determine if we need to make one.
|
|
141
|
+
const objIsModelInstance =
|
|
142
|
+
Boolean(objToUse) && Boolean(objToUse.functions)
|
|
143
|
+
const instance = objIsModelInstance
|
|
144
|
+
? objToUse
|
|
145
|
+
: _getModel().create(objToUse)
|
|
146
|
+
return merge({}, instance, {
|
|
147
|
+
functions: {
|
|
148
|
+
toObj: _getId(instanceValues),
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (valueIsModelInstance) {
|
|
154
|
+
return _getInstanceReturn(instanceValues)
|
|
155
|
+
}
|
|
156
|
+
if (config.fetcher) {
|
|
157
|
+
const id = await _getId(instanceValues)()
|
|
158
|
+
const model = _getModel()
|
|
159
|
+
const obj = await config.fetcher(model, id)
|
|
160
|
+
return _getInstanceReturn(obj)
|
|
161
|
+
}
|
|
162
|
+
return _getId(instanceValues)()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Property(
|
|
166
|
+
PROPERTY_TYPES.ReferenceProperty,
|
|
167
|
+
merge({}, config, {
|
|
168
|
+
validators,
|
|
169
|
+
lazyLoadMethod,
|
|
170
|
+
}),
|
|
171
|
+
{
|
|
172
|
+
...additionalMetadata,
|
|
173
|
+
meta: {
|
|
174
|
+
getReferencedId: (instanceValues) => _getId(instanceValues)(),
|
|
175
|
+
getReferencedModel: _getModel,
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const ArrayProperty = (config = {}, additionalMetadata={}) =>
|
|
182
|
+
Property(
|
|
183
|
+
PROPERTY_TYPES.ArrayProperty,
|
|
184
|
+
{
|
|
185
|
+
defaultValue: [],
|
|
186
|
+
...config,
|
|
187
|
+
isArray: true,
|
|
188
|
+
}, additionalMetadata)
|
|
189
|
+
|
|
190
|
+
const ObjectProperty = (config = {}, additionalMetadata={}) =>
|
|
191
|
+
Property(
|
|
192
|
+
PROPERTY_TYPES.ObjectProperty,
|
|
193
|
+
merge(config, {
|
|
194
|
+
validators: _mergeValidators(config, [isType('object')]),
|
|
195
|
+
}),
|
|
196
|
+
additionalMetadata
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
const TextProperty = (config = {}, additionalMetadata={} ) =>
|
|
200
|
+
Property(
|
|
201
|
+
PROPERTY_TYPES.TextProperty,
|
|
202
|
+
merge(config, {
|
|
203
|
+
isString: true,
|
|
204
|
+
validators: _mergeValidators(config, [
|
|
205
|
+
_getValidatorFromConfigElseEmpty(config, 'maxLength', value =>
|
|
206
|
+
maxTextLength(value)
|
|
207
|
+
),
|
|
208
|
+
_getValidatorFromConfigElseEmpty(config, 'minLength', value =>
|
|
209
|
+
minTextLength(value)
|
|
210
|
+
),
|
|
211
|
+
]),
|
|
212
|
+
}),
|
|
213
|
+
additionalMetadata
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const IntegerProperty = (config = {}, additionalMetadata={}) =>
|
|
217
|
+
Property(
|
|
218
|
+
PROPERTY_TYPES.IntegerProperty,
|
|
219
|
+
merge(config, {
|
|
220
|
+
isInteger: true,
|
|
221
|
+
validators: _mergeValidators(config, [
|
|
222
|
+
_getValidatorFromConfigElseEmpty(config, 'minValue', value =>
|
|
223
|
+
minNumber(value)
|
|
224
|
+
),
|
|
225
|
+
_getValidatorFromConfigElseEmpty(config, 'maxValue', value =>
|
|
226
|
+
maxNumber(value)
|
|
227
|
+
),
|
|
228
|
+
]),
|
|
229
|
+
}),
|
|
230
|
+
additionalMetadata
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
const NumberProperty = (config = {}, additionalMetadata={}) =>
|
|
234
|
+
Property(
|
|
235
|
+
PROPERTY_TYPES.NumberProperty,
|
|
236
|
+
merge(config, {
|
|
237
|
+
isNumber: true,
|
|
238
|
+
validators: _mergeValidators(config, [
|
|
239
|
+
_getValidatorFromConfigElseEmpty(config, 'minValue', value =>
|
|
240
|
+
minNumber(value)
|
|
241
|
+
),
|
|
242
|
+
_getValidatorFromConfigElseEmpty(config, 'maxValue', value =>
|
|
243
|
+
maxNumber(value)
|
|
244
|
+
),
|
|
245
|
+
]),
|
|
246
|
+
}),
|
|
247
|
+
additionalMetadata
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const ConstantValueProperty = (value, config = {}, additionalMetadata={}) =>
|
|
251
|
+
TextProperty(
|
|
252
|
+
merge(config, {
|
|
253
|
+
type: PROPERTY_TYPES.ConstantValueProperty,
|
|
254
|
+
value,
|
|
255
|
+
}),
|
|
256
|
+
additionalMetadata
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
const EmailProperty = (config = {}, additionalMetadata={}) =>
|
|
260
|
+
TextProperty(
|
|
261
|
+
merge(config, {
|
|
262
|
+
type: PROPERTY_TYPES.EmailProperty,
|
|
263
|
+
validators: _mergeValidators(config, [meetsRegex(EMAIL_REGEX)]),
|
|
264
|
+
}),
|
|
265
|
+
additionalMetadata
|
|
266
|
+
)
|
|
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
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
Property,
|
|
301
|
+
UniqueId,
|
|
302
|
+
DateProperty,
|
|
303
|
+
ArrayProperty,
|
|
304
|
+
ReferenceProperty,
|
|
305
|
+
IntegerProperty,
|
|
306
|
+
TextProperty,
|
|
307
|
+
ConstantValueProperty,
|
|
308
|
+
NumberProperty,
|
|
309
|
+
ObjectProperty,
|
|
310
|
+
EmailProperty,
|
|
311
|
+
BooleanProperty,
|
|
312
|
+
createPropertyTitle,
|
|
313
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const { loweredTitleCase } = require('./utils')
|
|
2
|
+
|
|
3
|
+
const SIZE_OF_GET = 'get'.length
|
|
4
|
+
const IGNORABLE_KEYS = ['meta', 'functions']
|
|
5
|
+
|
|
6
|
+
const _getValue = async value => {
|
|
7
|
+
if (value === undefined) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
if (value === null) {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
const type = typeof value
|
|
14
|
+
if (type === 'function') {
|
|
15
|
+
return _getValue(await value())
|
|
16
|
+
}
|
|
17
|
+
// Nested Json
|
|
18
|
+
if (type === 'object' && value.functions && value.functions.toObj) {
|
|
19
|
+
return _getValue(await value.functions.toObj())
|
|
20
|
+
}
|
|
21
|
+
// Dates
|
|
22
|
+
if (type === 'object' && value.toISOString) {
|
|
23
|
+
return _getValue(value.toISOString())
|
|
24
|
+
}
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const _getKey = key => {
|
|
29
|
+
return key.startsWith('get') ? loweredTitleCase(key.slice(SIZE_OF_GET)) : key
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const _shouldIgnoreKey = key => {
|
|
33
|
+
return IGNORABLE_KEYS.includes(key)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const toObj = keyToFunc => async () => {
|
|
37
|
+
return Object.entries(keyToFunc).reduce(async (acc, [key, value]) => {
|
|
38
|
+
const realAcc = await acc
|
|
39
|
+
if (_shouldIgnoreKey(key)) {
|
|
40
|
+
return realAcc
|
|
41
|
+
}
|
|
42
|
+
const keyToUse = _getKey(key)
|
|
43
|
+
const trueValue = await _getValue(value)
|
|
44
|
+
return { ...realAcc, [keyToUse]: trueValue }
|
|
45
|
+
}, Promise.resolve({}))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
toObj,
|
|
50
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const keyBy = require('lodash/keyBy')
|
|
2
|
+
|
|
3
|
+
const HEX = 16
|
|
4
|
+
const FOUR = 4
|
|
5
|
+
const FIFTEEN = 15
|
|
6
|
+
|
|
7
|
+
const getRandomValues = () => {
|
|
8
|
+
const array = new Uint8Array(1)
|
|
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
|
+
return (window.crypto || window.msCrypto).getRandomValues
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return require('get-random-values')(array)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const createUuid = () => {
|
|
23
|
+
// eslint-disable-next-line no-magic-numbers,require-unicode-regexp
|
|
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
|
+
}
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const toTitleCase = string => {
|
|
32
|
+
return `${string.slice(0, 1).toUpperCase()}${string.slice(1)}`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const loweredTitleCase = string => {
|
|
36
|
+
return `${string.slice(0, 1).toLowerCase()}${string.slice(1)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const getObjToArray = array => {
|
|
40
|
+
const obj = keyBy(array)
|
|
41
|
+
return {
|
|
42
|
+
...obj,
|
|
43
|
+
toArray: () => array,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
loweredTitleCase,
|
|
49
|
+
toTitleCase,
|
|
50
|
+
createUuid,
|
|
51
|
+
getObjToArray,
|
|
52
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
const isEmpty = require('lodash/isEmpty')
|
|
2
|
+
const merge = require('lodash/merge')
|
|
3
|
+
const isFunction = require('lodash/isFunction')
|
|
4
|
+
const flatMap = require('lodash/flatMap')
|
|
5
|
+
const get = require('lodash/get')
|
|
6
|
+
|
|
7
|
+
const TYPE_PRIMATIVES = {
|
|
8
|
+
boolean: 'boolean',
|
|
9
|
+
string: 'string',
|
|
10
|
+
object: 'object',
|
|
11
|
+
number: 'number',
|
|
12
|
+
integer: 'integer',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const _trueOrError = (method, error) => value => {
|
|
16
|
+
if (method(value) === false) {
|
|
17
|
+
return error
|
|
18
|
+
}
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const _typeOrError = (type, errorMessage) => value => {
|
|
23
|
+
if (typeof value !== type) {
|
|
24
|
+
return errorMessage
|
|
25
|
+
}
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isType = type => value => {
|
|
30
|
+
return _typeOrError(type, `Must be a ${type}`)(value)
|
|
31
|
+
}
|
|
32
|
+
const isNumber = isType('number')
|
|
33
|
+
const isInteger = _trueOrError(Number.isInteger, 'Must be an integer')
|
|
34
|
+
|
|
35
|
+
const isBoolean = isType('boolean')
|
|
36
|
+
const isString = isType('string')
|
|
37
|
+
const isArray = _trueOrError(v => Array.isArray(v), 'Value is not an array')
|
|
38
|
+
|
|
39
|
+
const PRIMATIVE_TO_SPECIAL_TYPE_VALIDATOR = {
|
|
40
|
+
[TYPE_PRIMATIVES.boolean]: isBoolean,
|
|
41
|
+
[TYPE_PRIMATIVES.string]: isString,
|
|
42
|
+
[TYPE_PRIMATIVES.integer]: isInteger,
|
|
43
|
+
[TYPE_PRIMATIVES.number]: isNumber,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const arrayType = type => value => {
|
|
47
|
+
const arrayError = isArray(value)
|
|
48
|
+
if (arrayError) {
|
|
49
|
+
return arrayError
|
|
50
|
+
}
|
|
51
|
+
const validator = PRIMATIVE_TO_SPECIAL_TYPE_VALIDATOR[type] || isType(type)
|
|
52
|
+
return value.reduce((acc, v) => {
|
|
53
|
+
if (acc) {
|
|
54
|
+
return acc
|
|
55
|
+
}
|
|
56
|
+
return validator(v)
|
|
57
|
+
}, undefined)
|
|
58
|
+
}
|
|
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
|
+
|
|
69
|
+
const meetsRegex =
|
|
70
|
+
(regex, flags, errorMessage = 'Format was invalid') =>
|
|
71
|
+
value => {
|
|
72
|
+
const reg = new RegExp(regex, flags)
|
|
73
|
+
return _trueOrError(v => reg.test(v), errorMessage)(value)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const choices = choiceArray => value => {
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
const bad = value.find(v => !choiceArray.includes(v))
|
|
79
|
+
if (bad) {
|
|
80
|
+
return `${bad} is not a valid choice`
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
if (choiceArray.includes(value) === false) {
|
|
84
|
+
return `${value} is not a valid choice`
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const isDate = value => {
|
|
91
|
+
if (!value) {
|
|
92
|
+
return 'Date value is empty'
|
|
93
|
+
}
|
|
94
|
+
if (!value.toISOString) {
|
|
95
|
+
return 'Value is not a date'
|
|
96
|
+
}
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isRequired = value => {
|
|
101
|
+
if (value === true || value === false) {
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
if (isNumber(value) === undefined) {
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
const empty = isEmpty(value)
|
|
108
|
+
if (empty) {
|
|
109
|
+
if (isDate(value)) {
|
|
110
|
+
return 'A value is required'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const maxNumber = max => value => {
|
|
117
|
+
const numberError = isNumber(value)
|
|
118
|
+
if (numberError) {
|
|
119
|
+
return numberError
|
|
120
|
+
}
|
|
121
|
+
if (value > max) {
|
|
122
|
+
return `The maximum is ${max}`
|
|
123
|
+
}
|
|
124
|
+
return undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const minNumber = min => value => {
|
|
128
|
+
const numberError = isNumber(value)
|
|
129
|
+
if (numberError) {
|
|
130
|
+
return numberError
|
|
131
|
+
}
|
|
132
|
+
if (value < min) {
|
|
133
|
+
return `The minimum is ${min}`
|
|
134
|
+
}
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const maxTextLength = max => value => {
|
|
139
|
+
const stringError = isString(value)
|
|
140
|
+
if (stringError) {
|
|
141
|
+
return stringError
|
|
142
|
+
}
|
|
143
|
+
if (value.length > max) {
|
|
144
|
+
return `The maximum length is ${max}`
|
|
145
|
+
}
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const minTextLength = min => value => {
|
|
150
|
+
const stringError = isString(value)
|
|
151
|
+
if (stringError) {
|
|
152
|
+
return stringError
|
|
153
|
+
}
|
|
154
|
+
if (value.length < min) {
|
|
155
|
+
return `The minimum length is ${min}`
|
|
156
|
+
}
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const referenceTypeMatch = referencedModel => {
|
|
161
|
+
return value => {
|
|
162
|
+
// This needs to stay here, as it delays the creation long enough for
|
|
163
|
+
// self referencing types.
|
|
164
|
+
const model = isFunction(referencedModel)
|
|
165
|
+
? referencedModel()
|
|
166
|
+
: referencedModel
|
|
167
|
+
// Assumption: By the time this is received, value === a model instance.
|
|
168
|
+
const eModel = model.getName()
|
|
169
|
+
const aModel = value.meta.getModel().getName()
|
|
170
|
+
if (eModel !== aModel) {
|
|
171
|
+
return `Model should be ${eModel} instead, recieved ${aModel}`
|
|
172
|
+
}
|
|
173
|
+
return undefined
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const aggregateValidator = methodOrMethods => {
|
|
178
|
+
const toDo = Array.isArray(methodOrMethods)
|
|
179
|
+
? methodOrMethods
|
|
180
|
+
: [methodOrMethods]
|
|
181
|
+
|
|
182
|
+
const _aggregativeValidator = async (...args) => {
|
|
183
|
+
const values = await Promise.all(
|
|
184
|
+
toDo.map(method => {
|
|
185
|
+
return method(...args)
|
|
186
|
+
})
|
|
187
|
+
)
|
|
188
|
+
return values.filter(x => x)
|
|
189
|
+
}
|
|
190
|
+
return _aggregativeValidator
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const emptyValidator = () => []
|
|
194
|
+
|
|
195
|
+
const _boolChoice = method => value => {
|
|
196
|
+
return value ? method : undefined
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const CONFIG_TO_VALIDATE_METHOD = {
|
|
200
|
+
required: _boolChoice(isRequired),
|
|
201
|
+
isInteger: _boolChoice(isInteger),
|
|
202
|
+
isNumber: _boolChoice(isNumber),
|
|
203
|
+
isString: _boolChoice(isString),
|
|
204
|
+
isArray: _boolChoice(isArray),
|
|
205
|
+
isBoolean: _boolChoice(isBoolean),
|
|
206
|
+
choices,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const createPropertyValidator = config => {
|
|
210
|
+
const validators = [
|
|
211
|
+
...Object.entries(config).map(([key, value]) => {
|
|
212
|
+
return (CONFIG_TO_VALIDATE_METHOD[key] || (() => undefined))(value)
|
|
213
|
+
}),
|
|
214
|
+
...(config.validators ? config.validators : []),
|
|
215
|
+
].filter(x => x)
|
|
216
|
+
const isRequiredValue = config.required
|
|
217
|
+
? true
|
|
218
|
+
: validators.includes(isRequired)
|
|
219
|
+
const validator =
|
|
220
|
+
validators.length > 0 ? aggregateValidator(validators) : emptyValidator
|
|
221
|
+
const _propertyValidator = async (value, instance, instanceData={}, options) => {
|
|
222
|
+
if (!value && !isRequiredValue) {
|
|
223
|
+
return []
|
|
224
|
+
}
|
|
225
|
+
const errors = await validator(value, instance, instanceData, options)
|
|
226
|
+
return [...new Set(flatMap(errors))]
|
|
227
|
+
}
|
|
228
|
+
return _propertyValidator
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const createModelValidator = (properties, modelValidators = []) => {
|
|
232
|
+
const _modelValidator = async (instance, options) => {
|
|
233
|
+
if (!instance) {
|
|
234
|
+
throw new Error(`Instance cannot be empty`)
|
|
235
|
+
}
|
|
236
|
+
const keysAndFunctions = Object.entries(
|
|
237
|
+
get(properties, 'functions.validators', {})
|
|
238
|
+
)
|
|
239
|
+
const instanceData = await instance.functions.toObj()
|
|
240
|
+
const propertyValidationErrors = await Promise.all(
|
|
241
|
+
keysAndFunctions.map(async ([key, validator]) => {
|
|
242
|
+
return [key, await validator(instance, instanceData, options)]
|
|
243
|
+
})
|
|
244
|
+
)
|
|
245
|
+
const modelValidationErrors = (
|
|
246
|
+
await Promise.all(
|
|
247
|
+
modelValidators.map(validator => validator(instance, instanceData, options))
|
|
248
|
+
)
|
|
249
|
+
).filter(x => x)
|
|
250
|
+
const propertyErrors = propertyValidationErrors
|
|
251
|
+
.filter(([_, errors]) => Boolean(errors) && errors.length > 0)
|
|
252
|
+
.reduce((acc, [key, errors]) => {
|
|
253
|
+
return { ...acc, [key]: errors }
|
|
254
|
+
}, {})
|
|
255
|
+
return modelValidationErrors.length > 0
|
|
256
|
+
? merge(propertyErrors, { overall: modelValidationErrors })
|
|
257
|
+
: propertyErrors
|
|
258
|
+
}
|
|
259
|
+
return _modelValidator
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = {
|
|
263
|
+
isNumber,
|
|
264
|
+
isBoolean,
|
|
265
|
+
isString,
|
|
266
|
+
isInteger,
|
|
267
|
+
isType,
|
|
268
|
+
isDate,
|
|
269
|
+
isArray,
|
|
270
|
+
isRequired,
|
|
271
|
+
maxNumber,
|
|
272
|
+
minNumber,
|
|
273
|
+
choices,
|
|
274
|
+
maxTextLength,
|
|
275
|
+
minTextLength,
|
|
276
|
+
meetsRegex,
|
|
277
|
+
aggregateValidator,
|
|
278
|
+
emptyValidator,
|
|
279
|
+
createPropertyValidator,
|
|
280
|
+
createModelValidator,
|
|
281
|
+
arrayType,
|
|
282
|
+
referenceTypeMatch,
|
|
283
|
+
multiplePropertiesMustMatch,
|
|
284
|
+
TYPE_PRIMATIVES,
|
|
285
|
+
}
|