@lowgular/code-graph 0.1.1 → 0.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/bin/cli.js +2458 -5
- package/lib.js +2430 -0
- package/package.json +3 -2
- package/code-graph-v2/code.graph.js +0 -37
- package/code-graph-v2/config-from-query.js +0 -131
- package/code-graph-v2/extractors/extractor.js +0 -27
- package/code-graph-v2/extractors/index.js +0 -1
- package/code-graph-v2/graph-builder/code-graph.builder.js +0 -49
- package/code-graph-v2/graph-builder/index.js +0 -1
- package/code-graph-v2/graph-builder/node.processor.js +0 -22
- package/code-graph-v2/graph-builder/relationship.processor.js +0 -55
- package/code-graph-v2/graph-builder/type.processor.js +0 -21
- package/code-graph-v2/index.js +0 -4
- package/code-graph-v2/tools/build-code-graph.tool.js +0 -19
- package/code-graph-v2/utils.js +0 -34
- package/codegular/index.js +0 -5
- package/codegular/node.js +0 -71
- package/codegular/program.js +0 -100
- package/codegular/string.js +0 -121
- package/codegular/type-checker.js +0 -133
- package/codegular/type.js +0 -356
- package/codegular/utils.js +0 -335
- package/cypher/index.js +0 -1
- package/cypher/lib/executor/condition-evaluator.js +0 -135
- package/cypher/lib/executor/executor.js +0 -60
- package/cypher/lib/executor/graph.js +0 -0
- package/cypher/lib/executor/match-engine.js +0 -130
- package/cypher/lib/executor/pattern-matcher.js +0 -86
- package/cypher/lib/executor/relationship-navigator.js +0 -41
- package/cypher/lib/executor/result-formatter.js +0 -149
- package/cypher/lib/executor/traverse-engine.js +0 -141
- package/cypher/lib/executor/utils.js +0 -14
- package/cypher/lib/graph.stub.js +0 -38
- package/cypher/lib/index.js +0 -32
- package/cypher/lib/lexer.js +0 -376
- package/cypher/lib/parser.js +0 -586
- package/cypher/lib/validator/query-validator.js +0 -75
- package/cypher/lib/validator/supported-features.config.js +0 -83
- package/cypher/lib/validator/unsupported-features.config.js +0 -124
- package/cypher-cli.js +0 -41
- package/infra/code-graph.js +0 -147
- package/main.js +0 -0
- package/resources-cli.js +0 -75
- package/run-cli.js +0 -43
package/cypher/lib/parser.js
DELETED
|
@@ -1,586 +0,0 @@
|
|
|
1
|
-
import { TokenType } from "./lexer.js";
|
|
2
|
-
class Parser {
|
|
3
|
-
constructor(tokens) {
|
|
4
|
-
this.tokens = [];
|
|
5
|
-
this.position = 0;
|
|
6
|
-
this.tokens = tokens;
|
|
7
|
-
}
|
|
8
|
-
current() {
|
|
9
|
-
return this.tokens[this.position];
|
|
10
|
-
}
|
|
11
|
-
advance() {
|
|
12
|
-
const token = this.current();
|
|
13
|
-
if (this.position < this.tokens.length - 1) {
|
|
14
|
-
this.position++;
|
|
15
|
-
}
|
|
16
|
-
return token;
|
|
17
|
-
}
|
|
18
|
-
peek() {
|
|
19
|
-
return this.tokens[this.position + 1] || this.tokens[this.tokens.length - 1];
|
|
20
|
-
}
|
|
21
|
-
expect(type) {
|
|
22
|
-
const token = this.current();
|
|
23
|
-
if (token.type !== type) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
`Expected ${type}, got ${token.type} at position ${token.position}`
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return this.advance();
|
|
29
|
-
}
|
|
30
|
-
isKeyword(keyword) {
|
|
31
|
-
return this.current().type === keyword;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Parse a property object: { key: value, ... }
|
|
35
|
-
* Note: Cypher uses colon (:) for properties in node patterns, not equals (=)
|
|
36
|
-
*/
|
|
37
|
-
parseProperties() {
|
|
38
|
-
this.expect(TokenType.LBRACE);
|
|
39
|
-
const properties = {};
|
|
40
|
-
if (this.current().type === TokenType.RBRACE) {
|
|
41
|
-
this.advance();
|
|
42
|
-
return properties;
|
|
43
|
-
}
|
|
44
|
-
while (true) {
|
|
45
|
-
const key = this.expect(TokenType.IDENTIFIER).value;
|
|
46
|
-
this.expect(TokenType.LABEL);
|
|
47
|
-
let value;
|
|
48
|
-
if (this.current().type === TokenType.LBRACKET) {
|
|
49
|
-
value = this.parseArrayLiteral();
|
|
50
|
-
} else if (this.current().type === TokenType.STRING) {
|
|
51
|
-
value = this.advance().value;
|
|
52
|
-
} else if (this.current().type === TokenType.NUMBER) {
|
|
53
|
-
value = parseFloat(this.advance().value);
|
|
54
|
-
} else if (this.current().type === TokenType.TRUE) {
|
|
55
|
-
value = true;
|
|
56
|
-
this.advance();
|
|
57
|
-
} else if (this.current().type === TokenType.FALSE) {
|
|
58
|
-
value = false;
|
|
59
|
-
this.advance();
|
|
60
|
-
} else if (this.current().type === TokenType.IDENTIFIER) {
|
|
61
|
-
value = this.advance().value;
|
|
62
|
-
} else {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`Unexpected token type for property value: ${this.current().type}`
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
properties[key] = value;
|
|
68
|
-
if (this.current().type === TokenType.RBRACE) {
|
|
69
|
-
this.advance();
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
this.expect(TokenType.COMMA);
|
|
73
|
-
}
|
|
74
|
-
return properties;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Parse a node pattern: (var:Label {prop: value}) or (:Label {prop: value})
|
|
78
|
-
*/
|
|
79
|
-
parseNodePattern() {
|
|
80
|
-
this.expect(TokenType.LPAREN);
|
|
81
|
-
const pattern = {
|
|
82
|
-
type: "NodePattern",
|
|
83
|
-
labels: []
|
|
84
|
-
};
|
|
85
|
-
if (this.current().type === TokenType.IDENTIFIER) {
|
|
86
|
-
const nextToken = this.peek();
|
|
87
|
-
if (nextToken.type === TokenType.LABEL || nextToken.type === TokenType.LBRACE || nextToken.type === TokenType.RPAREN) {
|
|
88
|
-
pattern.variable = this.advance().value;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (this.current().type === TokenType.LABEL) {
|
|
92
|
-
this.advance();
|
|
93
|
-
if (this.current().type !== TokenType.IDENTIFIER) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Expected identifier after :, got ${this.current().type}`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
pattern.labels.push(this.advance().value);
|
|
99
|
-
if (this.current().type === TokenType.LABEL) {
|
|
100
|
-
pattern.labelOperator = "AND";
|
|
101
|
-
while (this.current().type === TokenType.LABEL) {
|
|
102
|
-
this.advance();
|
|
103
|
-
if (this.current().type === TokenType.IDENTIFIER) {
|
|
104
|
-
pattern.labels.push(this.advance().value);
|
|
105
|
-
} else {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`Expected identifier after :, got ${this.current().type}`
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
} else if (this.current().type === TokenType.PIPE) {
|
|
112
|
-
pattern.labelOperator = "OR";
|
|
113
|
-
while (this.current().type === TokenType.PIPE) {
|
|
114
|
-
this.advance();
|
|
115
|
-
if (this.current().type === TokenType.IDENTIFIER) {
|
|
116
|
-
pattern.labels.push(this.advance().value);
|
|
117
|
-
} else {
|
|
118
|
-
throw new Error(
|
|
119
|
-
`Expected identifier after |, got ${this.current().type}`
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
} else {
|
|
124
|
-
pattern.labelOperator = "AND";
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (this.current().type === TokenType.LBRACE) {
|
|
128
|
-
pattern.properties = this.parseProperties();
|
|
129
|
-
}
|
|
130
|
-
this.expect(TokenType.RPAREN);
|
|
131
|
-
return pattern;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Parse relationship pattern: -[:RELATIONSHIP_TYPE]-> or <-[:RELATIONSHIP_TYPE]- or -[r:RELATIONSHIP_TYPE]->
|
|
135
|
-
*/
|
|
136
|
-
parseRelationship() {
|
|
137
|
-
const relationship = {
|
|
138
|
-
type: "RelationshipPattern",
|
|
139
|
-
direction: "outgoing"
|
|
140
|
-
};
|
|
141
|
-
if (this.current().type === TokenType.ARROW && this.current().value === "<-") {
|
|
142
|
-
relationship.direction = "incoming";
|
|
143
|
-
this.advance();
|
|
144
|
-
} else {
|
|
145
|
-
this.expect(TokenType.DASH);
|
|
146
|
-
}
|
|
147
|
-
if (this.current().type === TokenType.LBRACKET) {
|
|
148
|
-
this.advance();
|
|
149
|
-
if (this.current().type === TokenType.IDENTIFIER) {
|
|
150
|
-
const nextToken = this.peek();
|
|
151
|
-
if (nextToken.type === TokenType.LABEL || nextToken.type === TokenType.ASTERISK) {
|
|
152
|
-
relationship.variable = this.advance().value;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (this.current().type === TokenType.ASTERISK) {
|
|
156
|
-
this.advance();
|
|
157
|
-
relationship.variableLength = true;
|
|
158
|
-
relationship.minLength = 0;
|
|
159
|
-
relationship.maxLength = void 0;
|
|
160
|
-
} else if (this.current().type === TokenType.LABEL) {
|
|
161
|
-
this.advance();
|
|
162
|
-
const edgeTypes = [];
|
|
163
|
-
edgeTypes.push(this.expect(TokenType.IDENTIFIER).value);
|
|
164
|
-
while (this.current().type === TokenType.PIPE) {
|
|
165
|
-
this.advance();
|
|
166
|
-
edgeTypes.push(this.expect(TokenType.IDENTIFIER).value);
|
|
167
|
-
}
|
|
168
|
-
relationship.edgeType = edgeTypes.length === 1 ? edgeTypes[0] : edgeTypes;
|
|
169
|
-
if (this.current().type === TokenType.ASTERISK) {
|
|
170
|
-
this.advance();
|
|
171
|
-
relationship.variableLength = true;
|
|
172
|
-
relationship.minLength = 0;
|
|
173
|
-
relationship.maxLength = void 0;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
this.expect(TokenType.RBRACKET);
|
|
177
|
-
}
|
|
178
|
-
if (relationship.direction === "incoming") {
|
|
179
|
-
this.expect(TokenType.DASH);
|
|
180
|
-
} else {
|
|
181
|
-
if (this.current().type === TokenType.ARROW && this.current().value === "->") {
|
|
182
|
-
this.advance();
|
|
183
|
-
} else if (this.current().type === TokenType.DASH) {
|
|
184
|
-
relationship.direction = "both";
|
|
185
|
-
this.advance();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return relationship;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Parse MATCH clause (supports OPTIONAL MATCH)
|
|
192
|
-
*/
|
|
193
|
-
parseMatch() {
|
|
194
|
-
const isOptional = this.isKeyword(TokenType.OPTIONAL);
|
|
195
|
-
if (isOptional) {
|
|
196
|
-
this.advance();
|
|
197
|
-
}
|
|
198
|
-
this.expect(TokenType.MATCH);
|
|
199
|
-
const firstPattern = this.parsePatternChain();
|
|
200
|
-
const matchClause = {
|
|
201
|
-
type: "MatchClause",
|
|
202
|
-
patterns: firstPattern.patterns,
|
|
203
|
-
relationships: firstPattern.relationships,
|
|
204
|
-
additionalMatches: []
|
|
205
|
-
};
|
|
206
|
-
while (this.current().type === TokenType.COMMA) {
|
|
207
|
-
this.advance();
|
|
208
|
-
const additionalPattern = this.parsePatternChain();
|
|
209
|
-
matchClause.additionalMatches.push(additionalPattern);
|
|
210
|
-
}
|
|
211
|
-
if (isOptional) {
|
|
212
|
-
matchClause.optionalMatches = [
|
|
213
|
-
{
|
|
214
|
-
type: "MatchStatement",
|
|
215
|
-
patterns: firstPattern.patterns,
|
|
216
|
-
relationships: firstPattern.relationships
|
|
217
|
-
},
|
|
218
|
-
...matchClause.additionalMatches || []
|
|
219
|
-
];
|
|
220
|
-
matchClause.patterns = [];
|
|
221
|
-
matchClause.relationships = void 0;
|
|
222
|
-
matchClause.additionalMatches = void 0;
|
|
223
|
-
}
|
|
224
|
-
return matchClause;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Parse a pattern chain: (node)-[:rel]->(node)-[:rel]->(node)...
|
|
228
|
-
* Returns patterns and relationships for a single connected pattern
|
|
229
|
-
*/
|
|
230
|
-
parsePatternChain() {
|
|
231
|
-
const patterns = [];
|
|
232
|
-
const relationships = [];
|
|
233
|
-
patterns.push(this.parseNodePattern());
|
|
234
|
-
while (this.current().type === TokenType.DASH || this.current().type === TokenType.ARROW) {
|
|
235
|
-
relationships.push(this.parseRelationship());
|
|
236
|
-
patterns.push(this.parseNodePattern());
|
|
237
|
-
}
|
|
238
|
-
return {
|
|
239
|
-
type: "MatchStatement",
|
|
240
|
-
patterns,
|
|
241
|
-
relationships: relationships.length > 0 ? relationships : void 0
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Parse WHERE clause
|
|
246
|
-
* Example: WHERE m.type = "Command" AND "param" IN m.parameters
|
|
247
|
-
* Note: Flat properties only (no nesting, like Neo4j Cypher)
|
|
248
|
-
*/
|
|
249
|
-
parseWhere() {
|
|
250
|
-
this.expect(TokenType.WHERE);
|
|
251
|
-
const conditions = [];
|
|
252
|
-
let logic;
|
|
253
|
-
while (true) {
|
|
254
|
-
const variable = this.expect(TokenType.IDENTIFIER).value;
|
|
255
|
-
let property;
|
|
256
|
-
if (this.current().type === TokenType.DOT) {
|
|
257
|
-
this.advance();
|
|
258
|
-
property = this.expect(TokenType.IDENTIFIER).value;
|
|
259
|
-
}
|
|
260
|
-
let operator = "=";
|
|
261
|
-
if (this.current().type === TokenType.EQUALS) {
|
|
262
|
-
operator = "=";
|
|
263
|
-
this.advance();
|
|
264
|
-
} else if (this.current().type === TokenType.NOT_EQUALS) {
|
|
265
|
-
operator = "!=";
|
|
266
|
-
this.advance();
|
|
267
|
-
} else if (this.current().type === TokenType.GT) {
|
|
268
|
-
operator = ">";
|
|
269
|
-
this.advance();
|
|
270
|
-
} else if (this.current().type === TokenType.LT) {
|
|
271
|
-
operator = "<";
|
|
272
|
-
this.advance();
|
|
273
|
-
} else if (this.current().type === TokenType.GTE) {
|
|
274
|
-
operator = ">=";
|
|
275
|
-
this.advance();
|
|
276
|
-
} else if (this.current().type === TokenType.LTE) {
|
|
277
|
-
operator = "<=";
|
|
278
|
-
this.advance();
|
|
279
|
-
} else if (this.current().type === TokenType.NOT) {
|
|
280
|
-
this.advance();
|
|
281
|
-
this.expect(TokenType.IN);
|
|
282
|
-
operator = "NOT IN";
|
|
283
|
-
} else if (this.current().type === TokenType.IN) {
|
|
284
|
-
this.advance();
|
|
285
|
-
operator = "IN";
|
|
286
|
-
} else if (this.current().type === TokenType.IS) {
|
|
287
|
-
this.advance();
|
|
288
|
-
if (this.isKeyword(TokenType.NULL)) {
|
|
289
|
-
this.advance();
|
|
290
|
-
operator = "IS NULL";
|
|
291
|
-
} else if (this.isKeyword(TokenType.NOT)) {
|
|
292
|
-
this.advance();
|
|
293
|
-
this.expect(TokenType.NULL);
|
|
294
|
-
operator = "IS NOT NULL";
|
|
295
|
-
} else {
|
|
296
|
-
throw new Error(
|
|
297
|
-
`Expected NULL or NOT NULL after IS at position ${this.current().position}`
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
} else if (this.current().type === TokenType.STARTS) {
|
|
301
|
-
this.advance();
|
|
302
|
-
this.expect(TokenType.WITH);
|
|
303
|
-
operator = "STARTS WITH";
|
|
304
|
-
} else if (this.current().type === TokenType.ENDS) {
|
|
305
|
-
this.advance();
|
|
306
|
-
this.expect(TokenType.WITH);
|
|
307
|
-
operator = "ENDS WITH";
|
|
308
|
-
} else if (this.current().type === TokenType.CONTAINS) {
|
|
309
|
-
operator = "CONTAINS";
|
|
310
|
-
this.advance();
|
|
311
|
-
} else if (this.current().type === TokenType.REGEX_MATCH) {
|
|
312
|
-
operator = "=~";
|
|
313
|
-
this.advance();
|
|
314
|
-
} else {
|
|
315
|
-
throw new Error(
|
|
316
|
-
`Unsupported operator at position ${this.current().position}.`
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
let value = null;
|
|
320
|
-
if (operator === "IS NULL" || operator === "IS NOT NULL") {
|
|
321
|
-
} else if (operator === "STARTS WITH" || operator === "ENDS WITH" || operator === "CONTAINS" || operator === "=~") {
|
|
322
|
-
if (this.current().type === TokenType.STRING) {
|
|
323
|
-
value = this.advance().value;
|
|
324
|
-
} else {
|
|
325
|
-
throw new Error(
|
|
326
|
-
`String matching operators (${operator}) require a string value at position ${this.current().position}`
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
} else if (this.current().type === TokenType.LBRACKET) {
|
|
330
|
-
value = this.parseArrayLiteral();
|
|
331
|
-
} else if (this.current().type === TokenType.STRING) {
|
|
332
|
-
value = this.advance().value;
|
|
333
|
-
} else if (this.current().type === TokenType.NUMBER) {
|
|
334
|
-
value = parseFloat(this.advance().value);
|
|
335
|
-
} else if (this.current().type === TokenType.TRUE) {
|
|
336
|
-
value = true;
|
|
337
|
-
this.advance();
|
|
338
|
-
} else if (this.current().type === TokenType.FALSE) {
|
|
339
|
-
value = false;
|
|
340
|
-
this.advance();
|
|
341
|
-
} else if (this.current().type === TokenType.IDENTIFIER) {
|
|
342
|
-
value = this.advance().value;
|
|
343
|
-
} else {
|
|
344
|
-
throw new Error(
|
|
345
|
-
`Unexpected token type for WHERE value: ${this.current().type}`
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
conditions.push({ variable, property, operator, value });
|
|
349
|
-
if (this.isKeyword(TokenType.AND)) {
|
|
350
|
-
if (logic === void 0) {
|
|
351
|
-
logic = "AND";
|
|
352
|
-
} else if (logic !== "AND") {
|
|
353
|
-
throw new Error("Cannot mix AND and OR in WHERE clause");
|
|
354
|
-
}
|
|
355
|
-
this.advance();
|
|
356
|
-
continue;
|
|
357
|
-
} else if (this.isKeyword(TokenType.OR)) {
|
|
358
|
-
if (logic === void 0) {
|
|
359
|
-
logic = "OR";
|
|
360
|
-
} else if (logic !== "OR") {
|
|
361
|
-
throw new Error("Cannot mix AND and OR in WHERE clause");
|
|
362
|
-
}
|
|
363
|
-
this.advance();
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
break;
|
|
367
|
-
}
|
|
368
|
-
return {
|
|
369
|
-
type: "WhereClause",
|
|
370
|
-
conditions,
|
|
371
|
-
logic
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Parse array literal: [value1, value2, ...]
|
|
376
|
-
*/
|
|
377
|
-
parseArrayLiteral() {
|
|
378
|
-
this.expect(TokenType.LBRACKET);
|
|
379
|
-
const array = [];
|
|
380
|
-
if (this.current().type === TokenType.RBRACKET) {
|
|
381
|
-
this.advance();
|
|
382
|
-
return array;
|
|
383
|
-
}
|
|
384
|
-
while (true) {
|
|
385
|
-
if (this.current().type === TokenType.STRING) {
|
|
386
|
-
array.push(this.advance().value);
|
|
387
|
-
} else if (this.current().type === TokenType.NUMBER) {
|
|
388
|
-
array.push(parseFloat(this.advance().value));
|
|
389
|
-
} else if (this.current().type === TokenType.TRUE) {
|
|
390
|
-
array.push(true);
|
|
391
|
-
this.advance();
|
|
392
|
-
} else if (this.current().type === TokenType.FALSE) {
|
|
393
|
-
array.push(false);
|
|
394
|
-
this.advance();
|
|
395
|
-
} else if (this.current().type === TokenType.IDENTIFIER) {
|
|
396
|
-
array.push(this.advance().value);
|
|
397
|
-
} else {
|
|
398
|
-
throw new Error(
|
|
399
|
-
`Unexpected token type in array literal: ${this.current().type}`
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
if (this.current().type === TokenType.COMMA) {
|
|
403
|
-
this.advance();
|
|
404
|
-
} else if (this.current().type === TokenType.RBRACKET) {
|
|
405
|
-
this.advance();
|
|
406
|
-
break;
|
|
407
|
-
} else {
|
|
408
|
-
throw new Error(
|
|
409
|
-
`Expected comma or closing bracket in array literal at position ${this.current().position}`
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return array;
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Parse RETURN clause
|
|
417
|
-
* Supports: RETURN *, RETURN DISTINCT, RETURN var1, var2, RETURN var AS alias, LIMIT n
|
|
418
|
-
*/
|
|
419
|
-
parseReturn() {
|
|
420
|
-
this.expect(TokenType.RETURN);
|
|
421
|
-
const distinct = this.isKeyword(TokenType.DISTINCT);
|
|
422
|
-
if (distinct) {
|
|
423
|
-
this.advance();
|
|
424
|
-
}
|
|
425
|
-
if (this.current().type === TokenType.ASTERISK) {
|
|
426
|
-
this.advance();
|
|
427
|
-
const limit2 = this.parseOptionalLimit();
|
|
428
|
-
return {
|
|
429
|
-
type: "ReturnClause",
|
|
430
|
-
distinct,
|
|
431
|
-
returnAll: true,
|
|
432
|
-
items: [],
|
|
433
|
-
limit: limit2
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
const items = [];
|
|
437
|
-
while (true) {
|
|
438
|
-
const expression = this.expect(TokenType.IDENTIFIER).value;
|
|
439
|
-
let alias;
|
|
440
|
-
if (this.isKeyword(TokenType.AS)) {
|
|
441
|
-
this.advance();
|
|
442
|
-
alias = this.expect(TokenType.IDENTIFIER).value;
|
|
443
|
-
}
|
|
444
|
-
items.push({ expression, alias });
|
|
445
|
-
if (this.current().type === TokenType.COMMA) {
|
|
446
|
-
this.advance();
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
const limit = this.parseOptionalLimit();
|
|
452
|
-
return {
|
|
453
|
-
type: "ReturnClause",
|
|
454
|
-
distinct,
|
|
455
|
-
returnAll: false,
|
|
456
|
-
items,
|
|
457
|
-
limit
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Parse optional LIMIT clause
|
|
462
|
-
* Returns the limit number or undefined if not present
|
|
463
|
-
*/
|
|
464
|
-
parseOptionalLimit() {
|
|
465
|
-
if (this.isKeyword(TokenType.LIMIT)) {
|
|
466
|
-
this.advance();
|
|
467
|
-
const limitToken = this.expect(TokenType.NUMBER);
|
|
468
|
-
return parseInt(limitToken.value, 10);
|
|
469
|
-
}
|
|
470
|
-
return void 0;
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Parse ORDER BY clause
|
|
474
|
-
* Supports: ORDER BY n.name, ORDER BY n.name ASC, ORDER BY n.name DESC
|
|
475
|
-
* Also supports multiple fields: ORDER BY n.type DESC, n.name ASC
|
|
476
|
-
*/
|
|
477
|
-
parseOrderBy() {
|
|
478
|
-
this.expect(TokenType.ORDER);
|
|
479
|
-
this.expect(TokenType.BY);
|
|
480
|
-
const items = [];
|
|
481
|
-
while (true) {
|
|
482
|
-
const variable = this.expect(TokenType.IDENTIFIER).value;
|
|
483
|
-
let property;
|
|
484
|
-
if (this.current().type === TokenType.DOT) {
|
|
485
|
-
this.advance();
|
|
486
|
-
property = this.expect(TokenType.IDENTIFIER).value;
|
|
487
|
-
}
|
|
488
|
-
let direction = "ASC";
|
|
489
|
-
if (this.isKeyword(TokenType.ASC)) {
|
|
490
|
-
this.advance();
|
|
491
|
-
direction = "ASC";
|
|
492
|
-
} else if (this.isKeyword(TokenType.DESC)) {
|
|
493
|
-
this.advance();
|
|
494
|
-
direction = "DESC";
|
|
495
|
-
}
|
|
496
|
-
items.push({ variable, property, direction });
|
|
497
|
-
if (this.current().type === TokenType.COMMA) {
|
|
498
|
-
this.advance();
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
return { type: "OrderByStatement", items };
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Parse entire Cypher query
|
|
507
|
-
* Returns statements in execution order
|
|
508
|
-
*/
|
|
509
|
-
parse() {
|
|
510
|
-
const statements = [];
|
|
511
|
-
while (this.current().type !== TokenType.EOF) {
|
|
512
|
-
if (this.isKeyword(TokenType.MATCH) || this.isKeyword(TokenType.OPTIONAL)) {
|
|
513
|
-
const isOptional = this.isKeyword(TokenType.OPTIONAL);
|
|
514
|
-
const matchClause = this.parseMatch();
|
|
515
|
-
if (matchClause.optionalMatches) {
|
|
516
|
-
for (const optionalMatch of matchClause.optionalMatches) {
|
|
517
|
-
statements.push({
|
|
518
|
-
type: "MatchStatement",
|
|
519
|
-
patterns: optionalMatch.patterns,
|
|
520
|
-
relationships: optionalMatch.relationships,
|
|
521
|
-
optional: true
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
} else {
|
|
525
|
-
if (matchClause.patterns.length > 0) {
|
|
526
|
-
statements.push({
|
|
527
|
-
type: "MatchStatement",
|
|
528
|
-
patterns: matchClause.patterns,
|
|
529
|
-
relationships: matchClause.relationships,
|
|
530
|
-
optional: false
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
if (matchClause.additionalMatches) {
|
|
534
|
-
for (const additionalMatch of matchClause.additionalMatches) {
|
|
535
|
-
statements.push({
|
|
536
|
-
type: "MatchStatement",
|
|
537
|
-
patterns: additionalMatch.patterns,
|
|
538
|
-
relationships: additionalMatch.relationships,
|
|
539
|
-
optional: false
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
if (this.isKeyword(TokenType.WHERE)) {
|
|
547
|
-
const whereClause = this.parseWhere();
|
|
548
|
-
statements.push({
|
|
549
|
-
type: "WhereStatement",
|
|
550
|
-
conditions: whereClause.conditions,
|
|
551
|
-
logic: whereClause.logic
|
|
552
|
-
});
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
if (this.isKeyword(TokenType.ORDER)) {
|
|
556
|
-
const orderByStatement = this.parseOrderBy();
|
|
557
|
-
statements.push(orderByStatement);
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
if (this.isKeyword(TokenType.RETURN)) {
|
|
561
|
-
const returnClause = this.parseReturn();
|
|
562
|
-
statements.push({
|
|
563
|
-
type: "ReturnStatement",
|
|
564
|
-
distinct: returnClause.distinct,
|
|
565
|
-
returnAll: returnClause.returnAll,
|
|
566
|
-
items: returnClause.items,
|
|
567
|
-
limit: returnClause.limit
|
|
568
|
-
});
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
break;
|
|
572
|
-
}
|
|
573
|
-
if (this.current().type !== TokenType.EOF) {
|
|
574
|
-
throw new Error(
|
|
575
|
-
`Unexpected token: ${this.current().type} at position ${this.current().position}`
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
return {
|
|
579
|
-
type: "CypherQuery",
|
|
580
|
-
statements
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
export {
|
|
585
|
-
Parser
|
|
586
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FORBIDDEN_AGGREGATIONS,
|
|
3
|
-
FORBIDDEN_KEYWORDS,
|
|
4
|
-
FORBIDDEN_MODIFIERS,
|
|
5
|
-
FORBIDDEN_PATTERNS
|
|
6
|
-
} from "./unsupported-features.config.js";
|
|
7
|
-
const UNSUPPORTED_FEATURES = [
|
|
8
|
-
...FORBIDDEN_KEYWORDS,
|
|
9
|
-
...FORBIDDEN_AGGREGATIONS,
|
|
10
|
-
...FORBIDDEN_MODIFIERS,
|
|
11
|
-
...FORBIDDEN_PATTERNS
|
|
12
|
-
];
|
|
13
|
-
class UnsupportedFeatureError extends Error {
|
|
14
|
-
constructor(feature, query) {
|
|
15
|
-
const message = `Unsupported Cypher feature detected: ${feature.name}
|
|
16
|
-
|
|
17
|
-
${feature.errorMessage}${feature.alternative ? `
|
|
18
|
-
|
|
19
|
-
Alternative: ${feature.alternative}` : ""}
|
|
20
|
-
|
|
21
|
-
Query: ${query}`;
|
|
22
|
-
super(message);
|
|
23
|
-
this.feature = feature;
|
|
24
|
-
this.query = query;
|
|
25
|
-
this.name = "UnsupportedFeatureError";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function validateQuery(query) {
|
|
29
|
-
if (!query || typeof query !== "string") {
|
|
30
|
-
throw new Error("Query must be a non-empty string");
|
|
31
|
-
}
|
|
32
|
-
const normalizedQuery = query.trim();
|
|
33
|
-
if (normalizedQuery.length === 0) {
|
|
34
|
-
throw new Error("Query cannot be empty");
|
|
35
|
-
}
|
|
36
|
-
for (const feature of UNSUPPORTED_FEATURES) {
|
|
37
|
-
const pattern = typeof feature.pattern === "string" ? new RegExp(feature.pattern, "i") : feature.pattern;
|
|
38
|
-
if (pattern.test(normalizedQuery)) {
|
|
39
|
-
if (feature.name === "WITH clause") {
|
|
40
|
-
const withMatches = normalizedQuery.matchAll(/\bWITH\b/gi);
|
|
41
|
-
let isAllowed = false;
|
|
42
|
-
for (const match of withMatches) {
|
|
43
|
-
const beforeMatch = normalizedQuery.substring(
|
|
44
|
-
Math.max(0, match.index - 10),
|
|
45
|
-
match.index
|
|
46
|
-
);
|
|
47
|
-
if (/\bSTARTS\s+$/i.test(beforeMatch) || /\bENDS\s+$/i.test(beforeMatch)) {
|
|
48
|
-
isAllowed = true;
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (isAllowed) {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
throw new UnsupportedFeatureError(feature, normalizedQuery);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function validateQuerySafe(query) {
|
|
61
|
-
try {
|
|
62
|
-
validateQuery(query);
|
|
63
|
-
return { valid: true };
|
|
64
|
-
} catch (error) {
|
|
65
|
-
if (error instanceof UnsupportedFeatureError) {
|
|
66
|
-
return { valid: false, error };
|
|
67
|
-
}
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
export {
|
|
72
|
-
UnsupportedFeatureError,
|
|
73
|
-
validateQuery,
|
|
74
|
-
validateQuerySafe
|
|
75
|
-
};
|