@sap/eslint-plugin-cds 2.5.0 → 2.6.1
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/README.md +2 -1
- package/lib/api/index.js +9 -9
- package/lib/conf/all.js +20 -19
- package/lib/conf/index.js +10 -10
- package/lib/conf/recommended.js +17 -16
- package/lib/constants.js +16 -14
- package/lib/index.js +17 -11
- package/lib/parser.js +90 -82
- package/lib/rules/assoc2many-ambiguous-key.js +71 -70
- package/lib/rules/auth-no-empty-restrictions.js +16 -15
- package/lib/rules/auth-use-requires.js +19 -18
- package/lib/rules/auth-valid-restrict-grant.js +49 -46
- package/lib/rules/auth-valid-restrict-keys.js +19 -18
- package/lib/rules/auth-valid-restrict-to.js +68 -64
- package/lib/rules/auth-valid-restrict-where.js +44 -43
- package/lib/rules/extension-restrictions.js +69 -0
- package/lib/rules/index.js +23 -22
- package/lib/rules/latest-cds-version.js +21 -20
- package/lib/rules/min-node-version.js +22 -22
- package/lib/rules/no-db-keywords.js +21 -27
- package/lib/rules/no-dollar-prefixed-names.js +12 -11
- package/lib/rules/no-join-on-draft.js +27 -0
- package/lib/rules/require-2many-oncond.js +8 -8
- package/lib/rules/sql-cast-suggestion.js +13 -12
- package/lib/rules/start-elements-lowercase.js +42 -41
- package/lib/rules/start-entities-uppercase.js +26 -25
- package/lib/rules/valid-csv-header.js +58 -57
- package/lib/types.d.ts +1 -0
- package/lib/utils/Cache.js +17 -17
- package/lib/utils/Colors.js +8 -8
- package/lib/utils/createRule.js +172 -153
- package/lib/utils/findFuzzy.js +37 -38
- package/lib/utils/genDocs.js +224 -242
- package/lib/utils/getConfigPath.js +27 -27
- package/lib/utils/getConfiguredFileTypes.js +4 -4
- package/lib/utils/getFileExtensions.js +3 -3
- package/lib/utils/getProjectRootPath.js +25 -0
- package/lib/utils/isConfiguredFileType.js +11 -11
- package/lib/utils/rules.js +59 -59
- package/lib/utils/runRuleTester.js +76 -71
- package/package.json +7 -1
- package/lib/rules/no-join-on-draft-enabled-entities.js +0 -25
- package/lib/utils/createRuleDocs.js +0 -361
- package/lib/utils/jsonc.js +0 -1
- package/lib/utils/jsoncParser.js +0 -1
|
@@ -1,153 +1,154 @@
|
|
|
1
|
-
const cds = require(
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
2
|
|
|
3
3
|
/** @type {import('../types').Rule} */
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
6
|
meta: {
|
|
7
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
7
8
|
docs: {
|
|
8
9
|
description:
|
|
9
|
-
|
|
10
|
-
category:
|
|
10
|
+
'Ambiguous key with a `TO MANY` relationship since entries could appear multiple times with the same key.',
|
|
11
|
+
category: 'Model Validation',
|
|
11
12
|
recommended: true
|
|
12
13
|
},
|
|
13
|
-
type:
|
|
14
|
-
model:
|
|
14
|
+
type: 'problem',
|
|
15
|
+
model: 'inferred'
|
|
15
16
|
},
|
|
16
|
-
create(context) {
|
|
17
|
-
return
|
|
17
|
+
create (context) {
|
|
18
|
+
return checkAssocs
|
|
18
19
|
|
|
19
|
-
function
|
|
20
|
-
let csnOdata
|
|
21
|
-
const m = context.getModel()
|
|
22
|
-
if (!m) return
|
|
20
|
+
function checkAssocs () {
|
|
21
|
+
let csnOdata
|
|
22
|
+
const m = context.getModel()
|
|
23
|
+
if (!m) return
|
|
23
24
|
if (m && m.definitions) {
|
|
24
|
-
csnOdata = cds.compile.for.odata(m)
|
|
25
|
-
const csnOdataLinked = cds.linked(csnOdata)
|
|
26
|
-
associationCardinalityFlaw(csnOdataLinked, context)
|
|
25
|
+
csnOdata = cds.compile.for.odata(m)
|
|
26
|
+
const csnOdataLinked = cds.linked(csnOdata)
|
|
27
|
+
associationCardinalityFlaw(csnOdataLinked, context)
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
}
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
function associationCardinalityFlaw(csn, context) {
|
|
33
|
+
function associationCardinalityFlaw (csn, context) {
|
|
33
34
|
processEntity(csn, (definition, sourceEntity, sourceAlias) => {
|
|
34
|
-
let refCardinalityMult = false
|
|
35
|
-
let refPlainElement = false
|
|
35
|
+
let refCardinalityMult = false
|
|
36
|
+
let refPlainElement = false
|
|
36
37
|
processElement(
|
|
37
38
|
csn,
|
|
38
39
|
definition,
|
|
39
40
|
sourceEntity,
|
|
40
41
|
sourceAlias,
|
|
41
42
|
() => {
|
|
42
|
-
refCardinalityMult = false
|
|
43
|
-
refPlainElement = false
|
|
43
|
+
refCardinalityMult = false
|
|
44
|
+
refPlainElement = false
|
|
44
45
|
},
|
|
45
46
|
(refEntity, refElement) => {
|
|
46
|
-
if (refElement.type ===
|
|
47
|
-
if (refElement.cardinality && refElement.cardinality.max ===
|
|
48
|
-
refCardinalityMult = true
|
|
47
|
+
if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') {
|
|
48
|
+
if (refElement.cardinality && refElement.cardinality.max === '*') {
|
|
49
|
+
refCardinalityMult = true
|
|
49
50
|
}
|
|
50
51
|
} else {
|
|
51
|
-
refPlainElement = true
|
|
52
|
+
refPlainElement = true
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
55
|
(column) => {
|
|
55
56
|
if (
|
|
56
57
|
definition.keys &&
|
|
57
58
|
Object.keys(definition.keys).length === 1 &&
|
|
58
|
-
Object.keys(definition.keys)[0] ===
|
|
59
|
+
Object.keys(definition.keys)[0] === 'ID' &&
|
|
59
60
|
refCardinalityMult &&
|
|
60
61
|
refPlainElement
|
|
61
62
|
) {
|
|
62
|
-
const keyName = Object.keys(definition.keys)[0]
|
|
63
|
-
const key = definition.keys[keyName]
|
|
64
|
-
const keyLoc = context.getLocation(keyName, key, csn)
|
|
65
|
-
const colName = column.as ? column.as : column.name
|
|
63
|
+
const keyName = Object.keys(definition.keys)[0]
|
|
64
|
+
const key = definition.keys[keyName]
|
|
65
|
+
const keyLoc = context.getLocation(keyName, key, csn)
|
|
66
|
+
const colName = column.as ? column.as : column.name
|
|
66
67
|
context.report({
|
|
67
68
|
message: `Ambiguous key in '${definition.name}'. Element '${colName}' leads to multiple entries so that key '${keyName}' is not unique.`,
|
|
68
69
|
loc: keyLoc,
|
|
69
70
|
file: key.$location.file
|
|
70
|
-
})
|
|
71
|
+
})
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
|
-
)
|
|
74
|
-
})
|
|
74
|
+
)
|
|
75
|
+
})
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function processEntity(csn, eachCallback) {
|
|
78
|
+
function processEntity (csn, eachCallback) {
|
|
78
79
|
Object.keys(csn.definitions).forEach((name) => {
|
|
79
|
-
if (name.startsWith(
|
|
80
|
-
return
|
|
80
|
+
if (name.startsWith('localized.')) {
|
|
81
|
+
return
|
|
81
82
|
}
|
|
82
|
-
const definition = csn.definitions[name]
|
|
83
|
+
const definition = csn.definitions[name]
|
|
83
84
|
if (
|
|
84
|
-
definition.kind ===
|
|
85
|
+
definition.kind === 'entity' &&
|
|
85
86
|
definition.query &&
|
|
86
87
|
definition.query.SELECT &&
|
|
87
88
|
definition.query.SELECT.columns
|
|
88
89
|
) {
|
|
89
|
-
let sourceEntity
|
|
90
|
-
const sourceAlias = []
|
|
90
|
+
let sourceEntity
|
|
91
|
+
const sourceAlias = []
|
|
91
92
|
if (definition.query.SELECT.from.ref) {
|
|
92
93
|
// From
|
|
93
|
-
sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join(
|
|
94
|
+
sourceEntity = csn.definitions[definition.query.SELECT.from.ref.join('_')]
|
|
94
95
|
sourceAlias.push({
|
|
95
96
|
from: sourceEntity.name,
|
|
96
|
-
as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split(
|
|
97
|
-
})
|
|
97
|
+
as: definition.query.SELECT.from.as || definition.query.SELECT.from.ref.slice(-1)[0].split('.').pop()
|
|
98
|
+
})
|
|
98
99
|
} else if (definition.query.SELECT.from.args && definition.query.SELECT.from.args[0].ref) {
|
|
99
100
|
// Join
|
|
100
|
-
sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join(
|
|
101
|
+
sourceEntity = csn.definitions[definition.query.SELECT.from.args[0].ref.join('_')]
|
|
101
102
|
definition.query.SELECT.from.args.forEach((arg) => {
|
|
102
103
|
sourceAlias.push({
|
|
103
|
-
from: arg.ref.join(
|
|
104
|
-
as: arg.as || arg.ref.slice(-1)[0].split(
|
|
105
|
-
})
|
|
106
|
-
})
|
|
104
|
+
from: arg.ref.join('_'),
|
|
105
|
+
as: arg.as || arg.ref.slice(-1)[0].split('.').pop()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
107
108
|
}
|
|
108
109
|
if (!sourceEntity) {
|
|
109
|
-
return
|
|
110
|
+
return
|
|
110
111
|
}
|
|
111
|
-
eachCallback(definition, sourceEntity, sourceAlias)
|
|
112
|
+
eachCallback(definition, sourceEntity, sourceAlias)
|
|
112
113
|
}
|
|
113
|
-
})
|
|
114
|
+
})
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
function processElement(csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) {
|
|
117
|
+
function processElement (csn, definition, sourceEntity, sourceAlias, beforeCallback, eachCallback, afterCallback) {
|
|
117
118
|
definition.query.SELECT.columns.forEach((column) => {
|
|
118
119
|
if (column.ref && column.ref.length > 1) {
|
|
119
|
-
let refEntity = sourceEntity
|
|
120
|
-
let refAlias = sourceAlias
|
|
121
|
-
beforeCallback()
|
|
120
|
+
let refEntity = sourceEntity
|
|
121
|
+
let refAlias = sourceAlias
|
|
122
|
+
beforeCallback()
|
|
122
123
|
column.ref.forEach((ref) => {
|
|
123
|
-
ref = ref.id || ref
|
|
124
|
+
ref = ref.id || ref
|
|
124
125
|
// Alias
|
|
125
126
|
const matchAlias = refAlias.find((alias) => {
|
|
126
|
-
return alias.as === ref
|
|
127
|
-
})
|
|
128
|
-
let refElement
|
|
127
|
+
return alias.as === ref
|
|
128
|
+
})
|
|
129
|
+
let refElement
|
|
129
130
|
if (matchAlias) {
|
|
130
|
-
refEntity = csn.definitions[matchAlias.from]
|
|
131
|
+
refEntity = csn.definitions[matchAlias.from]
|
|
131
132
|
} else {
|
|
132
|
-
refElement = refEntity.elements[ref]
|
|
133
|
+
refElement = refEntity.elements[ref]
|
|
133
134
|
// Mixin
|
|
134
135
|
if (!refElement) {
|
|
135
|
-
refElement = definition.elements[ref]
|
|
136
|
+
refElement = definition.elements[ref]
|
|
136
137
|
if (!refElement && definition.query.SELECT.mixin) {
|
|
137
|
-
refElement = definition.query.SELECT.mixin[ref]
|
|
138
|
+
refElement = definition.query.SELECT.mixin[ref]
|
|
138
139
|
if (!refElement && definition.query.SELECT.mixin[column.ref[0]]) {
|
|
139
|
-
refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref]
|
|
140
|
+
refElement = definition.query.SELECT.mixin[column.ref[0]]._target.elements[ref]
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
|
-
eachCallback(refEntity, refElement)
|
|
144
|
-
if (refElement.type ===
|
|
145
|
-
refEntity = csn.definitions[refElement.target]
|
|
144
|
+
eachCallback(refEntity, refElement)
|
|
145
|
+
if (refElement.type === 'cds.Association' || refElement.type === 'cds.Composition') {
|
|
146
|
+
refEntity = csn.definitions[refElement.target]
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
|
-
refAlias = []
|
|
149
|
-
})
|
|
150
|
-
afterCallback(column)
|
|
149
|
+
refAlias = []
|
|
150
|
+
})
|
|
151
|
+
afterCallback(column)
|
|
151
152
|
}
|
|
152
|
-
})
|
|
153
|
+
})
|
|
153
154
|
}
|
|
@@ -1,37 +1,38 @@
|
|
|
1
|
-
const LABELS = [
|
|
1
|
+
const LABELS = ['@restrict', '@requires']
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
meta: {
|
|
5
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
5
6
|
docs: {
|
|
6
|
-
description:
|
|
7
|
-
category:
|
|
7
|
+
description: '`@restrict` and `@requires` must not be empty.',
|
|
8
|
+
category: 'Model Validation',
|
|
8
9
|
recommended: true
|
|
9
10
|
},
|
|
10
11
|
hasSuggestions: true,
|
|
11
12
|
messages: {
|
|
12
|
-
InvalidItem:
|
|
13
|
-
ReplaceItemWith:
|
|
13
|
+
InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
|
|
14
|
+
ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
|
|
14
15
|
},
|
|
15
|
-
type:
|
|
16
|
-
model:
|
|
16
|
+
type: 'problem',
|
|
17
|
+
model: 'inferred'
|
|
17
18
|
},
|
|
18
|
-
create(context) {
|
|
19
|
+
create (context) {
|
|
19
20
|
return {
|
|
20
|
-
entity:
|
|
21
|
-
service:
|
|
22
|
-
}
|
|
21
|
+
entity: checkRestrictions,
|
|
22
|
+
service: checkRestrictions
|
|
23
|
+
}
|
|
23
24
|
|
|
24
|
-
function
|
|
25
|
+
function checkRestrictions (e) {
|
|
25
26
|
for (const l of LABELS) {
|
|
26
|
-
const invalid = (typeof e[l] ===
|
|
27
|
+
const invalid = (typeof e[l] === 'object' && e[l].length === 0) || (typeof e[l] === 'string' && e[l] === '')
|
|
27
28
|
if (invalid) {
|
|
28
29
|
context.report({
|
|
29
30
|
message: `No explicit restrictions provided on ${e.kind} \`${e.name}\` at \`${l}\`.`,
|
|
30
31
|
node: context.getNode(e),
|
|
31
32
|
file: e.$location.file
|
|
32
|
-
})
|
|
33
|
+
})
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
}
|
|
38
|
+
}
|
|
@@ -1,38 +1,39 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
meta: {
|
|
3
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
3
4
|
docs: {
|
|
4
|
-
description:
|
|
5
|
-
category:
|
|
5
|
+
description: 'Use `@requires` instead of `@restrict.to` in actions and services with unrestricted events.',
|
|
6
|
+
category: 'Model Validation',
|
|
6
7
|
recommended: true,
|
|
7
|
-
version:
|
|
8
|
+
version: '2.4.1'
|
|
8
9
|
},
|
|
9
10
|
hasSuggestions: true,
|
|
10
11
|
messages: {
|
|
11
|
-
InvalidItem:
|
|
12
|
-
ReplaceItemWith:
|
|
12
|
+
InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
|
|
13
|
+
ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
|
|
13
14
|
},
|
|
14
|
-
type:
|
|
15
|
-
model:
|
|
15
|
+
type: 'problem',
|
|
16
|
+
model: 'inferred'
|
|
16
17
|
},
|
|
17
|
-
create(context) {
|
|
18
|
+
create (context) {
|
|
18
19
|
return {
|
|
19
|
-
service:
|
|
20
|
-
action:
|
|
21
|
-
}
|
|
20
|
+
service: checkRestrict,
|
|
21
|
+
action: checkRestrict
|
|
22
|
+
}
|
|
22
23
|
|
|
23
|
-
function
|
|
24
|
-
if (e && e[
|
|
25
|
-
for (const entry of e[
|
|
26
|
-
const keys = Object.keys(entry)
|
|
27
|
-
if (keys.includes(
|
|
24
|
+
function checkRestrict (e) {
|
|
25
|
+
if (e && e['@restrict'] && typeof e['@restrict'] === 'object') {
|
|
26
|
+
for (const entry of e['@restrict']) {
|
|
27
|
+
const keys = Object.keys(entry)
|
|
28
|
+
if (keys.includes('to') && keys.includes('grant') && entry.grant === '*') {
|
|
28
29
|
context.report({
|
|
29
30
|
message: `Use \`@requires\` instead of \`@restrict.to\` at ${e.kind} \`${e.name}\`.`,
|
|
30
31
|
node: context.getNode(e),
|
|
31
32
|
file: e.$location.file
|
|
32
|
-
})
|
|
33
|
+
})
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
}
|
|
39
|
+
}
|
|
@@ -1,105 +1,108 @@
|
|
|
1
|
-
const { findFuzzy, isEmptyString, isEmptyObject, isStringInArray } = require(
|
|
1
|
+
const { findFuzzy, isEmptyString, isEmptyObject, isStringInArray } = require('../utils/rules')
|
|
2
2
|
|
|
3
|
-
const REPLACE_AS_WRITE_EVENTS = [
|
|
4
|
-
const VALID_EVENTS = REPLACE_AS_WRITE_EVENTS.concat([
|
|
3
|
+
const REPLACE_AS_WRITE_EVENTS = ['READ', 'CREATE', 'UPDATE', 'DELETE']
|
|
4
|
+
const VALID_EVENTS = REPLACE_AS_WRITE_EVENTS.concat(['INSERT', 'UPSERT', 'WRITE', '*'])
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
meta: {
|
|
8
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
8
9
|
docs: {
|
|
9
|
-
description:
|
|
10
|
-
category:
|
|
10
|
+
description: '`@restrict.grant` must have valid values.',
|
|
11
|
+
category: 'Model Validation',
|
|
11
12
|
recommended: true
|
|
12
13
|
},
|
|
13
14
|
messages: {
|
|
14
|
-
InvalidItem:
|
|
15
|
-
ReplaceItemWith:
|
|
15
|
+
InvalidItem: "Invalid item '{{invalid}}'. Did you mean '{{candidates}}'?",
|
|
16
|
+
ReplaceItemWith: "Replace '{{invalid}}' with '{{candidates}}'"
|
|
16
17
|
},
|
|
17
|
-
type:
|
|
18
|
-
model:
|
|
18
|
+
type: 'problem',
|
|
19
|
+
model: 'inferred'
|
|
19
20
|
},
|
|
20
|
-
create(context) {
|
|
21
|
+
create (context) {
|
|
21
22
|
return {
|
|
22
|
-
entity:
|
|
23
|
-
}
|
|
23
|
+
entity: checkRestrictGrant
|
|
24
|
+
}
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
-
const node = context.getNode(e)
|
|
27
|
-
const file = e.$location.file
|
|
28
|
-
if (e[
|
|
29
|
-
const actions = e.actions
|
|
30
|
-
const actionNames = actions ? Object.keys(actions).map((s) => actions[s].name) : []
|
|
31
|
-
const validEventsAndActions = VALID_EVENTS.concat(actionNames)
|
|
26
|
+
function checkRestrictGrant (e) {
|
|
27
|
+
const node = context.getNode(e)
|
|
28
|
+
const file = e.$location.file
|
|
29
|
+
if (e['@restrict']) {
|
|
30
|
+
const actions = e.actions
|
|
31
|
+
const actionNames = actions ? Object.keys(actions).map((s) => actions[s].name) : []
|
|
32
|
+
const validEventsAndActions = VALID_EVENTS.concat(actionNames)
|
|
32
33
|
|
|
33
|
-
for (const entry of e[
|
|
34
|
-
if (Object.keys(entry).includes(
|
|
35
|
-
const grantValue = entry.grant
|
|
34
|
+
for (const entry of e['@restrict']) {
|
|
35
|
+
if (Object.keys(entry).includes('grant')) {
|
|
36
|
+
const grantValue = entry.grant
|
|
36
37
|
switch (typeof grantValue) {
|
|
37
|
-
case
|
|
38
|
+
case 'string': {
|
|
38
39
|
if (isEmptyString(grantValue)) {
|
|
39
40
|
context.report({
|
|
40
41
|
message: `Missing event/action on ${e.name} for \`@restrict.grant\`.`,
|
|
41
42
|
node,
|
|
42
43
|
file
|
|
43
|
-
})
|
|
44
|
+
})
|
|
44
45
|
} else {
|
|
45
46
|
if (!isStringInArray(grantValue, validEventsAndActions, true)) {
|
|
46
|
-
const candidates = findFuzzy(grantValue, validEventsAndActions.sort())
|
|
47
|
+
const candidates = findFuzzy(grantValue, validEventsAndActions.sort())
|
|
47
48
|
context.report({
|
|
48
|
-
messageId:
|
|
49
|
+
messageId: 'InvalidItem',
|
|
49
50
|
data: { invalid: grantValue, candidates },
|
|
50
51
|
node,
|
|
51
52
|
file
|
|
52
|
-
})
|
|
53
|
+
})
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
break
|
|
56
|
+
break
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
case
|
|
59
|
+
case 'object':
|
|
59
60
|
if (isEmptyObject(grantValue)) {
|
|
60
61
|
context.report({
|
|
61
62
|
message: `Missing event/action on ${e.name} for \`@restrict.grant\`.`,
|
|
62
63
|
node,
|
|
63
64
|
file
|
|
64
|
-
})
|
|
65
|
+
})
|
|
65
66
|
} else {
|
|
66
|
-
|
|
67
|
-
return item !==
|
|
68
|
-
})
|
|
67
|
+
const valuesForWrite = grantValue.filter(function (item) {
|
|
68
|
+
return item !== 'READ' && item !== 'WRITE' && item !== '*'
|
|
69
|
+
})
|
|
69
70
|
for (const value of grantValue) {
|
|
70
71
|
if (!validEventsAndActions.includes(value)) {
|
|
71
|
-
const candidates = findFuzzy(value, validEventsAndActions.sort())
|
|
72
|
+
const candidates = findFuzzy(value, validEventsAndActions.sort())
|
|
72
73
|
context.report({
|
|
73
|
-
messageId:
|
|
74
|
+
messageId: 'InvalidItem',
|
|
74
75
|
data: { invalid: value, candidates },
|
|
75
76
|
node,
|
|
76
77
|
file
|
|
77
|
-
})
|
|
78
|
+
})
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
// If values do not contain 'READ, WRITE, *', 'WRITE' only is enough
|
|
82
|
+
const allValuesIncluded = grantValue.every((v) => valuesForWrite.includes(v))
|
|
81
83
|
if (allValuesIncluded) {
|
|
82
84
|
context.report({
|
|
83
|
-
messageId:
|
|
84
|
-
data: { invalid: [`[${grantValue}]`], candidates: [
|
|
85
|
+
messageId: 'InvalidItem',
|
|
86
|
+
data: { invalid: [`[${grantValue}]`], candidates: ['["WRITE"]'] },
|
|
85
87
|
node,
|
|
86
88
|
file
|
|
87
|
-
})
|
|
89
|
+
})
|
|
88
90
|
}
|
|
89
|
-
|
|
91
|
+
// If values contain '*', '*' only is enough
|
|
92
|
+
if (grantValue.length > 1 && grantValue.includes('*')) {
|
|
90
93
|
context.report({
|
|
91
|
-
messageId:
|
|
92
|
-
data: { invalid: `[${grantValue}]`, candidates: [
|
|
94
|
+
messageId: 'InvalidItem',
|
|
95
|
+
data: { invalid: `[${grantValue}]`, candidates: ['["*"]'] },
|
|
93
96
|
node,
|
|
94
97
|
file
|
|
95
|
-
})
|
|
98
|
+
})
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
|
-
break
|
|
101
|
+
break
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
|
-
}
|
|
108
|
+
}
|
|
@@ -1,38 +1,39 @@
|
|
|
1
|
-
const { isEmptyObject, findFuzzy } = require(
|
|
1
|
+
const { isEmptyObject, findFuzzy } = require('../utils/rules')
|
|
2
2
|
|
|
3
|
-
const VALID_RESTRICT_KEYS = [
|
|
3
|
+
const VALID_RESTRICT_KEYS = ['grant', 'to', 'where']
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
6
|
meta: {
|
|
7
|
+
schema: [{/* to avoid deprecation warning for ESLint 9 */}],
|
|
7
8
|
docs: {
|
|
8
|
-
description:
|
|
9
|
-
category:
|
|
9
|
+
description: '`@restrict` must have properly spelled `to`, `grant`, and `where` keys.',
|
|
10
|
+
category: 'Model Validation',
|
|
10
11
|
recommended: true
|
|
11
12
|
},
|
|
12
13
|
messages: {
|
|
13
|
-
InvalidItem:
|
|
14
|
+
InvalidItem: "Misspelled key '{{invalid}}'. Did you mean '{{candidates}}'?"
|
|
14
15
|
},
|
|
15
|
-
type:
|
|
16
|
-
model:
|
|
16
|
+
type: 'problem',
|
|
17
|
+
model: 'inferred'
|
|
17
18
|
},
|
|
18
|
-
create(context) {
|
|
19
|
+
create (context) {
|
|
19
20
|
return {
|
|
20
|
-
entity:
|
|
21
|
-
}
|
|
21
|
+
entity: checkRestrictKeys
|
|
22
|
+
}
|
|
22
23
|
|
|
23
|
-
function
|
|
24
|
-
if (e[
|
|
25
|
-
for (const entry of e[
|
|
26
|
-
if (typeof entry ===
|
|
24
|
+
function checkRestrictKeys (e) {
|
|
25
|
+
if (e['@restrict']) {
|
|
26
|
+
for (const entry of e['@restrict']) {
|
|
27
|
+
if (typeof entry === 'object' && !isEmptyObject(entry)) {
|
|
27
28
|
for (const key of Object.keys(entry)) {
|
|
28
29
|
if (!VALID_RESTRICT_KEYS.includes(key)) {
|
|
29
|
-
const candidates = findFuzzy(key, VALID_RESTRICT_KEYS.sort())
|
|
30
|
+
const candidates = findFuzzy(key, VALID_RESTRICT_KEYS.sort())
|
|
30
31
|
context.report({
|
|
31
|
-
messageId:
|
|
32
|
+
messageId: 'InvalidItem',
|
|
32
33
|
data: { invalid: key, candidates },
|
|
33
34
|
node: context.getNode(e),
|
|
34
35
|
file: e.$location.file
|
|
35
|
-
})
|
|
36
|
+
})
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -40,4 +41,4 @@ module.exports = {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
}
|
|
44
|
+
}
|