functional-models 1.0.1 → 1.0.5
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 +1 -0
- package/.github/workflows/feature.yml +26 -0
- package/.github/workflows/{ci.yml → ut.yml} +6 -7
- package/.prettierignore +1 -0
- package/README.md +4 -5
- package/features/model.feature +7 -0
- package/features/stepDefinitions/steps.js +87 -0
- package/features/validation.feature +12 -0
- package/package.json +10 -3
- package/src/fields.js +106 -0
- package/src/index.js +4 -4
- package/src/lazy.js +13 -10
- package/src/models.js +55 -0
- package/src/utils.js +1 -17
- package/src/validation.js +173 -0
- package/test/src/fields.test.js +160 -0
- package/test/src/lazy.test.js +9 -19
- package/test/src/models.test.js +97 -0
- package/test/src/utils.test.js +1 -10
- package/test/src/validation.test.js +318 -0
- package/src/dates.js +0 -13
- package/src/objects.js +0 -23
- package/src/properties.js +0 -31
- package/src/references.js +0 -38
- package/test/src/dates.test.js +0 -33
- package/test/src/objects.test.js +0 -50
- package/test/src/properties.test.js +0 -60
- package/test/src/references.test.js +0 -84
package/.eslintignore
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Feature Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
node-version: [12.x, 14.x, 15.x]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v2
|
|
19
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
20
|
+
uses: actions/setup-node@v2
|
|
21
|
+
with:
|
|
22
|
+
node-version: ${{ matrix.node-version }}
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm install
|
|
25
|
+
- name: Run Cucumber Tests
|
|
26
|
+
run: npm run feature-tests
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: Unit Tests
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
-
branches: [
|
|
5
|
+
branches: [master]
|
|
6
6
|
pull_request:
|
|
7
|
-
branches: [
|
|
7
|
+
branches: [master]
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
build:
|
|
11
|
-
|
|
12
11
|
runs-on: ubuntu-latest
|
|
13
12
|
|
|
14
13
|
strategy:
|
|
@@ -23,11 +22,11 @@ jobs:
|
|
|
23
22
|
node-version: ${{ matrix.node-version }}
|
|
24
23
|
- name: Install dependencies
|
|
25
24
|
run: npm install
|
|
26
|
-
-
|
|
27
|
-
|
|
25
|
+
- name: Run Unit Tests
|
|
26
|
+
run: npm test
|
|
27
|
+
- run: npm run coverage
|
|
28
28
|
|
|
29
29
|
- name: Coveralls
|
|
30
30
|
uses: coverallsapp/github-action@master
|
|
31
31
|
with:
|
|
32
32
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
33
|
-
|
package/.prettierignore
CHANGED
package/README.md
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Functional Models
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
3
5
|
[](https://coveralls.io/github/monolithst/functional-models?branch=master)
|
|
4
6
|
|
|
5
|
-
Love functional javascript but still like composing objects/models? This is the library for you.
|
|
7
|
+
Love functional javascript but still like composing objects/models? This is the library for you.
|
|
6
8
|
This library empowers the creation of pure JavaScript function based models that can be used on a client, a web frontend, and/or a backend all the same time. Use this library to create readable, read-only, models.
|
|
7
9
|
|
|
8
|
-
|
|
9
10
|
## Example Usage
|
|
10
11
|
|
|
11
12
|
const {
|
|
@@ -51,5 +52,3 @@ This library empowers the creation of pure JavaScript function based models that
|
|
|
51
52
|
console.log(sameTruck.getModel()) // 'F-150'
|
|
52
53
|
console.log(sameTruck.getColor()) // 'White'
|
|
53
54
|
console.log(sameTruck.getYear()) // 2013
|
|
54
|
-
|
|
55
|
-
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const assert = require('chai').assert
|
|
2
|
+
const flatMap = require('lodash/flatMap')
|
|
3
|
+
const { Given, When, Then } = require('@cucumber/cucumber')
|
|
4
|
+
|
|
5
|
+
const { createModel, field } = require('../../index')
|
|
6
|
+
|
|
7
|
+
const MODEL_DEFINITIONS = {
|
|
8
|
+
TestModel1: createModel({
|
|
9
|
+
name: field({ required: true }),
|
|
10
|
+
type: field({ required: true, isString: true }),
|
|
11
|
+
flag: field({ required: true, isNumber: true }),
|
|
12
|
+
}),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MODEL_INPUT_VALUES = {
|
|
16
|
+
TestModel1a: {
|
|
17
|
+
name: 'my-name',
|
|
18
|
+
type: 1,
|
|
19
|
+
flag: '1',
|
|
20
|
+
},
|
|
21
|
+
TestModel1b: {
|
|
22
|
+
name: 'my-name',
|
|
23
|
+
type: 'a-type',
|
|
24
|
+
flag: 1,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const EXPECTED_FIELDS = {
|
|
29
|
+
TestModel1b: ['getName', 'getType', 'getFlag', 'meta', 'functions'],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Given(
|
|
33
|
+
'the {word} has been created, with {word} inputs provided',
|
|
34
|
+
function (modelDefinition, modelInputValues) {
|
|
35
|
+
const def = MODEL_DEFINITIONS[modelDefinition]
|
|
36
|
+
const input = MODEL_INPUT_VALUES[modelInputValues]
|
|
37
|
+
if (!def) {
|
|
38
|
+
throw new Error(`${modelDefinition} did not result in a definition`)
|
|
39
|
+
}
|
|
40
|
+
if (!input) {
|
|
41
|
+
throw new Error(`${modelInputValues} did not result in an input`)
|
|
42
|
+
}
|
|
43
|
+
this.instance = def(input)
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
When('functions.validate is called', function () {
|
|
48
|
+
return this.instance.functions.validate.model().then(x => {
|
|
49
|
+
this.errors = x
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
Then('an array of {int} errors is shown', function (errorCount) {
|
|
54
|
+
const errors = flatMap(Object.values(this.errors))
|
|
55
|
+
if (errors.length !== errorCount) {
|
|
56
|
+
console.error(this.errors)
|
|
57
|
+
}
|
|
58
|
+
assert.equal(errors.length, errorCount)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
Given('{word} is used', function (modelDefinition) {
|
|
62
|
+
const def = MODEL_DEFINITIONS[modelDefinition]
|
|
63
|
+
if (!def) {
|
|
64
|
+
throw new Error(`${modelDefinition} did not result in a definition`)
|
|
65
|
+
}
|
|
66
|
+
this.modelDefinition = def
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
When('{word} data is inserted', function (modelInputValues) {
|
|
70
|
+
const input = MODEL_INPUT_VALUES[modelInputValues]
|
|
71
|
+
if (!input) {
|
|
72
|
+
throw new Error(`${modelInputValues} did not result in an input`)
|
|
73
|
+
}
|
|
74
|
+
this.instance = this.modelDefinition(input)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
Then('{word} expected fields are found', function (fields) {
|
|
78
|
+
const propertyArray = EXPECTED_FIELDS[fields]
|
|
79
|
+
if (!propertyArray) {
|
|
80
|
+
throw new Error(`${fields} did not result in fields`)
|
|
81
|
+
}
|
|
82
|
+
propertyArray.forEach(key => {
|
|
83
|
+
if (!(key in this.instance)) {
|
|
84
|
+
throw new Error(`Did not find ${key} in model`)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Feature: Validation
|
|
2
|
+
|
|
3
|
+
Scenario: Creating TestModel1 with required arguments but not having them.
|
|
4
|
+
Given the TestModel1 has been created, with TestModel1a inputs provided
|
|
5
|
+
When functions.validate is called
|
|
6
|
+
Then an array of 2 errors is shown
|
|
7
|
+
|
|
8
|
+
Scenario: Creating TestModel1 with required arguments and having them.
|
|
9
|
+
Given the TestModel1 has been created, with TestModel1b inputs provided
|
|
10
|
+
When functions.validate is called
|
|
11
|
+
Then an array of 0 errors is shown
|
|
12
|
+
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "functional-models",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "A library for creating JavaScript function based models.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "nyc --all mocha --recursive ./test/**/*.test.js",
|
|
8
|
+
"feature-tests": "./node_modules/@cucumber/cucumber/bin/cucumber-js",
|
|
8
9
|
"coverage": "nyc --all --reporter=lcov npm test"
|
|
9
10
|
},
|
|
10
11
|
"repository": {
|
|
@@ -26,11 +27,16 @@
|
|
|
26
27
|
},
|
|
27
28
|
"homepage": "https://github.com/monolithst/functional-models#readme",
|
|
28
29
|
"nyc": {
|
|
29
|
-
"all": true
|
|
30
|
+
"all": true,
|
|
31
|
+
"exclude": [
|
|
32
|
+
"features/stepDefinitions/*",
|
|
33
|
+
"test/*"
|
|
34
|
+
]
|
|
30
35
|
},
|
|
31
36
|
"devDependencies": {
|
|
32
37
|
"babel-eslint": "^10.1.0",
|
|
33
38
|
"chai": "^4.3.0",
|
|
39
|
+
"cucumber": "^7.0.0-rc.0",
|
|
34
40
|
"eslint": "^7.19.0",
|
|
35
41
|
"eslint-config-prettier": "^7.2.0",
|
|
36
42
|
"eslint-plugin-functional": "^3.2.1",
|
|
@@ -42,6 +48,7 @@
|
|
|
42
48
|
},
|
|
43
49
|
"dependencies": {
|
|
44
50
|
"get-random-values": "^1.2.2",
|
|
45
|
-
"lazy-property": "^1.0.0"
|
|
51
|
+
"lazy-property": "^1.0.0",
|
|
52
|
+
"lodash": "^4.17.21"
|
|
46
53
|
}
|
|
47
54
|
}
|
package/src/fields.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const identity = require('lodash/identity')
|
|
2
|
+
const { createFieldValidator } = require('./validation')
|
|
3
|
+
const { createUuid } = require('./utils')
|
|
4
|
+
const { lazyValue } = require('./lazy')
|
|
5
|
+
|
|
6
|
+
const field = (config = {}) => {
|
|
7
|
+
const value = config.value || undefined
|
|
8
|
+
const defaultValue = config.defaultValue || undefined
|
|
9
|
+
const lazyLoadMethod = config.lazyLoadMethod || false
|
|
10
|
+
const valueSelector = config.valueSelector || identity
|
|
11
|
+
if (typeof valueSelector !== 'function') {
|
|
12
|
+
throw new Error(`valueSelector must be a function`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
createGetter: instanceValue => {
|
|
17
|
+
if (value !== undefined) {
|
|
18
|
+
return () => value
|
|
19
|
+
}
|
|
20
|
+
if (
|
|
21
|
+
defaultValue !== undefined &&
|
|
22
|
+
(instanceValue === null || instanceValue === undefined)
|
|
23
|
+
) {
|
|
24
|
+
return () => defaultValue
|
|
25
|
+
}
|
|
26
|
+
const method = lazyLoadMethod
|
|
27
|
+
? lazyValue(lazyLoadMethod)
|
|
28
|
+
: typeof instanceValue === 'function'
|
|
29
|
+
? instanceValue
|
|
30
|
+
: () => instanceValue
|
|
31
|
+
return async () => {
|
|
32
|
+
return valueSelector(await method(instanceValue))
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
getValidator: valueGetter => {
|
|
36
|
+
return async () => {
|
|
37
|
+
return createFieldValidator(config)(await valueGetter())
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const uniqueId = config =>
|
|
44
|
+
field({
|
|
45
|
+
...config,
|
|
46
|
+
lazyLoadMethod: value => {
|
|
47
|
+
if (!value) {
|
|
48
|
+
return createUuid()
|
|
49
|
+
}
|
|
50
|
+
return value
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const dateField = config =>
|
|
55
|
+
field({
|
|
56
|
+
...config,
|
|
57
|
+
lazyLoadMethod: value => {
|
|
58
|
+
if (!value && config.autoNow) {
|
|
59
|
+
return new Date()
|
|
60
|
+
}
|
|
61
|
+
return value
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const referenceField = config => {
|
|
66
|
+
return field({
|
|
67
|
+
...config,
|
|
68
|
+
lazyLoadMethod: async smartObj => {
|
|
69
|
+
const _getId = () => {
|
|
70
|
+
if (!smartObj) {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
return smartObj && smartObj.id
|
|
74
|
+
? smartObj.id
|
|
75
|
+
: smartObj.getId
|
|
76
|
+
? smartObj.getId()
|
|
77
|
+
: smartObj
|
|
78
|
+
}
|
|
79
|
+
const _getSmartObjReturn = objToUse => {
|
|
80
|
+
return {
|
|
81
|
+
...objToUse,
|
|
82
|
+
functions: {
|
|
83
|
+
...(objToUse.functions ? objToUse.functions : {}),
|
|
84
|
+
toJson: _getId,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const valueIsSmartObj = smartObj && smartObj.functions
|
|
89
|
+
if (valueIsSmartObj) {
|
|
90
|
+
return _getSmartObjReturn(smartObj)
|
|
91
|
+
}
|
|
92
|
+
if (config.fetcher) {
|
|
93
|
+
const obj = await config.fetcher(smartObj)
|
|
94
|
+
return _getSmartObjReturn(obj)
|
|
95
|
+
}
|
|
96
|
+
return _getId(smartObj)
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
field,
|
|
103
|
+
uniqueId,
|
|
104
|
+
dateField,
|
|
105
|
+
referenceField,
|
|
106
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
...require('./
|
|
3
|
-
...require('./
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
...require('./fields'),
|
|
3
|
+
...require('./models'),
|
|
4
|
+
validation: require('./validation'),
|
|
5
|
+
serialization: require('./serialization'),
|
|
6
6
|
}
|
package/src/lazy.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
const
|
|
1
|
+
const lazyValue = method => {
|
|
2
|
+
/* eslint-disable functional/no-let */
|
|
3
|
+
let value = undefined
|
|
4
|
+
let called = false
|
|
5
|
+
return async (...args) => {
|
|
6
|
+
if (!called) {
|
|
7
|
+
value = await method(...args)
|
|
8
|
+
// eslint-disable-next-line require-atomic-updates
|
|
9
|
+
called = true
|
|
10
|
+
}
|
|
2
11
|
|
|
3
|
-
|
|
4
|
-
const lazy = lazyValue(method)
|
|
5
|
-
const propertyKey = createPropertyTitle(key)
|
|
6
|
-
return {
|
|
7
|
-
[propertyKey]: async () => {
|
|
8
|
-
const value = await lazy()
|
|
9
|
-
return selector ? selector(value) : value
|
|
10
|
-
},
|
|
12
|
+
return value
|
|
11
13
|
}
|
|
14
|
+
/* eslint-enable functional/no-let */
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
module.exports = {
|
|
15
|
-
|
|
18
|
+
lazyValue,
|
|
16
19
|
}
|
package/src/models.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const merge = require('lodash/merge')
|
|
2
|
+
const get = require('lodash/get')
|
|
3
|
+
const { toJson } = require('./serialization')
|
|
4
|
+
const { createPropertyTitle } = require('./utils')
|
|
5
|
+
const { createModelValidator } = require('./validation')
|
|
6
|
+
|
|
7
|
+
const SYSTEM_KEYS = ['meta', 'functions']
|
|
8
|
+
|
|
9
|
+
const PROTECTED_KEYS = ['model']
|
|
10
|
+
|
|
11
|
+
const createModel = keyToField => {
|
|
12
|
+
PROTECTED_KEYS.forEach(key => {
|
|
13
|
+
if (key in keyToField) {
|
|
14
|
+
throw new Error(`Cannot use ${key}. This is a protected value.`)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
const systemProperties = SYSTEM_KEYS.reduce((acc, key) => {
|
|
18
|
+
const value = get(keyToField, key, {})
|
|
19
|
+
return { ...acc, [key]: value }
|
|
20
|
+
}, {})
|
|
21
|
+
const nonSystemProperties = Object.entries(keyToField).filter(
|
|
22
|
+
([key, _]) => !(key in SYSTEM_KEYS)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
return instanceValues => {
|
|
26
|
+
const loadedInternals = nonSystemProperties.reduce((acc, [key, field]) => {
|
|
27
|
+
const fieldGetter = field.createGetter(instanceValues[key])
|
|
28
|
+
const fieldValidator = field.getValidator(fieldGetter)
|
|
29
|
+
const getFieldKey = createPropertyTitle(key)
|
|
30
|
+
const fleshedOutField = {
|
|
31
|
+
[getFieldKey]: fieldGetter,
|
|
32
|
+
functions: {
|
|
33
|
+
validate: {
|
|
34
|
+
[key]: fieldValidator,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
return merge(acc, fleshedOutField)
|
|
39
|
+
}, {})
|
|
40
|
+
const allUserData = merge(systemProperties, loadedInternals)
|
|
41
|
+
const internalFunctions = {
|
|
42
|
+
functions: {
|
|
43
|
+
toJson: toJson(loadedInternals),
|
|
44
|
+
validate: {
|
|
45
|
+
model: createModelValidator(loadedInternals),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
return merge(allUserData, internalFunctions)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
createModel,
|
|
55
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -11,22 +11,6 @@ const createPropertyTitle = key => {
|
|
|
11
11
|
return `get${goodName}`
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const lazyValue = method => {
|
|
15
|
-
/* eslint-disable functional/no-let */
|
|
16
|
-
let value = undefined
|
|
17
|
-
let called = false
|
|
18
|
-
return async () => {
|
|
19
|
-
if (!called) {
|
|
20
|
-
value = await method()
|
|
21
|
-
// eslint-disable-next-line require-atomic-updates
|
|
22
|
-
called = true
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return value
|
|
26
|
-
}
|
|
27
|
-
/* eslint-enable functional/no-let */
|
|
28
|
-
}
|
|
29
|
-
|
|
30
14
|
const getCryptoRandomValues = () => {
|
|
31
15
|
if (typeof window !== 'undefined') {
|
|
32
16
|
return (window.crypto || window.msCrypto).getRandomValues
|
|
@@ -53,6 +37,6 @@ const loweredTitleCase = string => {
|
|
|
53
37
|
module.exports = {
|
|
54
38
|
createUuid,
|
|
55
39
|
loweredTitleCase,
|
|
56
|
-
lazyValue,
|
|
57
40
|
createPropertyTitle,
|
|
41
|
+
toTitleCase,
|
|
58
42
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const isEmpty = require('lodash/isEmpty')
|
|
2
|
+
const flatMap = require('lodash/flatMap')
|
|
3
|
+
const get = require('lodash/get')
|
|
4
|
+
|
|
5
|
+
const _trueOrError = (method, error) => value => {
|
|
6
|
+
if (method(value) === false) {
|
|
7
|
+
return error
|
|
8
|
+
}
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const _typeOrError = (type, errorMessage) => value => {
|
|
13
|
+
if (typeof value !== type) {
|
|
14
|
+
return errorMessage
|
|
15
|
+
}
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isType = type => value => {
|
|
20
|
+
return _typeOrError(type, `Must be a ${type}`)(value)
|
|
21
|
+
}
|
|
22
|
+
const isNumber = isType('number')
|
|
23
|
+
const isInteger = _trueOrError(v => {
|
|
24
|
+
const numberError = isNumber(v)
|
|
25
|
+
if (numberError) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
return Number.isNaN(parseInt(v, 10)) === false
|
|
29
|
+
}, 'Must be an integer')
|
|
30
|
+
|
|
31
|
+
const isBoolean = isType('boolean')
|
|
32
|
+
const isString = isType('string')
|
|
33
|
+
|
|
34
|
+
const meetsRegex =
|
|
35
|
+
(regex, flags, errorMessage = 'Format was invalid') =>
|
|
36
|
+
value => {
|
|
37
|
+
const reg = new RegExp(regex, flags)
|
|
38
|
+
return _trueOrError(v => reg.test(v), errorMessage)(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const choices = choiceArray => value => {
|
|
42
|
+
if (choiceArray.includes(value) === false) {
|
|
43
|
+
return 'Not a valid choice'
|
|
44
|
+
}
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isRequired = value => {
|
|
49
|
+
if (value === true || value === false) {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
if (isNumber(value) === undefined) {
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
return isEmpty(value) ? 'A value is required' : undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const maxNumber = max => value => {
|
|
59
|
+
const numberError = isNumber(value)
|
|
60
|
+
if (numberError) {
|
|
61
|
+
return numberError
|
|
62
|
+
}
|
|
63
|
+
if (value > max) {
|
|
64
|
+
return `The maximum is ${max}`
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const minNumber = min => value => {
|
|
70
|
+
const numberError = isNumber(value)
|
|
71
|
+
if (numberError) {
|
|
72
|
+
return numberError
|
|
73
|
+
}
|
|
74
|
+
if (value < min) {
|
|
75
|
+
return `The minimum is ${min}`
|
|
76
|
+
}
|
|
77
|
+
return undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const maxTextLength = max => value => {
|
|
81
|
+
const stringError = isString(value)
|
|
82
|
+
if (stringError) {
|
|
83
|
+
return stringError
|
|
84
|
+
}
|
|
85
|
+
if (value.length > max) {
|
|
86
|
+
return `The maximum length is ${max}`
|
|
87
|
+
}
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const minTextLength = min => value => {
|
|
92
|
+
const stringError = isString(value)
|
|
93
|
+
if (stringError) {
|
|
94
|
+
return stringError
|
|
95
|
+
}
|
|
96
|
+
if (value.length < min) {
|
|
97
|
+
return `The minimum length is ${min}`
|
|
98
|
+
}
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const aggregateValidator = methodOrMethods => async value => {
|
|
103
|
+
const toDo = Array.isArray(methodOrMethods)
|
|
104
|
+
? methodOrMethods
|
|
105
|
+
: [methodOrMethods]
|
|
106
|
+
const values = await Promise.all(
|
|
107
|
+
toDo.map(method => {
|
|
108
|
+
return method(value)
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
return values.filter(x => x)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const emptyValidator = () => []
|
|
115
|
+
|
|
116
|
+
const _boolChoice = method => value => {
|
|
117
|
+
return value ? method : undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const CONFIG_TO_VALIDATE_METHOD = {
|
|
121
|
+
required: _boolChoice(isRequired),
|
|
122
|
+
isInteger: _boolChoice(isInteger),
|
|
123
|
+
isNumber: _boolChoice(isNumber),
|
|
124
|
+
isString: _boolChoice(isString),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const createFieldValidator = config => {
|
|
128
|
+
const validators = [
|
|
129
|
+
...Object.entries(config).map(([key, value]) => {
|
|
130
|
+
return (CONFIG_TO_VALIDATE_METHOD[key] || (() => undefined))(value)
|
|
131
|
+
}),
|
|
132
|
+
...(config.validators ? config.validators : []),
|
|
133
|
+
].filter(x => x)
|
|
134
|
+
const validator =
|
|
135
|
+
validators.length > 0 ? aggregateValidator(validators) : emptyValidator
|
|
136
|
+
return async value => {
|
|
137
|
+
const errors = await validator(value)
|
|
138
|
+
return flatMap(errors)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const createModelValidator = fields => async () => {
|
|
143
|
+
const keysAndFunctions = Object.entries(get(fields, 'functions.validate', {}))
|
|
144
|
+
const data = await Promise.all(
|
|
145
|
+
keysAndFunctions.map(async ([key, validator]) => {
|
|
146
|
+
return [key, await validator()]
|
|
147
|
+
})
|
|
148
|
+
)
|
|
149
|
+
return data
|
|
150
|
+
.filter(([_, errors]) => Boolean(errors) && errors.length > 0)
|
|
151
|
+
.reduce((acc, [key, errors]) => {
|
|
152
|
+
return { ...acc, [key]: errors }
|
|
153
|
+
}, {})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
isNumber,
|
|
158
|
+
isBoolean,
|
|
159
|
+
isString,
|
|
160
|
+
isInteger,
|
|
161
|
+
isType,
|
|
162
|
+
isRequired,
|
|
163
|
+
maxNumber,
|
|
164
|
+
minNumber,
|
|
165
|
+
choices,
|
|
166
|
+
maxTextLength,
|
|
167
|
+
minTextLength,
|
|
168
|
+
meetsRegex,
|
|
169
|
+
aggregateValidator,
|
|
170
|
+
emptyValidator,
|
|
171
|
+
createFieldValidator,
|
|
172
|
+
createModelValidator,
|
|
173
|
+
}
|