Skip to main content
Glama
query-builder.ts9.29 kB
/** * @fileoverview GraphQL Query Builder for constructing type-safe queries * * This module provides a fluent API for building GraphQL queries with: * - Type safety for fields and arguments * - Automatic fragment generation * - Query optimization * - Reusable field selections */ import { ProjectKey, RunId } from '../../types/branded.js'; /** * GraphQL field selection */ export interface FieldSelection { name: string; alias?: string; args?: Record<string, unknown>; fields?: FieldSelection[]; } /** * GraphQL fragment definition */ export interface Fragment { name: string; type: string; fields: FieldSelection[]; } /** * Query operation type */ export type OperationType = 'query' | 'mutation' | 'subscription'; /** * GraphQL query builder for constructing type-safe queries */ export class GraphQLQueryBuilder { private operation: OperationType = 'query'; private operationName?: string; private variables: Map<string, { type: string; value: unknown }> = new Map(); private selections: FieldSelection[] = []; private fragments: Map<string, Fragment> = new Map(); /** * Creates a new query builder * @param operation - The operation type (default: 'query') */ constructor(operation: OperationType = 'query') { this.operation = operation; } /** * Sets the operation name */ withName(name: string): this { this.operationName = name; return this; } /** * Adds a variable to the query */ withVariable(name: string, type: string, value: unknown): this { this.variables.set(name, { type, value }); return this; } /** * Adds a field selection */ select(field: string | FieldSelection): this { if (typeof field === 'string') { this.selections.push({ name: field }); } else { this.selections.push(field); } return this; } /** * Adds multiple field selections */ selectMany(...fields: (string | FieldSelection)[]): this { fields.forEach((field) => this.select(field)); return this; } /** * Adds a nested field selection */ selectNested( name: string, fields: (string | FieldSelection)[], args?: Record<string, unknown> ): this { const nestedFields = fields.map((f) => (typeof f === 'string' ? { name: f } : f)); const selection: FieldSelection = { name, fields: nestedFields }; if (args !== undefined) { selection.args = args; } this.selections.push(selection); return this; } /** * Adds a fragment */ withFragment(fragment: Fragment): this { this.fragments.set(fragment.name, fragment); return this; } /** * Uses a fragment in the selection */ useFragment(fragmentName: string): this { this.selections.push({ name: `...${fragmentName}` }); return this; } /** * Builds the query string */ build(): { query: string; variables: Record<string, unknown> } { const variableDeclarations = this.buildVariableDeclarations(); const selectionSet = this.buildSelectionSet(this.selections); const fragmentDefinitions = this.buildFragments(); const operationDef = this.operationName ? `${this.operation} ${this.operationName}` : this.operation; const queryParts = [ variableDeclarations ? `${operationDef}(${variableDeclarations})` : operationDef, `{${selectionSet}\n}`, ]; if (fragmentDefinitions) { queryParts.push(fragmentDefinitions); } const query = queryParts.join(' '); const variables: Record<string, unknown> = {}; this.variables.forEach((varDef, name) => { variables[name] = varDef.value; }); return { query, variables }; } /** * Builds variable declarations */ private buildVariableDeclarations(): string { if (this.variables.size === 0) return ''; const declarations: string[] = []; this.variables.forEach((varDef, name) => { declarations.push(`$${name}: ${varDef.type}`); }); return declarations.join(', '); } /** * Builds a selection set */ private buildSelectionSet(selections: FieldSelection[], indent = 1): string { const indentStr = ' '.repeat(indent); const fields = selections.map((sel) => { // Handle fragment spreads if (sel.name?.startsWith('...')) { return `${indentStr}${sel.name}`; } // Handle string selections that get converted to FieldSelection if (!sel.name) { return `${indentStr}${sel}`; } let field = sel.alias ? `${sel.alias}: ${sel.name}` : sel.name; if (sel.args && Object.keys(sel.args).length > 0) { const args = GraphQLQueryBuilder.buildArguments(sel.args); field += `(${args})`; } if (sel.fields && sel.fields.length > 0) { const nestedFields = this.buildSelectionSet(sel.fields, indent + 1); field += ` {${nestedFields}\n${indentStr}}`; } return `${indentStr}${field}`; }); return fields.length > 0 ? `\n${fields.join('\n')}` : ''; } /** * Builds argument string */ private static buildArguments(args: Record<string, unknown>): string { return Object.entries(args) .map(([key, value]) => { if (typeof value === 'string' && value.startsWith('$')) { // Variable reference return `${key}: ${value}`; } return `${key}: ${JSON.stringify(value)}`; }) .join(', '); } /** * Builds fragment definitions */ private buildFragments(): string { if (this.fragments.size === 0) return ''; const fragmentDefs: string[] = []; this.fragments.forEach((fragment) => { const fields = this.buildSelectionSet(fragment.fields); fragmentDefs.push(`fragment ${fragment.name} on ${fragment.type} {${fields}\n}`); }); return `\n${fragmentDefs.join('\n')}`; } /** * Static factory methods for common queries */ /** * Creates a query builder for fetching projects */ static projects(): GraphQLQueryBuilder { return new GraphQLQueryBuilder().withName('GetProjects').selectNested('viewer', [ { name: 'accounts', fields: [ { name: 'id' }, { name: 'name' }, { name: 'repositories', fields: [{ name: 'edges' }], }, ], }, ]); } /** * Creates a query builder for fetching a specific run */ static run(projectKey: ProjectKey, runId: RunId): GraphQLQueryBuilder { return new GraphQLQueryBuilder() .withName('GetRun') .withVariable('projectKey', 'String!', projectKey) .withVariable('runId', 'ID!', runId) .selectNested( 'run', [ { name: 'id' }, { name: 'commitOid' }, { name: 'createdAt' }, { name: 'updatedAt' }, { name: 'status' }, { name: 'checks', fields: [{ name: 'analyzer' }, { name: 'status' }, { name: 'summary' }], }, ], { projectKey: '$projectKey', runUid: '$runId' } ); } /** * Creates a query builder for fetching quality metrics */ static qualityMetrics(projectKey: ProjectKey): GraphQLQueryBuilder { return new GraphQLQueryBuilder() .withName('GetQualityMetrics') .withVariable('projectKey', 'String!', projectKey) .selectNested( 'repository', [ { name: 'metrics', fields: [ { name: 'name' }, { name: 'shortcode' }, { name: 'description' }, { name: 'positiveDirection' }, { name: 'unit' }, { name: 'minValueAllowed' }, { name: 'maxValueAllowed' }, { name: 'isReported' }, { name: 'isThresholdEnforced' }, { name: 'items', fields: [ { name: 'id' }, { name: 'key' }, { name: 'threshold' }, { name: 'latestValue' }, { name: 'latestValueDisplay' }, { name: 'thresholdStatus' }, ], }, ], }, ], { key: '$projectKey' } ); } /** * Creates a reusable fragment for issue fields */ static issueFieldsFragment(): Fragment { return { name: 'IssueFields', type: 'Issue', fields: [ { name: 'id' }, { name: 'title' }, { name: 'category' }, { name: 'issueCode' }, { name: 'issuePriority' }, { name: 'shortDescription' }, { name: 'beginLine' }, { name: 'endLine' }, { name: 'beginColumn' }, { name: 'endColumn' }, { name: 'path' }, { name: 'analyzer' }, { name: 'tags' }, ], }; } /** * Creates a reusable fragment for run fields */ static runFieldsFragment(): Fragment { return { name: 'RunFields', type: 'Run', fields: [ { name: 'runUid' }, { name: 'commitOid' }, { name: 'createdAt' }, { name: 'updatedAt' }, { name: 'status' }, { name: 'summary' }, { name: 'triggeredVia' }, { name: 'branch' }, ], }; } }

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