@sailshq/language-server 0.2.2 → 0.3.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.
|
@@ -18,18 +18,12 @@ module.exports = function modelAttributesCompletion(
|
|
|
18
18
|
const criteriaMatch = before.match(
|
|
19
19
|
/(?:sails\.models\.([A-Za-z_$][\w$]*)|([A-Za-z_$][\w$]*))\s*\.\w+\s*\(\s*\{[^}]*([a-zA-Z0-9_]*)?$/
|
|
20
20
|
)
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
)
|
|
24
|
-
const selectArrayMatch = before.match(
|
|
21
|
+
const sortStringMatch = before.match(/sort\s*:\s*['"]([a-zA-Z0-9_]*)?$/)
|
|
22
|
+
const criteriaOptionsArrayMatch = before.match(
|
|
25
23
|
/(?:select|omit|sort)\s*:\s*\[\s*['"]([a-zA-Z0-9_]*)?$/
|
|
26
24
|
)
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
const sortStringMatch = before.match(/sort\s*:\s*['"]([a-zA-Z0-9_]*)?$/)
|
|
31
|
-
const sortArrayStringMatch = before.match(
|
|
32
|
-
/sort\s*:\s*\[\s*[^\{\]]*['"]([a-zA-Z0-9_]*)?$/
|
|
25
|
+
const popuplateMethodMatch = before.match(
|
|
26
|
+
/\.populate\s*\(\s*['"]([a-zA-Z0-9_]*)?$/
|
|
33
27
|
)
|
|
34
28
|
const sortArrayObjectMatch = before.match(
|
|
35
29
|
/sort\s*:\s*\[\s*\{\s*([a-zA-Z0-9_]*)?$/
|
|
@@ -44,7 +38,7 @@ module.exports = function modelAttributesCompletion(
|
|
|
44
38
|
// Detect if we are inside a .select([]), .omit([]), .sort([]), etc. as a method call (e.g. User.find().select([]))
|
|
45
39
|
// This matches e.g. .select(['foo', '']) or .omit(["bar", '']) or .select('foo')
|
|
46
40
|
const chainableDirectCallMatch = before.match(
|
|
47
|
-
/\.(select|omit|sort|populate|where)\s*\(\s*\[?\s*['"]
|
|
41
|
+
/\.(select|omit|sort|populate|where)\s*\(\s*\[.*(?:,|\[)?\s*['"`]([a-zA-Z0-9_]*)?$/
|
|
48
42
|
)
|
|
49
43
|
|
|
50
44
|
// Also allow completions in .where({ ... }) chainable method call context
|
|
@@ -56,41 +50,52 @@ module.exports = function modelAttributesCompletion(
|
|
|
56
50
|
/([A-Za-z_$][\w$]*)\s*\.where\s*\(\s*\{[^}]*([a-zA-Z0-9_]*)?$/
|
|
57
51
|
)
|
|
58
52
|
|
|
53
|
+
// Support chained .where({ ... }) completions ---
|
|
54
|
+
// Try to infer model name from chained calls like User.find().where({ ... })
|
|
55
|
+
const chainedWhereMatch = before.match(
|
|
56
|
+
/([A-Za-z_$][\w$]*)\s*\.[\w$]+\s*\(.*?\)\s*\.where\s*\(\s*\{[^}]*([a-zA-Z0-9_]*)?$/
|
|
57
|
+
)
|
|
58
|
+
|
|
59
59
|
// Only suppress completions after a colon (:) in object literals for static methods,
|
|
60
60
|
// but always allow completions in .select(['']), .omit(['']), .sort(['']), .where({}), etc.
|
|
61
61
|
const inChainableString =
|
|
62
|
-
|
|
63
|
-
selectArrayMatch ||
|
|
62
|
+
criteriaOptionsArrayMatch ||
|
|
64
63
|
sortStringMatch ||
|
|
65
|
-
sortArrayStringMatch ||
|
|
66
64
|
sortArrayObjectMatch ||
|
|
67
|
-
|
|
65
|
+
popuplateMethodMatch ||
|
|
68
66
|
isInChainableMethodCall ||
|
|
69
67
|
isInWhereMethodCall ||
|
|
70
68
|
!!chainableDirectCallMatch
|
|
71
69
|
|
|
72
70
|
// Suppress completions after a colon only if NOT in a chainable string/array context
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// If
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
(
|
|
89
|
-
|
|
71
|
+
// Also suppress completions after colon in .where({ ... }) context, unless after a comma or at start
|
|
72
|
+
// FIX: Do not run this suppression logic at all if we are in a select/omit/sort array (object property form)
|
|
73
|
+
if (!criteriaOptionsArrayMatch) {
|
|
74
|
+
if (
|
|
75
|
+
!inChainableString ||
|
|
76
|
+
((isInWhereMethodCall || chainedWhereMatch) && !criteriaOptionsArrayMatch)
|
|
77
|
+
) {
|
|
78
|
+
const lines = before.split('\n')
|
|
79
|
+
const line = lines[lines.length - 1]
|
|
80
|
+
const beforeCursor = line.slice(0, position.character)
|
|
81
|
+
// If the last non-whitespace character before the cursor is a colon, suppress completion
|
|
82
|
+
// (but allow after comma, or at start of line/object)
|
|
83
|
+
const lastColon = beforeCursor.lastIndexOf(':')
|
|
84
|
+
const lastComma = beforeCursor.lastIndexOf(',')
|
|
85
|
+
if (lastColon > lastComma && lastColon > beforeCursor.lastIndexOf('{')) {
|
|
86
|
+
// Check if we are inside a string (e.g. after a colon and inside quotes)
|
|
87
|
+
// If so, suppress completion
|
|
88
|
+
const quoteBefore = beforeCursor.lastIndexOf("'")
|
|
89
|
+
const dquoteBefore = beforeCursor.lastIndexOf('"')
|
|
90
|
+
if (
|
|
91
|
+
(quoteBefore > lastColon && quoteBefore > lastComma) ||
|
|
92
|
+
(dquoteBefore > lastColon && dquoteBefore > lastComma)
|
|
93
|
+
) {
|
|
94
|
+
return []
|
|
95
|
+
}
|
|
96
|
+
// Otherwise, suppress completion after colon
|
|
90
97
|
return []
|
|
91
98
|
}
|
|
92
|
-
// Otherwise, suppress completion after colon
|
|
93
|
-
return []
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
@@ -114,23 +119,35 @@ module.exports = function modelAttributesCompletion(
|
|
|
114
119
|
return last[1] || last[2] || null
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
// Determine the current Sails.js model context and attribute prefix for completions
|
|
123
|
+
// by matching the code before the cursor against various Sails.js query patterns.
|
|
124
|
+
// This enables context-aware attribute completions for all supported query forms.
|
|
117
125
|
if (criteriaMatch) {
|
|
118
126
|
modelName = criteriaMatch[1] || criteriaMatch[2]
|
|
119
127
|
prefix = criteriaMatch[3] || ''
|
|
120
|
-
} else if (
|
|
128
|
+
} else if (criteriaOptionsArrayMatch) {
|
|
121
129
|
modelName = inferModelName(before)
|
|
122
|
-
prefix =
|
|
123
|
-
} else if (
|
|
130
|
+
prefix = criteriaOptionsArrayMatch[1] || ''
|
|
131
|
+
} else if (popuplateMethodMatch) {
|
|
124
132
|
isPopulate = true
|
|
125
133
|
modelName = inferModelName(before)
|
|
126
|
-
prefix =
|
|
127
|
-
} else if (
|
|
134
|
+
prefix = popuplateMethodMatch[1] || ''
|
|
135
|
+
} else if (
|
|
136
|
+
sortStringMatch ||
|
|
137
|
+
criteriaOptionsArrayMatch ||
|
|
138
|
+
sortArrayObjectMatch
|
|
139
|
+
) {
|
|
128
140
|
modelName = inferModelName(before)
|
|
129
141
|
prefix =
|
|
130
|
-
(sortStringMatch ||
|
|
142
|
+
(sortStringMatch ||
|
|
143
|
+
criteriaOptionsArrayMatch ||
|
|
144
|
+
sortArrayObjectMatch)[1] || ''
|
|
131
145
|
} else if (whereMethodCallMatch) {
|
|
132
146
|
modelName = whereMethodCallMatch[1]
|
|
133
147
|
prefix = whereMethodCallMatch[2] || ''
|
|
148
|
+
} else if (chainedWhereMatch) {
|
|
149
|
+
modelName = chainedWhereMatch[1]
|
|
150
|
+
prefix = chainedWhereMatch[2] || ''
|
|
134
151
|
} else if (chainableDirectCallMatch) {
|
|
135
152
|
modelName = inferModelName(before)
|
|
136
153
|
prefix = chainableDirectCallMatch[2] || ''
|
|
@@ -166,6 +183,59 @@ module.exports = function modelAttributesCompletion(
|
|
|
166
183
|
}
|
|
167
184
|
}
|
|
168
185
|
|
|
186
|
+
// Remove already-used attributes for both object and array/chainable forms
|
|
187
|
+
// Collect all used attributes from any select/omit/sort array in object or chainable form up to the cursor
|
|
188
|
+
const allArrayRegex =
|
|
189
|
+
/(select|omit|sort)\s*:\s*\[([^\]]*)\]|\.(select|omit|sort)\s*\(\s*\[([^\]]*)/g
|
|
190
|
+
let match
|
|
191
|
+
while ((match = allArrayRegex.exec(before)) !== null) {
|
|
192
|
+
const arrayContent = match[2] || match[4] || ''
|
|
193
|
+
const usedInArray = Array.from(
|
|
194
|
+
arrayContent.matchAll(/['"`]\s*([a-zA-Z0-9_]+)\s*['"`]/g)
|
|
195
|
+
).map((m) => m[1])
|
|
196
|
+
usedInArray.forEach((attr) => usedProps.add(attr))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Improved: Only trigger completions in object form select/omit/sort arrays when inside a string (between quotes)
|
|
200
|
+
if (criteriaOptionsArrayMatch) {
|
|
201
|
+
// Find the last '[' before the cursor
|
|
202
|
+
const arrayStart = before.lastIndexOf('[')
|
|
203
|
+
if (arrayStart !== -1) {
|
|
204
|
+
const arrayContent = before.slice(arrayStart, offset)
|
|
205
|
+
// Use the same logic as chainable: check for a quote before the cursor (inside a string)
|
|
206
|
+
const quoteMatch = arrayContent.match(/['"`]([^'"`]*)$/)
|
|
207
|
+
if (!quoteMatch) {
|
|
208
|
+
// Not inside a string, suppress completions
|
|
209
|
+
return []
|
|
210
|
+
}
|
|
211
|
+
// Also: filter out already-used attributes in this array
|
|
212
|
+
const usedInArray = Array.from(
|
|
213
|
+
arrayContent.matchAll(/['"`]\s*([a-zA-Z0-9_]+)\s*['"`]/g)
|
|
214
|
+
).map((m) => m[1])
|
|
215
|
+
usedInArray.forEach((attr) => usedProps.add(attr))
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (chainableDirectCallMatch) {
|
|
220
|
+
// For array/chainable forms, parse the array up to the cursor and collect used attributes
|
|
221
|
+
const arrayMatch = before.match(/\[([^\]]*)$/)
|
|
222
|
+
if (arrayMatch) {
|
|
223
|
+
const arrayContent = arrayMatch[1]
|
|
224
|
+
// Fix: allow completions for any string in the array, not just the first
|
|
225
|
+
// Find the last quote and ensure the cursor is after it (inside a string)
|
|
226
|
+
const quoteMatch = arrayContent.match(/['"`][^'"`]*$/)
|
|
227
|
+
if (!quoteMatch) {
|
|
228
|
+
// Not inside a string, suppress completions
|
|
229
|
+
return []
|
|
230
|
+
}
|
|
231
|
+
// Also: filter out already-used attributes in this array
|
|
232
|
+
const usedInArray = Array.from(
|
|
233
|
+
arrayContent.matchAll(/['"`]\s*([a-zA-Z0-9_]+)\s*['"`]/g)
|
|
234
|
+
).map((m) => m[1])
|
|
235
|
+
usedInArray.forEach((attr) => usedProps.add(attr))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
169
239
|
if (isPopulate) {
|
|
170
240
|
attributes = Object.entries(model.attributes || {})
|
|
171
241
|
.filter(([, def]) => def && (def.model || def.collection))
|
|
@@ -174,22 +244,25 @@ module.exports = function modelAttributesCompletion(
|
|
|
174
244
|
attributes = Object.keys(model.attributes || {})
|
|
175
245
|
}
|
|
176
246
|
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
247
|
+
return Array.from(
|
|
248
|
+
new Set(
|
|
249
|
+
attributes
|
|
250
|
+
.filter((attr) => attr.toLowerCase().startsWith(prefix.toLowerCase()))
|
|
251
|
+
.filter((attr) => !usedProps.has(attr))
|
|
252
|
+
)
|
|
253
|
+
).map((attr) => {
|
|
254
|
+
const attrDef = model.attributes && model.attributes[attr]
|
|
255
|
+
let type = attrDef && attrDef.type ? attrDef.type : ''
|
|
256
|
+
let required = attrDef && attrDef.required ? 'required' : 'optional'
|
|
257
|
+
let detail = type ? `${type} (${required})` : required
|
|
258
|
+
return {
|
|
259
|
+
label: attr,
|
|
260
|
+
kind: lsp.CompletionItemKind.Field,
|
|
261
|
+
detail,
|
|
262
|
+
documentation: `${modelName}.${attr}`,
|
|
263
|
+
sortText: attr,
|
|
264
|
+
filterText: attr,
|
|
265
|
+
insertText: attr
|
|
266
|
+
}
|
|
267
|
+
})
|
|
195
268
|
}
|
|
@@ -15,10 +15,10 @@ module.exports = function modelMethodsCompletion(document, position, typeMap) {
|
|
|
15
15
|
const staticCallMatch = before.match(
|
|
16
16
|
/(?:sails\.models\.([A-Za-z_$][\w$]*)|([A-Za-z_$][\w$]*))\.\s*([a-zA-Z]*)?$/
|
|
17
17
|
)
|
|
18
|
-
// Match chainable calls
|
|
19
|
-
const
|
|
20
|
-
/([A-Za-z_$][\w$]*)\.[a-zA-Z_]+\([^)]*\)
|
|
21
|
-
|
|
18
|
+
// Match all chainable calls and get the last one for completion
|
|
19
|
+
const chainableCallMatches = [
|
|
20
|
+
...before.matchAll(/([A-Za-z_$][\w$]*)\.[a-zA-Z_]+\([^)]*\)/g)
|
|
21
|
+
]
|
|
22
22
|
|
|
23
23
|
let modelName, prefix, methods
|
|
24
24
|
|
|
@@ -26,11 +26,15 @@ module.exports = function modelMethodsCompletion(document, position, typeMap) {
|
|
|
26
26
|
const models = typeMap.models || {}
|
|
27
27
|
const modelKeys = Object.keys(models)
|
|
28
28
|
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
if (chainableCallMatches.length > 0) {
|
|
30
|
+
// Use the last chainable call in the chain
|
|
31
|
+
const lastMatch = chainableCallMatches[chainableCallMatches.length - 1]
|
|
32
|
+
modelName = lastMatch[1]
|
|
33
|
+
// Get the prefix after the last dot (if user is typing e.g. .select)
|
|
34
|
+
const afterLastChain = before.slice(lastMatch.index + lastMatch[0].length)
|
|
35
|
+
const prefixMatch = afterLastChain.match(/\.\s*([a-zA-Z]*)?$/)
|
|
36
|
+
prefix = (prefixMatch && prefixMatch[1]) || ''
|
|
37
|
+
const foundKey = Object.keys(models).find(
|
|
34
38
|
(k) => k.toLowerCase() === modelName.toLowerCase()
|
|
35
39
|
)
|
|
36
40
|
methods = foundKey ? models[foundKey].chainableMethods || [] : []
|
|
@@ -52,7 +56,7 @@ module.exports = function modelMethodsCompletion(document, position, typeMap) {
|
|
|
52
56
|
let insertText = method.name + '($0)'
|
|
53
57
|
// For chainable .select or .omit, insert ([''])
|
|
54
58
|
if (
|
|
55
|
-
|
|
59
|
+
chainableCallMatches.length > 0 &&
|
|
56
60
|
(method.name === 'select' || method.name === 'omit')
|
|
57
61
|
) {
|
|
58
62
|
insertText = method.name + '([$0])'
|
|
@@ -3,7 +3,7 @@ const path = require('path')
|
|
|
3
3
|
|
|
4
4
|
module.exports = async function goToAction(document, position, typeMap) {
|
|
5
5
|
const fileName = path.basename(document.uri)
|
|
6
|
-
if (fileName !== 'routes.js') return
|
|
6
|
+
if (fileName !== 'routes.js') return []
|
|
7
7
|
|
|
8
8
|
const text = document.getText()
|
|
9
9
|
const offset = document.offsetAt(position)
|
|
@@ -39,5 +39,5 @@ module.exports = async function goToAction(document, position, typeMap) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
return
|
|
42
|
+
return []
|
|
43
43
|
}
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -34,167 +34,422 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
|
|
|
34
34
|
})
|
|
35
35
|
walk.simple(ast, {
|
|
36
36
|
CallExpression(node) {
|
|
37
|
-
if (
|
|
38
|
-
node.callee &&
|
|
39
|
-
node.callee.type === 'MemberExpression' &&
|
|
40
|
-
node.arguments &&
|
|
41
|
-
node.arguments.length > 0
|
|
42
|
-
) {
|
|
37
|
+
if (node.callee && node.callee.type === 'MemberExpression') {
|
|
43
38
|
const method = node.callee.property.name
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
// --- Robust model name extraction for ALL method calls (including chainable) ---
|
|
40
|
+
let effectiveModelName = undefined
|
|
41
|
+
let obj = node.callee.object
|
|
42
|
+
while (obj) {
|
|
43
|
+
if (obj.type === 'Identifier') {
|
|
44
|
+
effectiveModelName = obj.name
|
|
45
|
+
break
|
|
46
|
+
} else if (
|
|
47
|
+
obj.type === 'CallExpression' &&
|
|
48
|
+
obj.callee &&
|
|
49
|
+
obj.callee.type === 'MemberExpression'
|
|
50
|
+
) {
|
|
51
|
+
obj = obj.callee.object
|
|
52
|
+
} else {
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const model = getModelByName(effectiveModelName)
|
|
57
|
+
if (!model) return
|
|
58
|
+
|
|
59
|
+
// Only validate chainable methods that are select, omit, sort, or populate
|
|
60
|
+
const allowedChainable = ['select', 'omit', 'sort', 'populate']
|
|
61
|
+
|
|
62
|
+
// --- Ignore validation for arguments to non-model chainable methods like .intercept ---
|
|
63
|
+
// If the current method is NOT a model method or allowedChainable, skip validation for its arguments
|
|
64
|
+
const modelMethods = [
|
|
65
|
+
'create',
|
|
66
|
+
'createEach',
|
|
67
|
+
'count',
|
|
68
|
+
'find',
|
|
69
|
+
'findOne',
|
|
70
|
+
'update',
|
|
71
|
+
'destroy',
|
|
72
|
+
'where',
|
|
73
|
+
'findOrCreate',
|
|
74
|
+
'sum',
|
|
75
|
+
...allowedChainable
|
|
76
|
+
]
|
|
77
|
+
if (!modelMethods.includes(method)) {
|
|
78
|
+
// This is a non-model method (e.g., intercept, using, etc.), skip validation for its arguments
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// handle createEach array of objects
|
|
46
83
|
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'count',
|
|
51
|
-
'find',
|
|
52
|
-
'findOne',
|
|
53
|
-
'update',
|
|
54
|
-
'destroy',
|
|
55
|
-
'where',
|
|
56
|
-
'findOrCreate',
|
|
57
|
-
'sum'
|
|
58
|
-
].includes(method)
|
|
84
|
+
method === 'createEach' &&
|
|
85
|
+
node.arguments[0] &&
|
|
86
|
+
node.arguments[0].type === 'ArrayExpression'
|
|
59
87
|
) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
for (const el of node.arguments[0].elements) {
|
|
89
|
+
if (!el || el.type !== 'ObjectExpression') continue
|
|
90
|
+
for (const prop of el.properties) {
|
|
91
|
+
const attribute = prop.key && (prop.key.name || prop.key.value)
|
|
92
|
+
if (
|
|
93
|
+
!model.attributes ||
|
|
94
|
+
!Object.prototype.hasOwnProperty.call(
|
|
95
|
+
model.attributes,
|
|
96
|
+
attribute
|
|
97
|
+
)
|
|
98
|
+
) {
|
|
99
|
+
diagnostics.push(
|
|
100
|
+
lsp.Diagnostic.create(
|
|
101
|
+
lsp.Range.create(
|
|
102
|
+
document.positionAt(prop.key.start),
|
|
103
|
+
document.positionAt(prop.key.end)
|
|
104
|
+
),
|
|
105
|
+
`'${attribute}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
106
|
+
lsp.DiagnosticSeverity.Error,
|
|
107
|
+
'sails-lsp'
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Validate attributes in chained .where({ ... }) calls
|
|
117
|
+
if (
|
|
118
|
+
method === 'where' &&
|
|
119
|
+
node.arguments[0] &&
|
|
120
|
+
node.arguments[0].type === 'ObjectExpression'
|
|
121
|
+
) {
|
|
122
|
+
for (const prop of node.arguments[0].properties) {
|
|
123
|
+
if (!prop.key) continue
|
|
124
|
+
const whereAttr = prop.key.name || prop.key.value
|
|
125
|
+
if (
|
|
126
|
+
!model.attributes ||
|
|
127
|
+
!Object.prototype.hasOwnProperty.call(
|
|
128
|
+
model.attributes,
|
|
129
|
+
whereAttr
|
|
130
|
+
)
|
|
131
|
+
) {
|
|
132
|
+
diagnostics.push(
|
|
133
|
+
lsp.Diagnostic.create(
|
|
134
|
+
lsp.Range.create(
|
|
135
|
+
document.positionAt(prop.key.start),
|
|
136
|
+
document.positionAt(prop.key.end)
|
|
137
|
+
),
|
|
138
|
+
`'${whereAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
139
|
+
lsp.DiagnosticSeverity.Error,
|
|
140
|
+
'sails-lsp'
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Unified validation for .select([]), .omit([]), .sort([]), .populate([]) chainable calls only ---
|
|
149
|
+
if (
|
|
150
|
+
allowedChainable.includes(method) &&
|
|
151
|
+
node.arguments[0] &&
|
|
152
|
+
node.arguments[0].type === 'ArrayExpression'
|
|
153
|
+
) {
|
|
154
|
+
for (const el of node.arguments[0].elements) {
|
|
155
|
+
if (!el) continue
|
|
156
|
+
let arrAttr = undefined
|
|
157
|
+
if (el.type === 'Literal' || el.type === 'StringLiteral') {
|
|
158
|
+
arrAttr = el.value
|
|
159
|
+
} else if (
|
|
160
|
+
el.type === 'TemplateLiteral' &&
|
|
161
|
+
el.expressions.length === 0
|
|
162
|
+
) {
|
|
163
|
+
arrAttr = el.quasis[0].value.cooked
|
|
164
|
+
} else if (el.type === 'ObjectExpression' && method === 'sort') {
|
|
165
|
+
// For sort([{ foo: 1 }])
|
|
166
|
+
for (const sortProp of el.properties) {
|
|
167
|
+
if (!sortProp.key) continue
|
|
168
|
+
const sortAttr = sortProp.key.name || sortProp.key.value
|
|
72
169
|
if (
|
|
73
170
|
!model.attributes ||
|
|
74
171
|
!Object.prototype.hasOwnProperty.call(
|
|
75
172
|
model.attributes,
|
|
76
|
-
|
|
173
|
+
sortAttr
|
|
77
174
|
)
|
|
78
175
|
) {
|
|
79
176
|
diagnostics.push(
|
|
80
177
|
lsp.Diagnostic.create(
|
|
81
178
|
lsp.Range.create(
|
|
82
|
-
document.positionAt(
|
|
83
|
-
document.positionAt(
|
|
179
|
+
document.positionAt(sortProp.key.start),
|
|
180
|
+
document.positionAt(sortProp.key.end)
|
|
84
181
|
),
|
|
85
|
-
`'${
|
|
182
|
+
`'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
86
183
|
lsp.DiagnosticSeverity.Error,
|
|
87
184
|
'sails-lsp'
|
|
88
185
|
)
|
|
89
186
|
)
|
|
90
187
|
}
|
|
91
188
|
}
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
if (arrAttr !== undefined) {
|
|
192
|
+
// For sort, allow 'foo ASC' or 'foo DESC'
|
|
193
|
+
let checkAttr = arrAttr
|
|
194
|
+
if (method === 'sort' && typeof arrAttr === 'string') {
|
|
195
|
+
checkAttr = arrAttr.split(' ')[0]
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
!checkAttr ||
|
|
199
|
+
typeof checkAttr !== 'string' ||
|
|
200
|
+
checkAttr.trim() === ''
|
|
201
|
+
) {
|
|
202
|
+
diagnostics.push(
|
|
203
|
+
lsp.Diagnostic.create(
|
|
204
|
+
lsp.Range.create(
|
|
205
|
+
document.positionAt(el.start),
|
|
206
|
+
document.positionAt(el.end)
|
|
207
|
+
),
|
|
208
|
+
`Empty or invalid attribute in .${method}() for model '${effectiveModelName}'.`,
|
|
209
|
+
lsp.DiagnosticSeverity.Error,
|
|
210
|
+
'sails-lsp'
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
!model.attributes ||
|
|
217
|
+
!Object.prototype.hasOwnProperty.call(
|
|
218
|
+
model.attributes,
|
|
219
|
+
checkAttr
|
|
220
|
+
)
|
|
221
|
+
) {
|
|
222
|
+
diagnostics.push(
|
|
223
|
+
lsp.Diagnostic.create(
|
|
224
|
+
lsp.Range.create(
|
|
225
|
+
document.positionAt(el.start),
|
|
226
|
+
document.positionAt(el.end)
|
|
227
|
+
),
|
|
228
|
+
`'${checkAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
229
|
+
lsp.DiagnosticSeverity.Error,
|
|
230
|
+
'sails-lsp'
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
}
|
|
92
234
|
}
|
|
93
|
-
return
|
|
94
235
|
}
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
236
|
+
// Do NOT return here; allow walker to continue to chained calls
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Validate attributes in .find({ ... }) and similar methods
|
|
240
|
+
if (
|
|
241
|
+
node.arguments[0] &&
|
|
242
|
+
node.arguments[0].type === 'ObjectExpression'
|
|
243
|
+
) {
|
|
244
|
+
// Only validate if this is a top-level model method call, not an argument to another method (e.g., intercept)
|
|
245
|
+
// Check that the callee is a direct property of an Identifier (the model), not a nested CallExpression
|
|
246
|
+
let isTopLevelModelCall = false
|
|
247
|
+
let calleeObj = node.callee.object
|
|
248
|
+
if (calleeObj && calleeObj.type === 'Identifier') {
|
|
249
|
+
isTopLevelModelCall = true
|
|
250
|
+
} else if (calleeObj && calleeObj.type === 'CallExpression') {
|
|
251
|
+
// If the parent is a CallExpression, but the root is an Identifier, still allow
|
|
252
|
+
let root = calleeObj
|
|
253
|
+
while (
|
|
254
|
+
root &&
|
|
255
|
+
root.type === 'CallExpression' &&
|
|
256
|
+
root.callee &&
|
|
257
|
+
root.callee.type === 'MemberExpression'
|
|
258
|
+
) {
|
|
259
|
+
root = root.callee.object
|
|
260
|
+
}
|
|
261
|
+
if (root && root.type === 'Identifier') {
|
|
262
|
+
isTopLevelModelCall = true
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// If this is an argument to a non-model method (e.g., intercept), skip validation
|
|
266
|
+
if (!isTopLevelModelCall) return
|
|
267
|
+
for (const prop of node.arguments[0].properties) {
|
|
268
|
+
const attribute = prop.key && (prop.key.name || prop.key.value)
|
|
269
|
+
const queryOptionKeys = [
|
|
270
|
+
'where',
|
|
271
|
+
'select',
|
|
272
|
+
'omit',
|
|
273
|
+
'sort',
|
|
274
|
+
'limit',
|
|
275
|
+
'skip',
|
|
276
|
+
'page',
|
|
277
|
+
'populate',
|
|
278
|
+
'groupBy',
|
|
279
|
+
'having',
|
|
280
|
+
'sum',
|
|
281
|
+
'average',
|
|
282
|
+
'min',
|
|
283
|
+
'max',
|
|
284
|
+
'distinct',
|
|
285
|
+
'meta',
|
|
286
|
+
'or',
|
|
287
|
+
'and',
|
|
288
|
+
'not'
|
|
289
|
+
]
|
|
290
|
+
// For non-create methods, validate all top-level keys except query option keys
|
|
291
|
+
if (
|
|
292
|
+
method !== 'create' &&
|
|
293
|
+
method !== 'createEach' &&
|
|
294
|
+
!queryOptionKeys.includes(attribute)
|
|
295
|
+
) {
|
|
118
296
|
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
297
|
+
!model.attributes ||
|
|
298
|
+
!Object.prototype.hasOwnProperty.call(
|
|
299
|
+
model.attributes,
|
|
300
|
+
attribute
|
|
301
|
+
)
|
|
122
302
|
) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
303
|
+
diagnostics.push(
|
|
304
|
+
lsp.Diagnostic.create(
|
|
305
|
+
lsp.Range.create(
|
|
306
|
+
document.positionAt(prop.key.start),
|
|
307
|
+
document.positionAt(prop.key.end)
|
|
308
|
+
),
|
|
309
|
+
`'${attribute}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
310
|
+
lsp.DiagnosticSeverity.Error,
|
|
311
|
+
'sails-lsp'
|
|
128
312
|
)
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
if (
|
|
318
|
+
method !== 'create' &&
|
|
319
|
+
method !== 'createEach' &&
|
|
320
|
+
queryOptionKeys.includes(attribute)
|
|
321
|
+
) {
|
|
322
|
+
if (
|
|
323
|
+
attribute === 'where' &&
|
|
324
|
+
prop.value &&
|
|
325
|
+
prop.value.type === 'ObjectExpression'
|
|
326
|
+
) {
|
|
327
|
+
for (const whereProp of prop.value.properties) {
|
|
328
|
+
if (!whereProp.key) continue
|
|
329
|
+
const whereAttr = whereProp.key.name || whereProp.key.value
|
|
330
|
+
if (
|
|
331
|
+
!model.attributes ||
|
|
332
|
+
!Object.prototype.hasOwnProperty.call(
|
|
333
|
+
model.attributes,
|
|
334
|
+
whereAttr
|
|
139
335
|
)
|
|
140
|
-
)
|
|
336
|
+
) {
|
|
337
|
+
diagnostics.push(
|
|
338
|
+
lsp.Diagnostic.create(
|
|
339
|
+
lsp.Range.create(
|
|
340
|
+
document.positionAt(whereProp.key.start),
|
|
341
|
+
document.positionAt(whereProp.key.end)
|
|
342
|
+
),
|
|
343
|
+
`'${whereAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
344
|
+
lsp.DiagnosticSeverity.Error,
|
|
345
|
+
'sails-lsp'
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
}
|
|
141
349
|
}
|
|
142
350
|
continue
|
|
143
351
|
}
|
|
144
352
|
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
353
|
+
(attribute === 'select' || attribute === 'omit') &&
|
|
354
|
+
prop.value &&
|
|
355
|
+
prop.value.type === 'ArrayExpression'
|
|
148
356
|
) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
) {
|
|
154
|
-
for (const whereProp of prop.value.properties) {
|
|
155
|
-
if (!whereProp.key) continue
|
|
156
|
-
const whereAttr =
|
|
157
|
-
whereProp.key.name || whereProp.key.value
|
|
357
|
+
for (const el of prop.value.elements) {
|
|
358
|
+
if (!el) continue
|
|
359
|
+
if (el.type === 'Literal' || el.type === 'StringLiteral') {
|
|
360
|
+
const arrAttr = el.value
|
|
158
361
|
if (
|
|
159
362
|
!model.attributes ||
|
|
160
363
|
!Object.prototype.hasOwnProperty.call(
|
|
161
364
|
model.attributes,
|
|
162
|
-
|
|
365
|
+
arrAttr
|
|
163
366
|
)
|
|
164
367
|
) {
|
|
165
368
|
diagnostics.push(
|
|
166
369
|
lsp.Diagnostic.create(
|
|
167
370
|
lsp.Range.create(
|
|
168
|
-
document.positionAt(
|
|
169
|
-
document.positionAt(
|
|
371
|
+
document.positionAt(el.start),
|
|
372
|
+
document.positionAt(el.end)
|
|
170
373
|
),
|
|
171
|
-
`'${
|
|
374
|
+
`'${arrAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
172
375
|
lsp.DiagnosticSeverity.Error,
|
|
173
376
|
'sails-lsp'
|
|
174
377
|
)
|
|
175
378
|
)
|
|
176
379
|
}
|
|
177
380
|
}
|
|
178
|
-
continue
|
|
179
381
|
}
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
if (attribute === 'sort' && prop.value) {
|
|
180
385
|
if (
|
|
181
|
-
|
|
182
|
-
prop.value
|
|
183
|
-
prop.value.type === 'ArrayExpression'
|
|
386
|
+
prop.value.type === 'Literal' ||
|
|
387
|
+
prop.value.type === 'StringLiteral'
|
|
184
388
|
) {
|
|
389
|
+
const sortStr = prop.value.value
|
|
390
|
+
const sortAttr = sortStr && sortStr.split(' ')[0]
|
|
391
|
+
if (
|
|
392
|
+
sortAttr &&
|
|
393
|
+
(!model.attributes ||
|
|
394
|
+
!Object.prototype.hasOwnProperty.call(
|
|
395
|
+
model.attributes,
|
|
396
|
+
sortAttr
|
|
397
|
+
))
|
|
398
|
+
) {
|
|
399
|
+
diagnostics.push(
|
|
400
|
+
lsp.Diagnostic.create(
|
|
401
|
+
lsp.Range.create(
|
|
402
|
+
document.positionAt(prop.value.start),
|
|
403
|
+
document.positionAt(prop.value.end)
|
|
404
|
+
),
|
|
405
|
+
`'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
406
|
+
lsp.DiagnosticSeverity.Error,
|
|
407
|
+
'sails-lsp'
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
continue
|
|
412
|
+
} else if (prop.value.type === 'ArrayExpression') {
|
|
185
413
|
for (const el of prop.value.elements) {
|
|
186
414
|
if (!el) continue
|
|
187
|
-
if (
|
|
415
|
+
if (el.type === 'ObjectExpression') {
|
|
416
|
+
for (const sortProp of el.properties) {
|
|
417
|
+
if (!sortProp.key) continue
|
|
418
|
+
const sortAttr =
|
|
419
|
+
sortProp.key.name || sortProp.key.value
|
|
420
|
+
if (
|
|
421
|
+
!model.attributes ||
|
|
422
|
+
!Object.prototype.hasOwnProperty.call(
|
|
423
|
+
model.attributes,
|
|
424
|
+
sortAttr
|
|
425
|
+
)
|
|
426
|
+
) {
|
|
427
|
+
diagnostics.push(
|
|
428
|
+
lsp.Diagnostic.create(
|
|
429
|
+
lsp.Range.create(
|
|
430
|
+
document.positionAt(sortProp.key.start),
|
|
431
|
+
document.positionAt(sortProp.key.end)
|
|
432
|
+
),
|
|
433
|
+
`'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
434
|
+
lsp.DiagnosticSeverity.Error,
|
|
435
|
+
'sails-lsp'
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} else if (
|
|
188
441
|
el.type === 'Literal' ||
|
|
189
442
|
el.type === 'StringLiteral'
|
|
190
443
|
) {
|
|
191
|
-
const
|
|
444
|
+
const sortStr = el.value
|
|
445
|
+
const sortAttr = sortStr && sortStr.split(' ')[0]
|
|
192
446
|
if (
|
|
193
|
-
|
|
194
|
-
!
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
447
|
+
sortAttr &&
|
|
448
|
+
(!model.attributes ||
|
|
449
|
+
!Object.prototype.hasOwnProperty.call(
|
|
450
|
+
model.attributes,
|
|
451
|
+
sortAttr
|
|
452
|
+
))
|
|
198
453
|
) {
|
|
199
454
|
diagnostics.push(
|
|
200
455
|
lsp.Diagnostic.create(
|
|
@@ -202,7 +457,7 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
|
|
|
202
457
|
document.positionAt(el.start),
|
|
203
458
|
document.positionAt(el.end)
|
|
204
459
|
),
|
|
205
|
-
`'${
|
|
460
|
+
`'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
206
461
|
lsp.DiagnosticSeverity.Error,
|
|
207
462
|
'sails-lsp'
|
|
208
463
|
)
|
|
@@ -211,144 +466,56 @@ module.exports = function validateModelAttributeExist(document, typeMap) {
|
|
|
211
466
|
}
|
|
212
467
|
}
|
|
213
468
|
continue
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
prop.value.type === 'StringLiteral'
|
|
219
|
-
) {
|
|
220
|
-
const sortStr = prop.value.value
|
|
221
|
-
const sortAttr = sortStr && sortStr.split(' ')[0]
|
|
469
|
+
} else if (prop.value.type === 'ObjectExpression') {
|
|
470
|
+
for (const sortProp of prop.value.properties) {
|
|
471
|
+
if (!sortProp.key) continue
|
|
472
|
+
const sortAttr = sortProp.key.name || sortProp.key.value
|
|
222
473
|
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
))
|
|
474
|
+
!model.attributes ||
|
|
475
|
+
!Object.prototype.hasOwnProperty.call(
|
|
476
|
+
model.attributes,
|
|
477
|
+
sortAttr
|
|
478
|
+
)
|
|
229
479
|
) {
|
|
230
480
|
diagnostics.push(
|
|
231
481
|
lsp.Diagnostic.create(
|
|
232
482
|
lsp.Range.create(
|
|
233
|
-
document.positionAt(
|
|
234
|
-
document.positionAt(
|
|
483
|
+
document.positionAt(sortProp.key.start),
|
|
484
|
+
document.positionAt(sortProp.key.end)
|
|
235
485
|
),
|
|
236
|
-
`'${sortAttr}' is not a valid attribute of model '${
|
|
486
|
+
`'${sortAttr}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
237
487
|
lsp.DiagnosticSeverity.Error,
|
|
238
488
|
'sails-lsp'
|
|
239
489
|
)
|
|
240
490
|
)
|
|
241
491
|
}
|
|
242
|
-
continue
|
|
243
|
-
} else if (prop.value.type === 'ArrayExpression') {
|
|
244
|
-
for (const el of prop.value.elements) {
|
|
245
|
-
if (!el) continue
|
|
246
|
-
if (el.type === 'ObjectExpression') {
|
|
247
|
-
for (const sortProp of el.properties) {
|
|
248
|
-
if (!sortProp.key) continue
|
|
249
|
-
const sortAttr =
|
|
250
|
-
sortProp.key.name || sortProp.key.value
|
|
251
|
-
if (
|
|
252
|
-
!model.attributes ||
|
|
253
|
-
!Object.prototype.hasOwnProperty.call(
|
|
254
|
-
model.attributes,
|
|
255
|
-
sortAttr
|
|
256
|
-
)
|
|
257
|
-
) {
|
|
258
|
-
diagnostics.push(
|
|
259
|
-
lsp.Diagnostic.create(
|
|
260
|
-
lsp.Range.create(
|
|
261
|
-
document.positionAt(sortProp.key.start),
|
|
262
|
-
document.positionAt(sortProp.key.end)
|
|
263
|
-
),
|
|
264
|
-
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
265
|
-
lsp.DiagnosticSeverity.Error,
|
|
266
|
-
'sails-lsp'
|
|
267
|
-
)
|
|
268
|
-
)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
} else if (
|
|
272
|
-
el.type === 'Literal' ||
|
|
273
|
-
el.type === 'StringLiteral'
|
|
274
|
-
) {
|
|
275
|
-
const sortStr = el.value
|
|
276
|
-
const sortAttr = sortStr && sortStr.split(' ')[0]
|
|
277
|
-
if (
|
|
278
|
-
sortAttr &&
|
|
279
|
-
(!model.attributes ||
|
|
280
|
-
!Object.prototype.hasOwnProperty.call(
|
|
281
|
-
model.attributes,
|
|
282
|
-
sortAttr
|
|
283
|
-
))
|
|
284
|
-
) {
|
|
285
|
-
diagnostics.push(
|
|
286
|
-
lsp.Diagnostic.create(
|
|
287
|
-
lsp.Range.create(
|
|
288
|
-
document.positionAt(el.start),
|
|
289
|
-
document.positionAt(el.end)
|
|
290
|
-
),
|
|
291
|
-
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
292
|
-
lsp.DiagnosticSeverity.Error,
|
|
293
|
-
'sails-lsp'
|
|
294
|
-
)
|
|
295
|
-
)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
continue
|
|
300
|
-
} else if (prop.value.type === 'ObjectExpression') {
|
|
301
|
-
for (const sortProp of prop.value.properties) {
|
|
302
|
-
if (!sortProp.key) continue
|
|
303
|
-
const sortAttr = sortProp.key.name || sortProp.key.value
|
|
304
|
-
if (
|
|
305
|
-
!model.attributes ||
|
|
306
|
-
!Object.prototype.hasOwnProperty.call(
|
|
307
|
-
model.attributes,
|
|
308
|
-
sortAttr
|
|
309
|
-
)
|
|
310
|
-
) {
|
|
311
|
-
diagnostics.push(
|
|
312
|
-
lsp.Diagnostic.create(
|
|
313
|
-
lsp.Range.create(
|
|
314
|
-
document.positionAt(sortProp.key.start),
|
|
315
|
-
document.positionAt(sortProp.key.end)
|
|
316
|
-
),
|
|
317
|
-
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
318
|
-
lsp.DiagnosticSeverity.Error,
|
|
319
|
-
'sails-lsp'
|
|
320
|
-
)
|
|
321
|
-
)
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
continue
|
|
325
492
|
}
|
|
493
|
+
continue
|
|
326
494
|
}
|
|
327
|
-
// For all other query option keys, skip validation
|
|
328
|
-
continue
|
|
329
495
|
}
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
),
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
496
|
+
// For all other query option keys, skip validation
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
// Only validate top-level for create/createEach
|
|
500
|
+
if (
|
|
501
|
+
(method === 'create' || method === 'createEach') &&
|
|
502
|
+
(!model.attributes ||
|
|
503
|
+
!Object.prototype.hasOwnProperty.call(
|
|
504
|
+
model.attributes,
|
|
505
|
+
attribute
|
|
506
|
+
))
|
|
507
|
+
) {
|
|
508
|
+
diagnostics.push(
|
|
509
|
+
lsp.Diagnostic.create(
|
|
510
|
+
lsp.Range.create(
|
|
511
|
+
document.positionAt(prop.key.start),
|
|
512
|
+
document.positionAt(prop.key.end)
|
|
513
|
+
),
|
|
514
|
+
`'${attribute}' is not a valid attribute of model '${effectiveModelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
515
|
+
lsp.DiagnosticSeverity.Error,
|
|
516
|
+
'sails-lsp'
|
|
350
517
|
)
|
|
351
|
-
|
|
518
|
+
)
|
|
352
519
|
}
|
|
353
520
|
}
|
|
354
521
|
}
|