/**
* @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' },
],
};
}
}