Skip to main content
Glama
graphql-query-builder.md15.5 kB
# GraphQL Query Builder Documentation ## Overview The DeepSource MCP Server includes a powerful GraphQL query builder that provides a type-safe, maintainable way to construct GraphQL queries for the DeepSource API. This system eliminates string concatenation, reduces errors, and improves code readability. ## Architecture ### Core Components 1. **Query Builder (`src/utils/graphql/query-builder.ts`)** - Main builder class with fluent API - Type-safe field selection - Automatic formatting and validation 2. **Query Fragments (`src/utils/graphql/queries.ts`)** - Reusable query fragments - Centralized field definitions - Consistent data fetching 3. **Query Processor (`src/utils/graphql/processor.ts`)** - Response transformation - Error handling - Data normalization ## Basic Usage ### Simple Query ```typescript import { GraphQLQueryBuilder } from './utils/graphql/query-builder.js'; const query = new GraphQLQueryBuilder().query('viewer').select(['login', 'email']).build(); // Result: // query { // viewer { // login // email // } // } ``` ### Query with Arguments ```typescript const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select(['name', 'defaultBranch']) .build(); // Result: // query { // repository(dsn: "project-key") { // name // defaultBranch // } // } ``` ### Nested Selections ```typescript const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([ 'name', { field: 'metrics', args: { shortcodeIn: ['LCV', 'BCV'] }, selection: [ 'shortcode', 'name', { field: 'items', selection: ['key', 'latestValue', 'threshold'], }, ], }, ]) .build(); ``` ## Advanced Features ### Aliases ```typescript const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([ { field: 'name', alias: 'projectName' }, { field: 'defaultBranch', alias: 'mainBranch' }, ]) .build(); // Result: // query { // repository(dsn: "project-key") { // projectName: name // mainBranch: defaultBranch // } // } ``` ### Multiple Root Queries ```typescript const query = new GraphQLQueryBuilder() .addField('viewer', {}, ['login', 'email']) .addField('repository', { dsn: projectKey }, ['name', 'status']) .build(); // Result: // query { // viewer { // login // email // } // repository(dsn: "project-key") { // name // status // } // } ``` ### Fragments and Reusable Selections ```typescript // Define reusable fragments const METRIC_FIELDS = ['shortcode', 'name', 'description', 'unit', 'positiveDirection']; const METRIC_ITEM_FIELDS = [ 'key', 'threshold', 'latestValue', 'latestValueDisplay', 'thresholdStatus', ]; // Use in queries const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([ 'name', { field: 'metrics', selection: [ ...METRIC_FIELDS, { field: 'items', selection: METRIC_ITEM_FIELDS, }, ], }, ]) .build(); ``` ### Pagination Support ```typescript const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([ { field: 'issues', args: { first: 10, after: cursor, states: ['ACTIVE'], }, selection: [ { field: 'edges', selection: [ 'cursor', { field: 'node', selection: ['id', 'code', 'title'], }, ], }, { field: 'pageInfo', selection: ['hasNextPage', 'endCursor'], }, 'totalCount', ], }, ]) .build(); ``` ## Query Fragments Library ### Project Fragments ```typescript import { PROJECT_BASIC_FRAGMENT, PROJECT_METRICS_FRAGMENT, PROJECT_FULL_FRAGMENT, } from './utils/graphql/queries.js'; // Basic project info const basicQuery = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select(PROJECT_BASIC_FRAGMENT) .build(); // Project with metrics const metricsQuery = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select(PROJECT_METRICS_FRAGMENT) .build(); ``` ### Available Fragments ```typescript // Project fragments export const PROJECT_BASIC_FRAGMENT = ['name', 'defaultBranch', 'dsn']; // Metric fragments export const METRIC_FRAGMENT = [ 'shortcode', 'name', 'description', 'positiveDirection', 'unit', 'isReported', 'isThresholdEnforced', { field: 'items', selection: METRIC_ITEM_FRAGMENT, }, ]; // Issue fragments export const ISSUE_FRAGMENT = [ 'id', 'code', 'title', 'message', 'category', { field: 'location', selection: ['path', { field: 'position', selection: ['beginLine'] }], }, ]; // Run fragments export const RUN_FRAGMENT = [ 'runUid', 'commitOid', 'branchName', 'status', 'createdAt', { field: 'summary', selection: ['occurrencesIntroduced', 'occurrencesResolved'], }, ]; ``` ## Integration with DeepSource Client ### Using Query Builder in Client Methods ```typescript class DeepSourceClient { async getQualityMetrics(projectKey: string, filters?: MetricFilters) { const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([ { field: 'metrics', args: this.buildMetricArgs(filters), selection: METRIC_FRAGMENT, }, ]) .build(); const response = await this.graphqlClient.request(query); return this.processMetrics(response.repository?.metrics); } private buildMetricArgs(filters?: MetricFilters) { const args: any = {}; if (filters?.shortcodeIn) { args.shortcodeIn = filters.shortcodeIn; } return args; } } ``` ### Error Handling ```typescript try { const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select(['invalidField']) // This will cause an error .build(); const response = await client.request(query); } catch (error) { if (error.response?.errors) { // GraphQL errors const graphqlError = error.response.errors[0]; console.error(`Field error: ${graphqlError.message}`); } else { // Network or other errors console.error(`Request failed: ${error.message}`); } } ``` ## Best Practices ### 1. Use Fragments for Consistency Define fragments for commonly used field selections: ```typescript // Good - Reusable and consistent const AUTHOR_FRAGMENT = ['login', 'email', 'avatarUrl']; const query1 = builder.select([{ field: 'author', selection: AUTHOR_FRAGMENT }]); const query2 = builder.select([{ field: 'committer', selection: AUTHOR_FRAGMENT }]); // Avoid - Repetitive and error-prone const query1 = builder.select([{ field: 'author', selection: ['login', 'email', 'avatarUrl'] }]); const query2 = builder.select([{ field: 'committer', selection: ['login', 'email', 'avatarUrl'] }]); ``` ### 2. Type Arguments Properly Always type query arguments for better type safety: ```typescript interface RepositoryArgs { dsn: string; } interface IssuesArgs { first?: number; after?: string; states?: string[]; } const query = new GraphQLQueryBuilder() .query<RepositoryArgs>('repository', { dsn: projectKey }) .select([ { field: 'issues', args: { first: 10, states: ['ACTIVE'] } as IssuesArgs, selection: ISSUE_FRAGMENT, }, ]) .build(); ``` ### 3. Handle Nullable Fields Account for nullable fields in your queries: ```typescript const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([ 'name', { field: 'latestRun', // May be null selection: RUN_FRAGMENT, }, ]) .build(); // Handle in response const latestRun = response.repository?.latestRun || null; ``` ### 4. Optimize Query Depth Avoid deeply nested queries that can impact performance: ```typescript // Good - Reasonable nesting const query = builder .query('repository') .select([ 'name', { field: 'issues', selection: ['edges', { field: 'node', selection: ['id', 'title'] }] }, ]); // Avoid - Too deeply nested const query = builder.query('repository').select([ { field: 'issues', selection: [ { field: 'edges', selection: [ { field: 'node', selection: [ { field: 'author', selection: [ { field: 'organization', selection: [ { field: 'repositories', selection: ['edges'], // Too deep! }, ], }, ], }, ], }, ], }, ], }, ]); ``` ### 5. Use Aliases for Clarity Use aliases when fetching the same field with different arguments: ```typescript const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([ { field: 'issues', alias: 'openIssues', args: { states: ['ACTIVE'] }, selection: ['totalCount'], }, { field: 'issues', alias: 'closedIssues', args: { states: ['RESOLVED'] }, selection: ['totalCount'], }, ]) .build(); ``` ## Testing Queries ### Unit Testing Query Construction ```typescript describe('GraphQLQueryBuilder', () => { it('should build a simple query', () => { const query = new GraphQLQueryBuilder().query('viewer').select(['login']).build(); expect(query).toBe('query { viewer { login } }'); }); it('should handle nested selections', () => { const query = new GraphQLQueryBuilder() .query('repository', { dsn: 'test-project' }) .select([ 'name', { field: 'metrics', selection: ['shortcode', 'name'], }, ]) .build(); expect(query).toContain('repository(dsn: "test-project")'); expect(query).toContain('metrics { shortcode name }'); }); }); ``` ### Testing with Mock Responses ```typescript it('should fetch metrics using query builder', async () => { const mockResponse = { repository: { metrics: [{ shortcode: 'LCV', name: 'Line Coverage', latestValue: 85 }], }, }; mockGraphQLClient.request.mockResolvedValue(mockResponse); const metrics = await client.getQualityMetrics('test-project'); expect(mockGraphQLClient.request).toHaveBeenCalledWith(expect.stringContaining('metrics')); expect(metrics).toHaveLength(1); expect(metrics[0].shortcode).toBe('LCV'); }); ``` ## Common Patterns ### Fetching with Pagination ```typescript async function fetchAllIssues(projectKey: string): Promise<Issue[]> { const issues: Issue[] = []; let cursor: string | null = null; let hasMore = true; while (hasMore) { const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([ { field: 'issues', args: { first: 100, after: cursor }, selection: [ { field: 'edges', selection: ['cursor', { field: 'node', selection: ISSUE_FRAGMENT }] }, { field: 'pageInfo', selection: ['hasNextPage', 'endCursor'] }, ], }, ]) .build(); const response = await client.request(query); const issueData = response.repository?.issues; if (issueData?.edges) { issues.push(...issueData.edges.map((e) => e.node)); cursor = issueData.pageInfo.endCursor; hasMore = issueData.pageInfo.hasNextPage; } else { hasMore = false; } } return issues; } ``` ### Conditional Field Selection ```typescript function buildMetricsQuery(projectKey: string, includeHistory: boolean = false): string { const metricSelection = [...METRIC_FRAGMENT]; if (includeHistory) { metricSelection.push({ field: 'history', args: { last: 30 }, selection: ['value', 'measuredAt'], }); } return new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([{ field: 'metrics', selection: metricSelection }]) .build(); } ``` ### Batch Queries ```typescript function buildBatchQuery(projectKeys: string[]): string { const builder = new GraphQLQueryBuilder(); projectKeys.forEach((key, index) => { builder.addField( 'repository', { dsn: key }, PROJECT_BASIC_FRAGMENT, `project${index}` // Alias ); }); return builder.build(); } ``` ## Performance Considerations 1. **Minimize Over-fetching** - Only request fields you need - Use fragments to standardize field selection 2. **Batch Related Queries** - Combine multiple queries into one request - Use aliases for multiple instances of the same field 3. **Implement Caching** - Cache query results when appropriate - Use ETags or timestamps for cache invalidation 4. **Handle Large Result Sets** - Use pagination for lists - Implement streaming for very large datasets ## Troubleshooting ### Common Issues 1. **"Cannot query field X on type Y"** - Field doesn't exist in schema - Check API documentation for correct field names 2. **"Syntax Error: Expected Name"** - Malformed query structure - Check for missing commas or brackets 3. **"Variable $X of type Y was provided invalid value"** - Type mismatch in arguments - Ensure argument types match schema ### Debug Tips 1. **Log Generated Queries** ```typescript const query = builder.build(); console.log('Generated query:', query); ``` 2. **Use GraphQL Playground** - Test queries in GraphQL playground first - Verify field availability and types 3. **Enable Debug Mode** ```typescript const client = new DeepSourceClient(apiKey, { debug: true, // Logs all queries and responses }); ``` ## Migration Guide ### From String Concatenation ```typescript // Old approach const query = ` query { repository(dsn: "${projectKey}") { name metrics { shortcode name } } } `; // New approach const query = new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select(['name', { field: 'metrics', selection: ['shortcode', 'name'] }]) .build(); ``` ### From Template Literals ```typescript // Old approach const createMetricsQuery = (projectKey: string, metricTypes: string[]) => ` query { repository(dsn: "${projectKey}") { metrics(shortcodeIn: [${metricTypes.map((t) => `"${t}"`).join(', ')}]) { shortcode latestValue } } } `; // New approach const createMetricsQuery = (projectKey: string, metricTypes: string[]) => new GraphQLQueryBuilder() .query('repository', { dsn: projectKey }) .select([ { field: 'metrics', args: { shortcodeIn: metricTypes }, selection: ['shortcode', 'latestValue'], }, ]) .build(); ``` ## Future Enhancements 1. **Type Generation** - Generate TypeScript types from GraphQL schema - Compile-time query validation 2. **Query Optimization** - Automatic query deduplication - Field-level caching 3. **Developer Tools** - VS Code extension for query building - Query performance analyzer 4. **Advanced Features** - Subscription support - Directive handling - Fragment definitions

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server