codeguardian-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +348 -0
- package/dist/agent/agentTools.d.ts +26 -0
- package/dist/agent/agentTools.d.ts.map +1 -0
- package/dist/agent/agentTools.js +699 -0
- package/dist/agent/agentTools.js.map +1 -0
- package/dist/agent/autoValidator.d.ts +110 -0
- package/dist/agent/autoValidator.d.ts.map +1 -0
- package/dist/agent/autoValidator.js +964 -0
- package/dist/agent/autoValidator.js.map +1 -0
- package/dist/agent/fileWatcher.d.ts +28 -0
- package/dist/agent/fileWatcher.d.ts.map +1 -0
- package/dist/agent/fileWatcher.js +88 -0
- package/dist/agent/fileWatcher.js.map +1 -0
- package/dist/agent/guardianPersistence.d.ts +98 -0
- package/dist/agent/guardianPersistence.d.ts.map +1 -0
- package/dist/agent/guardianPersistence.js +296 -0
- package/dist/agent/guardianPersistence.js.map +1 -0
- package/dist/agent/mcpNotifications.d.ts +38 -0
- package/dist/agent/mcpNotifications.d.ts.map +1 -0
- package/dist/agent/mcpNotifications.js +81 -0
- package/dist/agent/mcpNotifications.js.map +1 -0
- package/dist/analyzers/aiPatterns.d.ts +16 -0
- package/dist/analyzers/aiPatterns.d.ts.map +1 -0
- package/dist/analyzers/aiPatterns.js +103 -0
- package/dist/analyzers/aiPatterns.js.map +1 -0
- package/dist/analyzers/antiPatterns.d.ts +60 -0
- package/dist/analyzers/antiPatterns.d.ts.map +1 -0
- package/dist/analyzers/antiPatterns.js +198 -0
- package/dist/analyzers/antiPatterns.js.map +1 -0
- package/dist/analyzers/builtinTypes.d.ts +18 -0
- package/dist/analyzers/builtinTypes.d.ts.map +1 -0
- package/dist/analyzers/builtinTypes.js +1275 -0
- package/dist/analyzers/builtinTypes.js.map +1 -0
- package/dist/analyzers/complexity.d.ts +14 -0
- package/dist/analyzers/complexity.d.ts.map +1 -0
- package/dist/analyzers/complexity.js +610 -0
- package/dist/analyzers/complexity.js.map +1 -0
- package/dist/analyzers/findingVerifier.d.ts +59 -0
- package/dist/analyzers/findingVerifier.d.ts.map +1 -0
- package/dist/analyzers/findingVerifier.js +1169 -0
- package/dist/analyzers/findingVerifier.js.map +1 -0
- package/dist/analyzers/impactAnalyzer.d.ts +53 -0
- package/dist/analyzers/impactAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/impactAnalyzer.js +152 -0
- package/dist/analyzers/impactAnalyzer.js.map +1 -0
- package/dist/analyzers/languageDetector.d.ts +48 -0
- package/dist/analyzers/languageDetector.d.ts.map +1 -0
- package/dist/analyzers/languageDetector.js +404 -0
- package/dist/analyzers/languageDetector.js.map +1 -0
- package/dist/analyzers/parsers/incrementalParser.d.ts +53 -0
- package/dist/analyzers/parsers/incrementalParser.d.ts.map +1 -0
- package/dist/analyzers/parsers/incrementalParser.js +193 -0
- package/dist/analyzers/parsers/incrementalParser.js.map +1 -0
- package/dist/analyzers/parsers/scopeResolver.d.ts +92 -0
- package/dist/analyzers/parsers/scopeResolver.d.ts.map +1 -0
- package/dist/analyzers/parsers/scopeResolver.js +324 -0
- package/dist/analyzers/parsers/scopeResolver.js.map +1 -0
- package/dist/analyzers/parsers/semanticIndex.d.ts +127 -0
- package/dist/analyzers/parsers/semanticIndex.d.ts.map +1 -0
- package/dist/analyzers/parsers/semanticIndex.js +429 -0
- package/dist/analyzers/parsers/semanticIndex.js.map +1 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.d.ts +42 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.js +233 -0
- package/dist/analyzers/parsers/sessionDiffAnalyzer.js.map +1 -0
- package/dist/analyzers/parsers/treeSitterParser.d.ts +76 -0
- package/dist/analyzers/parsers/treeSitterParser.d.ts.map +1 -0
- package/dist/analyzers/parsers/treeSitterParser.js +709 -0
- package/dist/analyzers/parsers/treeSitterParser.js.map +1 -0
- package/dist/analyzers/relevanceScorer.d.ts +43 -0
- package/dist/analyzers/relevanceScorer.d.ts.map +1 -0
- package/dist/analyzers/relevanceScorer.js +200 -0
- package/dist/analyzers/relevanceScorer.js.map +1 -0
- package/dist/analyzers/standardLibrary.d.ts +22 -0
- package/dist/analyzers/standardLibrary.d.ts.map +1 -0
- package/dist/analyzers/standardLibrary.js +211 -0
- package/dist/analyzers/standardLibrary.js.map +1 -0
- package/dist/analyzers/symbolGraph.d.ts +30 -0
- package/dist/analyzers/symbolGraph.d.ts.map +1 -0
- package/dist/analyzers/symbolGraph.js +380 -0
- package/dist/analyzers/symbolGraph.js.map +1 -0
- package/dist/analyzers/symbolTable.d.ts +18 -0
- package/dist/analyzers/symbolTable.d.ts.map +1 -0
- package/dist/analyzers/symbolTable.js +176 -0
- package/dist/analyzers/symbolTable.js.map +1 -0
- package/dist/analyzers/typeChecker.d.ts +13 -0
- package/dist/analyzers/typeChecker.d.ts.map +1 -0
- package/dist/analyzers/typeChecker.js +580 -0
- package/dist/analyzers/typeChecker.js.map +1 -0
- package/dist/analyzers/usagePatterns.d.ts +42 -0
- package/dist/analyzers/usagePatterns.d.ts.map +1 -0
- package/dist/analyzers/usagePatterns.js +75 -0
- package/dist/analyzers/usagePatterns.js.map +1 -0
- package/dist/api-contract/context/backend.d.ts +19 -0
- package/dist/api-contract/context/backend.d.ts.map +1 -0
- package/dist/api-contract/context/backend.js +64 -0
- package/dist/api-contract/context/backend.js.map +1 -0
- package/dist/api-contract/context/contract.d.ts +34 -0
- package/dist/api-contract/context/contract.d.ts.map +1 -0
- package/dist/api-contract/context/contract.js +306 -0
- package/dist/api-contract/context/contract.js.map +1 -0
- package/dist/api-contract/context/frontend.d.ts +19 -0
- package/dist/api-contract/context/frontend.d.ts.map +1 -0
- package/dist/api-contract/context/frontend.js +64 -0
- package/dist/api-contract/context/frontend.js.map +1 -0
- package/dist/api-contract/detector.d.ts +28 -0
- package/dist/api-contract/detector.d.ts.map +1 -0
- package/dist/api-contract/detector.js +393 -0
- package/dist/api-contract/detector.js.map +1 -0
- package/dist/api-contract/extractors/python.d.ts +32 -0
- package/dist/api-contract/extractors/python.d.ts.map +1 -0
- package/dist/api-contract/extractors/python.js +521 -0
- package/dist/api-contract/extractors/python.js.map +1 -0
- package/dist/api-contract/extractors/pythonAstUtils.d.ts +44 -0
- package/dist/api-contract/extractors/pythonAstUtils.d.ts.map +1 -0
- package/dist/api-contract/extractors/pythonAstUtils.js +489 -0
- package/dist/api-contract/extractors/pythonAstUtils.js.map +1 -0
- package/dist/api-contract/extractors/tsAstUtils.d.ts +47 -0
- package/dist/api-contract/extractors/tsAstUtils.d.ts.map +1 -0
- package/dist/api-contract/extractors/tsAstUtils.js +173 -0
- package/dist/api-contract/extractors/tsAstUtils.js.map +1 -0
- package/dist/api-contract/extractors/typescript.d.ts +32 -0
- package/dist/api-contract/extractors/typescript.d.ts.map +1 -0
- package/dist/api-contract/extractors/typescript.js +666 -0
- package/dist/api-contract/extractors/typescript.js.map +1 -0
- package/dist/api-contract/index.d.ts +104 -0
- package/dist/api-contract/index.d.ts.map +1 -0
- package/dist/api-contract/index.js +232 -0
- package/dist/api-contract/index.js.map +1 -0
- package/dist/api-contract/types.d.ts +151 -0
- package/dist/api-contract/types.d.ts.map +1 -0
- package/dist/api-contract/types.js +19 -0
- package/dist/api-contract/types.js.map +1 -0
- package/dist/api-contract/validators/endpoint.d.ts +21 -0
- package/dist/api-contract/validators/endpoint.d.ts.map +1 -0
- package/dist/api-contract/validators/endpoint.js +224 -0
- package/dist/api-contract/validators/endpoint.js.map +1 -0
- package/dist/api-contract/validators/index.d.ts +40 -0
- package/dist/api-contract/validators/index.d.ts.map +1 -0
- package/dist/api-contract/validators/index.js +875 -0
- package/dist/api-contract/validators/index.js.map +1 -0
- package/dist/api-contract/validators/parameter.d.ts +17 -0
- package/dist/api-contract/validators/parameter.d.ts.map +1 -0
- package/dist/api-contract/validators/parameter.js +250 -0
- package/dist/api-contract/validators/parameter.js.map +1 -0
- package/dist/api-contract/validators/type.d.ts +38 -0
- package/dist/api-contract/validators/type.d.ts.map +1 -0
- package/dist/api-contract/validators/type.js +244 -0
- package/dist/api-contract/validators/type.js.map +1 -0
- package/dist/context/apiContract/complexTypeSupport.d.ts +83 -0
- package/dist/context/apiContract/complexTypeSupport.d.ts.map +1 -0
- package/dist/context/apiContract/complexTypeSupport.js +665 -0
- package/dist/context/apiContract/complexTypeSupport.js.map +1 -0
- package/dist/context/apiContract/graphqlSupport.d.ts +105 -0
- package/dist/context/apiContract/graphqlSupport.d.ts.map +1 -0
- package/dist/context/apiContract/graphqlSupport.js +671 -0
- package/dist/context/apiContract/graphqlSupport.js.map +1 -0
- package/dist/context/apiContract/index.d.ts +14 -0
- package/dist/context/apiContract/index.d.ts.map +1 -0
- package/dist/context/apiContract/index.js +17 -0
- package/dist/context/apiContract/index.js.map +1 -0
- package/dist/context/apiContract/webSocketSupport.d.ts +104 -0
- package/dist/context/apiContract/webSocketSupport.d.ts.map +1 -0
- package/dist/context/apiContract/webSocketSupport.js +465 -0
- package/dist/context/apiContract/webSocketSupport.js.map +1 -0
- package/dist/context/apiContractContext.d.ts +15 -0
- package/dist/context/apiContractContext.d.ts.map +1 -0
- package/dist/context/apiContractContext.js +979 -0
- package/dist/context/apiContractContext.js.map +1 -0
- package/dist/context/apiContractExtraction.d.ts +52 -0
- package/dist/context/apiContractExtraction.d.ts.map +1 -0
- package/dist/context/apiContractExtraction.js +438 -0
- package/dist/context/apiContractExtraction.js.map +1 -0
- package/dist/context/contextLineage.d.ts +79 -0
- package/dist/context/contextLineage.d.ts.map +1 -0
- package/dist/context/contextLineage.js +259 -0
- package/dist/context/contextLineage.js.map +1 -0
- package/dist/context/contextOrchestrator.d.ts +57 -0
- package/dist/context/contextOrchestrator.d.ts.map +1 -0
- package/dist/context/contextOrchestrator.js +162 -0
- package/dist/context/contextOrchestrator.js.map +1 -0
- package/dist/context/intentTracker.d.ts +73 -0
- package/dist/context/intentTracker.d.ts.map +1 -0
- package/dist/context/intentTracker.js +168 -0
- package/dist/context/intentTracker.js.map +1 -0
- package/dist/context/projectContext.d.ts +219 -0
- package/dist/context/projectContext.d.ts.map +1 -0
- package/dist/context/projectContext.js +1984 -0
- package/dist/context/projectContext.js.map +1 -0
- package/dist/prompts/index.d.ts +17 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +260 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/library.d.ts +51 -0
- package/dist/prompts/library.d.ts.map +1 -0
- package/dist/prompts/library.js +65 -0
- package/dist/prompts/library.js.map +1 -0
- package/dist/prompts/templates.d.ts +44 -0
- package/dist/prompts/templates.d.ts.map +1 -0
- package/dist/prompts/templates.js +97 -0
- package/dist/prompts/templates.js.map +1 -0
- package/dist/queue/jobPersistence.d.ts +46 -0
- package/dist/queue/jobPersistence.d.ts.map +1 -0
- package/dist/queue/jobPersistence.js +158 -0
- package/dist/queue/jobPersistence.js.map +1 -0
- package/dist/queue/jobQueue.d.ts +116 -0
- package/dist/queue/jobQueue.d.ts.map +1 -0
- package/dist/queue/jobQueue.js +275 -0
- package/dist/queue/jobQueue.js.map +1 -0
- package/dist/queue/validationJob.d.ts +69 -0
- package/dist/queue/validationJob.d.ts.map +1 -0
- package/dist/queue/validationJob.js +435 -0
- package/dist/queue/validationJob.js.map +1 -0
- package/dist/resources/index.d.ts +15 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +328 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/validationReportStore.d.ts +170 -0
- package/dist/resources/validationReportStore.d.ts.map +1 -0
- package/dist/resources/validationReportStore.js +515 -0
- package/dist/resources/validationReportStore.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +102 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/asyncValidation.d.ts +19 -0
- package/dist/tools/asyncValidation.d.ts.map +1 -0
- package/dist/tools/asyncValidation.js +346 -0
- package/dist/tools/asyncValidation.js.map +1 -0
- package/dist/tools/buildContext.d.ts +17 -0
- package/dist/tools/buildContext.d.ts.map +1 -0
- package/dist/tools/buildContext.js +188 -0
- package/dist/tools/buildContext.js.map +1 -0
- package/dist/tools/getDependencyGraph.d.ts +16 -0
- package/dist/tools/getDependencyGraph.d.ts.map +1 -0
- package/dist/tools/getDependencyGraph.js +436 -0
- package/dist/tools/getDependencyGraph.js.map +1 -0
- package/dist/tools/incrementalValidation.d.ts +71 -0
- package/dist/tools/incrementalValidation.d.ts.map +1 -0
- package/dist/tools/incrementalValidation.js +203 -0
- package/dist/tools/incrementalValidation.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +106 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/validateCode.d.ts +17 -0
- package/dist/tools/validateCode.d.ts.map +1 -0
- package/dist/tools/validateCode.js +368 -0
- package/dist/tools/validateCode.js.map +1 -0
- package/dist/tools/validateCodeLite.d.ts +2 -0
- package/dist/tools/validateCodeLite.d.ts.map +1 -0
- package/dist/tools/validateCodeLite.js +2 -0
- package/dist/tools/validateCodeLite.js.map +1 -0
- package/dist/tools/validation/builtins.d.ts +92 -0
- package/dist/tools/validation/builtins.d.ts.map +1 -0
- package/dist/tools/validation/builtins.js +2184 -0
- package/dist/tools/validation/builtins.js.map +1 -0
- package/dist/tools/validation/contextualNaming.d.ts +99 -0
- package/dist/tools/validation/contextualNaming.d.ts.map +1 -0
- package/dist/tools/validation/contextualNaming.js +959 -0
- package/dist/tools/validation/contextualNaming.js.map +1 -0
- package/dist/tools/validation/deadCode.d.ts +115 -0
- package/dist/tools/validation/deadCode.d.ts.map +1 -0
- package/dist/tools/validation/deadCode.js +861 -0
- package/dist/tools/validation/deadCode.js.map +1 -0
- package/dist/tools/validation/extractors/index.d.ts +131 -0
- package/dist/tools/validation/extractors/index.d.ts.map +1 -0
- package/dist/tools/validation/extractors/index.js +233 -0
- package/dist/tools/validation/extractors/index.js.map +1 -0
- package/dist/tools/validation/extractors/javascript.d.ts +73 -0
- package/dist/tools/validation/extractors/javascript.d.ts.map +1 -0
- package/dist/tools/validation/extractors/javascript.js +1841 -0
- package/dist/tools/validation/extractors/javascript.js.map +1 -0
- package/dist/tools/validation/extractors/python.d.ts +93 -0
- package/dist/tools/validation/extractors/python.d.ts.map +1 -0
- package/dist/tools/validation/extractors/python.js +799 -0
- package/dist/tools/validation/extractors/python.js.map +1 -0
- package/dist/tools/validation/manifest.d.ts +45 -0
- package/dist/tools/validation/manifest.d.ts.map +1 -0
- package/dist/tools/validation/manifest.js +719 -0
- package/dist/tools/validation/manifest.js.map +1 -0
- package/dist/tools/validation/parser.d.ts +58 -0
- package/dist/tools/validation/parser.d.ts.map +1 -0
- package/dist/tools/validation/parser.js +232 -0
- package/dist/tools/validation/parser.js.map +1 -0
- package/dist/tools/validation/registry.d.ts +15 -0
- package/dist/tools/validation/registry.d.ts.map +1 -0
- package/dist/tools/validation/registry.js +169 -0
- package/dist/tools/validation/registry.js.map +1 -0
- package/dist/tools/validation/scoring.d.ts +54 -0
- package/dist/tools/validation/scoring.d.ts.map +1 -0
- package/dist/tools/validation/scoring.js +242 -0
- package/dist/tools/validation/scoring.js.map +1 -0
- package/dist/tools/validation/types.d.ts +120 -0
- package/dist/tools/validation/types.d.ts.map +1 -0
- package/dist/tools/validation/types.js +11 -0
- package/dist/tools/validation/types.js.map +1 -0
- package/dist/tools/validation/unusedLocals.d.ts +36 -0
- package/dist/tools/validation/unusedLocals.d.ts.map +1 -0
- package/dist/tools/validation/unusedLocals.js +333 -0
- package/dist/tools/validation/unusedLocals.js.map +1 -0
- package/dist/tools/validation/validation.d.ts +98 -0
- package/dist/tools/validation/validation.d.ts.map +1 -0
- package/dist/tools/validation/validation.js +1837 -0
- package/dist/tools/validation/validation.js.map +1 -0
- package/dist/types/codeGraph.d.ts +163 -0
- package/dist/types/codeGraph.d.ts.map +1 -0
- package/dist/types/codeGraph.js +9 -0
- package/dist/types/codeGraph.js.map +1 -0
- package/dist/types/symbolGraph.d.ts +68 -0
- package/dist/types/symbolGraph.d.ts.map +1 -0
- package/dist/types/symbolGraph.js +10 -0
- package/dist/types/symbolGraph.js.map +1 -0
- package/dist/types/tools.d.ts +43 -0
- package/dist/types/tools.d.ts.map +1 -0
- package/dist/types/tools.js +7 -0
- package/dist/types/tools.js.map +1 -0
- package/dist/utils/fileFilter.d.ts +37 -0
- package/dist/utils/fileFilter.d.ts.map +1 -0
- package/dist/utils/fileFilter.js +91 -0
- package/dist/utils/fileFilter.js.map +1 -0
- package/dist/utils/gitUtils.d.ts +28 -0
- package/dist/utils/gitUtils.d.ts.map +1 -0
- package/dist/utils/gitUtils.js +81 -0
- package/dist/utils/gitUtils.js.map +1 -0
- package/dist/utils/logger.d.ts +15 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +38 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/serialization.d.ts +25 -0
- package/dist/utils/serialization.d.ts.map +1 -0
- package/dist/utils/serialization.js +53 -0
- package/dist/utils/serialization.js.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Contract Guardian - GraphQL Support
|
|
3
|
+
*
|
|
4
|
+
* Extracts and validates GraphQL schemas, queries, and mutations.
|
|
5
|
+
* Detects mismatches between frontend GraphQL operations and backend schema.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "fs/promises";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import { glob } from "glob";
|
|
12
|
+
import { logger } from "../../utils/logger.js";
|
|
13
|
+
import { getParser } from "../../tools/validation/parser.js";
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Schema Extraction (Backend)
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Extract GraphQL schema from .graphql files or schema definitions
|
|
19
|
+
*/
|
|
20
|
+
export async function extractGraphQLSchema(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
23
|
+
// Check if it's a GraphQL schema file
|
|
24
|
+
if (filePath.endsWith(".graphql") || filePath.endsWith(".gql")) {
|
|
25
|
+
return parseGraphQLSchema(content, filePath);
|
|
26
|
+
}
|
|
27
|
+
// Check for GraphQL schema in TypeScript/JavaScript files
|
|
28
|
+
if (filePath.endsWith(".ts") || filePath.endsWith(".js") || filePath.endsWith(".tsx") || filePath.endsWith(".jsx")) {
|
|
29
|
+
return extractSchemaFromCode(content, filePath);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
logger.debug(`Failed to extract GraphQL schema from ${filePath}: ${err}`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function parseGraphQLSchema(content, filePath) {
|
|
39
|
+
const schema = {
|
|
40
|
+
types: [],
|
|
41
|
+
queries: [],
|
|
42
|
+
mutations: [],
|
|
43
|
+
subscriptions: [],
|
|
44
|
+
file: filePath,
|
|
45
|
+
line: 1,
|
|
46
|
+
};
|
|
47
|
+
const lines = content.split("\n");
|
|
48
|
+
let currentType = null;
|
|
49
|
+
let lineNum = 0;
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
lineNum = i + 1;
|
|
52
|
+
const line = lines[i].trim();
|
|
53
|
+
// Skip comments and empty lines
|
|
54
|
+
if (!line || line.startsWith("#"))
|
|
55
|
+
continue;
|
|
56
|
+
// Type definition: type User { ... }
|
|
57
|
+
const typeMatch = line.match(/^type\s+(\w+)\s*\{/);
|
|
58
|
+
if (typeMatch) {
|
|
59
|
+
const typeName = typeMatch[1];
|
|
60
|
+
// Handle Query, Mutation, Subscription specially
|
|
61
|
+
if (typeName === "Query" || typeName === "Mutation" || typeName === "Subscription") {
|
|
62
|
+
if (currentType) {
|
|
63
|
+
schema.types.push(currentType);
|
|
64
|
+
currentType = null;
|
|
65
|
+
}
|
|
66
|
+
// Parse the entire Query/Mutation/Subscription block
|
|
67
|
+
const { operations, endLine } = parseQueryTypeBlock(content, i, typeName.toLowerCase());
|
|
68
|
+
if (typeName === "Query") {
|
|
69
|
+
schema.queries.push(...operations);
|
|
70
|
+
}
|
|
71
|
+
else if (typeName === "Mutation") {
|
|
72
|
+
schema.mutations.push(...operations);
|
|
73
|
+
}
|
|
74
|
+
else if (typeName === "Subscription") {
|
|
75
|
+
schema.subscriptions.push(...operations);
|
|
76
|
+
}
|
|
77
|
+
// Skip to the end of this type block
|
|
78
|
+
i = endLine;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (currentType) {
|
|
82
|
+
schema.types.push(currentType);
|
|
83
|
+
}
|
|
84
|
+
currentType = {
|
|
85
|
+
name: typeName,
|
|
86
|
+
kind: "object",
|
|
87
|
+
fields: [],
|
|
88
|
+
file: filePath,
|
|
89
|
+
line: lineNum,
|
|
90
|
+
};
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Input type: input CreateUserInput { ... }
|
|
94
|
+
const inputMatch = line.match(/^input\s+(\w+)\s*\{/);
|
|
95
|
+
if (inputMatch) {
|
|
96
|
+
if (currentType) {
|
|
97
|
+
schema.types.push(currentType);
|
|
98
|
+
}
|
|
99
|
+
currentType = {
|
|
100
|
+
name: inputMatch[1],
|
|
101
|
+
kind: "input",
|
|
102
|
+
fields: [],
|
|
103
|
+
file: filePath,
|
|
104
|
+
line: lineNum,
|
|
105
|
+
};
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
// Enum type: enum Status { ... }
|
|
109
|
+
const enumMatch = line.match(/^enum\s+(\w+)\s*\{/);
|
|
110
|
+
if (enumMatch) {
|
|
111
|
+
if (currentType) {
|
|
112
|
+
schema.types.push(currentType);
|
|
113
|
+
}
|
|
114
|
+
currentType = {
|
|
115
|
+
name: enumMatch[1],
|
|
116
|
+
kind: "enum",
|
|
117
|
+
fields: [],
|
|
118
|
+
file: filePath,
|
|
119
|
+
line: lineNum,
|
|
120
|
+
};
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Interface type: interface Node { ... }
|
|
124
|
+
const interfaceMatch = line.match(/^interface\s+(\w+)\s*\{/);
|
|
125
|
+
if (interfaceMatch) {
|
|
126
|
+
if (currentType) {
|
|
127
|
+
schema.types.push(currentType);
|
|
128
|
+
}
|
|
129
|
+
currentType = {
|
|
130
|
+
name: interfaceMatch[1],
|
|
131
|
+
kind: "interface",
|
|
132
|
+
fields: [],
|
|
133
|
+
file: filePath,
|
|
134
|
+
line: lineNum,
|
|
135
|
+
};
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Type closing
|
|
139
|
+
if (line === "}" && currentType) {
|
|
140
|
+
schema.types.push(currentType);
|
|
141
|
+
currentType = null;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
// Field definition within type
|
|
145
|
+
if (currentType && line.includes(":")) {
|
|
146
|
+
const field = parseGraphQLField(line);
|
|
147
|
+
if (field) {
|
|
148
|
+
currentType.fields.push(field);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Don't forget the last type
|
|
153
|
+
if (currentType) {
|
|
154
|
+
schema.types.push(currentType);
|
|
155
|
+
}
|
|
156
|
+
return schema;
|
|
157
|
+
}
|
|
158
|
+
function parseGraphQLField(line) {
|
|
159
|
+
// Parse: fieldName: Type! or fieldName(arg: Type!): ReturnType!
|
|
160
|
+
const fieldMatch = line.match(/^(\w+)(?:\(([^)]+)\))?\s*:\s*(.+)$/);
|
|
161
|
+
if (!fieldMatch)
|
|
162
|
+
return null;
|
|
163
|
+
const name = fieldMatch[1];
|
|
164
|
+
const argsString = fieldMatch[2];
|
|
165
|
+
const typeString = fieldMatch[3].trim();
|
|
166
|
+
const { type, required } = parseGraphQLType(typeString);
|
|
167
|
+
const field = {
|
|
168
|
+
name,
|
|
169
|
+
type,
|
|
170
|
+
required,
|
|
171
|
+
};
|
|
172
|
+
if (argsString) {
|
|
173
|
+
field.arguments = parseGraphQLArguments(argsString);
|
|
174
|
+
}
|
|
175
|
+
return field;
|
|
176
|
+
}
|
|
177
|
+
function parseGraphQLType(typeString) {
|
|
178
|
+
const required = typeString.endsWith("!");
|
|
179
|
+
const type = typeString.replace(/!$/, "").trim();
|
|
180
|
+
return { type, required };
|
|
181
|
+
}
|
|
182
|
+
function parseGraphQLArguments(argsString) {
|
|
183
|
+
const args = [];
|
|
184
|
+
const argMatches = argsString.matchAll(/(\w+)\s*:\s*([^,]+)/g);
|
|
185
|
+
for (const match of argMatches) {
|
|
186
|
+
const name = match[1];
|
|
187
|
+
const typeStr = match[2].trim();
|
|
188
|
+
const { type, required } = parseGraphQLType(typeStr);
|
|
189
|
+
args.push({
|
|
190
|
+
name,
|
|
191
|
+
type,
|
|
192
|
+
required,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return args;
|
|
196
|
+
}
|
|
197
|
+
function parseQueryTypeBlock(content, startLine, type) {
|
|
198
|
+
const operations = [];
|
|
199
|
+
const lines = content.split("\n");
|
|
200
|
+
let braceCount = 1; // We start inside the type definition (the opening { on startLine)
|
|
201
|
+
let i = startLine + 1; // Start from the line AFTER the type declaration
|
|
202
|
+
while (i < lines.length && braceCount > 0) {
|
|
203
|
+
const line = lines[i];
|
|
204
|
+
const trimmedLine = line.trim();
|
|
205
|
+
// Count braces more carefully
|
|
206
|
+
for (const char of line) {
|
|
207
|
+
if (char === "{")
|
|
208
|
+
braceCount++;
|
|
209
|
+
if (char === "}")
|
|
210
|
+
braceCount--;
|
|
211
|
+
}
|
|
212
|
+
// If we've closed the type, stop
|
|
213
|
+
if (braceCount <= 0)
|
|
214
|
+
break;
|
|
215
|
+
// Parse operation field - must be at braceCount == 1 (inside the type but not nested)
|
|
216
|
+
// and the line should contain a field definition
|
|
217
|
+
if (braceCount === 1 && trimmedLine.includes(":")) {
|
|
218
|
+
// Match field name, optional args, colon, and return type (including arrays and non-null)
|
|
219
|
+
const opMatch = trimmedLine.match(/^(\w+)(?:\(([^)]+)\))?\s*:\s*([\w\[\]!]+)/);
|
|
220
|
+
if (opMatch) {
|
|
221
|
+
const name = opMatch[1];
|
|
222
|
+
const argsString = opMatch[2];
|
|
223
|
+
const returnType = opMatch[3];
|
|
224
|
+
operations.push({
|
|
225
|
+
name,
|
|
226
|
+
type,
|
|
227
|
+
returnType,
|
|
228
|
+
arguments: argsString ? parseGraphQLArguments(argsString) : [],
|
|
229
|
+
file: "schema.graphql",
|
|
230
|
+
line: i + 1,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
i++;
|
|
235
|
+
}
|
|
236
|
+
return { operations, endLine: i };
|
|
237
|
+
}
|
|
238
|
+
function extractSchemaFromCode(content, filePath) {
|
|
239
|
+
// Look for schema definitions in code (e.g., gql`...` or buildSchema(`...`))
|
|
240
|
+
const gqlMatch = content.match(/gql`([\s\S]*?)`/);
|
|
241
|
+
const buildSchemaMatch = content.match(/buildSchema\(`([\s\S]*?)`\)/);
|
|
242
|
+
const schemaMatch = content.match(/typeDefs\s*=\s*`([\s\S]*?)`/);
|
|
243
|
+
const schemaContent = gqlMatch?.[1] || buildSchemaMatch?.[1] || schemaMatch?.[1];
|
|
244
|
+
if (schemaContent) {
|
|
245
|
+
return parseGraphQLSchema(schemaContent, filePath);
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Frontend Operation Extraction
|
|
251
|
+
// ============================================================================
|
|
252
|
+
/**
|
|
253
|
+
* Extract GraphQL operations from frontend code
|
|
254
|
+
*/
|
|
255
|
+
export async function extractGraphQLOperations(filePath) {
|
|
256
|
+
const operations = [];
|
|
257
|
+
try {
|
|
258
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
259
|
+
// Detect framework
|
|
260
|
+
const framework = detectGraphQLFramework(content);
|
|
261
|
+
// Extract operations based on framework patterns
|
|
262
|
+
if (framework === "apollo") {
|
|
263
|
+
operations.push(...extractApolloOperations(content, filePath));
|
|
264
|
+
}
|
|
265
|
+
else if (framework === "relay") {
|
|
266
|
+
operations.push(...extractRelayOperations(content, filePath));
|
|
267
|
+
}
|
|
268
|
+
else if (framework === "urql") {
|
|
269
|
+
operations.push(...extractUrqlOperations(content, filePath));
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
// Generic extraction
|
|
273
|
+
operations.push(...extractGenericOperations(content, filePath));
|
|
274
|
+
}
|
|
275
|
+
// Set framework on all operations
|
|
276
|
+
operations.forEach(op => op.framework = framework);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
logger.debug(`Failed to extract GraphQL operations from ${filePath}: ${err}`);
|
|
280
|
+
}
|
|
281
|
+
return operations;
|
|
282
|
+
}
|
|
283
|
+
function detectGraphQLFramework(content) {
|
|
284
|
+
if (content.includes("@apollo/client") || content.includes("useQuery") || content.includes("useMutation")) {
|
|
285
|
+
return "apollo";
|
|
286
|
+
}
|
|
287
|
+
if (content.includes("react-relay") || content.includes("graphql") && content.includes("@argumentDefinitions")) {
|
|
288
|
+
return "relay";
|
|
289
|
+
}
|
|
290
|
+
if (content.includes("urql") || content.includes("useQuery") && content.includes("urql")) {
|
|
291
|
+
return "urql";
|
|
292
|
+
}
|
|
293
|
+
if (content.includes("graphql-request")) {
|
|
294
|
+
return "graphql-request";
|
|
295
|
+
}
|
|
296
|
+
return "raw";
|
|
297
|
+
}
|
|
298
|
+
function extractApolloOperations(content, filePath) {
|
|
299
|
+
const operations = [];
|
|
300
|
+
try {
|
|
301
|
+
const parser = getParser("typescript");
|
|
302
|
+
const tree = parser.parse(content);
|
|
303
|
+
// Traverse AST to find gql`...` (Tree-sitter TSX parses this as a call_expression
|
|
304
|
+
// with a template_string argument) and, in some grammars, tagged_template_expression.
|
|
305
|
+
function traverse(node) {
|
|
306
|
+
if (!node)
|
|
307
|
+
return;
|
|
308
|
+
const addOperationFromTemplate = (templateNode) => {
|
|
309
|
+
if (!templateNode)
|
|
310
|
+
return;
|
|
311
|
+
const raw = content.slice(templateNode.startIndex, templateNode.endIndex);
|
|
312
|
+
const graphqlString = raw.replace(/^`/, "").replace(/`$/, "").trim();
|
|
313
|
+
const line = node.startPosition.row + 1;
|
|
314
|
+
const operation = parseGraphQLOperationString(graphqlString, filePath, line);
|
|
315
|
+
if (operation)
|
|
316
|
+
operations.push(operation);
|
|
317
|
+
};
|
|
318
|
+
// TSX grammar: gql`...` => call_expression(function: identifier, arguments: template_string)
|
|
319
|
+
if (node.type === "call_expression") {
|
|
320
|
+
const functionNode = node.childForFieldName("function");
|
|
321
|
+
const argsNode = node.childForFieldName("arguments");
|
|
322
|
+
if (functionNode && argsNode) {
|
|
323
|
+
const funcText = content.slice(functionNode.startIndex, functionNode.endIndex).trim();
|
|
324
|
+
if (funcText === "gql") {
|
|
325
|
+
const templateNode = argsNode.type === "template_string"
|
|
326
|
+
? argsNode
|
|
327
|
+
: (argsNode.children || []).find((c) => c?.type === "template_string");
|
|
328
|
+
addOperationFromTemplate(templateNode);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Other grammars: tagged_template_expression(tag: gql, template: template_string)
|
|
333
|
+
if (node.type === "tagged_template_expression") {
|
|
334
|
+
const tagNode = node.childForFieldName("tag");
|
|
335
|
+
const templateNode = node.childForFieldName("template");
|
|
336
|
+
if (tagNode && templateNode) {
|
|
337
|
+
const tagName = content.slice(tagNode.startIndex, tagNode.endIndex).trim();
|
|
338
|
+
if (tagName === "gql") {
|
|
339
|
+
addOperationFromTemplate(templateNode);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Recursively traverse children
|
|
344
|
+
for (const child of node.children || []) {
|
|
345
|
+
traverse(child);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
traverse(tree.rootNode);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
logger.debug(`AST parsing failed for ${filePath}: ${err}`);
|
|
352
|
+
}
|
|
353
|
+
return operations;
|
|
354
|
+
}
|
|
355
|
+
function extractRelayOperations(content, filePath) {
|
|
356
|
+
// Similar to Apollo but with Relay-specific syntax
|
|
357
|
+
return extractApolloOperations(content, filePath);
|
|
358
|
+
}
|
|
359
|
+
function extractUrqlOperations(content, filePath) {
|
|
360
|
+
// Similar pattern
|
|
361
|
+
return extractApolloOperations(content, filePath);
|
|
362
|
+
}
|
|
363
|
+
function extractGenericOperations(content, filePath) {
|
|
364
|
+
// Look for any GraphQL-like strings
|
|
365
|
+
const operations = [];
|
|
366
|
+
// Match query/mutation/subscription strings
|
|
367
|
+
const queryRegex = /(?:query|mutation|subscription)\s+(\w+)/g;
|
|
368
|
+
let match;
|
|
369
|
+
while ((match = queryRegex.exec(content)) !== null) {
|
|
370
|
+
const operationName = match[1];
|
|
371
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
372
|
+
// Try to extract the full operation
|
|
373
|
+
const startIdx = match.index;
|
|
374
|
+
let endIdx = startIdx;
|
|
375
|
+
let braceCount = 0;
|
|
376
|
+
let inString = false;
|
|
377
|
+
for (let i = startIdx; i < content.length; i++) {
|
|
378
|
+
const char = content[i];
|
|
379
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
380
|
+
inString = !inString;
|
|
381
|
+
}
|
|
382
|
+
if (!inString) {
|
|
383
|
+
if (char === "{")
|
|
384
|
+
braceCount++;
|
|
385
|
+
if (char === "}")
|
|
386
|
+
braceCount--;
|
|
387
|
+
if (braceCount === 0 && char === "}") {
|
|
388
|
+
endIdx = i + 1;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const operationString = content.substring(startIdx, endIdx);
|
|
394
|
+
const operation = parseGraphQLOperationString(operationString, filePath, line);
|
|
395
|
+
if (operation) {
|
|
396
|
+
operations.push(operation);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return operations;
|
|
400
|
+
}
|
|
401
|
+
function parseGraphQLOperationString(operationString, filePath, line) {
|
|
402
|
+
const trimmed = operationString.trim();
|
|
403
|
+
if (!trimmed)
|
|
404
|
+
return null;
|
|
405
|
+
// Find the first operation keyword (allow leading whitespace/comments/newlines)
|
|
406
|
+
const opMatch = /\b(query|mutation|subscription)\b\s*(\w+)?/i.exec(trimmed);
|
|
407
|
+
if (!opMatch) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const type = opMatch[1].toLowerCase();
|
|
411
|
+
const name = opMatch[2] || "anonymous";
|
|
412
|
+
const headerEnd = trimmed.indexOf("{", opMatch.index);
|
|
413
|
+
const header = headerEnd >= 0 ? trimmed.slice(opMatch.index, headerEnd) : trimmed.slice(opMatch.index);
|
|
414
|
+
// Extract variables
|
|
415
|
+
const variables = [];
|
|
416
|
+
const varMatch = header.match(/\(([^)]+)\)/);
|
|
417
|
+
if (varMatch) {
|
|
418
|
+
const varDefs = varMatch[1].split(",");
|
|
419
|
+
for (const varDef of varDefs) {
|
|
420
|
+
const parts = varDef.trim().match(/\$(\w+)\s*:\s*(\w+!?)/);
|
|
421
|
+
if (parts) {
|
|
422
|
+
variables.push({
|
|
423
|
+
name: parts[1],
|
|
424
|
+
type: parts[2].replace("!", ""),
|
|
425
|
+
required: parts[2].endsWith("!"),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Extract selections (fields being requested)
|
|
431
|
+
const selections = parseSelections(trimmed);
|
|
432
|
+
return {
|
|
433
|
+
name,
|
|
434
|
+
type,
|
|
435
|
+
operationString,
|
|
436
|
+
variables,
|
|
437
|
+
selections,
|
|
438
|
+
file: filePath,
|
|
439
|
+
line,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function parseSelections(operationString) {
|
|
443
|
+
const selections = [];
|
|
444
|
+
// Extract content between outer braces
|
|
445
|
+
const braceMatch = operationString.match(/\{([\s\S]*)\}$/);
|
|
446
|
+
if (!braceMatch)
|
|
447
|
+
return selections;
|
|
448
|
+
const content = braceMatch[1];
|
|
449
|
+
const lines = content.split("\n");
|
|
450
|
+
let currentSelection = null;
|
|
451
|
+
let braceCount = 0;
|
|
452
|
+
for (const line of lines) {
|
|
453
|
+
const trimmed = line.trim();
|
|
454
|
+
if (!trimmed)
|
|
455
|
+
continue;
|
|
456
|
+
if (trimmed.includes("{")) {
|
|
457
|
+
braceCount++;
|
|
458
|
+
if (currentSelection) {
|
|
459
|
+
// This is a nested selection
|
|
460
|
+
if (!currentSelection.subSelections) {
|
|
461
|
+
currentSelection.subSelections = [];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (trimmed.includes("}")) {
|
|
466
|
+
braceCount--;
|
|
467
|
+
if (braceCount === 0 && currentSelection) {
|
|
468
|
+
selections.push(currentSelection);
|
|
469
|
+
currentSelection = null;
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
// Parse field with optional alias: fieldName or alias: fieldName
|
|
474
|
+
const fieldMatch = trimmed.match(/^(?:(\w+)\s*:\s*)?(\w+)/);
|
|
475
|
+
if (fieldMatch && braceCount === 0) {
|
|
476
|
+
const alias = fieldMatch[1];
|
|
477
|
+
const name = fieldMatch[2];
|
|
478
|
+
if (currentSelection) {
|
|
479
|
+
selections.push(currentSelection);
|
|
480
|
+
}
|
|
481
|
+
currentSelection = {
|
|
482
|
+
name,
|
|
483
|
+
...(alias && { alias }),
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (currentSelection) {
|
|
488
|
+
selections.push(currentSelection);
|
|
489
|
+
}
|
|
490
|
+
return selections;
|
|
491
|
+
}
|
|
492
|
+
// ============================================================================
|
|
493
|
+
// Validation
|
|
494
|
+
// ============================================================================
|
|
495
|
+
/**
|
|
496
|
+
* Validate frontend GraphQL operations against backend schema
|
|
497
|
+
*/
|
|
498
|
+
export function validateGraphQLOperations(operations, schemas) {
|
|
499
|
+
const results = [];
|
|
500
|
+
// Combine all schema operations
|
|
501
|
+
const allSchemaOperations = [
|
|
502
|
+
...schemas.flatMap(s => s.queries),
|
|
503
|
+
...schemas.flatMap(s => s.mutations),
|
|
504
|
+
...schemas.flatMap(s => s.subscriptions),
|
|
505
|
+
];
|
|
506
|
+
// Build type map
|
|
507
|
+
const typeMap = new Map();
|
|
508
|
+
for (const schema of schemas) {
|
|
509
|
+
for (const type of schema.types) {
|
|
510
|
+
typeMap.set(type.name, type);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
for (const operation of operations) {
|
|
514
|
+
const result = validateSingleOperation(operation, allSchemaOperations, typeMap);
|
|
515
|
+
results.push(result);
|
|
516
|
+
}
|
|
517
|
+
return results;
|
|
518
|
+
}
|
|
519
|
+
function validateSingleOperation(operation, schemaOperations, typeMap) {
|
|
520
|
+
const issues = [];
|
|
521
|
+
let score = 100;
|
|
522
|
+
const schemaOpsOfType = schemaOperations.filter(op => op.type === operation.type);
|
|
523
|
+
const opByName = schemaOpsOfType.find(op => op.name === operation.name);
|
|
524
|
+
const rootSelections = operation.selections || [];
|
|
525
|
+
const matchedRoot = rootSelections
|
|
526
|
+
.map(sel => ({
|
|
527
|
+
selection: sel,
|
|
528
|
+
schemaOp: schemaOpsOfType.find(op => op.name === sel.name),
|
|
529
|
+
}))
|
|
530
|
+
.filter((m) => Boolean(m.schemaOp));
|
|
531
|
+
// If none of the root selections match and the operation name doesn't match a schema field, treat as missing.
|
|
532
|
+
if (matchedRoot.length === 0 && !opByName) {
|
|
533
|
+
issues.push({
|
|
534
|
+
severity: "error",
|
|
535
|
+
type: "missing_operation",
|
|
536
|
+
message: `Operation '${operation.name}' of type '${operation.type}' not found in schema`,
|
|
537
|
+
});
|
|
538
|
+
return { operation, issues, score: 0 };
|
|
539
|
+
}
|
|
540
|
+
const validateFieldsOnType = (selections, typeName) => {
|
|
541
|
+
const t = typeMap.get(typeName);
|
|
542
|
+
if (!t)
|
|
543
|
+
return;
|
|
544
|
+
for (const selection of selections) {
|
|
545
|
+
const field = t.fields.find(f => f.name === selection.name);
|
|
546
|
+
if (!field) {
|
|
547
|
+
issues.push({
|
|
548
|
+
severity: "error",
|
|
549
|
+
type: "missing_field",
|
|
550
|
+
message: `Field '${selection.name}' does not exist on type '${t.name}'`,
|
|
551
|
+
field: selection.name,
|
|
552
|
+
});
|
|
553
|
+
score -= 20;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (selection.subSelections && selection.subSelections.length > 0) {
|
|
557
|
+
validateFieldsOnType(selection.subSelections, field.type.replace(/[\[\]!]/g, ""));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
// Strategy A: validate using schema root fields (Query/Mutation/Subscription fields)
|
|
562
|
+
// This is the primary strategy when we can match root selections.
|
|
563
|
+
if (matchedRoot.length > 0) {
|
|
564
|
+
for (const { selection, schemaOp } of matchedRoot) {
|
|
565
|
+
if (selection.subSelections && selection.subSelections.length > 0) {
|
|
566
|
+
validateFieldsOnType(selection.subSelections, schemaOp.returnType);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else if (opByName) {
|
|
571
|
+
// Strategy B (fallback): match by operation name and validate selections directly on return type.
|
|
572
|
+
const returnType = typeMap.get(opByName.returnType);
|
|
573
|
+
if (returnType) {
|
|
574
|
+
const anyDirectMatch = rootSelections.some(sel => returnType.fields.some(f => f.name === sel.name));
|
|
575
|
+
const selectionsToValidate = !anyDirectMatch && rootSelections.length === 1 && rootSelections[0]?.subSelections
|
|
576
|
+
? rootSelections[0].subSelections
|
|
577
|
+
: rootSelections;
|
|
578
|
+
validateFieldsOnType(selectionsToValidate || [], opByName.returnType);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// Variables: validate against the first matched root schema field if present, else operation-name fallback.
|
|
582
|
+
const schemaForArgs = matchedRoot[0]?.schemaOp ?? opByName;
|
|
583
|
+
if (schemaForArgs) {
|
|
584
|
+
for (const variable of operation.variables) {
|
|
585
|
+
const schemaArg = schemaForArgs.arguments?.find(a => a.name === variable.name);
|
|
586
|
+
if (!schemaArg) {
|
|
587
|
+
issues.push({
|
|
588
|
+
severity: "warning",
|
|
589
|
+
type: "unused_variable",
|
|
590
|
+
message: `Variable '$${variable.name}' is not used by operation '${operation.name}'`,
|
|
591
|
+
field: variable.name,
|
|
592
|
+
});
|
|
593
|
+
score -= 10;
|
|
594
|
+
}
|
|
595
|
+
else if (schemaArg.type !== variable.type) {
|
|
596
|
+
issues.push({
|
|
597
|
+
severity: "error",
|
|
598
|
+
type: "type_mismatch",
|
|
599
|
+
message: `Variable '$${variable.name}' type mismatch: expected '${schemaArg.type}', got '${variable.type}'`,
|
|
600
|
+
field: variable.name,
|
|
601
|
+
expectedType: schemaArg.type,
|
|
602
|
+
actualType: variable.type,
|
|
603
|
+
});
|
|
604
|
+
score -= 15;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
for (const arg of schemaForArgs.arguments || []) {
|
|
608
|
+
if (!arg.required)
|
|
609
|
+
continue;
|
|
610
|
+
const provided = operation.variables.find(v => v.name === arg.name);
|
|
611
|
+
if (!provided) {
|
|
612
|
+
issues.push({
|
|
613
|
+
severity: "error",
|
|
614
|
+
type: "missing_field",
|
|
615
|
+
message: `Required argument '${arg.name}' is missing`,
|
|
616
|
+
field: arg.name,
|
|
617
|
+
});
|
|
618
|
+
score -= 20;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
operation,
|
|
624
|
+
schemaOperation: schemaForArgs,
|
|
625
|
+
issues,
|
|
626
|
+
score: Math.max(0, score),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
// ============================================================================
|
|
630
|
+
// Main Entry Point
|
|
631
|
+
// ============================================================================
|
|
632
|
+
/**
|
|
633
|
+
* Extract complete GraphQL context from project
|
|
634
|
+
*/
|
|
635
|
+
export async function extractGraphQLContext(projectPath) {
|
|
636
|
+
const context = {
|
|
637
|
+
schemas: [],
|
|
638
|
+
frontendOperations: [],
|
|
639
|
+
validationResults: [],
|
|
640
|
+
unmatchedOperations: [],
|
|
641
|
+
};
|
|
642
|
+
// Find schema files
|
|
643
|
+
const schemaFiles = await glob("**/*.{graphql,gql}", { cwd: projectPath });
|
|
644
|
+
for (const file of schemaFiles) {
|
|
645
|
+
const fullPath = path.join(projectPath, file);
|
|
646
|
+
const schema = await extractGraphQLSchema(fullPath);
|
|
647
|
+
if (schema) {
|
|
648
|
+
context.schemas.push(schema);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// Find frontend files with GraphQL operations
|
|
652
|
+
const frontendFiles = await glob("**/*.{ts,tsx,js,jsx}", { cwd: projectPath });
|
|
653
|
+
for (const file of frontendFiles) {
|
|
654
|
+
const fullPath = path.join(projectPath, file);
|
|
655
|
+
const operations = await extractGraphQLOperations(fullPath);
|
|
656
|
+
context.frontendOperations.push(...operations);
|
|
657
|
+
}
|
|
658
|
+
// Validate operations against schemas
|
|
659
|
+
if (context.schemas.length > 0 && context.frontendOperations.length > 0) {
|
|
660
|
+
context.validationResults = validateGraphQLOperations(context.frontendOperations, context.schemas);
|
|
661
|
+
// Find unmatched operations
|
|
662
|
+
context.unmatchedOperations = context.validationResults
|
|
663
|
+
.filter(r => r.issues.some(i => i.type === "missing_operation"))
|
|
664
|
+
.map(r => r.operation);
|
|
665
|
+
}
|
|
666
|
+
logger.info(`GraphQL context extracted: ${context.schemas.length} schemas, ` +
|
|
667
|
+
`${context.frontendOperations.length} operations, ` +
|
|
668
|
+
`${context.validationResults.length} validation results`);
|
|
669
|
+
return context;
|
|
670
|
+
}
|
|
671
|
+
//# sourceMappingURL=graphqlSupport.js.map
|