synaptra
Version:
A high-performance Model Context Protocol server for GraphQL APIs with advanced features, type-safety, and developer experience improvements
214 lines • 8.42 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GraphQLClientService = void 0;
const graphql_1 = require("graphql");
const graphql_request_1 = require("graphql-request");
const types_1 = require("../types");
class GraphQLClientService {
maxRetries;
endpoint;
client;
schema = null;
constructor(endpoint, maxRetries = 3) {
this.maxRetries = maxRetries;
this.endpoint = endpoint;
this.client = new graphql_request_1.GraphQLClient(endpoint.url, {
headers: endpoint.headers,
});
}
async introspectSchema(requestHeaders) {
try {
const introspectionQuery = (0, graphql_1.getIntrospectionQuery)();
const queryInfo = {
query: introspectionQuery,
};
if (requestHeaders) {
queryInfo.headers = requestHeaders;
}
const queryResult = await this.executeQuery(queryInfo);
if (queryResult.errors || !queryResult.data) {
throw new types_1.SchemaError('Failed to introspect schema', { errors: queryResult.errors });
}
const schema = (0, graphql_1.buildClientSchema)(queryResult.data);
const sdl = (0, graphql_1.printSchema)(schema);
const schemaInfo = {
schema,
sdl,
lastUpdated: new Date(),
version: this.generateSchemaVersion(sdl),
};
this.schema = schemaInfo;
return schemaInfo;
}
catch (error) {
if (error instanceof types_1.SchemaError) {
throw error;
}
throw new types_1.SchemaError(`Schema introspection failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { originalError: error });
}
}
async executeQuery(queryInfo, dryRun = false) {
const metrics = { startTime: Date.now() };
try {
// Parse query if it's a string
const queryDoc = typeof queryInfo.query === 'string'
? (0, graphql_1.parse)(queryInfo.query)
: queryInfo.query;
const queryString = (0, graphql_1.print)(queryDoc);
// Validate query against schema if available
if (this.schema) {
const validationErrors = (0, graphql_1.validate)(this.schema.schema, queryDoc);
if (validationErrors.length > 0) {
throw new types_1.ValidationError('Query validation failed', { errors: validationErrors });
}
}
if (dryRun) {
return {
data: null,
extensions: {
dryRun: true,
query: queryString,
variables: queryInfo.variables,
metrics,
},
};
}
// Merge default headers with request-specific headers
const headers = {
...this.endpoint.headers,
...(queryInfo.headers || {}),
};
// Execute query with retry logic
let lastError = null;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
const result = await this.client.request(queryDoc, queryInfo.variables, headers);
metrics.endTime = Date.now();
metrics.duration = metrics.endTime - metrics.startTime;
const queryResult = {
data: result,
extensions: { metrics },
};
return queryResult;
}
catch (error) {
lastError = error;
if (attempt < this.maxRetries) {
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
}
throw new types_1.NetworkError(`Query execution failed after ${this.maxRetries + 1} attempts`, { originalError: lastError });
}
catch (error) {
if (error instanceof types_1.ValidationError || error instanceof types_1.NetworkError) {
throw error;
}
throw new types_1.QueryError(`Query execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { originalError: error });
}
}
async analyzeQuery(query, variables) {
try {
const queryDoc = (0, graphql_1.parse)(query);
const analysis = {
operation: this.getOperationType(queryDoc),
complexity: this.calculateComplexity(queryDoc),
depth: this.calculateDepth(queryDoc),
fields: this.extractFields(queryDoc),
variables: variables ? Object.keys(variables) : [],
fragments: this.extractFragments(queryDoc),
};
return analysis;
}
catch (error) {
throw new types_1.ValidationError(`Query analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { originalError: error });
}
}
getSchema() {
return this.schema;
}
updateEndpoint(endpoint) {
this.endpoint = { ...this.endpoint, ...endpoint };
this.client = new graphql_request_1.GraphQLClient(this.endpoint.url, {
headers: this.endpoint.headers,
});
}
generateSchemaVersion(sdl) {
const crypto = require('crypto');
return crypto.createHash('md5').update(sdl).digest('hex').substring(0, 8);
}
getOperationType(query) {
const operationDef = query.definitions.find(def => def.kind === 'OperationDefinition');
return operationDef?.kind === 'OperationDefinition' ? operationDef.operation : 'unknown';
}
calculateComplexity(query) {
// Simple complexity calculation - count field selections
let complexity = 0;
const visit = (node) => {
if (node.kind === 'Field') {
complexity++;
}
if (node.selectionSet) {
node.selectionSet.selections.forEach(visit);
}
};
query.definitions.forEach(def => {
if (def.kind === 'OperationDefinition' && def.selectionSet) {
def.selectionSet.selections.forEach(visit);
}
});
return complexity;
}
calculateDepth(query) {
let maxDepth = 0;
const visit = (node, depth) => {
maxDepth = Math.max(maxDepth, depth);
if (node.selectionSet) {
node.selectionSet.selections.forEach((selection) => {
if (selection.kind === 'Field') {
visit(selection, depth + 1);
}
});
}
};
query.definitions.forEach(def => {
if (def.kind === 'OperationDefinition' && def.selectionSet) {
def.selectionSet.selections.forEach((selection) => {
if (selection.kind === 'Field') {
visit(selection, 1);
}
});
}
});
return maxDepth;
}
extractFields(query) {
const fields = [];
const visit = (node) => {
if (node.kind === 'Field') {
fields.push(node.name.value);
}
if (node.selectionSet) {
node.selectionSet.selections.forEach(visit);
}
};
query.definitions.forEach(def => {
if (def.kind === 'OperationDefinition' && def.selectionSet) {
def.selectionSet.selections.forEach(visit);
}
});
return [...new Set(fields)]; // Remove duplicates
}
extractFragments(query) {
const fragments = [];
query.definitions.forEach(def => {
if (def.kind === 'FragmentDefinition') {
fragments.push(def.name.value);
}
});
return fragments;
}
}
exports.GraphQLClientService = GraphQLClientService;
//# sourceMappingURL=graphql-client.js.map