@sap/eslint-plugin-cds 3.1.0 → 3.1.2
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/CHANGELOG.md +24 -0
- package/lib/conf/recommended.js +0 -1
- package/lib/index.js +1 -3
- package/lib/parser.js +5 -0
- package/lib/rules/auth-no-empty-restrictions.js +4 -2
- package/lib/rules/auth-restrict-grant-service.js +22 -21
- package/lib/rules/auth-use-requires.js +6 -4
- package/lib/rules/auth-valid-restrict-grant.js +2 -2
- package/lib/rules/auth-valid-restrict-keys.js +2 -2
- package/lib/rules/auth-valid-restrict-to.js +53 -109
- package/lib/rules/auth-valid-restrict-where.js +18 -4
- package/lib/rules/sql-null-comparison.js +1 -1
- package/lib/utils/createRule.js +2 -2
- package/lib/utils/findFuzzy.js +7 -6
- package/lib/utils/rules.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,30 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
8
8
|
|
|
9
|
+
## [3.1.2] - 2024-10-31
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- run inferred rules correctly on Microsoft Windows
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.1.1] - 2024-10-08
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `no-db-keywords` is no longer part of the 'recommended' rules,
|
|
21
|
+
as the cds-compiler takes care of quoting SQL keywords, if they are used as identifiers.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `auth-restrict-grant-service` can now handle invalid values for `@restrict`
|
|
26
|
+
- `auth-use-requires` now handles `null` values for `@restrict.grant.to`
|
|
27
|
+
- `auth-valid-restrict-to` is now more robust against invalid properties such as `__proto__`
|
|
28
|
+
and reduces the number of false positives
|
|
29
|
+
- `auth-valid-restrict-where` now handles and reports invalid value `@restrict: [{where: null}]`
|
|
30
|
+
- `auth-no-empty-restrictions` now handles invalid value `@restrict: [null]`
|
|
31
|
+
|
|
32
|
+
|
|
9
33
|
## [3.1.0] - 2024-09-26
|
|
10
34
|
|
|
11
35
|
### Added
|
package/lib/conf/recommended.js
CHANGED
|
@@ -9,7 +9,6 @@ module.exports = {
|
|
|
9
9
|
'@sap/cds/auth-valid-restrict-keys': 'warn',
|
|
10
10
|
'@sap/cds/auth-valid-restrict-to': 'warn',
|
|
11
11
|
'@sap/cds/auth-valid-restrict-where': 'warn',
|
|
12
|
-
'@sap/cds/no-db-keywords': 'warn',
|
|
13
12
|
'@sap/cds/no-dollar-prefixed-names': 'warn',
|
|
14
13
|
'@sap/cds/no-join-on-draft': 'warn',
|
|
15
14
|
'@sap/cds/sql-cast-suggestion': 'warn',
|
package/lib/index.js
CHANGED
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
* - Expose any 'rules' for use in ESLint
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
const path = require('node:path')
|
|
20
|
-
|
|
21
19
|
const api = require('./api')
|
|
22
20
|
const getConfigs = require('./conf')
|
|
23
21
|
const rules = Object.assign(
|
|
@@ -25,7 +23,7 @@ const rules = Object.assign(
|
|
|
25
23
|
...Object.entries(require('./rules')).map(([k, v]) => ({ [k]: v() }))
|
|
26
24
|
)
|
|
27
25
|
|
|
28
|
-
const packageJson = require(
|
|
26
|
+
const packageJson = require('../package.json')
|
|
29
27
|
|
|
30
28
|
const plugin = {
|
|
31
29
|
meta: {
|
package/lib/parser.js
CHANGED
|
@@ -15,8 +15,13 @@ const Cache = require('./utils/Cache')
|
|
|
15
15
|
const LOG = cds.debug('lint:plugin')
|
|
16
16
|
const colors = require('./utils/Colors')
|
|
17
17
|
const { splitDefName } = require('./utils/rules')
|
|
18
|
+
const packageJson = require('../package.json')
|
|
18
19
|
|
|
19
20
|
module.exports = {
|
|
21
|
+
meta: {
|
|
22
|
+
name: packageJson.name,
|
|
23
|
+
version: packageJson.version
|
|
24
|
+
},
|
|
20
25
|
parse: function (code, options) {
|
|
21
26
|
return module.exports.parseForESLint(code, options).ast
|
|
22
27
|
},
|
|
@@ -58,7 +58,9 @@ function isEmptyRestriction(obj) {
|
|
|
58
58
|
return obj === ''
|
|
59
59
|
if (Array.isArray(obj))
|
|
60
60
|
return obj.length === 0 || obj.some(isEmptyRestriction)
|
|
61
|
-
if (typeof obj === 'object')
|
|
62
|
-
|
|
61
|
+
if (typeof obj === 'object') {
|
|
62
|
+
// handle `null` as non-empty (i.e. ignore)
|
|
63
|
+
return obj && isEmptyRestriction(obj.to)
|
|
64
|
+
}
|
|
63
65
|
return false
|
|
64
66
|
}
|
|
@@ -22,29 +22,30 @@ module.exports = {
|
|
|
22
22
|
service: checkRestrictGrant
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function checkRestrictGrant
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
function checkRestrictGrant(def) {
|
|
26
|
+
if (!Array.isArray(def['@restrict']))
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
const node = context.getNode(def)
|
|
30
|
+
const file = def.$location.file
|
|
31
|
+
const data = { kind: def.kind, name: def.name }
|
|
32
|
+
|
|
33
|
+
for (const entry of def['@restrict']) {
|
|
34
|
+
if (entry?.grant !== undefined) {
|
|
35
|
+
|
|
36
|
+
if (typeof entry.grant === 'string') {
|
|
37
|
+
if (entry.grant !== '*')
|
|
38
|
+
context.report({ messageId: 'limitedGrant', data, node, file })
|
|
39
|
+
|
|
40
|
+
} else if (Array.isArray(entry.grant)) {
|
|
41
|
+
if (entry.grant.length === 0 || !entry.grant.some(val => val === '*'))
|
|
42
|
+
context.report({ messageId: 'limitedGrant', data, node, file })
|
|
43
|
+
|
|
44
|
+
} else {
|
|
45
|
+
// invalid grant value; ignored by this rule
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
}
|
|
@@ -24,23 +24,25 @@ module.exports = {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function checkRestrict (e) {
|
|
27
|
-
if (!
|
|
27
|
+
if (!Array.isArray(e?.['@restrict']))
|
|
28
28
|
return
|
|
29
29
|
|
|
30
30
|
for (const entry of e['@restrict']) {
|
|
31
31
|
// Scenario: `@restrict: [ { to: 'Foo', grant: '*' } ]`
|
|
32
32
|
// There must be no `where` condition, as otherwise it wouldn't be equivalent
|
|
33
|
-
// to `@requires`.
|
|
33
|
+
// to `@requires`. `to` must not be `null`, as `@requires: null` is not the
|
|
34
|
+
// same as `@restrict: [{to:null}]`.
|
|
34
35
|
// See https://cap.cloud.sap/docs/guides/security/authorization#supported-combinations-with-cds-resources
|
|
35
36
|
// for documentation.
|
|
36
|
-
if (entry?.to !== undefined &&
|
|
37
|
+
if (entry?.to !== undefined && entry.to !== null &&
|
|
38
|
+
(entry.grant === '*' || !entry.grant) && entry.where === undefined) {
|
|
37
39
|
context.report({
|
|
38
40
|
messageId: 'useRequires',
|
|
39
41
|
data: { kind: e.kind, name: e.name },
|
|
40
42
|
node: context.getNode(e),
|
|
41
43
|
file: e.$location.file
|
|
42
44
|
})
|
|
43
|
-
|
|
45
|
+
break // max one report per annotation
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { findFuzzy
|
|
3
|
+
const { findFuzzy } = require('../utils/rules')
|
|
4
4
|
|
|
5
5
|
// See https://cap.cloud.sap/docs/guides/security/authorization#restrict-annotation
|
|
6
6
|
// The combination of these events is equivalent to the virtual 'WRITE' event.
|
|
@@ -40,7 +40,7 @@ module.exports = {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function checkRestrictGrant(e) {
|
|
43
|
-
if (!
|
|
43
|
+
if (!Array.isArray(e?.['@restrict']))
|
|
44
44
|
return
|
|
45
45
|
|
|
46
46
|
const node = context.getNode(e)
|
|
@@ -24,7 +24,7 @@ module.exports = {
|
|
|
24
24
|
},
|
|
25
25
|
create (context) {
|
|
26
26
|
return {
|
|
27
|
-
any: checkRestrictKeys
|
|
27
|
+
any: checkRestrictKeys,
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function checkRestrictKeys (e) {
|
|
@@ -32,7 +32,7 @@ module.exports = {
|
|
|
32
32
|
return
|
|
33
33
|
|
|
34
34
|
for (const entry of e['@restrict']) {
|
|
35
|
-
if (typeof entry === 'object' && !isEmptyObject(entry)) {
|
|
35
|
+
if (entry && typeof entry === 'object' && !isEmptyObject(entry)) {
|
|
36
36
|
for (const key of Object.keys(entry)) {
|
|
37
37
|
if (VALID_RESTRICT_PROPERTIES.includes(key))
|
|
38
38
|
continue
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const VALID_PSEUDO_ROLES = ['authenticated-user', 'system-user', 'any']
|
|
3
|
+
const VALID_PSEUDO_ROLES = new Set(['authenticated-user', 'system-user', 'internal-user', 'any'])
|
|
6
4
|
|
|
7
5
|
module.exports = {
|
|
8
6
|
meta: {
|
|
@@ -14,126 +12,72 @@ module.exports = {
|
|
|
14
12
|
url: 'https://cap.cloud.sap/docs/tools/cds-lint/meta/auth-valid-restrict-to',
|
|
15
13
|
},
|
|
16
14
|
messages: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
invalidType: 'Invalid type for value of `@restrict.to`. Must either be string or array of strings.',
|
|
16
|
+
pseudoRoleTypo: "Did you mean pseudo-role '{{valid}}' instead of '{{role}}'?",
|
|
17
|
+
any: 'Role \'any\' overrides all other roles. Replace by \'any\' only.'
|
|
20
18
|
},
|
|
21
19
|
type: 'problem',
|
|
22
20
|
model: 'inferred'
|
|
23
21
|
},
|
|
24
22
|
create (context) {
|
|
25
23
|
return {
|
|
26
|
-
entity: checkRestrictTo
|
|
24
|
+
entity: checkRestrictTo,
|
|
25
|
+
service: checkRestrictTo,
|
|
26
|
+
action: checkRestrictTo,
|
|
27
|
+
function: checkRestrictTo,
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function checkRestrictTo
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!model)
|
|
30
|
+
function checkRestrictTo(def) {
|
|
31
|
+
// TODO: This check also applies to `@requires`. Test that.
|
|
32
|
+
if (!Array.isArray(def?.['@restrict']))
|
|
33
33
|
return
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (p.to) {
|
|
39
|
-
switch (typeof p.to) {
|
|
40
|
-
case 'string':
|
|
41
|
-
if (p.to !== p.to.toLowerCase() && !USER_ROLES.includes(p.to)) {
|
|
42
|
-
USER_ROLES.push(p.to)
|
|
43
|
-
}
|
|
44
|
-
break
|
|
45
|
-
case 'object':
|
|
46
|
-
for (const r in p.to) {
|
|
47
|
-
if (r !== r.toLowerCase() && !USER_ROLES.includes(r)) {
|
|
48
|
-
USER_ROLES.push(r)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
const ROLES = USER_ROLES.concat(VALID_PSEUDO_ROLES)
|
|
57
|
-
|
|
58
|
-
if (e['@restrict']) {
|
|
59
|
-
const node = context.getNode(e)
|
|
60
|
-
const file = e.$location.file
|
|
35
|
+
const node = context.getNode(def)
|
|
36
|
+
const file = def.$location.file
|
|
37
|
+
def['@restrict'].forEach(checkRestrictEntry)
|
|
61
38
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// })
|
|
73
|
-
|
|
74
|
-
for (const entry of e['@restrict']) {
|
|
75
|
-
if (Object.keys(entry).includes('to')) {
|
|
76
|
-
const toValue = entry.to
|
|
39
|
+
function checkRestrictEntry(entry) {
|
|
40
|
+
if (entry?.to !== undefined) {
|
|
41
|
+
const roles = getUserRoles(entry)
|
|
42
|
+
if (roles.every(checkRole)) {
|
|
43
|
+
// all roles are valid
|
|
44
|
+
if (roles.length > 1 && roles.includes('any'))
|
|
45
|
+
context.report({ messageId: 'any', node, file })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
77
49
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
context.report({
|
|
92
|
-
messageId: 'invalidItem',
|
|
93
|
-
data: { invalid: toValue, candidates },
|
|
94
|
-
node,
|
|
95
|
-
file,
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
break
|
|
100
|
-
}
|
|
50
|
+
function getUserRoles(entry) {
|
|
51
|
+
if (typeof entry.to === 'string') {
|
|
52
|
+
return [ entry.to ]
|
|
53
|
+
}
|
|
54
|
+
else if (Array.isArray(entry.to)) {
|
|
55
|
+
return entry.to
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// neither string nor array: report invalid type
|
|
59
|
+
context.report({ messageId: 'invalidType', node, file })
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
}
|
|
101
63
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
file,
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
toValue.forEach(value => {
|
|
121
|
-
if (!ROLES.includes(value)) {
|
|
122
|
-
const candidates = findFuzzy(value, ROLES.sort(), undefined, false, 2)
|
|
123
|
-
if (candidates.length > 0) {
|
|
124
|
-
context.report({
|
|
125
|
-
messageId: 'invalidItem',
|
|
126
|
-
data: { invalid: value, candidates },
|
|
127
|
-
node,
|
|
128
|
-
file,
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
break
|
|
135
|
-
}
|
|
64
|
+
function checkRole(role) {
|
|
65
|
+
if (typeof role !== 'string') {
|
|
66
|
+
context.report({ messageId: 'invalidType', node, file })
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
else if (role !== '') {
|
|
70
|
+
// empty roles handled by auth-no-empty-restrictions
|
|
71
|
+
const roleLowercase = role.toLowerCase()
|
|
72
|
+
if (roleLowercase !== role && VALID_PSEUDO_ROLES.has(roleLowercase)) {
|
|
73
|
+
context.report({
|
|
74
|
+
messageId: 'pseudoRoleTypo',
|
|
75
|
+
data: { role, valid: roleLowercase },
|
|
76
|
+
node, file,
|
|
77
|
+
})
|
|
78
|
+
return false
|
|
136
79
|
}
|
|
80
|
+
return true
|
|
137
81
|
}
|
|
138
82
|
}
|
|
139
83
|
}
|
|
@@ -13,7 +13,8 @@ module.exports = {
|
|
|
13
13
|
},
|
|
14
14
|
severity: 'error',
|
|
15
15
|
messages: {
|
|
16
|
-
|
|
16
|
+
compilationFailed: 'Invalid `where` expression, CDS compilation failed.',
|
|
17
|
+
invalidType: 'Invalid `where` type. Must be a string or expression.',
|
|
17
18
|
},
|
|
18
19
|
type: 'problem',
|
|
19
20
|
model: 'inferred'
|
|
@@ -32,22 +33,35 @@ module.exports = {
|
|
|
32
33
|
return
|
|
33
34
|
|
|
34
35
|
for (const entry of def['@restrict']) {
|
|
35
|
-
if (entry
|
|
36
|
+
if (entry?.where !== undefined) {
|
|
36
37
|
// TODO: Check return value (where xpr)
|
|
37
38
|
checkAndCompileWhereExpression(entry.where)
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function checkAndCompileWhereExpression(where) {
|
|
43
|
+
if (where === null || Array.isArray(where) ||
|
|
44
|
+
(typeof where !== 'object' && typeof where !== 'string')) {
|
|
45
|
+
context.report({
|
|
46
|
+
messageId: 'invalidType',
|
|
47
|
+
node: context.getNode(def),
|
|
48
|
+
file: def.$location.file,
|
|
49
|
+
})
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
try {
|
|
43
54
|
const compileOptions = {}
|
|
44
55
|
return typeof where === 'string'
|
|
45
56
|
? cds.parse.expr(where, compileOptions)
|
|
46
57
|
: where
|
|
47
58
|
|
|
48
|
-
} catch {
|
|
59
|
+
} catch(e) {
|
|
60
|
+
if (e.code !== 'ERR_CDS_COMPILATION_FAILURE')
|
|
61
|
+
throw e
|
|
62
|
+
|
|
49
63
|
context.report({
|
|
50
|
-
messageId: '
|
|
64
|
+
messageId: 'compilationFailed',
|
|
51
65
|
node: context.getNode(def),
|
|
52
66
|
file: def.$location.file,
|
|
53
67
|
})
|
|
@@ -17,7 +17,7 @@ module.exports = {
|
|
|
17
17
|
type: 'problem',
|
|
18
18
|
model: 'parsed',
|
|
19
19
|
messages: {
|
|
20
|
-
nullComparison: `Comparisons against 'null' are always
|
|
20
|
+
nullComparison: `Comparisons against 'null' are always null. Did you mean 'is not null'?`,
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
create(context) {
|
package/lib/utils/createRule.js
CHANGED
|
@@ -105,11 +105,11 @@ module.exports = function createRule(spec) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function isRunningWithCDSLint () {
|
|
108
|
-
return process.argv[
|
|
108
|
+
return process.argv[1].match(/cds(\.js)?$/) && process.argv[2]?.toLowerCase() === 'lint'
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function isRunningWithESLint () {
|
|
112
|
-
return process.argv[
|
|
112
|
+
return process.argv[1].match(/eslint(\.js)?$/)
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
function checkEntryCriteria (meta, cdscontext) {
|
package/lib/utils/findFuzzy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const cache =
|
|
3
|
+
const cache = new Map()
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Levenshtein distance algorithm using recursive calls and cache
|
|
@@ -65,9 +65,9 @@ module.exports = function findFuzzy(input, list, log = null, keepCase = false, t
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function levDistance(a, b) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const cachedObj = cache.get(a)?.get(b)
|
|
69
|
+
if (cachedObj)
|
|
70
|
+
return cachedObj
|
|
71
71
|
|
|
72
72
|
if (a.length === 0) {
|
|
73
73
|
return addToCache(a, b, b.length)
|
|
@@ -93,7 +93,8 @@ function levDistance(a, b) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
function addToCache(a, b, value) {
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
if (!cache.has(a))
|
|
97
|
+
cache.set(a, new Map())
|
|
98
|
+
cache.get(a).set(b, value)
|
|
98
99
|
return value
|
|
99
100
|
}
|
package/lib/utils/rules.js
CHANGED
|
@@ -60,7 +60,7 @@ module.exports = {
|
|
|
60
60
|
* @param {string} value
|
|
61
61
|
* @returns {boolean}
|
|
62
62
|
*/
|
|
63
|
-
isEmptyString
|
|
63
|
+
isEmptyString(value) {
|
|
64
64
|
return value?.trim() === ''
|
|
65
65
|
},
|
|
66
66
|
|
|
@@ -71,7 +71,7 @@ module.exports = {
|
|
|
71
71
|
}
|
|
72
72
|
return true
|
|
73
73
|
}
|
|
74
|
-
if (typeof value !== 'object' || (typeof value === 'object' && !isEmpty(value)) ||
|
|
74
|
+
if (!value || typeof value !== 'object' || (typeof value === 'object' && !isEmpty(value)) ||
|
|
75
75
|
(typeof value === 'object' && value && value.length > 0)) {
|
|
76
76
|
return false
|
|
77
77
|
}
|
package/package.json
CHANGED