@lowgular/code-graph 0.0.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 +2463 -0
- package/lib.js +2430 -0
- package/package.json +26 -7
- package/apps/code-graph/src/main.js +0 -50
- package/libs/cli/code-graph/src/index.js +0 -39
- package/libs/cli/code-graph/src/lib/class-declarations/abstractions/service.js +0 -72
- package/libs/cli/code-graph/src/lib/class-declarations/abstractions/service.stateful.js +0 -68
- package/libs/cli/code-graph/src/lib/class-declarations/class-declarations.doc.js +0 -147
- package/libs/cli/code-graph/src/lib/class-declarations/class-declarations.graph.js +0 -55
- package/libs/cli/code-graph/src/lib/class-declarations/index.js +0 -27
- package/libs/cli/code-graph/src/lib/code.graph.factory.js +0 -63
- package/libs/cli/code-graph/src/lib/code.graph.js +0 -62
- package/libs/cli/code-graph/src/lib/core/core.js +0 -58
- package/libs/cli/code-graph/src/lib/core.js +0 -85
- package/libs/cli/code-graph/src/lib/decorators/index.js +0 -36
- package/libs/cli/code-graph/src/lib/docs/index.js +0 -21
- package/libs/cli/code-graph/src/lib/docs/specs.js +0 -36
- package/libs/cli/code-graph/src/lib/expressions/call-expressions.graph.js +0 -28
- package/libs/cli/code-graph/src/lib/fixtures/reactive-event-bus/src/app/examples.event-bus.js +0 -53
- package/libs/cli/code-graph/src/lib/method-declarations/index.js +0 -23
- package/libs/cli/code-graph/src/lib/method-declarations/method-declarations.doc.js +0 -106
- package/libs/cli/code-graph/src/lib/method-declarations/method-declarations.graph.js +0 -56
- package/libs/cli/code-graph/src/lib/parameters/index.js +0 -42
- package/libs/cli/code-graph/src/lib/property-declarations/abstractions/reactive-state.evals.js +0 -139
- package/libs/cli/code-graph/src/lib/property-declarations/abstractions/reactive-state.js +0 -104
- package/libs/cli/code-graph/src/lib/property-declarations/index.js +0 -25
- package/libs/cli/code-graph/src/lib/property-declarations/property-declarations.doc.js +0 -74
- package/libs/cli/code-graph/src/lib/property-declarations/property-declarations.graph.js +0 -43
- package/libs/cli/code-graph/src/lib/tools/index.js +0 -34
- package/libs/cli/code-graph/src/lib/tools/match-code-graph.tool.js +0 -64
- package/libs/cli/code-graph/src/lib/types/index.js +0 -99
- package/libs/cli/code-graph/src/lib/v2/extractors/extractor.doc.js +0 -345
- package/libs/cli/code-graph/src/lib/v2/extractors/extractor.js +0 -52
- package/libs/cli/code-graph/src/lib/v2/extractors/index.js +0 -23
- package/libs/cli/code-graph/src/lib/v2/graph-builder/code-graph.builder.js +0 -76
- package/libs/cli/code-graph/src/lib/v2/graph-builder/index.js +0 -21
- package/libs/cli/code-graph/src/lib/v2/graph-builder/node.processor.js +0 -48
- package/libs/cli/code-graph/src/lib/v2/graph-builder/relationship.processor.js +0 -79
- package/libs/cli/code-graph/src/lib/v2/graph-builder/type.processor.js +0 -45
- package/libs/cli/code-graph/src/lib/v2/index.js +0 -37
- package/libs/cli/code-graph/src/lib/v2/tools/build-code-graph.doc.js +0 -220
- package/libs/cli/code-graph/src/lib/v2/tools/build-code-graph.tool.js +0 -42
- package/libs/cli/cypher/src/index.js +0 -25
- package/libs/cli/cypher/src/lib/docs/features/advanced-optional-match.feature.js +0 -277
- package/libs/cli/cypher/src/lib/docs/features/anonymous-relationship.feature.js +0 -69
- package/libs/cli/cypher/src/lib/docs/features/anonymous-variable-length.feature.js +0 -105
- package/libs/cli/cypher/src/lib/docs/features/basic-filtering.feature.js +0 -70
- package/libs/cli/cypher/src/lib/docs/features/basic-optional-match.feature.js +0 -99
- package/libs/cli/cypher/src/lib/docs/features/basic-relationship.feature.js +0 -125
- package/libs/cli/cypher/src/lib/docs/features/index.js +0 -51
- package/libs/cli/cypher/src/lib/docs/features/limit.feature.js +0 -50
- package/libs/cli/cypher/src/lib/docs/features/match-nodes.feature.js +0 -106
- package/libs/cli/cypher/src/lib/docs/features/multiple-relationship-types.feature.js +0 -105
- package/libs/cli/cypher/src/lib/docs/features/order-by.feature.js +0 -143
- package/libs/cli/cypher/src/lib/docs/features/property-matching.feature.js +0 -152
- package/libs/cli/cypher/src/lib/docs/features/relationship-without-variable.feature.js +0 -48
- package/libs/cli/cypher/src/lib/docs/features/return-statement.feature.js +0 -65
- package/libs/cli/cypher/src/lib/docs/features/unidirected-relationship.feature.js +0 -82
- package/libs/cli/cypher/src/lib/docs/features/variable-length-path.feature.js +0 -136
- package/libs/cli/cypher/src/lib/docs/features/where-conditional.feature.js +0 -187
- package/libs/cli/cypher/src/lib/docs/features/where-operators.feature.js +0 -302
- package/libs/cli/cypher/src/lib/docs/prompts.js +0 -36
- package/libs/cli/cypher/src/lib/docs/specs.js +0 -181
- package/libs/cli/cypher/src/lib/executor/condition-evaluator.js +0 -158
- package/libs/cli/cypher/src/lib/executor/executor.js +0 -83
- package/libs/cli/cypher/src/lib/executor/graph.js +0 -15
- package/libs/cli/cypher/src/lib/executor/match-engine.js +0 -153
- package/libs/cli/cypher/src/lib/executor/pattern-matcher.js +0 -109
- package/libs/cli/cypher/src/lib/executor/relationship-navigator.js +0 -64
- package/libs/cli/cypher/src/lib/executor/result-formatter.js +0 -143
- package/libs/cli/cypher/src/lib/executor/traverse-engine.js +0 -164
- package/libs/cli/cypher/src/lib/executor/utils.js +0 -37
- package/libs/cli/cypher/src/lib/graph.stub.js +0 -63
- package/libs/cli/cypher/src/lib/index.js +0 -74
- package/libs/cli/cypher/src/lib/lexer.js +0 -398
- package/libs/cli/cypher/src/lib/parser.js +0 -602
- package/libs/cli/cypher/src/lib/validator/query-validator.js +0 -95
- package/libs/cli/cypher/src/lib/validator/supported-features.config.js +0 -111
- package/libs/cli/cypher/src/lib/validator/unsupported-features.config.js +0 -150
- package/libs/cli/shared/src/index.js +0 -53
- package/libs/cli/shared/src/lib/admin-token.js +0 -121
- package/libs/cli/shared/src/lib/config.js +0 -49
- package/libs/cli/shared/src/lib/consts.js +0 -57
- package/libs/cli/shared/src/lib/core.js +0 -46
- package/libs/cli/shared/src/lib/infrastructure/cli.js +0 -47
- package/libs/cli/shared/src/lib/infrastructure/config.js +0 -47
- package/libs/cli/shared/src/lib/infrastructure/config.reporitory.js +0 -51
- package/libs/cli/shared/src/lib/infrastructure/formatters/console-formatter.js +0 -96
- package/libs/cli/shared/src/lib/infrastructure/formatters/index.js +0 -41
- package/libs/cli/shared/src/lib/infrastructure/formatters/json-formatter.js +0 -64
- package/libs/cli/shared/src/lib/infrastructure/formatters/markdown-formatter.js +0 -120
- package/libs/cli/shared/src/lib/infrastructure/formatters/types.js +0 -15
- package/libs/cli/shared/src/lib/infrastructure/formatters/utils.js +0 -72
- package/libs/cli/shared/src/lib/infrastructure/git.js +0 -147
- package/libs/cli/shared/src/lib/infrastructure/http.js +0 -257
- package/libs/cli/shared/src/lib/infrastructure/markdown.js +0 -95
- package/libs/cli/shared/src/lib/infrastructure/transformers/embeddings.js +0 -88
- package/libs/cli/shared/src/lib/infrastructure/transformers/index.js +0 -25
- package/libs/cli/shared/src/lib/infrastructure/transformers/rag-eval.js +0 -154
- package/libs/cli/shared/src/lib/infrastructure/transformers/similarity.js +0 -210
- package/libs/cli/shared/src/lib/infrastructure/workspace.js +0 -297
- package/libs/cli/shared/src/lib/infrastructure/yaml.js +0 -237
- package/libs/cli/shared/src/lib/lowgular.config.js +0 -120
- package/libs/cli/shared/src/lib/token.js +0 -135
- package/libs/cli/shared/src/lib/utils/solution.util.js +0 -35
- package/libs/cli/shared/src/lib/utils/utils.js +0 -89
- package/libs/cli/typescript/src/index.js +0 -21
- package/libs/cli/typescript/src/lib/base/index.js +0 -23
- package/libs/cli/typescript/src/lib/base/nameable.js +0 -34
- package/libs/cli/typescript/src/lib/base/nodeable.js +0 -36
- package/libs/cli/typescript/src/lib/class.implementation.js +0 -52
- package/libs/cli/typescript/src/lib/core/declaration.registry.js +0 -107
- package/libs/cli/typescript/src/lib/core/design-pattern.js +0 -55
- package/libs/cli/typescript/src/lib/core/index.js +0 -35
- package/libs/cli/typescript/src/lib/core/navigable-declaration.js +0 -69
- package/libs/cli/typescript/src/lib/core/program-context.js +0 -71
- package/libs/cli/typescript/src/lib/core/syntax-kind.utils.js +0 -40
- package/libs/cli/typescript/src/lib/core/type.js +0 -374
- package/libs/cli/typescript/src/lib/declaration.abstraction.js +0 -90
- package/libs/cli/typescript/src/lib/decorator.implementation.js +0 -35
- package/libs/cli/typescript/src/lib/enum.implementation.js +0 -35
- package/libs/cli/typescript/src/lib/function-declaration.implementation.js +0 -35
- package/libs/cli/typescript/src/lib/index.js +0 -57
- package/libs/cli/typescript/src/lib/interface.implementation.js +0 -51
- package/libs/cli/typescript/src/lib/members/method-abstraction.resolver.js +0 -90
- package/libs/cli/typescript/src/lib/members/method-declaration.implementation.js +0 -44
- package/libs/cli/typescript/src/lib/members/method-signature.implementation.js +0 -46
- package/libs/cli/typescript/src/lib/members/method.util.js +0 -32
- package/libs/cli/typescript/src/lib/members/parameter.implementation.js +0 -38
- package/libs/cli/typescript/src/lib/members/property-declaration.implementation.js +0 -64
- package/libs/cli/typescript/src/lib/members/property-signature.implementation.js +0 -34
- package/libs/cli/typescript/src/lib/members/property.util.js +0 -61
- package/libs/cli/typescript/src/lib/members/statements/expressions/binary-expression.implementation.js +0 -42
- package/libs/cli/typescript/src/lib/members/statements/expressions/call-expression.implementation.js +0 -114
- package/libs/cli/typescript/src/lib/members/statements/expressions/expression.abstraction.js +0 -188
- package/libs/cli/typescript/src/lib/members/statements/expressions/property-access-expression.implementation.js +0 -35
- package/libs/cli/typescript/src/lib/members/statements/expressions/unary-expression.implementation.js +0 -51
- package/libs/cli/typescript/src/lib/members/statements/statement.implementation.js +0 -49
- package/libs/cli/typescript/src/lib/members/statements/utils.js +0 -51
- package/libs/cli/typescript/src/lib/plugins/built-in.plugin.js +0 -258
- package/libs/cli/typescript/src/lib/plugins/plugin.interface.js +0 -15
- package/libs/cli/typescript/src/lib/plugins/rxjs.plugin.js +0 -228
- package/libs/cli/typescript/src/lib/plugins/signal.plugin.js +0 -126
- package/libs/cli/typescript/src/lib/plugins/stubs.js +0 -66
- package/libs/cli/typescript/src/lib/type-alias.implementation.js +0 -72
- package/libs/cli/typescript/src/lib/utils.js +0 -39
- package/libs/cli/typescript/src/lib/variable-declaration.implementation.js +0 -35
- package/libs/core/codegular/src/index.js +0 -21
- package/libs/core/codegular/src/lib/index.js +0 -29
- package/libs/core/codegular/src/lib/node.js +0 -112
- package/libs/core/codegular/src/lib/program.js +0 -92
- package/libs/core/codegular/src/lib/string.js +0 -168
- package/libs/core/codegular/src/lib/type-checker.js +0 -170
- package/libs/core/codegular/src/lib/utils.js +0 -347
- package/main.js +0 -43
package/lib.js
ADDED
|
@@ -0,0 +1,2430 @@
|
|
|
1
|
+
// libs/cli/code-graph-core/src/lib/code-graph-v2/config-from-query.ts
|
|
2
|
+
var STANDARD_NODE_PROPERTIES = {
|
|
3
|
+
firstIdentifier: `helpers.getDescendantsBy(args.current, (node) => node.kind === args.ts.SyntaxKind.Identifier)[0]?.getText() ?? null`,
|
|
4
|
+
initializer: `args.current.initializer ? args.current.initializer?.getText() : null`,
|
|
5
|
+
name: `args.current.name?.getText() ?? null`,
|
|
6
|
+
filePath: `args.sourceFile.fileName`,
|
|
7
|
+
text: `args.current.getText()`,
|
|
8
|
+
type: `(() => { try { const t = args.typeChecker.getTypeAtLocation(args.current); return t ? args.typeChecker.typeToString(t) : null; } catch { return null; } })()`
|
|
9
|
+
};
|
|
10
|
+
var SUPPORTED_RELATIONSHIPS = {
|
|
11
|
+
// --- Traversal relationships ---
|
|
12
|
+
HAS_DESCENDANTS: (labels) => labels.length > 0 ? `helpers.getDescendantsBy(args.current, (node) => ${buildKindFilter(labels)})` : `helpers.getDescendantsBy(args.current, () => true)`,
|
|
13
|
+
HAS_ANCESTORS: (labels) => labels.length > 0 ? `helpers.getAncestorsBy(args.current, (node) => ${buildKindFilter(labels)})` : `helpers.getAncestorsBy(args.current, () => true)`,
|
|
14
|
+
HAS_TYPE: () => `helpers.getType(args.current)`,
|
|
15
|
+
HAS_NAME: () => `args.current.name ? [args.current.name] : []`,
|
|
16
|
+
// --- Direct AST property relationships ---
|
|
17
|
+
HAS_INITIALIZER: () => `args.current.initializer ? [args.current.initializer] : []`,
|
|
18
|
+
HAS_PARAMETERS: () => `args.current.parameters ? [...args.current.parameters] : []`,
|
|
19
|
+
HAS_EXPRESSION: () => `args.current.expression ? [args.current.expression] : []`,
|
|
20
|
+
HAS_TYPE_ANNOTATION: () => `args.current.type ? [args.current.type] : []`,
|
|
21
|
+
HAS_BODY: () => `args.current.body ? [args.current.body] : []`,
|
|
22
|
+
HAS_MEMBERS: () => `args.current.members ? [...args.current.members] : []`,
|
|
23
|
+
HAS_PROPERTIES: () => `args.current.properties ? [...args.current.properties] : []`,
|
|
24
|
+
HAS_ARGUMENTS: () => `args.current.arguments ? [...args.current.arguments] : []`,
|
|
25
|
+
HAS_DECORATORS: () => `args.ts.getDecorators(args.current) || []`,
|
|
26
|
+
// --- Heritage relationships ---
|
|
27
|
+
EXTENDS: () => `(() => { const c = (args.current.heritageClauses || []).find(c => c.token === args.ts.SyntaxKind.ExtendsKeyword); return c ? [...c.types] : []; })()`,
|
|
28
|
+
IMPLEMENTS: () => `(() => { const c = (args.current.heritageClauses || []).find(c => c.token === args.ts.SyntaxKind.ImplementsKeyword); return c ? [...c.types] : []; })()`
|
|
29
|
+
};
|
|
30
|
+
var buildConfigFromQuery = (query2) => {
|
|
31
|
+
const nodeExtractors = {};
|
|
32
|
+
const matchStatements = query2.statements.filter(
|
|
33
|
+
(s) => s.type === "MatchStatement"
|
|
34
|
+
);
|
|
35
|
+
const variableToLabels = {};
|
|
36
|
+
for (const match of matchStatements) {
|
|
37
|
+
for (const pattern of match.patterns) {
|
|
38
|
+
if (pattern.variable && pattern.labels.length > 0) {
|
|
39
|
+
variableToLabels[pattern.variable] = pattern.labels;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const match of matchStatements) {
|
|
44
|
+
buildExtractorsFromMatch(match, nodeExtractors, variableToLabels);
|
|
45
|
+
}
|
|
46
|
+
return { nodeExtractors };
|
|
47
|
+
};
|
|
48
|
+
var resolveLabels = (pattern, variableToLabels) => {
|
|
49
|
+
if (pattern.labels.length > 0)
|
|
50
|
+
return pattern.labels;
|
|
51
|
+
if (pattern.variable && variableToLabels[pattern.variable]) {
|
|
52
|
+
return variableToLabels[pattern.variable];
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
};
|
|
56
|
+
var buildExtractorsFromMatch = (match, rootNodeExtractors, variableToLabels) => {
|
|
57
|
+
const { patterns, relationships } = match;
|
|
58
|
+
if (patterns.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
const rootPattern = patterns[0];
|
|
61
|
+
const rootLabels = resolveLabels(rootPattern, variableToLabels);
|
|
62
|
+
for (const label of rootLabels) {
|
|
63
|
+
rootNodeExtractors[label] ??= {
|
|
64
|
+
properties: { ...STANDARD_NODE_PROPERTIES }
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (!relationships || relationships.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
let currentExtractors = rootNodeExtractors;
|
|
70
|
+
for (let i = 0; i < relationships.length; i++) {
|
|
71
|
+
const rel = relationships[i];
|
|
72
|
+
const sourcePattern = patterns[i];
|
|
73
|
+
const targetPattern = patterns[i + 1];
|
|
74
|
+
if (!targetPattern)
|
|
75
|
+
break;
|
|
76
|
+
const edgeType = resolveEdgeType(rel.edgeType);
|
|
77
|
+
const codeGenerator = getCodeGenerator(edgeType);
|
|
78
|
+
const sourceLabels = resolveLabels(sourcePattern, variableToLabels);
|
|
79
|
+
const targetLabels = resolveLabels(targetPattern, variableToLabels);
|
|
80
|
+
const targetNodeExtractors = {};
|
|
81
|
+
if (targetLabels.length === 0) {
|
|
82
|
+
targetNodeExtractors["*"] = {
|
|
83
|
+
properties: { ...STANDARD_NODE_PROPERTIES }
|
|
84
|
+
};
|
|
85
|
+
} else {
|
|
86
|
+
for (const label of targetLabels) {
|
|
87
|
+
targetNodeExtractors[label] = {
|
|
88
|
+
properties: { ...STANDARD_NODE_PROPERTIES }
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const sourceLabel of sourceLabels) {
|
|
93
|
+
currentExtractors[sourceLabel] ??= {
|
|
94
|
+
properties: { ...STANDARD_NODE_PROPERTIES }
|
|
95
|
+
};
|
|
96
|
+
currentExtractors[sourceLabel].relationships ??= {};
|
|
97
|
+
currentExtractors[sourceLabel].relationships[edgeType] = {
|
|
98
|
+
extractorCode: codeGenerator(targetLabels),
|
|
99
|
+
nodeExtractors: targetNodeExtractors
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
currentExtractors = targetNodeExtractors;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var resolveEdgeType = (edgeType) => {
|
|
106
|
+
if (!edgeType) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Relationship type is required in auto-config mode. Supported: ${Object.keys(SUPPORTED_RELATIONSHIPS).join(", ")}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(edgeType)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Union relationship types are not supported in auto-config. Got: ${edgeType.join("|")}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return edgeType;
|
|
117
|
+
};
|
|
118
|
+
var getCodeGenerator = (edgeType) => {
|
|
119
|
+
const generator = SUPPORTED_RELATIONSHIPS[edgeType];
|
|
120
|
+
if (!generator) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Unsupported relationship type: "${edgeType}". Auto-config supports: ${Object.keys(SUPPORTED_RELATIONSHIPS).join(", ")}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return generator;
|
|
126
|
+
};
|
|
127
|
+
var buildKindFilter = (labels) => {
|
|
128
|
+
return labels.map((l) => `node.kind === args.ts.SyntaxKind.${l}`).join(" || ");
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// libs/cli/code-graph-core/src/lib/code-graph-v2/extractors/extractor.ts
|
|
132
|
+
function executeExtractor(code, context) {
|
|
133
|
+
try {
|
|
134
|
+
const fn = new Function(
|
|
135
|
+
...Object.keys(context),
|
|
136
|
+
`
|
|
137
|
+
return ${code}
|
|
138
|
+
`
|
|
139
|
+
);
|
|
140
|
+
return fn(...Object.values(context));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new Error(`Extractor execution failed: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function extractNodeProperties(context, propertyExectorMap = {}) {
|
|
146
|
+
const properties = {};
|
|
147
|
+
for (const [propertyName, extractorCode] of Object.entries(
|
|
148
|
+
propertyExectorMap
|
|
149
|
+
)) {
|
|
150
|
+
const value = executeExtractor(extractorCode, context);
|
|
151
|
+
properties[propertyName] = value;
|
|
152
|
+
}
|
|
153
|
+
return properties;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// libs/cli/code-graph-core/src/lib/code-graph-v2/graph-builder/code-graph.builder.ts
|
|
157
|
+
import * as ts7 from "typescript";
|
|
158
|
+
|
|
159
|
+
// libs/cli/code-graph-core/src/lib/codegular/node.ts
|
|
160
|
+
import * as ts3 from "typescript";
|
|
161
|
+
|
|
162
|
+
// libs/cli/code-graph-core/src/lib/codegular/string.ts
|
|
163
|
+
import * as ts2 from "typescript";
|
|
164
|
+
|
|
165
|
+
// libs/cli/code-graph-core/src/lib/codegular/utils.ts
|
|
166
|
+
import * as ts from "typescript";
|
|
167
|
+
var { forEachChild: forEachChild2, SyntaxKind } = ts;
|
|
168
|
+
function getAncestorsBy(node, predicate) {
|
|
169
|
+
let currentNode = node;
|
|
170
|
+
const nodes = [];
|
|
171
|
+
while (currentNode) {
|
|
172
|
+
if (predicate(currentNode)) {
|
|
173
|
+
nodes.push(currentNode);
|
|
174
|
+
}
|
|
175
|
+
const parent = currentNode?.parent;
|
|
176
|
+
if (!parent) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
currentNode = parent;
|
|
180
|
+
}
|
|
181
|
+
return nodes;
|
|
182
|
+
}
|
|
183
|
+
function getDescendantsBy(node, predicate) {
|
|
184
|
+
let foundElements = [];
|
|
185
|
+
let queue = [node];
|
|
186
|
+
while (queue.length > 0) {
|
|
187
|
+
const currentNode = queue.shift();
|
|
188
|
+
const children = Array.from(
|
|
189
|
+
currentNode?.getChildren(node.getSourceFile()) || []
|
|
190
|
+
);
|
|
191
|
+
foundElements = [
|
|
192
|
+
...foundElements,
|
|
193
|
+
...children.filter((node2) => predicate(node2))
|
|
194
|
+
];
|
|
195
|
+
queue = [...queue, ...children];
|
|
196
|
+
}
|
|
197
|
+
return foundElements;
|
|
198
|
+
}
|
|
199
|
+
function getDescendantsByKind(node, kind) {
|
|
200
|
+
return getDescendantsBy(node, (node2) => node2.kind === kind);
|
|
201
|
+
}
|
|
202
|
+
var getSolutionSourceFiles = (program) => {
|
|
203
|
+
const allSourceFiles = program.getSourceFiles();
|
|
204
|
+
const sourceFiles = allSourceFiles.filter((sourceFile) => {
|
|
205
|
+
const fileName = sourceFile.fileName;
|
|
206
|
+
if (fileName.includes("node_modules")) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (fileName.endsWith(".d.ts")) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
});
|
|
214
|
+
return sourceFiles;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// libs/cli/code-graph-core/src/lib/codegular/string.ts
|
|
218
|
+
var { SyntaxKind: SyntaxKind3 } = ts2;
|
|
219
|
+
|
|
220
|
+
// libs/cli/code-graph-core/src/lib/codegular/node.ts
|
|
221
|
+
var { forEachChild: forEachChild3, SyntaxKind: SyntaxKind4 } = ts3;
|
|
222
|
+
|
|
223
|
+
// libs/cli/code-graph-core/src/lib/codegular/program.ts
|
|
224
|
+
import * as path from "path";
|
|
225
|
+
import * as ts4 from "typescript";
|
|
226
|
+
var createProgramFromFileSystem = (fileSystem) => {
|
|
227
|
+
const compilerOptions = {
|
|
228
|
+
target: ts4.ScriptTarget.ES2020,
|
|
229
|
+
module: ts4.ModuleKind.CommonJS,
|
|
230
|
+
moduleResolution: ts4.ModuleResolutionKind.NodeJs,
|
|
231
|
+
allowSyntheticDefaultImports: true,
|
|
232
|
+
esModuleInterop: true,
|
|
233
|
+
skipLibCheck: true,
|
|
234
|
+
lib: ["ES2020", "DOM"]
|
|
235
|
+
};
|
|
236
|
+
const compilerHost = ts4.createCompilerHost(compilerOptions);
|
|
237
|
+
const originalGetSourceFile = compilerHost.getSourceFile;
|
|
238
|
+
compilerHost.getSourceFile = (fileName, languageVersion) => {
|
|
239
|
+
if (fileSystem[fileName]) {
|
|
240
|
+
return ts4.createSourceFile(
|
|
241
|
+
fileName,
|
|
242
|
+
fileSystem[fileName],
|
|
243
|
+
languageVersion,
|
|
244
|
+
true
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return originalGetSourceFile(fileName, languageVersion);
|
|
248
|
+
};
|
|
249
|
+
const fileNames = Object.keys(fileSystem);
|
|
250
|
+
return ts4.createProgram(fileNames, compilerOptions, compilerHost);
|
|
251
|
+
};
|
|
252
|
+
var createProgramFromTsConfig = (configPath) => {
|
|
253
|
+
const configFile = ts4.readConfigFile(configPath, ts4.sys.readFile);
|
|
254
|
+
if (configFile.error) {
|
|
255
|
+
const errorMessage = ts4.formatDiagnostic(configFile.error, {
|
|
256
|
+
getCurrentDirectory: () => process.cwd(),
|
|
257
|
+
getNewLine: () => "\n",
|
|
258
|
+
getCanonicalFileName: (fileName) => fileName
|
|
259
|
+
});
|
|
260
|
+
throw new Error(`Error reading tsconfig at ${configPath}: ${errorMessage}`);
|
|
261
|
+
}
|
|
262
|
+
const parsedConfig = ts4.parseJsonConfigFileContent(
|
|
263
|
+
configFile.config,
|
|
264
|
+
ts4.sys,
|
|
265
|
+
path.dirname(configPath)
|
|
266
|
+
);
|
|
267
|
+
return ts4.createProgram(
|
|
268
|
+
parsedConfig.fileNames,
|
|
269
|
+
parsedConfig.options,
|
|
270
|
+
ts4.createCompilerHost(parsedConfig.options)
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// libs/cli/code-graph-core/src/lib/codegular/type-checker.ts
|
|
275
|
+
import * as ts5 from "typescript";
|
|
276
|
+
|
|
277
|
+
// libs/cli/code-graph-core/src/lib/code-graph-v2/code.graph.ts
|
|
278
|
+
var addNode = (graph, node) => {
|
|
279
|
+
graph.nodesById[node.id] = node;
|
|
280
|
+
return graph;
|
|
281
|
+
};
|
|
282
|
+
var getEdgeKey = (edge) => {
|
|
283
|
+
return `${edge.source}|${edge.target}|${edge.type}`;
|
|
284
|
+
};
|
|
285
|
+
var addEdge = (graph, edge) => {
|
|
286
|
+
const edgeKey = getEdgeKey(edge);
|
|
287
|
+
const sourceEdges = graph.edgesBySource[edge.source];
|
|
288
|
+
if (sourceEdges) {
|
|
289
|
+
const sourceEdgeKeys = new Set(sourceEdges.map(getEdgeKey));
|
|
290
|
+
if (sourceEdgeKeys.has(edgeKey)) {
|
|
291
|
+
return graph;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const targetEdges = graph.edgesByTarget[edge.target];
|
|
295
|
+
if (targetEdges) {
|
|
296
|
+
const targetEdgeKeys = new Set(targetEdges.map(getEdgeKey));
|
|
297
|
+
if (targetEdgeKeys.has(edgeKey)) {
|
|
298
|
+
return graph;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!graph.edgesBySource[edge.source]) {
|
|
302
|
+
graph.edgesBySource[edge.source] = [];
|
|
303
|
+
}
|
|
304
|
+
graph.edgesBySource[edge.source].push(edge);
|
|
305
|
+
if (!graph.edgesByTarget[edge.target]) {
|
|
306
|
+
graph.edgesByTarget[edge.target] = [];
|
|
307
|
+
}
|
|
308
|
+
graph.edgesByTarget[edge.target].push(edge);
|
|
309
|
+
return graph;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// libs/cli/code-graph-core/src/lib/code-graph-v2/utils.ts
|
|
313
|
+
import * as ts6 from "typescript";
|
|
314
|
+
var getSyntaxKindName = (kind) => {
|
|
315
|
+
return ts6.SyntaxKind[kind];
|
|
316
|
+
};
|
|
317
|
+
var generateNodeId = (n) => {
|
|
318
|
+
const s = n.getSourceFile();
|
|
319
|
+
const start = s.getLineAndCharacterOfPosition(n.getStart(s));
|
|
320
|
+
const end = s.getLineAndCharacterOfPosition(n.getEnd());
|
|
321
|
+
return `${s.fileName}.${start.line}.${start.character}.${end.line}.${end.character}`;
|
|
322
|
+
};
|
|
323
|
+
var extractFromNodeId = (nodeId) => {
|
|
324
|
+
const parts = nodeId.split(".");
|
|
325
|
+
const endCharacter = parts.pop();
|
|
326
|
+
const endLine = parts.pop();
|
|
327
|
+
const startCharacter = parts.pop();
|
|
328
|
+
const startLine = parts.pop();
|
|
329
|
+
const filePath = parts.join(".");
|
|
330
|
+
return {
|
|
331
|
+
filePath,
|
|
332
|
+
start: {
|
|
333
|
+
line: parseInt(startLine),
|
|
334
|
+
character: parseInt(startCharacter)
|
|
335
|
+
},
|
|
336
|
+
end: {
|
|
337
|
+
line: parseInt(endLine),
|
|
338
|
+
character: parseInt(endCharacter)
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// libs/cli/code-graph-core/src/lib/code-graph-v2/graph-builder/type.processor.ts
|
|
344
|
+
var processType = (context, nodeExtractors, codeGraph) => {
|
|
345
|
+
const nodeId = context.args.typeChecker.typeToString(
|
|
346
|
+
context.args.current
|
|
347
|
+
);
|
|
348
|
+
const kindName = "Type";
|
|
349
|
+
const nodeConfig = nodeExtractors[kindName];
|
|
350
|
+
const graphNode = {
|
|
351
|
+
...nodeConfig && nodeConfig.properties ? extractNodeProperties(context, nodeConfig.properties) : {},
|
|
352
|
+
id: nodeId,
|
|
353
|
+
labels: [kindName]
|
|
354
|
+
};
|
|
355
|
+
addNode(codeGraph, graphNode);
|
|
356
|
+
processRelationships(nodeId, context, nodeConfig, codeGraph);
|
|
357
|
+
return graphNode;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// libs/cli/code-graph-core/src/lib/code-graph-v2/graph-builder/relationship.processor.ts
|
|
361
|
+
function isTsNode(obj) {
|
|
362
|
+
return obj && "kind" in obj && typeof obj.kind === "number";
|
|
363
|
+
}
|
|
364
|
+
function isTsType(obj) {
|
|
365
|
+
return obj && "flags" in obj && typeof obj.flags === "number" && !("kind" in obj);
|
|
366
|
+
}
|
|
367
|
+
var processRelationships = (parentId, context, nodeConfig, codeGraph) => {
|
|
368
|
+
if (nodeConfig?.relationships) {
|
|
369
|
+
for (const relationshipName of Object.keys(nodeConfig.relationships)) {
|
|
370
|
+
const relationshipExtractorFnCode = nodeConfig.relationships[relationshipName].extractorCode;
|
|
371
|
+
const relationshipNodeExtractors = nodeConfig.relationships[relationshipName].nodeExtractors;
|
|
372
|
+
Object.keys(relationshipNodeExtractors).forEach((label) => {
|
|
373
|
+
const children = executeExtractor(relationshipExtractorFnCode, context);
|
|
374
|
+
children.forEach(
|
|
375
|
+
(child) => {
|
|
376
|
+
if (isTsType(child)) {
|
|
377
|
+
const childNode = processType(
|
|
378
|
+
{ ...context, args: { ...context.args, current: child } },
|
|
379
|
+
relationshipNodeExtractors,
|
|
380
|
+
codeGraph
|
|
381
|
+
);
|
|
382
|
+
addEdge(codeGraph, {
|
|
383
|
+
source: parentId,
|
|
384
|
+
target: childNode.id,
|
|
385
|
+
type: relationshipName
|
|
386
|
+
});
|
|
387
|
+
} else if (isTsNode(child)) {
|
|
388
|
+
const childNode = processNode(
|
|
389
|
+
{ ...context, args: { ...context.args, current: child } },
|
|
390
|
+
relationshipNodeExtractors,
|
|
391
|
+
codeGraph
|
|
392
|
+
);
|
|
393
|
+
addEdge(codeGraph, {
|
|
394
|
+
source: parentId,
|
|
395
|
+
target: childNode.id,
|
|
396
|
+
type: relationshipName
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`The code provided: ${relationshipExtractorFnCode} returned an invalid child: ${child}. We support only ts.Node and ts.Type array returns from executed code`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// libs/cli/code-graph-core/src/lib/code-graph-v2/graph-builder/node.processor.ts
|
|
411
|
+
var processNode = (context, nodeExtractors, codeGraph) => {
|
|
412
|
+
const nodeId = generateNodeId(context.args.current);
|
|
413
|
+
const kindName = getSyntaxKindName(context.args.current.kind);
|
|
414
|
+
const nodeConfig = nodeExtractors[kindName] ?? nodeExtractors["*"];
|
|
415
|
+
const graphNode = {
|
|
416
|
+
...nodeConfig && nodeConfig.properties ? extractNodeProperties(context, nodeConfig.properties) : {},
|
|
417
|
+
id: nodeId,
|
|
418
|
+
labels: [kindName]
|
|
419
|
+
};
|
|
420
|
+
addNode(codeGraph, graphNode);
|
|
421
|
+
processRelationships(nodeId, context, nodeConfig, codeGraph);
|
|
422
|
+
return graphNode;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// libs/cli/code-graph-core/src/lib/code-graph-v2/graph-builder/code-graph.builder.ts
|
|
426
|
+
var buildCodeGraph = (program, config) => {
|
|
427
|
+
const codeGraph = {
|
|
428
|
+
nodesById: {},
|
|
429
|
+
edgesBySource: {},
|
|
430
|
+
edgesByTarget: {}
|
|
431
|
+
};
|
|
432
|
+
const sourceFiles = getSolutionSourceFiles(program);
|
|
433
|
+
sourceFiles.forEach((sourceFile) => {
|
|
434
|
+
Object.keys(config.nodeExtractors).forEach((kindName) => {
|
|
435
|
+
const nodes = getDescendantsByKind(sourceFile, ts7.SyntaxKind[kindName]);
|
|
436
|
+
nodes.forEach((node) => {
|
|
437
|
+
const extractorContext = {
|
|
438
|
+
args: {
|
|
439
|
+
current: node,
|
|
440
|
+
sourceFile,
|
|
441
|
+
typeChecker: program.getTypeChecker(),
|
|
442
|
+
ts: ts7
|
|
443
|
+
},
|
|
444
|
+
helpers: {
|
|
445
|
+
getDescendantsBy,
|
|
446
|
+
getAncestorsBy,
|
|
447
|
+
getType: (node2) => []
|
|
448
|
+
// TODO: add type extraction
|
|
449
|
+
// getType(
|
|
450
|
+
// node,
|
|
451
|
+
// program.getTypeChecker(),
|
|
452
|
+
// new TypeResolutionTracker(),
|
|
453
|
+
// ),
|
|
454
|
+
// getDeclarations: (node: ts.Type | ts.Node) =>
|
|
455
|
+
// getDeclarations(node, context.typeChecker),
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
processNode(extractorContext, config.nodeExtractors, codeGraph);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
return codeGraph;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/executor/condition-evaluator.ts
|
|
466
|
+
var ConditionEvaluator = class {
|
|
467
|
+
// @TODO: apply where at match level
|
|
468
|
+
/**
|
|
469
|
+
* Apply WHERE clause filtering to matches
|
|
470
|
+
*/
|
|
471
|
+
applyWhereClauseIfNeeded(matches, where) {
|
|
472
|
+
return matches.filter((match) => {
|
|
473
|
+
const conditionResults = where.conditions.map(
|
|
474
|
+
(condition) => this.evaluateCondition(match, condition)
|
|
475
|
+
);
|
|
476
|
+
const logic = where.logic || "AND";
|
|
477
|
+
if (logic === "AND") {
|
|
478
|
+
return conditionResults.every((result) => result === true);
|
|
479
|
+
} else {
|
|
480
|
+
return conditionResults.some((result) => result === true);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
// @fixme move to utils
|
|
485
|
+
/**
|
|
486
|
+
* Escape special regex characters in a string
|
|
487
|
+
*/
|
|
488
|
+
escapeRegex(str) {
|
|
489
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Evaluate a single WHERE condition
|
|
493
|
+
* Supports flat properties only (no nesting, like Neo4j Cypher)
|
|
494
|
+
* Handles null values from optional matches (null values fail the condition)
|
|
495
|
+
*/
|
|
496
|
+
evaluateCondition(match, condition) {
|
|
497
|
+
const value = match[condition.variable];
|
|
498
|
+
if ((condition.operator === "IS NULL" || condition.operator === "IS NOT NULL") && !condition.property) {
|
|
499
|
+
const isNull = value === null || value === void 0;
|
|
500
|
+
return condition.operator === "IS NULL" ? isNull : !isNull;
|
|
501
|
+
}
|
|
502
|
+
if (!value || value === null) {
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
let baseObject;
|
|
506
|
+
if ("id" in value) {
|
|
507
|
+
baseObject = value;
|
|
508
|
+
} else if ("type" in value && "source" in value) {
|
|
509
|
+
baseObject = value;
|
|
510
|
+
} else {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
let actualValue;
|
|
514
|
+
if (!condition.property) {
|
|
515
|
+
actualValue = baseObject;
|
|
516
|
+
} else {
|
|
517
|
+
actualValue = baseObject[condition.property];
|
|
518
|
+
if (actualValue === void 0) {
|
|
519
|
+
if (condition.operator === "IS NULL" || condition.operator === "IS NOT NULL") {
|
|
520
|
+
actualValue = void 0;
|
|
521
|
+
} else {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const conditionValue = condition.value;
|
|
527
|
+
switch (condition.operator) {
|
|
528
|
+
case "=":
|
|
529
|
+
return actualValue === conditionValue;
|
|
530
|
+
case "!=":
|
|
531
|
+
return actualValue !== conditionValue;
|
|
532
|
+
case ">":
|
|
533
|
+
return actualValue > conditionValue;
|
|
534
|
+
case "<":
|
|
535
|
+
return actualValue < conditionValue;
|
|
536
|
+
case ">=":
|
|
537
|
+
return actualValue >= conditionValue;
|
|
538
|
+
case "<=":
|
|
539
|
+
return actualValue <= conditionValue;
|
|
540
|
+
case "IS NULL":
|
|
541
|
+
return actualValue === null || actualValue === void 0;
|
|
542
|
+
case "IS NOT NULL":
|
|
543
|
+
return actualValue !== null && actualValue !== void 0;
|
|
544
|
+
case "IN":
|
|
545
|
+
if (Array.isArray(conditionValue)) {
|
|
546
|
+
if (conditionValue.length > 0 && typeof conditionValue[0] === "object" && conditionValue[0] !== null) {
|
|
547
|
+
const extractedValues = conditionValue.map((item) => item?.name).filter((name) => name !== void 0);
|
|
548
|
+
return extractedValues.includes(actualValue);
|
|
549
|
+
}
|
|
550
|
+
return conditionValue.includes(actualValue);
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
case "NOT IN":
|
|
554
|
+
if (Array.isArray(conditionValue)) {
|
|
555
|
+
if (conditionValue.length > 0 && typeof conditionValue[0] === "object" && conditionValue[0] !== null) {
|
|
556
|
+
const extractedValues = conditionValue.map((item) => item?.name).filter((name) => name !== void 0);
|
|
557
|
+
return !extractedValues.includes(actualValue);
|
|
558
|
+
}
|
|
559
|
+
return !conditionValue.includes(actualValue);
|
|
560
|
+
}
|
|
561
|
+
return true;
|
|
562
|
+
case "STARTS WITH":
|
|
563
|
+
return this.testRegex(
|
|
564
|
+
new RegExp(`^${this.escapeRegex(conditionValue)}`),
|
|
565
|
+
actualValue
|
|
566
|
+
);
|
|
567
|
+
case "ENDS WITH":
|
|
568
|
+
return this.testRegex(
|
|
569
|
+
new RegExp(`${this.escapeRegex(conditionValue)}$`),
|
|
570
|
+
actualValue
|
|
571
|
+
);
|
|
572
|
+
case "CONTAINS":
|
|
573
|
+
return this.testRegex(
|
|
574
|
+
new RegExp(this.escapeRegex(conditionValue)),
|
|
575
|
+
actualValue
|
|
576
|
+
);
|
|
577
|
+
case "=~":
|
|
578
|
+
return this.testRegex(new RegExp(conditionValue), actualValue);
|
|
579
|
+
default:
|
|
580
|
+
throw new Error(`Unsupported operator: ${condition.operator}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
testRegex(regexp, actualValue) {
|
|
584
|
+
if (actualValue === null || actualValue === void 0) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
if (typeof actualValue !== "string" && typeof actualValue !== "number" && typeof actualValue !== "boolean") {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
const regexStr = typeof actualValue === "string" ? actualValue : String(actualValue);
|
|
591
|
+
try {
|
|
592
|
+
return regexp.test(regexStr);
|
|
593
|
+
} catch {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/executor/utils.ts
|
|
600
|
+
var Annonymizer = class {
|
|
601
|
+
constructor() {
|
|
602
|
+
this.anonymousVariablePrefix = "annonymous_";
|
|
603
|
+
}
|
|
604
|
+
generate(node) {
|
|
605
|
+
return this.anonymousVariablePrefix + node.id;
|
|
606
|
+
}
|
|
607
|
+
isAnonymous(variable) {
|
|
608
|
+
return variable.startsWith(this.anonymousVariablePrefix);
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/executor/match-engine.ts
|
|
613
|
+
var MatchEngine = class {
|
|
614
|
+
constructor(patternMatcher, traverseEngine) {
|
|
615
|
+
this.patternMatcher = patternMatcher;
|
|
616
|
+
this.traverseEngine = traverseEngine;
|
|
617
|
+
this.annonymizer = new Annonymizer();
|
|
618
|
+
}
|
|
619
|
+
processMatchStatement(graph, match, existingResults = []) {
|
|
620
|
+
if (match.patterns.length === 0)
|
|
621
|
+
return existingResults;
|
|
622
|
+
const starting = match.patterns[0];
|
|
623
|
+
const allResults = [];
|
|
624
|
+
this.patternMatcher.filterMatchingNodes(graph, starting).forEach((node) => {
|
|
625
|
+
const matches = this.traverseEngine.traverseRelationships(
|
|
626
|
+
graph,
|
|
627
|
+
node,
|
|
628
|
+
starting.variable || this.annonymizer.generate(node),
|
|
629
|
+
match.patterns.slice(1),
|
|
630
|
+
match.relationships
|
|
631
|
+
);
|
|
632
|
+
allResults.push(...matches);
|
|
633
|
+
});
|
|
634
|
+
if (existingResults.length > 0) {
|
|
635
|
+
if (match.optional) {
|
|
636
|
+
return this.mergeOptionalResults(existingResults, allResults, match);
|
|
637
|
+
} else {
|
|
638
|
+
return this.mergeResults(existingResults, allResults);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return allResults;
|
|
642
|
+
}
|
|
643
|
+
mergeResults(existingResults, newResults) {
|
|
644
|
+
if (existingResults.length === 0) {
|
|
645
|
+
return newResults;
|
|
646
|
+
}
|
|
647
|
+
const mergedResults = [];
|
|
648
|
+
for (const existingResult of existingResults) {
|
|
649
|
+
for (const newResult of newResults) {
|
|
650
|
+
if (this.canMergeResult(existingResult, newResult)) {
|
|
651
|
+
mergedResults.push({ ...existingResult, ...newResult });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return mergedResults;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Merge optional match results with existing results (left outer join)
|
|
659
|
+
* - Keeps all existing results
|
|
660
|
+
* - Merges optional results where variables overlap
|
|
661
|
+
* - Adds nulls for optional variables when no match is found
|
|
662
|
+
*/
|
|
663
|
+
mergeOptionalResults(existingResults, optionalResults, match) {
|
|
664
|
+
if (existingResults.length === 0) {
|
|
665
|
+
return optionalResults;
|
|
666
|
+
}
|
|
667
|
+
const optionalVarNames = /* @__PURE__ */ new Set();
|
|
668
|
+
for (const pattern of match.patterns) {
|
|
669
|
+
if (pattern.variable) {
|
|
670
|
+
optionalVarNames.add(pattern.variable);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (match.relationships) {
|
|
674
|
+
for (const rel of match.relationships) {
|
|
675
|
+
if (rel.variable) {
|
|
676
|
+
optionalVarNames.add(rel.variable);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (optionalResults.length === 0) {
|
|
681
|
+
return existingResults.map((result) => {
|
|
682
|
+
const resultWithNulls = { ...result };
|
|
683
|
+
for (const varName of optionalVarNames) {
|
|
684
|
+
if (!(varName in resultWithNulls)) {
|
|
685
|
+
resultWithNulls[varName] = null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return resultWithNulls;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
for (const optionalResult of optionalResults) {
|
|
692
|
+
for (const key in optionalResult) {
|
|
693
|
+
optionalVarNames.add(key);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const mergedResults = [];
|
|
697
|
+
for (const existingResult of existingResults) {
|
|
698
|
+
let hasMatch = false;
|
|
699
|
+
for (const optionalResult of optionalResults) {
|
|
700
|
+
if (this.canMergeResult(existingResult, optionalResult)) {
|
|
701
|
+
mergedResults.push({ ...existingResult, ...optionalResult });
|
|
702
|
+
hasMatch = true;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!hasMatch) {
|
|
706
|
+
const resultWithNulls = { ...existingResult };
|
|
707
|
+
for (const varName of optionalVarNames) {
|
|
708
|
+
if (!(varName in resultWithNulls)) {
|
|
709
|
+
resultWithNulls[varName] = null;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
mergedResults.push(resultWithNulls);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return mergedResults;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Check if additionalResult can be merged with existingResult
|
|
719
|
+
* Returns true if shared variables match (same node/edge) or if there are no conflicts
|
|
720
|
+
* Does NOT mutate any parameters
|
|
721
|
+
*/
|
|
722
|
+
canMergeResult(additionalResult, existingResult) {
|
|
723
|
+
for (const key in additionalResult) {
|
|
724
|
+
if (key in existingResult) {
|
|
725
|
+
const existing = existingResult[key];
|
|
726
|
+
const additional = additionalResult[key];
|
|
727
|
+
if (existing !== null && additional !== null) {
|
|
728
|
+
if ("id" in existing && "id" in additional && existing.id !== additional.id) {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
} else if (existing !== additional) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return true;
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/executor/pattern-matcher.ts
|
|
741
|
+
var PatternMatcher = class {
|
|
742
|
+
/**
|
|
743
|
+
* Check if an edge type matches the relationship pattern's edge type(s)
|
|
744
|
+
* Supports both single edge type (string) and multiple edge types (array)
|
|
745
|
+
*/
|
|
746
|
+
matchesEdgeType(edgeType, patternEdgeType) {
|
|
747
|
+
if (!patternEdgeType) {
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
if (typeof patternEdgeType === "string") {
|
|
751
|
+
return edgeType === patternEdgeType;
|
|
752
|
+
}
|
|
753
|
+
return patternEdgeType.includes(edgeType);
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Deep equality check for arrays
|
|
757
|
+
*/
|
|
758
|
+
arraysEqual(a, b) {
|
|
759
|
+
if (a.length !== b.length) {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
for (let i = 0; i < a.length; i++) {
|
|
763
|
+
if (Array.isArray(a[i]) && Array.isArray(b[i])) {
|
|
764
|
+
if (!this.arraysEqual(a[i], b[i])) {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
} else if (a[i] !== b[i]) {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Check if two values are equal (handles arrays with deep equality)
|
|
775
|
+
*/
|
|
776
|
+
valuesEqual(expected, actual) {
|
|
777
|
+
if (Array.isArray(expected) && Array.isArray(actual)) {
|
|
778
|
+
return this.arraysEqual(expected, actual);
|
|
779
|
+
}
|
|
780
|
+
return expected === actual;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Check if a node matches a pattern (labels and properties)
|
|
784
|
+
*/
|
|
785
|
+
doesNodeMatchPattern(node, pattern) {
|
|
786
|
+
if (pattern.labels.length > 0) {
|
|
787
|
+
const labelOperator = pattern.labelOperator || "AND";
|
|
788
|
+
if (labelOperator === "AND") {
|
|
789
|
+
if (!pattern.labels.every((label) => node.labels.includes(label))) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
if (!pattern.labels.some((label) => node.labels.includes(label))) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (pattern.properties) {
|
|
799
|
+
const notFound = Object.entries(pattern.properties).find(
|
|
800
|
+
([key, expectedValue]) => !this.valuesEqual(expectedValue, node[key])
|
|
801
|
+
) !== void 0;
|
|
802
|
+
if (notFound) {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
findMatchingNodes(graph, pattern) {
|
|
809
|
+
return Object.values(graph.nodesById).find(
|
|
810
|
+
(node) => this.doesNodeMatchPattern(node, pattern)
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Match a single node pattern
|
|
815
|
+
* Finds all nodes in the graph that match the given pattern
|
|
816
|
+
* Reuses matchesPattern to avoid code duplication
|
|
817
|
+
*/
|
|
818
|
+
filterMatchingNodes(graph, pattern) {
|
|
819
|
+
return Object.values(graph.nodesById).filter(
|
|
820
|
+
(node) => this.doesNodeMatchPattern(node, pattern)
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/executor/relationship-navigator.ts
|
|
826
|
+
var RelationshipNavigator = class {
|
|
827
|
+
constructor(patternMatcher) {
|
|
828
|
+
this.patternMatcher = patternMatcher;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Find nodes and edges connected via a relationship from the given node
|
|
832
|
+
* Returns both the connected node and the edge that connects them
|
|
833
|
+
*/
|
|
834
|
+
findConnectedNodesAndEdges(graph, node, relationship) {
|
|
835
|
+
const connections = [];
|
|
836
|
+
const seenNodeIds = /* @__PURE__ */ new Set();
|
|
837
|
+
if (relationship.direction === "outgoing" || relationship.direction === "both") {
|
|
838
|
+
const outgoingEdges = graph.edgesBySource[node.id] || [];
|
|
839
|
+
for (const edge of outgoingEdges) {
|
|
840
|
+
if (this.patternMatcher.matchesEdgeType(edge.type, relationship.edgeType)) {
|
|
841
|
+
const targetNode = graph.nodesById[edge.target];
|
|
842
|
+
if (targetNode && !seenNodeIds.has(targetNode.id)) {
|
|
843
|
+
seenNodeIds.add(targetNode.id);
|
|
844
|
+
connections.push({ node: targetNode, edge });
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (relationship.direction === "incoming" || relationship.direction === "both") {
|
|
850
|
+
const incomingEdges = graph.edgesByTarget[node.id] || [];
|
|
851
|
+
for (const edge of incomingEdges) {
|
|
852
|
+
if (this.patternMatcher.matchesEdgeType(edge.type, relationship.edgeType)) {
|
|
853
|
+
const sourceNode = graph.nodesById[edge.source];
|
|
854
|
+
if (sourceNode && !seenNodeIds.has(sourceNode.id)) {
|
|
855
|
+
seenNodeIds.add(sourceNode.id);
|
|
856
|
+
connections.push({ node: sourceNode, edge });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return connections;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/executor/result-formatter.ts
|
|
866
|
+
var ResultFormatter = class {
|
|
867
|
+
constructor() {
|
|
868
|
+
this.annonymizer = new Annonymizer();
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Format a single match value (node or edge) based on extractData flag
|
|
872
|
+
*/
|
|
873
|
+
formatValue(value) {
|
|
874
|
+
if (value === null || value === void 0) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
if ("id" in value && "labels" in value) {
|
|
878
|
+
const node = value;
|
|
879
|
+
return node;
|
|
880
|
+
} else {
|
|
881
|
+
const edge = value;
|
|
882
|
+
return edge;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Sort results based on ORDER BY clause
|
|
887
|
+
*/
|
|
888
|
+
sortResults(results, orderBy) {
|
|
889
|
+
return [...results].sort((a, b) => {
|
|
890
|
+
for (const item of orderBy.items) {
|
|
891
|
+
const valA = this.getPropertyValue(a[item.variable], item.property);
|
|
892
|
+
const valB = this.getPropertyValue(b[item.variable], item.property);
|
|
893
|
+
const comparison = this.compareValues(valA, valB, item.direction);
|
|
894
|
+
if (comparison !== 0)
|
|
895
|
+
return comparison;
|
|
896
|
+
}
|
|
897
|
+
return 0;
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Get property value from an object, handling nested property access
|
|
902
|
+
*/
|
|
903
|
+
getPropertyValue(obj, property) {
|
|
904
|
+
if (obj === null || obj === void 0)
|
|
905
|
+
return null;
|
|
906
|
+
return property ? obj[property] : obj;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Compare two values with Neo4j-style NULL handling
|
|
910
|
+
* ASC: NULLs first, then ascending values
|
|
911
|
+
* DESC: Values descending, then NULLs last
|
|
912
|
+
*/
|
|
913
|
+
compareValues(a, b, direction) {
|
|
914
|
+
const aIsNull = a === null || a === void 0;
|
|
915
|
+
const bIsNull = b === null || b === void 0;
|
|
916
|
+
if (aIsNull && bIsNull)
|
|
917
|
+
return 0;
|
|
918
|
+
if (aIsNull)
|
|
919
|
+
return direction === "ASC" ? -1 : 1;
|
|
920
|
+
if (bIsNull)
|
|
921
|
+
return direction === "ASC" ? 1 : -1;
|
|
922
|
+
let result;
|
|
923
|
+
if (typeof a === "string" && typeof b === "string") {
|
|
924
|
+
result = a.localeCompare(b);
|
|
925
|
+
} else {
|
|
926
|
+
result = a < b ? -1 : a > b ? 1 : 0;
|
|
927
|
+
}
|
|
928
|
+
return direction === "DESC" ? -result : result;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Format results with RETURN clause
|
|
932
|
+
* Only includes variables specified in RETURN clause, with optional aliases
|
|
933
|
+
* If returnAll is true (RETURN *), returns all variables
|
|
934
|
+
* If distinct is true, deduplicate results based on returned variables' ids
|
|
935
|
+
*/
|
|
936
|
+
formatResults(results, returnClause) {
|
|
937
|
+
let processedResults = returnClause.limit ? results.slice(0, returnClause.limit) : results;
|
|
938
|
+
if (returnClause.distinct) {
|
|
939
|
+
processedResults = this.deduplicateResults(processedResults, returnClause);
|
|
940
|
+
}
|
|
941
|
+
if (returnClause.returnAll) {
|
|
942
|
+
return this.formatDefault(processedResults);
|
|
943
|
+
}
|
|
944
|
+
return processedResults.map((match) => {
|
|
945
|
+
const result = {};
|
|
946
|
+
for (const item of returnClause.items) {
|
|
947
|
+
const variable = item.expression;
|
|
948
|
+
if (this.annonymizer.isAnonymous(variable)) {
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
const alias = item.alias || variable;
|
|
952
|
+
if (variable in match) {
|
|
953
|
+
const value = match[variable];
|
|
954
|
+
result[alias] = this.formatValue(value);
|
|
955
|
+
} else {
|
|
956
|
+
throw new Error(`Variable ${variable} not found in match results`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return result;
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Deduplicate results based on returned variables' ids
|
|
964
|
+
*/
|
|
965
|
+
deduplicateResults(results, returnClause) {
|
|
966
|
+
const seen = /* @__PURE__ */ new Map();
|
|
967
|
+
for (const match of results) {
|
|
968
|
+
const key = returnClause.items.map((item) => this.getValueId(match[item.expression])).join("|");
|
|
969
|
+
if (!seen.has(key)) {
|
|
970
|
+
seen.set(key, match);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return Array.from(seen.values());
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Get a unique identifier for a match result value
|
|
977
|
+
*/
|
|
978
|
+
getValueId(value) {
|
|
979
|
+
if (value === null || value === void 0) {
|
|
980
|
+
return "null";
|
|
981
|
+
}
|
|
982
|
+
if ("id" in value && "labels" in value) {
|
|
983
|
+
return value.id;
|
|
984
|
+
}
|
|
985
|
+
const edge = value;
|
|
986
|
+
return `edge:${edge.source}|${edge.target}|${edge.type}`;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Format results without RETURN clause (default)
|
|
990
|
+
* Returns all matched variables keyed by their variable names
|
|
991
|
+
*/
|
|
992
|
+
formatDefault(matches) {
|
|
993
|
+
return matches.map((match) => {
|
|
994
|
+
const result = {};
|
|
995
|
+
for (const [varName, value] of Object.entries(match)) {
|
|
996
|
+
if (this.annonymizer.isAnonymous(varName)) {
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
result[varName] = this.formatValue(value);
|
|
1000
|
+
}
|
|
1001
|
+
return result;
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/executor/traverse-engine.ts
|
|
1007
|
+
var TraverseEngine = class {
|
|
1008
|
+
constructor(patternMatcher, relationshipNavigator) {
|
|
1009
|
+
this.patternMatcher = patternMatcher;
|
|
1010
|
+
this.relationshipNavigator = relationshipNavigator;
|
|
1011
|
+
this.annonymizer = new Annonymizer();
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Recursively traverse relationships to match subsequent patterns
|
|
1015
|
+
* Supports returning edges as variables when relationship variable is specified
|
|
1016
|
+
*/
|
|
1017
|
+
traverseRelationships(graph, currentNode, currentVarName, remainingPatterns, relationships) {
|
|
1018
|
+
if (remainingPatterns.length === 0) {
|
|
1019
|
+
return [{ [currentVarName]: currentNode }];
|
|
1020
|
+
}
|
|
1021
|
+
const results = [];
|
|
1022
|
+
const nextPattern = remainingPatterns[0];
|
|
1023
|
+
const nextRelationship = relationships[0];
|
|
1024
|
+
const nextVarName = nextPattern.variable || this.annonymizer.generate(currentNode);
|
|
1025
|
+
const remainingPatterns_ = remainingPatterns.slice(1);
|
|
1026
|
+
const remainingRelationships = relationships.slice(1);
|
|
1027
|
+
if (nextRelationship.variableLength) {
|
|
1028
|
+
return this.traverseVariableLengthPath(
|
|
1029
|
+
graph,
|
|
1030
|
+
currentNode,
|
|
1031
|
+
currentVarName,
|
|
1032
|
+
nextRelationship,
|
|
1033
|
+
nextPattern,
|
|
1034
|
+
nextVarName,
|
|
1035
|
+
remainingPatterns_,
|
|
1036
|
+
remainingRelationships
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
const connections = this.relationshipNavigator.findConnectedNodesAndEdges(
|
|
1040
|
+
graph,
|
|
1041
|
+
currentNode,
|
|
1042
|
+
nextRelationship
|
|
1043
|
+
);
|
|
1044
|
+
for (const { node: connectedNode, edge } of connections) {
|
|
1045
|
+
if (this.patternMatcher.doesNodeMatchPattern(connectedNode, nextPattern)) {
|
|
1046
|
+
const subMatches = this.traverseRelationships(
|
|
1047
|
+
graph,
|
|
1048
|
+
connectedNode,
|
|
1049
|
+
nextVarName,
|
|
1050
|
+
remainingPatterns_,
|
|
1051
|
+
remainingRelationships
|
|
1052
|
+
);
|
|
1053
|
+
for (const subMatch of subMatches) {
|
|
1054
|
+
const result = {
|
|
1055
|
+
[currentVarName]: currentNode,
|
|
1056
|
+
[nextVarName]: connectedNode,
|
|
1057
|
+
// Always include the connected node
|
|
1058
|
+
...subMatch
|
|
1059
|
+
};
|
|
1060
|
+
if (nextRelationship.variable && edge) {
|
|
1061
|
+
result[nextRelationship.variable] = edge;
|
|
1062
|
+
}
|
|
1063
|
+
results.push(result);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return results;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Traverse variable-length paths (zero or more edges)
|
|
1071
|
+
* Handles cycles by tracking visited nodes
|
|
1072
|
+
*/
|
|
1073
|
+
traverseVariableLengthPath(graph, startNode, startVarName, relationship, targetPattern, targetVarName, remainingPatterns, remainingRelationships) {
|
|
1074
|
+
const results = [];
|
|
1075
|
+
const minLength = relationship.minLength ?? 0;
|
|
1076
|
+
const maxLength = relationship.maxLength ?? Infinity;
|
|
1077
|
+
const traverse = (currentNode, pathLength, visited, pathEdges) => {
|
|
1078
|
+
if (pathLength >= minLength && this.patternMatcher.doesNodeMatchPattern(currentNode, targetPattern)) {
|
|
1079
|
+
const subMatches = this.traverseRelationships(
|
|
1080
|
+
graph,
|
|
1081
|
+
currentNode,
|
|
1082
|
+
targetVarName,
|
|
1083
|
+
remainingPatterns,
|
|
1084
|
+
remainingRelationships
|
|
1085
|
+
);
|
|
1086
|
+
for (const subMatch of subMatches) {
|
|
1087
|
+
const result = {
|
|
1088
|
+
[startVarName]: startNode,
|
|
1089
|
+
[targetVarName]: currentNode,
|
|
1090
|
+
...subMatch
|
|
1091
|
+
};
|
|
1092
|
+
if (relationship.variable && pathEdges.length > 0) {
|
|
1093
|
+
result[relationship.variable] = pathEdges[pathEdges.length - 1];
|
|
1094
|
+
}
|
|
1095
|
+
results.push(result);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (pathLength < maxLength) {
|
|
1099
|
+
const connections2 = this.relationshipNavigator.findConnectedNodesAndEdges(
|
|
1100
|
+
graph,
|
|
1101
|
+
currentNode,
|
|
1102
|
+
relationship
|
|
1103
|
+
);
|
|
1104
|
+
for (const { node: nextNode, edge } of connections2) {
|
|
1105
|
+
if (!visited.has(nextNode.id)) {
|
|
1106
|
+
const newVisited = new Set(visited);
|
|
1107
|
+
newVisited.add(nextNode.id);
|
|
1108
|
+
traverse(nextNode, pathLength + 1, newVisited, [
|
|
1109
|
+
...pathEdges,
|
|
1110
|
+
edge
|
|
1111
|
+
]);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
if (minLength === 0 && this.patternMatcher.doesNodeMatchPattern(startNode, targetPattern)) {
|
|
1117
|
+
const subMatches = this.traverseRelationships(
|
|
1118
|
+
graph,
|
|
1119
|
+
startNode,
|
|
1120
|
+
targetVarName,
|
|
1121
|
+
remainingPatterns,
|
|
1122
|
+
remainingRelationships
|
|
1123
|
+
);
|
|
1124
|
+
for (const subMatch of subMatches) {
|
|
1125
|
+
const result = {
|
|
1126
|
+
[startVarName]: startNode,
|
|
1127
|
+
[targetVarName]: startNode,
|
|
1128
|
+
...subMatch
|
|
1129
|
+
};
|
|
1130
|
+
results.push(result);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const connections = this.relationshipNavigator.findConnectedNodesAndEdges(
|
|
1134
|
+
graph,
|
|
1135
|
+
startNode,
|
|
1136
|
+
relationship
|
|
1137
|
+
);
|
|
1138
|
+
for (const { node: nextNode, edge } of connections) {
|
|
1139
|
+
traverse(nextNode, 1, /* @__PURE__ */ new Set([startNode.id, nextNode.id]), [edge]);
|
|
1140
|
+
}
|
|
1141
|
+
return results;
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/executor/executor.ts
|
|
1146
|
+
var CypherExecutor = class {
|
|
1147
|
+
constructor(graph) {
|
|
1148
|
+
this.graph = graph;
|
|
1149
|
+
this.conditionEvaluator = new ConditionEvaluator();
|
|
1150
|
+
const patternMatcher = new PatternMatcher();
|
|
1151
|
+
const relationshipNavigator = new RelationshipNavigator(patternMatcher);
|
|
1152
|
+
const traverseEngine = new TraverseEngine(
|
|
1153
|
+
patternMatcher,
|
|
1154
|
+
relationshipNavigator
|
|
1155
|
+
);
|
|
1156
|
+
this.matchEngine = new MatchEngine(patternMatcher, traverseEngine);
|
|
1157
|
+
this.resultFormatter = new ResultFormatter();
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Execute a parsed Cypher query
|
|
1161
|
+
* @param extractData - If true (default), returns node.data. If false, returns full GraphNode objects.
|
|
1162
|
+
* For edges, always returns the full GraphEdge object (edges have no data property).
|
|
1163
|
+
* Returns results where keys are variable names and values are either node data or edge objects
|
|
1164
|
+
*/
|
|
1165
|
+
execute(query2) {
|
|
1166
|
+
if (query2.statements.length === 0) {
|
|
1167
|
+
throw new Error("Query must have at least one statement");
|
|
1168
|
+
}
|
|
1169
|
+
if (query2.statements[query2.statements.length - 1].type !== "ReturnStatement") {
|
|
1170
|
+
throw new Error("MATCH Query must have a RETURN statement");
|
|
1171
|
+
}
|
|
1172
|
+
let results = [];
|
|
1173
|
+
for (const statement of query2.statements) {
|
|
1174
|
+
if (statement.type === "MatchStatement") {
|
|
1175
|
+
results = this.matchEngine.processMatchStatement(
|
|
1176
|
+
this.graph,
|
|
1177
|
+
statement,
|
|
1178
|
+
results
|
|
1179
|
+
);
|
|
1180
|
+
} else if (statement.type === "WhereStatement") {
|
|
1181
|
+
results = this.conditionEvaluator.applyWhereClauseIfNeeded(
|
|
1182
|
+
results,
|
|
1183
|
+
statement
|
|
1184
|
+
);
|
|
1185
|
+
} else if (statement.type === "OrderByStatement") {
|
|
1186
|
+
results = this.resultFormatter.sortResults(results, statement);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
const returnStatement = query2.statements[query2.statements.length - 1];
|
|
1190
|
+
let formattedResults = this.resultFormatter.formatResults(
|
|
1191
|
+
results,
|
|
1192
|
+
returnStatement
|
|
1193
|
+
);
|
|
1194
|
+
return formattedResults;
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/lexer.ts
|
|
1199
|
+
var Lexer = class {
|
|
1200
|
+
constructor(input) {
|
|
1201
|
+
this.input = input;
|
|
1202
|
+
this.position = 0;
|
|
1203
|
+
this.currentChar = input.length > 0 ? input[0] : null;
|
|
1204
|
+
}
|
|
1205
|
+
advance() {
|
|
1206
|
+
this.position++;
|
|
1207
|
+
this.currentChar = this.position >= this.input.length ? null : this.input[this.position];
|
|
1208
|
+
}
|
|
1209
|
+
skipWhitespace() {
|
|
1210
|
+
while (this.currentChar && /\s/.test(this.currentChar)) {
|
|
1211
|
+
this.advance();
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
readIdentifier() {
|
|
1215
|
+
let result = "";
|
|
1216
|
+
while (this.currentChar && /[a-zA-Z0-9_]/.test(this.currentChar)) {
|
|
1217
|
+
result += this.currentChar;
|
|
1218
|
+
this.advance();
|
|
1219
|
+
}
|
|
1220
|
+
return result;
|
|
1221
|
+
}
|
|
1222
|
+
readString() {
|
|
1223
|
+
const quote = this.currentChar;
|
|
1224
|
+
this.advance();
|
|
1225
|
+
let result = "";
|
|
1226
|
+
while (this.currentChar && this.currentChar !== quote) {
|
|
1227
|
+
if (this.currentChar === "\\") {
|
|
1228
|
+
this.advance();
|
|
1229
|
+
if (this.currentChar === "n") {
|
|
1230
|
+
result += "\n";
|
|
1231
|
+
} else if (this.currentChar === "t") {
|
|
1232
|
+
result += " ";
|
|
1233
|
+
} else {
|
|
1234
|
+
result += this.currentChar;
|
|
1235
|
+
}
|
|
1236
|
+
this.advance();
|
|
1237
|
+
} else {
|
|
1238
|
+
result += this.currentChar;
|
|
1239
|
+
this.advance();
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (this.currentChar === quote) {
|
|
1243
|
+
this.advance();
|
|
1244
|
+
}
|
|
1245
|
+
return result;
|
|
1246
|
+
}
|
|
1247
|
+
readNumber() {
|
|
1248
|
+
let result = "";
|
|
1249
|
+
while (this.currentChar && /[0-9.]/.test(this.currentChar)) {
|
|
1250
|
+
result += this.currentChar;
|
|
1251
|
+
this.advance();
|
|
1252
|
+
}
|
|
1253
|
+
return result;
|
|
1254
|
+
}
|
|
1255
|
+
peekKeyword() {
|
|
1256
|
+
const savedPos = this.position;
|
|
1257
|
+
const savedChar = this.currentChar;
|
|
1258
|
+
const keyword = this.readIdentifier();
|
|
1259
|
+
this.position = savedPos;
|
|
1260
|
+
this.currentChar = savedChar;
|
|
1261
|
+
return keyword.length > 0 ? keyword.toUpperCase() : null;
|
|
1262
|
+
}
|
|
1263
|
+
tokenize() {
|
|
1264
|
+
const tokens = [];
|
|
1265
|
+
this.position = 0;
|
|
1266
|
+
this.currentChar = this.input.length > 0 ? this.input[0] : null;
|
|
1267
|
+
while (this.currentChar !== null) {
|
|
1268
|
+
this.skipWhitespace();
|
|
1269
|
+
if (!this.currentChar)
|
|
1270
|
+
break;
|
|
1271
|
+
const startPos = this.position;
|
|
1272
|
+
const keyword = this.peekKeyword();
|
|
1273
|
+
const keywordMap = {
|
|
1274
|
+
MATCH: "MATCH" /* MATCH */,
|
|
1275
|
+
OPTIONAL: "OPTIONAL" /* OPTIONAL */,
|
|
1276
|
+
RETURN: "RETURN" /* RETURN */,
|
|
1277
|
+
DISTINCT: "DISTINCT" /* DISTINCT */,
|
|
1278
|
+
WHERE: "WHERE" /* WHERE */,
|
|
1279
|
+
AND: "AND" /* AND */,
|
|
1280
|
+
OR: "OR" /* OR */,
|
|
1281
|
+
AS: "AS" /* AS */,
|
|
1282
|
+
IN: "IN" /* IN */,
|
|
1283
|
+
NOT: "NOT" /* NOT */,
|
|
1284
|
+
IS: "IS" /* IS */,
|
|
1285
|
+
NULL: "NULL" /* NULL */,
|
|
1286
|
+
TRUE: "TRUE" /* TRUE */,
|
|
1287
|
+
FALSE: "FALSE" /* FALSE */,
|
|
1288
|
+
STARTS: "STARTS" /* STARTS */,
|
|
1289
|
+
ENDS: "ENDS" /* ENDS */,
|
|
1290
|
+
CONTAINS: "CONTAINS" /* CONTAINS */,
|
|
1291
|
+
WITH: "WITH" /* WITH */,
|
|
1292
|
+
LIMIT: "LIMIT" /* LIMIT */,
|
|
1293
|
+
ORDER: "ORDER" /* ORDER */,
|
|
1294
|
+
BY: "BY" /* BY */,
|
|
1295
|
+
ASC: "ASC" /* ASC */,
|
|
1296
|
+
DESC: "DESC" /* DESC */
|
|
1297
|
+
};
|
|
1298
|
+
if (keyword && keywordMap[keyword]) {
|
|
1299
|
+
tokens.push({
|
|
1300
|
+
type: keywordMap[keyword],
|
|
1301
|
+
value: keyword,
|
|
1302
|
+
position: startPos
|
|
1303
|
+
});
|
|
1304
|
+
this.readIdentifier();
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
switch (this.currentChar) {
|
|
1308
|
+
case "(":
|
|
1309
|
+
tokens.push({
|
|
1310
|
+
type: "LPAREN" /* LPAREN */,
|
|
1311
|
+
value: "(",
|
|
1312
|
+
position: startPos
|
|
1313
|
+
});
|
|
1314
|
+
this.advance();
|
|
1315
|
+
break;
|
|
1316
|
+
case ")":
|
|
1317
|
+
tokens.push({
|
|
1318
|
+
type: "RPAREN" /* RPAREN */,
|
|
1319
|
+
value: ")",
|
|
1320
|
+
position: startPos
|
|
1321
|
+
});
|
|
1322
|
+
this.advance();
|
|
1323
|
+
break;
|
|
1324
|
+
case "[":
|
|
1325
|
+
tokens.push({
|
|
1326
|
+
type: "LBRACKET" /* LBRACKET */,
|
|
1327
|
+
value: "[",
|
|
1328
|
+
position: startPos
|
|
1329
|
+
});
|
|
1330
|
+
this.advance();
|
|
1331
|
+
break;
|
|
1332
|
+
case "]":
|
|
1333
|
+
tokens.push({
|
|
1334
|
+
type: "RBRACKET" /* RBRACKET */,
|
|
1335
|
+
value: "]",
|
|
1336
|
+
position: startPos
|
|
1337
|
+
});
|
|
1338
|
+
this.advance();
|
|
1339
|
+
break;
|
|
1340
|
+
case "{":
|
|
1341
|
+
tokens.push({
|
|
1342
|
+
type: "LBRACE" /* LBRACE */,
|
|
1343
|
+
value: "{",
|
|
1344
|
+
position: startPos
|
|
1345
|
+
});
|
|
1346
|
+
this.advance();
|
|
1347
|
+
break;
|
|
1348
|
+
case "}":
|
|
1349
|
+
tokens.push({
|
|
1350
|
+
type: "RBRACE" /* RBRACE */,
|
|
1351
|
+
value: "}",
|
|
1352
|
+
position: startPos
|
|
1353
|
+
});
|
|
1354
|
+
this.advance();
|
|
1355
|
+
break;
|
|
1356
|
+
case "<":
|
|
1357
|
+
if (this.position + 1 < this.input.length && this.input[this.position + 1] === "-") {
|
|
1358
|
+
tokens.push({
|
|
1359
|
+
type: "ARROW" /* ARROW */,
|
|
1360
|
+
value: "<-",
|
|
1361
|
+
position: startPos
|
|
1362
|
+
});
|
|
1363
|
+
this.advance();
|
|
1364
|
+
this.advance();
|
|
1365
|
+
} else if (this.position + 1 < this.input.length && this.input[this.position + 1] === "=") {
|
|
1366
|
+
tokens.push({
|
|
1367
|
+
type: "LTE" /* LTE */,
|
|
1368
|
+
value: "<=",
|
|
1369
|
+
position: startPos
|
|
1370
|
+
});
|
|
1371
|
+
this.advance();
|
|
1372
|
+
this.advance();
|
|
1373
|
+
} else {
|
|
1374
|
+
tokens.push({
|
|
1375
|
+
type: "LT" /* LT */,
|
|
1376
|
+
value: "<",
|
|
1377
|
+
position: startPos
|
|
1378
|
+
});
|
|
1379
|
+
this.advance();
|
|
1380
|
+
}
|
|
1381
|
+
break;
|
|
1382
|
+
case "-":
|
|
1383
|
+
if (this.position + 1 < this.input.length && this.input[this.position + 1] === ">") {
|
|
1384
|
+
tokens.push({
|
|
1385
|
+
type: "ARROW" /* ARROW */,
|
|
1386
|
+
value: "->",
|
|
1387
|
+
position: startPos
|
|
1388
|
+
});
|
|
1389
|
+
this.advance();
|
|
1390
|
+
this.advance();
|
|
1391
|
+
} else {
|
|
1392
|
+
tokens.push({
|
|
1393
|
+
type: "DASH" /* DASH */,
|
|
1394
|
+
value: "-",
|
|
1395
|
+
position: startPos
|
|
1396
|
+
});
|
|
1397
|
+
this.advance();
|
|
1398
|
+
}
|
|
1399
|
+
break;
|
|
1400
|
+
case "=":
|
|
1401
|
+
if (this.position + 1 < this.input.length && this.input[this.position + 1] === "~") {
|
|
1402
|
+
tokens.push({
|
|
1403
|
+
type: "REGEX_MATCH" /* REGEX_MATCH */,
|
|
1404
|
+
value: "=~",
|
|
1405
|
+
position: startPos
|
|
1406
|
+
});
|
|
1407
|
+
this.advance();
|
|
1408
|
+
this.advance();
|
|
1409
|
+
} else {
|
|
1410
|
+
tokens.push({
|
|
1411
|
+
type: "EQUALS" /* EQUALS */,
|
|
1412
|
+
value: "=",
|
|
1413
|
+
position: startPos
|
|
1414
|
+
});
|
|
1415
|
+
this.advance();
|
|
1416
|
+
}
|
|
1417
|
+
break;
|
|
1418
|
+
case "!":
|
|
1419
|
+
if (this.position + 1 < this.input.length && this.input[this.position + 1] === "=") {
|
|
1420
|
+
tokens.push({
|
|
1421
|
+
type: "NOT_EQUALS" /* NOT_EQUALS */,
|
|
1422
|
+
value: "!=",
|
|
1423
|
+
position: startPos
|
|
1424
|
+
});
|
|
1425
|
+
this.advance();
|
|
1426
|
+
this.advance();
|
|
1427
|
+
} else {
|
|
1428
|
+
throw new Error(`Unexpected character: ! at position ${startPos}`);
|
|
1429
|
+
}
|
|
1430
|
+
break;
|
|
1431
|
+
case ">":
|
|
1432
|
+
if (this.position + 1 < this.input.length && this.input[this.position + 1] === "=") {
|
|
1433
|
+
tokens.push({
|
|
1434
|
+
type: "GTE" /* GTE */,
|
|
1435
|
+
value: ">=",
|
|
1436
|
+
position: startPos
|
|
1437
|
+
});
|
|
1438
|
+
this.advance();
|
|
1439
|
+
this.advance();
|
|
1440
|
+
} else {
|
|
1441
|
+
tokens.push({
|
|
1442
|
+
type: "GT" /* GT */,
|
|
1443
|
+
value: ">",
|
|
1444
|
+
position: startPos
|
|
1445
|
+
});
|
|
1446
|
+
this.advance();
|
|
1447
|
+
}
|
|
1448
|
+
break;
|
|
1449
|
+
case ",":
|
|
1450
|
+
tokens.push({
|
|
1451
|
+
type: "COMMA" /* COMMA */,
|
|
1452
|
+
value: ",",
|
|
1453
|
+
position: startPos
|
|
1454
|
+
});
|
|
1455
|
+
this.advance();
|
|
1456
|
+
break;
|
|
1457
|
+
case ".":
|
|
1458
|
+
tokens.push({
|
|
1459
|
+
type: "DOT" /* DOT */,
|
|
1460
|
+
value: ".",
|
|
1461
|
+
position: startPos
|
|
1462
|
+
});
|
|
1463
|
+
this.advance();
|
|
1464
|
+
break;
|
|
1465
|
+
case "|":
|
|
1466
|
+
tokens.push({
|
|
1467
|
+
type: "PIPE" /* PIPE */,
|
|
1468
|
+
value: "|",
|
|
1469
|
+
position: startPos
|
|
1470
|
+
});
|
|
1471
|
+
this.advance();
|
|
1472
|
+
break;
|
|
1473
|
+
case "*":
|
|
1474
|
+
tokens.push({
|
|
1475
|
+
type: "ASTERISK" /* ASTERISK */,
|
|
1476
|
+
value: "*",
|
|
1477
|
+
position: startPos
|
|
1478
|
+
});
|
|
1479
|
+
this.advance();
|
|
1480
|
+
break;
|
|
1481
|
+
case ":":
|
|
1482
|
+
tokens.push({
|
|
1483
|
+
type: "LABEL" /* LABEL */,
|
|
1484
|
+
value: ":",
|
|
1485
|
+
position: startPos
|
|
1486
|
+
});
|
|
1487
|
+
this.advance();
|
|
1488
|
+
break;
|
|
1489
|
+
case '"':
|
|
1490
|
+
case "'":
|
|
1491
|
+
tokens.push({
|
|
1492
|
+
type: "STRING" /* STRING */,
|
|
1493
|
+
value: this.readString(),
|
|
1494
|
+
position: startPos
|
|
1495
|
+
});
|
|
1496
|
+
break;
|
|
1497
|
+
default:
|
|
1498
|
+
if (/[0-9]/.test(this.currentChar)) {
|
|
1499
|
+
tokens.push({
|
|
1500
|
+
type: "NUMBER" /* NUMBER */,
|
|
1501
|
+
value: this.readNumber(),
|
|
1502
|
+
position: startPos
|
|
1503
|
+
});
|
|
1504
|
+
} else if (/[a-zA-Z_]/.test(this.currentChar)) {
|
|
1505
|
+
tokens.push({
|
|
1506
|
+
type: "IDENTIFIER" /* IDENTIFIER */,
|
|
1507
|
+
value: this.readIdentifier(),
|
|
1508
|
+
position: startPos
|
|
1509
|
+
});
|
|
1510
|
+
} else {
|
|
1511
|
+
throw new Error(
|
|
1512
|
+
`Unexpected character: ${this.currentChar} at position ${startPos}`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
tokens.push({ type: "EOF" /* EOF */, value: "", position: this.position });
|
|
1518
|
+
return tokens;
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/parser.ts
|
|
1523
|
+
var Parser = class {
|
|
1524
|
+
constructor(tokens) {
|
|
1525
|
+
this.tokens = [];
|
|
1526
|
+
this.position = 0;
|
|
1527
|
+
this.tokens = tokens;
|
|
1528
|
+
}
|
|
1529
|
+
current() {
|
|
1530
|
+
return this.tokens[this.position];
|
|
1531
|
+
}
|
|
1532
|
+
advance() {
|
|
1533
|
+
const token = this.current();
|
|
1534
|
+
if (this.position < this.tokens.length - 1) {
|
|
1535
|
+
this.position++;
|
|
1536
|
+
}
|
|
1537
|
+
return token;
|
|
1538
|
+
}
|
|
1539
|
+
peek() {
|
|
1540
|
+
return this.tokens[this.position + 1] || this.tokens[this.tokens.length - 1];
|
|
1541
|
+
}
|
|
1542
|
+
expect(type) {
|
|
1543
|
+
const token = this.current();
|
|
1544
|
+
if (token.type !== type) {
|
|
1545
|
+
throw new Error(
|
|
1546
|
+
`Expected ${type}, got ${token.type} at position ${token.position}`
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
return this.advance();
|
|
1550
|
+
}
|
|
1551
|
+
isKeyword(keyword) {
|
|
1552
|
+
return this.current().type === keyword;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Parse a property object: { key: value, ... }
|
|
1556
|
+
* Note: Cypher uses colon (:) for properties in node patterns, not equals (=)
|
|
1557
|
+
*/
|
|
1558
|
+
parseProperties() {
|
|
1559
|
+
this.expect("LBRACE" /* LBRACE */);
|
|
1560
|
+
const properties = {};
|
|
1561
|
+
if (this.current().type === "RBRACE" /* RBRACE */) {
|
|
1562
|
+
this.advance();
|
|
1563
|
+
return properties;
|
|
1564
|
+
}
|
|
1565
|
+
while (true) {
|
|
1566
|
+
const key = this.expect("IDENTIFIER" /* IDENTIFIER */).value;
|
|
1567
|
+
this.expect("LABEL" /* LABEL */);
|
|
1568
|
+
let value;
|
|
1569
|
+
if (this.current().type === "LBRACKET" /* LBRACKET */) {
|
|
1570
|
+
value = this.parseArrayLiteral();
|
|
1571
|
+
} else if (this.current().type === "STRING" /* STRING */) {
|
|
1572
|
+
value = this.advance().value;
|
|
1573
|
+
} else if (this.current().type === "NUMBER" /* NUMBER */) {
|
|
1574
|
+
value = parseFloat(this.advance().value);
|
|
1575
|
+
} else if (this.current().type === "TRUE" /* TRUE */) {
|
|
1576
|
+
value = true;
|
|
1577
|
+
this.advance();
|
|
1578
|
+
} else if (this.current().type === "FALSE" /* FALSE */) {
|
|
1579
|
+
value = false;
|
|
1580
|
+
this.advance();
|
|
1581
|
+
} else if (this.current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1582
|
+
value = this.advance().value;
|
|
1583
|
+
} else {
|
|
1584
|
+
throw new Error(
|
|
1585
|
+
`Unexpected token type for property value: ${this.current().type}`
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
properties[key] = value;
|
|
1589
|
+
if (this.current().type === "RBRACE" /* RBRACE */) {
|
|
1590
|
+
this.advance();
|
|
1591
|
+
break;
|
|
1592
|
+
}
|
|
1593
|
+
this.expect("COMMA" /* COMMA */);
|
|
1594
|
+
}
|
|
1595
|
+
return properties;
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Parse a node pattern: (var:Label {prop: value}) or (:Label {prop: value})
|
|
1599
|
+
*/
|
|
1600
|
+
parseNodePattern() {
|
|
1601
|
+
this.expect("LPAREN" /* LPAREN */);
|
|
1602
|
+
const pattern = {
|
|
1603
|
+
type: "NodePattern",
|
|
1604
|
+
labels: []
|
|
1605
|
+
};
|
|
1606
|
+
if (this.current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1607
|
+
const nextToken = this.peek();
|
|
1608
|
+
if (nextToken.type === "LABEL" /* LABEL */ || nextToken.type === "LBRACE" /* LBRACE */ || nextToken.type === "RPAREN" /* RPAREN */) {
|
|
1609
|
+
pattern.variable = this.advance().value;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (this.current().type === "LABEL" /* LABEL */) {
|
|
1613
|
+
this.advance();
|
|
1614
|
+
if (this.current().type !== "IDENTIFIER" /* IDENTIFIER */) {
|
|
1615
|
+
throw new Error(
|
|
1616
|
+
`Expected identifier after :, got ${this.current().type}`
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
pattern.labels.push(this.advance().value);
|
|
1620
|
+
if (this.current().type === "LABEL" /* LABEL */) {
|
|
1621
|
+
pattern.labelOperator = "AND";
|
|
1622
|
+
while (this.current().type === "LABEL" /* LABEL */) {
|
|
1623
|
+
this.advance();
|
|
1624
|
+
if (this.current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1625
|
+
pattern.labels.push(this.advance().value);
|
|
1626
|
+
} else {
|
|
1627
|
+
throw new Error(
|
|
1628
|
+
`Expected identifier after :, got ${this.current().type}`
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
} else if (this.current().type === "PIPE" /* PIPE */) {
|
|
1633
|
+
pattern.labelOperator = "OR";
|
|
1634
|
+
while (this.current().type === "PIPE" /* PIPE */) {
|
|
1635
|
+
this.advance();
|
|
1636
|
+
if (this.current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1637
|
+
pattern.labels.push(this.advance().value);
|
|
1638
|
+
} else {
|
|
1639
|
+
throw new Error(
|
|
1640
|
+
`Expected identifier after |, got ${this.current().type}`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
} else {
|
|
1645
|
+
pattern.labelOperator = "AND";
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
if (this.current().type === "LBRACE" /* LBRACE */) {
|
|
1649
|
+
pattern.properties = this.parseProperties();
|
|
1650
|
+
}
|
|
1651
|
+
this.expect("RPAREN" /* RPAREN */);
|
|
1652
|
+
return pattern;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Parse relationship pattern: -[:RELATIONSHIP_TYPE]-> or <-[:RELATIONSHIP_TYPE]- or -[r:RELATIONSHIP_TYPE]->
|
|
1656
|
+
*/
|
|
1657
|
+
parseRelationship() {
|
|
1658
|
+
const relationship = {
|
|
1659
|
+
type: "RelationshipPattern",
|
|
1660
|
+
direction: "outgoing"
|
|
1661
|
+
};
|
|
1662
|
+
if (this.current().type === "ARROW" /* ARROW */ && this.current().value === "<-") {
|
|
1663
|
+
relationship.direction = "incoming";
|
|
1664
|
+
this.advance();
|
|
1665
|
+
} else {
|
|
1666
|
+
this.expect("DASH" /* DASH */);
|
|
1667
|
+
}
|
|
1668
|
+
if (this.current().type === "LBRACKET" /* LBRACKET */) {
|
|
1669
|
+
this.advance();
|
|
1670
|
+
if (this.current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1671
|
+
const nextToken = this.peek();
|
|
1672
|
+
if (nextToken.type === "LABEL" /* LABEL */ || nextToken.type === "ASTERISK" /* ASTERISK */) {
|
|
1673
|
+
relationship.variable = this.advance().value;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
if (this.current().type === "ASTERISK" /* ASTERISK */) {
|
|
1677
|
+
this.advance();
|
|
1678
|
+
relationship.variableLength = true;
|
|
1679
|
+
relationship.minLength = 0;
|
|
1680
|
+
relationship.maxLength = void 0;
|
|
1681
|
+
} else if (this.current().type === "LABEL" /* LABEL */) {
|
|
1682
|
+
this.advance();
|
|
1683
|
+
const edgeTypes = [];
|
|
1684
|
+
edgeTypes.push(this.expect("IDENTIFIER" /* IDENTIFIER */).value);
|
|
1685
|
+
while (this.current().type === "PIPE" /* PIPE */) {
|
|
1686
|
+
this.advance();
|
|
1687
|
+
edgeTypes.push(this.expect("IDENTIFIER" /* IDENTIFIER */).value);
|
|
1688
|
+
}
|
|
1689
|
+
relationship.edgeType = edgeTypes.length === 1 ? edgeTypes[0] : edgeTypes;
|
|
1690
|
+
if (this.current().type === "ASTERISK" /* ASTERISK */) {
|
|
1691
|
+
this.advance();
|
|
1692
|
+
relationship.variableLength = true;
|
|
1693
|
+
relationship.minLength = 0;
|
|
1694
|
+
relationship.maxLength = void 0;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
this.expect("RBRACKET" /* RBRACKET */);
|
|
1698
|
+
}
|
|
1699
|
+
if (relationship.direction === "incoming") {
|
|
1700
|
+
this.expect("DASH" /* DASH */);
|
|
1701
|
+
} else {
|
|
1702
|
+
if (this.current().type === "ARROW" /* ARROW */ && this.current().value === "->") {
|
|
1703
|
+
this.advance();
|
|
1704
|
+
} else if (this.current().type === "DASH" /* DASH */) {
|
|
1705
|
+
relationship.direction = "both";
|
|
1706
|
+
this.advance();
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return relationship;
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Parse MATCH clause (supports OPTIONAL MATCH)
|
|
1713
|
+
*/
|
|
1714
|
+
parseMatch() {
|
|
1715
|
+
const isOptional = this.isKeyword("OPTIONAL" /* OPTIONAL */);
|
|
1716
|
+
if (isOptional) {
|
|
1717
|
+
this.advance();
|
|
1718
|
+
}
|
|
1719
|
+
this.expect("MATCH" /* MATCH */);
|
|
1720
|
+
const firstPattern = this.parsePatternChain();
|
|
1721
|
+
const matchClause = {
|
|
1722
|
+
type: "MatchClause",
|
|
1723
|
+
patterns: firstPattern.patterns,
|
|
1724
|
+
relationships: firstPattern.relationships,
|
|
1725
|
+
additionalMatches: []
|
|
1726
|
+
};
|
|
1727
|
+
while (this.current().type === "COMMA" /* COMMA */) {
|
|
1728
|
+
this.advance();
|
|
1729
|
+
const additionalPattern = this.parsePatternChain();
|
|
1730
|
+
matchClause.additionalMatches.push(additionalPattern);
|
|
1731
|
+
}
|
|
1732
|
+
if (isOptional) {
|
|
1733
|
+
matchClause.optionalMatches = [
|
|
1734
|
+
{
|
|
1735
|
+
type: "MatchStatement",
|
|
1736
|
+
patterns: firstPattern.patterns,
|
|
1737
|
+
relationships: firstPattern.relationships
|
|
1738
|
+
},
|
|
1739
|
+
...matchClause.additionalMatches || []
|
|
1740
|
+
];
|
|
1741
|
+
matchClause.patterns = [];
|
|
1742
|
+
matchClause.relationships = void 0;
|
|
1743
|
+
matchClause.additionalMatches = void 0;
|
|
1744
|
+
}
|
|
1745
|
+
return matchClause;
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Parse a pattern chain: (node)-[:rel]->(node)-[:rel]->(node)...
|
|
1749
|
+
* Returns patterns and relationships for a single connected pattern
|
|
1750
|
+
*/
|
|
1751
|
+
parsePatternChain() {
|
|
1752
|
+
const patterns = [];
|
|
1753
|
+
const relationships = [];
|
|
1754
|
+
patterns.push(this.parseNodePattern());
|
|
1755
|
+
while (this.current().type === "DASH" /* DASH */ || this.current().type === "ARROW" /* ARROW */) {
|
|
1756
|
+
relationships.push(this.parseRelationship());
|
|
1757
|
+
patterns.push(this.parseNodePattern());
|
|
1758
|
+
}
|
|
1759
|
+
return {
|
|
1760
|
+
type: "MatchStatement",
|
|
1761
|
+
patterns,
|
|
1762
|
+
relationships: relationships.length > 0 ? relationships : void 0
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Parse WHERE clause
|
|
1767
|
+
* Example: WHERE m.type = "Command" AND "param" IN m.parameters
|
|
1768
|
+
* Note: Flat properties only (no nesting, like Neo4j Cypher)
|
|
1769
|
+
*/
|
|
1770
|
+
parseWhere() {
|
|
1771
|
+
this.expect("WHERE" /* WHERE */);
|
|
1772
|
+
const conditions = [];
|
|
1773
|
+
let logic;
|
|
1774
|
+
while (true) {
|
|
1775
|
+
const variable = this.expect("IDENTIFIER" /* IDENTIFIER */).value;
|
|
1776
|
+
let property;
|
|
1777
|
+
if (this.current().type === "DOT" /* DOT */) {
|
|
1778
|
+
this.advance();
|
|
1779
|
+
property = this.expect("IDENTIFIER" /* IDENTIFIER */).value;
|
|
1780
|
+
}
|
|
1781
|
+
let operator = "=";
|
|
1782
|
+
if (this.current().type === "EQUALS" /* EQUALS */) {
|
|
1783
|
+
operator = "=";
|
|
1784
|
+
this.advance();
|
|
1785
|
+
} else if (this.current().type === "NOT_EQUALS" /* NOT_EQUALS */) {
|
|
1786
|
+
operator = "!=";
|
|
1787
|
+
this.advance();
|
|
1788
|
+
} else if (this.current().type === "GT" /* GT */) {
|
|
1789
|
+
operator = ">";
|
|
1790
|
+
this.advance();
|
|
1791
|
+
} else if (this.current().type === "LT" /* LT */) {
|
|
1792
|
+
operator = "<";
|
|
1793
|
+
this.advance();
|
|
1794
|
+
} else if (this.current().type === "GTE" /* GTE */) {
|
|
1795
|
+
operator = ">=";
|
|
1796
|
+
this.advance();
|
|
1797
|
+
} else if (this.current().type === "LTE" /* LTE */) {
|
|
1798
|
+
operator = "<=";
|
|
1799
|
+
this.advance();
|
|
1800
|
+
} else if (this.current().type === "NOT" /* NOT */) {
|
|
1801
|
+
this.advance();
|
|
1802
|
+
this.expect("IN" /* IN */);
|
|
1803
|
+
operator = "NOT IN";
|
|
1804
|
+
} else if (this.current().type === "IN" /* IN */) {
|
|
1805
|
+
this.advance();
|
|
1806
|
+
operator = "IN";
|
|
1807
|
+
} else if (this.current().type === "IS" /* IS */) {
|
|
1808
|
+
this.advance();
|
|
1809
|
+
if (this.isKeyword("NULL" /* NULL */)) {
|
|
1810
|
+
this.advance();
|
|
1811
|
+
operator = "IS NULL";
|
|
1812
|
+
} else if (this.isKeyword("NOT" /* NOT */)) {
|
|
1813
|
+
this.advance();
|
|
1814
|
+
this.expect("NULL" /* NULL */);
|
|
1815
|
+
operator = "IS NOT NULL";
|
|
1816
|
+
} else {
|
|
1817
|
+
throw new Error(
|
|
1818
|
+
`Expected NULL or NOT NULL after IS at position ${this.current().position}`
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
} else if (this.current().type === "STARTS" /* STARTS */) {
|
|
1822
|
+
this.advance();
|
|
1823
|
+
this.expect("WITH" /* WITH */);
|
|
1824
|
+
operator = "STARTS WITH";
|
|
1825
|
+
} else if (this.current().type === "ENDS" /* ENDS */) {
|
|
1826
|
+
this.advance();
|
|
1827
|
+
this.expect("WITH" /* WITH */);
|
|
1828
|
+
operator = "ENDS WITH";
|
|
1829
|
+
} else if (this.current().type === "CONTAINS" /* CONTAINS */) {
|
|
1830
|
+
operator = "CONTAINS";
|
|
1831
|
+
this.advance();
|
|
1832
|
+
} else if (this.current().type === "REGEX_MATCH" /* REGEX_MATCH */) {
|
|
1833
|
+
operator = "=~";
|
|
1834
|
+
this.advance();
|
|
1835
|
+
} else {
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
`Unsupported operator at position ${this.current().position}.`
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
let value = null;
|
|
1841
|
+
if (operator === "IS NULL" || operator === "IS NOT NULL") {
|
|
1842
|
+
} else if (operator === "STARTS WITH" || operator === "ENDS WITH" || operator === "CONTAINS" || operator === "=~") {
|
|
1843
|
+
if (this.current().type === "STRING" /* STRING */) {
|
|
1844
|
+
value = this.advance().value;
|
|
1845
|
+
} else {
|
|
1846
|
+
throw new Error(
|
|
1847
|
+
`String matching operators (${operator}) require a string value at position ${this.current().position}`
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
} else if (this.current().type === "LBRACKET" /* LBRACKET */) {
|
|
1851
|
+
value = this.parseArrayLiteral();
|
|
1852
|
+
} else if (this.current().type === "STRING" /* STRING */) {
|
|
1853
|
+
value = this.advance().value;
|
|
1854
|
+
} else if (this.current().type === "NUMBER" /* NUMBER */) {
|
|
1855
|
+
value = parseFloat(this.advance().value);
|
|
1856
|
+
} else if (this.current().type === "TRUE" /* TRUE */) {
|
|
1857
|
+
value = true;
|
|
1858
|
+
this.advance();
|
|
1859
|
+
} else if (this.current().type === "FALSE" /* FALSE */) {
|
|
1860
|
+
value = false;
|
|
1861
|
+
this.advance();
|
|
1862
|
+
} else if (this.current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1863
|
+
value = this.advance().value;
|
|
1864
|
+
} else {
|
|
1865
|
+
throw new Error(
|
|
1866
|
+
`Unexpected token type for WHERE value: ${this.current().type}`
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
conditions.push({ variable, property, operator, value });
|
|
1870
|
+
if (this.isKeyword("AND" /* AND */)) {
|
|
1871
|
+
if (logic === void 0) {
|
|
1872
|
+
logic = "AND";
|
|
1873
|
+
} else if (logic !== "AND") {
|
|
1874
|
+
throw new Error("Cannot mix AND and OR in WHERE clause");
|
|
1875
|
+
}
|
|
1876
|
+
this.advance();
|
|
1877
|
+
continue;
|
|
1878
|
+
} else if (this.isKeyword("OR" /* OR */)) {
|
|
1879
|
+
if (logic === void 0) {
|
|
1880
|
+
logic = "OR";
|
|
1881
|
+
} else if (logic !== "OR") {
|
|
1882
|
+
throw new Error("Cannot mix AND and OR in WHERE clause");
|
|
1883
|
+
}
|
|
1884
|
+
this.advance();
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
return {
|
|
1890
|
+
type: "WhereClause",
|
|
1891
|
+
conditions,
|
|
1892
|
+
logic
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Parse array literal: [value1, value2, ...]
|
|
1897
|
+
*/
|
|
1898
|
+
parseArrayLiteral() {
|
|
1899
|
+
this.expect("LBRACKET" /* LBRACKET */);
|
|
1900
|
+
const array = [];
|
|
1901
|
+
if (this.current().type === "RBRACKET" /* RBRACKET */) {
|
|
1902
|
+
this.advance();
|
|
1903
|
+
return array;
|
|
1904
|
+
}
|
|
1905
|
+
while (true) {
|
|
1906
|
+
if (this.current().type === "STRING" /* STRING */) {
|
|
1907
|
+
array.push(this.advance().value);
|
|
1908
|
+
} else if (this.current().type === "NUMBER" /* NUMBER */) {
|
|
1909
|
+
array.push(parseFloat(this.advance().value));
|
|
1910
|
+
} else if (this.current().type === "TRUE" /* TRUE */) {
|
|
1911
|
+
array.push(true);
|
|
1912
|
+
this.advance();
|
|
1913
|
+
} else if (this.current().type === "FALSE" /* FALSE */) {
|
|
1914
|
+
array.push(false);
|
|
1915
|
+
this.advance();
|
|
1916
|
+
} else if (this.current().type === "IDENTIFIER" /* IDENTIFIER */) {
|
|
1917
|
+
array.push(this.advance().value);
|
|
1918
|
+
} else {
|
|
1919
|
+
throw new Error(
|
|
1920
|
+
`Unexpected token type in array literal: ${this.current().type}`
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
if (this.current().type === "COMMA" /* COMMA */) {
|
|
1924
|
+
this.advance();
|
|
1925
|
+
} else if (this.current().type === "RBRACKET" /* RBRACKET */) {
|
|
1926
|
+
this.advance();
|
|
1927
|
+
break;
|
|
1928
|
+
} else {
|
|
1929
|
+
throw new Error(
|
|
1930
|
+
`Expected comma or closing bracket in array literal at position ${this.current().position}`
|
|
1931
|
+
);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
return array;
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Parse RETURN clause
|
|
1938
|
+
* Supports: RETURN *, RETURN DISTINCT, RETURN var1, var2, RETURN var AS alias, LIMIT n
|
|
1939
|
+
*/
|
|
1940
|
+
parseReturn() {
|
|
1941
|
+
this.expect("RETURN" /* RETURN */);
|
|
1942
|
+
const distinct = this.isKeyword("DISTINCT" /* DISTINCT */);
|
|
1943
|
+
if (distinct) {
|
|
1944
|
+
this.advance();
|
|
1945
|
+
}
|
|
1946
|
+
if (this.current().type === "ASTERISK" /* ASTERISK */) {
|
|
1947
|
+
this.advance();
|
|
1948
|
+
const limit2 = this.parseOptionalLimit();
|
|
1949
|
+
return {
|
|
1950
|
+
type: "ReturnClause",
|
|
1951
|
+
distinct,
|
|
1952
|
+
returnAll: true,
|
|
1953
|
+
items: [],
|
|
1954
|
+
limit: limit2
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
const items = [];
|
|
1958
|
+
while (true) {
|
|
1959
|
+
const expression = this.expect("IDENTIFIER" /* IDENTIFIER */).value;
|
|
1960
|
+
let alias;
|
|
1961
|
+
if (this.isKeyword("AS" /* AS */)) {
|
|
1962
|
+
this.advance();
|
|
1963
|
+
alias = this.expect("IDENTIFIER" /* IDENTIFIER */).value;
|
|
1964
|
+
}
|
|
1965
|
+
items.push({ expression, alias });
|
|
1966
|
+
if (this.current().type === "COMMA" /* COMMA */) {
|
|
1967
|
+
this.advance();
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
break;
|
|
1971
|
+
}
|
|
1972
|
+
const limit = this.parseOptionalLimit();
|
|
1973
|
+
return {
|
|
1974
|
+
type: "ReturnClause",
|
|
1975
|
+
distinct,
|
|
1976
|
+
returnAll: false,
|
|
1977
|
+
items,
|
|
1978
|
+
limit
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Parse optional LIMIT clause
|
|
1983
|
+
* Returns the limit number or undefined if not present
|
|
1984
|
+
*/
|
|
1985
|
+
parseOptionalLimit() {
|
|
1986
|
+
if (this.isKeyword("LIMIT" /* LIMIT */)) {
|
|
1987
|
+
this.advance();
|
|
1988
|
+
const limitToken = this.expect("NUMBER" /* NUMBER */);
|
|
1989
|
+
return parseInt(limitToken.value, 10);
|
|
1990
|
+
}
|
|
1991
|
+
return void 0;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Parse ORDER BY clause
|
|
1995
|
+
* Supports: ORDER BY n.name, ORDER BY n.name ASC, ORDER BY n.name DESC
|
|
1996
|
+
* Also supports multiple fields: ORDER BY n.type DESC, n.name ASC
|
|
1997
|
+
*/
|
|
1998
|
+
parseOrderBy() {
|
|
1999
|
+
this.expect("ORDER" /* ORDER */);
|
|
2000
|
+
this.expect("BY" /* BY */);
|
|
2001
|
+
const items = [];
|
|
2002
|
+
while (true) {
|
|
2003
|
+
const variable = this.expect("IDENTIFIER" /* IDENTIFIER */).value;
|
|
2004
|
+
let property;
|
|
2005
|
+
if (this.current().type === "DOT" /* DOT */) {
|
|
2006
|
+
this.advance();
|
|
2007
|
+
property = this.expect("IDENTIFIER" /* IDENTIFIER */).value;
|
|
2008
|
+
}
|
|
2009
|
+
let direction = "ASC";
|
|
2010
|
+
if (this.isKeyword("ASC" /* ASC */)) {
|
|
2011
|
+
this.advance();
|
|
2012
|
+
direction = "ASC";
|
|
2013
|
+
} else if (this.isKeyword("DESC" /* DESC */)) {
|
|
2014
|
+
this.advance();
|
|
2015
|
+
direction = "DESC";
|
|
2016
|
+
}
|
|
2017
|
+
items.push({ variable, property, direction });
|
|
2018
|
+
if (this.current().type === "COMMA" /* COMMA */) {
|
|
2019
|
+
this.advance();
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
break;
|
|
2023
|
+
}
|
|
2024
|
+
return { type: "OrderByStatement", items };
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Parse entire Cypher query
|
|
2028
|
+
* Returns statements in execution order
|
|
2029
|
+
*/
|
|
2030
|
+
parse() {
|
|
2031
|
+
const statements = [];
|
|
2032
|
+
while (this.current().type !== "EOF" /* EOF */) {
|
|
2033
|
+
if (this.isKeyword("MATCH" /* MATCH */) || this.isKeyword("OPTIONAL" /* OPTIONAL */)) {
|
|
2034
|
+
const isOptional = this.isKeyword("OPTIONAL" /* OPTIONAL */);
|
|
2035
|
+
const matchClause = this.parseMatch();
|
|
2036
|
+
if (matchClause.optionalMatches) {
|
|
2037
|
+
for (const optionalMatch of matchClause.optionalMatches) {
|
|
2038
|
+
statements.push({
|
|
2039
|
+
type: "MatchStatement",
|
|
2040
|
+
patterns: optionalMatch.patterns,
|
|
2041
|
+
relationships: optionalMatch.relationships,
|
|
2042
|
+
optional: true
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
} else {
|
|
2046
|
+
if (matchClause.patterns.length > 0) {
|
|
2047
|
+
statements.push({
|
|
2048
|
+
type: "MatchStatement",
|
|
2049
|
+
patterns: matchClause.patterns,
|
|
2050
|
+
relationships: matchClause.relationships,
|
|
2051
|
+
optional: false
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
if (matchClause.additionalMatches) {
|
|
2055
|
+
for (const additionalMatch of matchClause.additionalMatches) {
|
|
2056
|
+
statements.push({
|
|
2057
|
+
type: "MatchStatement",
|
|
2058
|
+
patterns: additionalMatch.patterns,
|
|
2059
|
+
relationships: additionalMatch.relationships,
|
|
2060
|
+
optional: false
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
if (this.isKeyword("WHERE" /* WHERE */)) {
|
|
2068
|
+
const whereClause = this.parseWhere();
|
|
2069
|
+
statements.push({
|
|
2070
|
+
type: "WhereStatement",
|
|
2071
|
+
conditions: whereClause.conditions,
|
|
2072
|
+
logic: whereClause.logic
|
|
2073
|
+
});
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
if (this.isKeyword("ORDER" /* ORDER */)) {
|
|
2077
|
+
const orderByStatement = this.parseOrderBy();
|
|
2078
|
+
statements.push(orderByStatement);
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
if (this.isKeyword("RETURN" /* RETURN */)) {
|
|
2082
|
+
const returnClause = this.parseReturn();
|
|
2083
|
+
statements.push({
|
|
2084
|
+
type: "ReturnStatement",
|
|
2085
|
+
distinct: returnClause.distinct,
|
|
2086
|
+
returnAll: returnClause.returnAll,
|
|
2087
|
+
items: returnClause.items,
|
|
2088
|
+
limit: returnClause.limit
|
|
2089
|
+
});
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
break;
|
|
2093
|
+
}
|
|
2094
|
+
if (this.current().type !== "EOF" /* EOF */) {
|
|
2095
|
+
throw new Error(
|
|
2096
|
+
`Unexpected token: ${this.current().type} at position ${this.current().position}`
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
return {
|
|
2100
|
+
type: "CypherQuery",
|
|
2101
|
+
statements
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2106
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/validator/unsupported-features.config.ts
|
|
2107
|
+
var FORBIDDEN_KEYWORDS = [
|
|
2108
|
+
{
|
|
2109
|
+
pattern: /\bWITH\b/i,
|
|
2110
|
+
name: "WITH clause",
|
|
2111
|
+
errorMessage: "WITH clause is not supported. Cannot use WITH for intermediate results or variable passing.",
|
|
2112
|
+
alternative: "Use multiple MATCH statements or inline variables directly in WHERE/RETURN clauses."
|
|
2113
|
+
},
|
|
2114
|
+
{
|
|
2115
|
+
pattern: /\bUNWIND\b/i,
|
|
2116
|
+
name: "UNWIND clause",
|
|
2117
|
+
errorMessage: "UNWIND clause is not supported. Cannot use UNWIND to expand arrays."
|
|
2118
|
+
},
|
|
2119
|
+
{
|
|
2120
|
+
pattern: /\bUNION\b/i,
|
|
2121
|
+
name: "UNION clause",
|
|
2122
|
+
errorMessage: "UNION clause is not supported. Cannot combine result sets with UNION."
|
|
2123
|
+
},
|
|
2124
|
+
{
|
|
2125
|
+
pattern: /\bCREATE\b/i,
|
|
2126
|
+
name: "CREATE clause",
|
|
2127
|
+
errorMessage: "CREATE clause is not supported. This is a read-only query engine (no mutations)."
|
|
2128
|
+
},
|
|
2129
|
+
{
|
|
2130
|
+
pattern: /\bMERGE\b/i,
|
|
2131
|
+
name: "MERGE clause",
|
|
2132
|
+
errorMessage: "MERGE clause is not supported. This is a read-only query engine (no mutations)."
|
|
2133
|
+
},
|
|
2134
|
+
{
|
|
2135
|
+
pattern: /\bDELETE\b/i,
|
|
2136
|
+
name: "DELETE clause",
|
|
2137
|
+
errorMessage: "DELETE clause is not supported. This is a read-only query engine (no mutations)."
|
|
2138
|
+
}
|
|
2139
|
+
// {
|
|
2140
|
+
// pattern: /\bSET\b/i,
|
|
2141
|
+
// name: 'SET clause',
|
|
2142
|
+
// errorMessage:
|
|
2143
|
+
// 'SET clause is not supported. This is a read-only query engine (no mutations).',
|
|
2144
|
+
// },
|
|
2145
|
+
];
|
|
2146
|
+
var FORBIDDEN_AGGREGATIONS = [
|
|
2147
|
+
{
|
|
2148
|
+
pattern: /\bCOUNT\s*\(/i,
|
|
2149
|
+
name: "COUNT() function",
|
|
2150
|
+
errorMessage: "COUNT() aggregation function is not supported. Aggregation functions are not available.",
|
|
2151
|
+
alternative: "Use manual counting patterns or return all results and count in application code."
|
|
2152
|
+
},
|
|
2153
|
+
{
|
|
2154
|
+
pattern: /\bSUM\s*\(/i,
|
|
2155
|
+
name: "SUM() function",
|
|
2156
|
+
errorMessage: "SUM() aggregation function is not supported. Aggregation functions are not available."
|
|
2157
|
+
},
|
|
2158
|
+
{
|
|
2159
|
+
pattern: /\bcollect\s*\(/i,
|
|
2160
|
+
name: "collect() function",
|
|
2161
|
+
errorMessage: "collect() aggregation function is not supported. Aggregation functions are not available."
|
|
2162
|
+
},
|
|
2163
|
+
{
|
|
2164
|
+
pattern: /\bMIN\s*\(/i,
|
|
2165
|
+
name: "MIN() function",
|
|
2166
|
+
errorMessage: "MIN() aggregation function is not supported. Aggregation functions are not available."
|
|
2167
|
+
},
|
|
2168
|
+
{
|
|
2169
|
+
pattern: /\bMAX\s*\(/i,
|
|
2170
|
+
name: "MAX() function",
|
|
2171
|
+
errorMessage: "MAX() aggregation function is not supported. Aggregation functions are not available."
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
pattern: /\bAVG\s*\(/i,
|
|
2175
|
+
name: "AVG() function",
|
|
2176
|
+
errorMessage: "AVG() aggregation function is not supported. Aggregation functions are not available."
|
|
2177
|
+
}
|
|
2178
|
+
];
|
|
2179
|
+
var FORBIDDEN_MODIFIERS = [
|
|
2180
|
+
{
|
|
2181
|
+
pattern: /\bSKIP\b/i,
|
|
2182
|
+
name: "SKIP clause",
|
|
2183
|
+
errorMessage: "SKIP clause is not supported. Pagination is not available."
|
|
2184
|
+
}
|
|
2185
|
+
];
|
|
2186
|
+
var FORBIDDEN_PATTERNS = [
|
|
2187
|
+
{
|
|
2188
|
+
pattern: /\[.*:\w+\*\d+\.\.\d+.*\]/,
|
|
2189
|
+
name: "Path range syntax",
|
|
2190
|
+
errorMessage: "Path range syntax is not supported. Patterns like `*1..3`, `*2..`, `*..3` are invalid.",
|
|
2191
|
+
alternative: "Use `*` (unlimited, zero or more) for variable-length paths."
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
pattern: /\[.*:\w+\*\d+\.\..*\]/,
|
|
2195
|
+
name: "Path range syntax (lower bound)",
|
|
2196
|
+
errorMessage: "Path range syntax is not supported. Patterns like `*2..` are invalid.",
|
|
2197
|
+
alternative: "Use `*` (unlimited, zero or more) for variable-length paths."
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
pattern: /\[.*:\w+\*\.\.\d+.*\]/,
|
|
2201
|
+
name: "Path range syntax (upper bound)",
|
|
2202
|
+
errorMessage: "Path range syntax is not supported. Patterns like `*..3` are invalid.",
|
|
2203
|
+
alternative: "Use `*` (unlimited, zero or more) for variable-length paths."
|
|
2204
|
+
},
|
|
2205
|
+
{
|
|
2206
|
+
pattern: /WHERE\s+NOT\s*\(/i,
|
|
2207
|
+
name: "WHERE NOT (pattern)",
|
|
2208
|
+
errorMessage: "WHERE NOT (pattern) is not supported. Cannot use pattern negation in WHERE clause.",
|
|
2209
|
+
alternative: "Use `OPTIONAL MATCH` + `WHERE IS NULL` instead. Example: `OPTIONAL MATCH (m)-[:HAS_PARAMETER]->(:ParameterDeclaration) WHERE ... IS NULL`."
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
pattern: /MATCH\s*\(:\w+[^)]*\)(?:\s*,\s*\(:\w+[^)]*\))*(?:\s*WHERE[^R]*)?/i,
|
|
2213
|
+
name: "Anonymous-only queries",
|
|
2214
|
+
errorMessage: "Queries with only anonymous nodes (no variable bindings) are not useful.",
|
|
2215
|
+
alternative: "Use bound variables in the pattern. Example: `MATCH (a:A)` instead of `MATCH (:A)`."
|
|
2216
|
+
}
|
|
2217
|
+
];
|
|
2218
|
+
|
|
2219
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/validator/query-validator.ts
|
|
2220
|
+
var UNSUPPORTED_FEATURES = [
|
|
2221
|
+
...FORBIDDEN_KEYWORDS,
|
|
2222
|
+
...FORBIDDEN_AGGREGATIONS,
|
|
2223
|
+
...FORBIDDEN_MODIFIERS,
|
|
2224
|
+
...FORBIDDEN_PATTERNS
|
|
2225
|
+
];
|
|
2226
|
+
var UnsupportedFeatureError = class extends Error {
|
|
2227
|
+
constructor(feature, query2) {
|
|
2228
|
+
const message = `Unsupported Cypher feature detected: ${feature.name}
|
|
2229
|
+
|
|
2230
|
+
${feature.errorMessage}${feature.alternative ? `
|
|
2231
|
+
|
|
2232
|
+
Alternative: ${feature.alternative}` : ""}
|
|
2233
|
+
|
|
2234
|
+
Query: ${query2}`;
|
|
2235
|
+
super(message);
|
|
2236
|
+
this.feature = feature;
|
|
2237
|
+
this.query = query2;
|
|
2238
|
+
this.name = "UnsupportedFeatureError";
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
function validateQuery(query2) {
|
|
2242
|
+
if (!query2 || typeof query2 !== "string") {
|
|
2243
|
+
throw new Error("Query must be a non-empty string");
|
|
2244
|
+
}
|
|
2245
|
+
const normalizedQuery = query2.trim();
|
|
2246
|
+
if (normalizedQuery.length === 0) {
|
|
2247
|
+
throw new Error("Query cannot be empty");
|
|
2248
|
+
}
|
|
2249
|
+
for (const feature of UNSUPPORTED_FEATURES) {
|
|
2250
|
+
const pattern = typeof feature.pattern === "string" ? new RegExp(feature.pattern, "i") : feature.pattern;
|
|
2251
|
+
if (pattern.test(normalizedQuery)) {
|
|
2252
|
+
if (feature.name === "WITH clause") {
|
|
2253
|
+
const withMatches = normalizedQuery.matchAll(/\bWITH\b/gi);
|
|
2254
|
+
let isAllowed = false;
|
|
2255
|
+
for (const match of withMatches) {
|
|
2256
|
+
const beforeMatch = normalizedQuery.substring(
|
|
2257
|
+
Math.max(0, match.index - 10),
|
|
2258
|
+
match.index
|
|
2259
|
+
);
|
|
2260
|
+
if (/\bSTARTS\s+$/i.test(beforeMatch) || /\bENDS\s+$/i.test(beforeMatch)) {
|
|
2261
|
+
isAllowed = true;
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
if (isAllowed) {
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
throw new UnsupportedFeatureError(feature, normalizedQuery);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// libs/cli/code-graph-core/src/lib/cypher/lib/index.ts
|
|
2275
|
+
var buildQueryObject = (query2) => {
|
|
2276
|
+
const joinedQuery = Array.isArray(query2) ? query2.join(" ") : query2;
|
|
2277
|
+
validateQuery(joinedQuery);
|
|
2278
|
+
const lexer = new Lexer(joinedQuery);
|
|
2279
|
+
const tokens = lexer.tokenize();
|
|
2280
|
+
const parser = new Parser(tokens);
|
|
2281
|
+
return parser.parse();
|
|
2282
|
+
};
|
|
2283
|
+
var query = (queryStatements, graph) => {
|
|
2284
|
+
const queryObject = buildQueryObject(queryStatements);
|
|
2285
|
+
const executor = new CypherExecutor(graph);
|
|
2286
|
+
return executor.execute(queryObject);
|
|
2287
|
+
};
|
|
2288
|
+
|
|
2289
|
+
// libs/cli/code-graph-core/src/lib/code-graph-core.ts
|
|
2290
|
+
var matchFromGeneratedFiles = (cypherQuery, config, generatedFiles) => {
|
|
2291
|
+
const fileSystem = generatedFiles.reduce((acc, file) => {
|
|
2292
|
+
if (file.filePath.endsWith(".ts")) {
|
|
2293
|
+
acc[file.filePath] = file.code;
|
|
2294
|
+
}
|
|
2295
|
+
return acc;
|
|
2296
|
+
}, {});
|
|
2297
|
+
return matchFromFileSystem(cypherQuery, config, fileSystem);
|
|
2298
|
+
};
|
|
2299
|
+
var matchFromFileSystem = (cypherQuery, config, fileSystem) => {
|
|
2300
|
+
const program = createProgramFromFileSystem(fileSystem);
|
|
2301
|
+
const codeGraph = buildCodeGraph(program, config);
|
|
2302
|
+
const results = query(cypherQuery, codeGraph);
|
|
2303
|
+
return results;
|
|
2304
|
+
};
|
|
2305
|
+
var mapGeneratedFilesToFileSystem = (generatedFiles) => {
|
|
2306
|
+
return generatedFiles.reduce(
|
|
2307
|
+
(acc, file) => {
|
|
2308
|
+
acc[file.filePath] = file.code;
|
|
2309
|
+
return acc;
|
|
2310
|
+
},
|
|
2311
|
+
{}
|
|
2312
|
+
);
|
|
2313
|
+
};
|
|
2314
|
+
var resolveProgramFromConfig = (env) => {
|
|
2315
|
+
return env.tsConfig ? createProgramFromTsConfig(env.tsConfig) : createProgramFromFileSystem(
|
|
2316
|
+
mapGeneratedFilesToFileSystem(env.generatedFiles)
|
|
2317
|
+
);
|
|
2318
|
+
};
|
|
2319
|
+
var autoMatch = (cypherQuery, program, options) => {
|
|
2320
|
+
const queryObject = buildQueryObject(cypherQuery);
|
|
2321
|
+
const config = buildConfigFromQuery(queryObject);
|
|
2322
|
+
const codeGraph = buildCodeGraph(program, config);
|
|
2323
|
+
const results = query(cypherQuery, codeGraph);
|
|
2324
|
+
return options?.skipRewrite ? results : rewriteOccurenceOf(results);
|
|
2325
|
+
};
|
|
2326
|
+
var PATH_ANCHOR_SRC_APP = "src/app";
|
|
2327
|
+
var rewriteUntilPath = (pathOrNodeId, pathAnchor) => {
|
|
2328
|
+
const searchSegment = pathAnchor.startsWith("/") && pathAnchor.endsWith("/") ? pathAnchor : `/${pathAnchor.replace(/^\/|\/$/g, "")}/`;
|
|
2329
|
+
const idx = pathOrNodeId.lastIndexOf(searchSegment);
|
|
2330
|
+
if (idx < 0)
|
|
2331
|
+
return pathOrNodeId;
|
|
2332
|
+
const sliced = pathOrNodeId.slice(idx);
|
|
2333
|
+
return sliced.startsWith("/") ? sliced.slice(1) : sliced;
|
|
2334
|
+
};
|
|
2335
|
+
var rewriteOccurenceOf = (result) => {
|
|
2336
|
+
return result.map((result2) => {
|
|
2337
|
+
return Object.keys(result2).reduce((acc, key) => {
|
|
2338
|
+
if (!result2[key]) {
|
|
2339
|
+
return acc;
|
|
2340
|
+
}
|
|
2341
|
+
const id = result2[key]["id"];
|
|
2342
|
+
if (!id) {
|
|
2343
|
+
return acc;
|
|
2344
|
+
}
|
|
2345
|
+
return {
|
|
2346
|
+
...acc,
|
|
2347
|
+
[key]: {
|
|
2348
|
+
...result2[key],
|
|
2349
|
+
id: rewriteUntilPath(id, PATH_ANCHOR_SRC_APP)
|
|
2350
|
+
}
|
|
2351
|
+
};
|
|
2352
|
+
}, {});
|
|
2353
|
+
});
|
|
2354
|
+
};
|
|
2355
|
+
var filterResultsByGeneratedFiles = (results, generatedFiles) => {
|
|
2356
|
+
const filePaths = generatedFiles.map((file) => file.filePath);
|
|
2357
|
+
return results.filter(
|
|
2358
|
+
(result) => Object.values(result).find(
|
|
2359
|
+
(n) => filePaths.includes(n["id"].split(".")[0] + ".ts")
|
|
2360
|
+
)
|
|
2361
|
+
);
|
|
2362
|
+
};
|
|
2363
|
+
var autoMatchFromGeneratedFiles = (cypherQuery, generatedFiles) => {
|
|
2364
|
+
const queryObject = buildQueryObject(cypherQuery);
|
|
2365
|
+
const config = buildConfigFromQuery(queryObject);
|
|
2366
|
+
return matchFromGeneratedFiles(cypherQuery, config, generatedFiles);
|
|
2367
|
+
};
|
|
2368
|
+
var NODE_PROPERTIES_PRESET = {
|
|
2369
|
+
NAMEABLE: {
|
|
2370
|
+
name: `helpers.getDescendantsBy(args.current, (node) => node.kind === args.ts.SyntaxKind.Identifier)[0]?.getText();`,
|
|
2371
|
+
filePath: "args.sourceFile.fileName;"
|
|
2372
|
+
},
|
|
2373
|
+
/**
|
|
2374
|
+
* Creates a preset that extracts a property value from an ObjectLiteralExpression
|
|
2375
|
+
* @param key - The property name to look for in the object literal
|
|
2376
|
+
* @returns An extractor code string that finds the property and returns its initializer text
|
|
2377
|
+
*
|
|
2378
|
+
* @example
|
|
2379
|
+
* ```typescript
|
|
2380
|
+
* properties: {
|
|
2381
|
+
* selector: NODE_PROPERTIES_PRESET.OBJECT_PROPERTY('selector'),
|
|
2382
|
+
* template: NODE_PROPERTIES_PRESET.OBJECT_PROPERTY('template'),
|
|
2383
|
+
* }
|
|
2384
|
+
* ```
|
|
2385
|
+
*/
|
|
2386
|
+
OBJECT_LITERAL_PROPERTY: (key, valueFns = []) => {
|
|
2387
|
+
return `(function() {
|
|
2388
|
+
const objectLiterals = helpers.getDescendantsBy(
|
|
2389
|
+
args.current,
|
|
2390
|
+
(node) => node.kind === args.ts.SyntaxKind.ObjectLiteralExpression
|
|
2391
|
+
);
|
|
2392
|
+
|
|
2393
|
+
for (const objLiteral of objectLiterals) {
|
|
2394
|
+
if (args.ts.isObjectLiteralExpression(objLiteral)) {
|
|
2395
|
+
for (const property of objLiteral.properties) {
|
|
2396
|
+
if (args.ts.isPropertyAssignment(property)) {
|
|
2397
|
+
const propName = property.name;
|
|
2398
|
+
let propNameText = '';
|
|
2399
|
+
|
|
2400
|
+
if (args.ts.isIdentifier(propName)) {
|
|
2401
|
+
propNameText = propName.text;
|
|
2402
|
+
} else if (args.ts.isStringLiteral(propName)) {
|
|
2403
|
+
propNameText = propName.text;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
if (propNameText === '${key}') {
|
|
2407
|
+
const value = property.initializer?.getText() || null;
|
|
2408
|
+
return [${valueFns.join(", ")}].reduce((acc, valueFn) => valueFn(acc), value);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
return null;
|
|
2415
|
+
})();`;
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
export {
|
|
2419
|
+
NODE_PROPERTIES_PRESET,
|
|
2420
|
+
autoMatch,
|
|
2421
|
+
autoMatchFromGeneratedFiles,
|
|
2422
|
+
createProgramFromFileSystem,
|
|
2423
|
+
createProgramFromTsConfig,
|
|
2424
|
+
extractFromNodeId,
|
|
2425
|
+
filterResultsByGeneratedFiles,
|
|
2426
|
+
matchFromFileSystem,
|
|
2427
|
+
matchFromGeneratedFiles,
|
|
2428
|
+
resolveProgramFromConfig,
|
|
2429
|
+
rewriteUntilPath
|
|
2430
|
+
};
|