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 CHANGED
@@ -1,3 +1,4 @@
1
1
  dist/
2
2
  node_modules/
3
3
  test/
4
+ features/
@@ -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: CI
1
+ name: Unit Tests
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [ master ]
5
+ branches: [master]
6
6
  pull_request:
7
- branches: [ master ]
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
- - run: npm test
27
- - run: npm run coverage
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
@@ -1,4 +1,5 @@
1
1
  node_modules/
2
+ *.json
2
3
  dist/
3
4
  *.html
4
5
  build
package/README.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # Functional Models
2
- ![CI](https://github.com/monolithst/functional-models/actions/workflows/ci.yml/badge.svg)
2
+
3
+ ![Unit Tests](https://github.com/monolithst/functional-models/actions/workflows/ut.yml/badge.svg?branch=master)
4
+ ![Feature Tests](https://github.com/monolithst/functional-models/actions/workflows/feature.yml/badge.svg?branch=master)
3
5
  [![Coverage Status](https://coveralls.io/repos/github/monolithst/functional-models/badge.svg?branch=master)](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,7 @@
1
+ Feature: Models
2
+
3
+ Scenario: A Model With a 4 fields
4
+ Given TestModel1 is used
5
+ When TestModel1b data is inserted
6
+ Then TestModel1b expected fields are found
7
+
@@ -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.1",
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('./dates'),
3
- ...require('./lazy'),
4
- ...require('./objects'),
5
- ...require('./properties'),
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 { lazyValue, createPropertyTitle } = require('./utils')
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
- const lazyProperty = (key, method, { selector = null } = {}) => {
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
- lazyProperty,
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
+ }