Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 23 additions & 193 deletions src/commands/executeStepZenRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,15 @@ import * as vscode from "vscode";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import * as https from "https";
import { resolveStepZenProjectRoot } from "../utils/stepzenProject";
import { clearResultsPanel, openResultsPanel } from "../panels/resultsPanel";
import { summariseDiagnostics, publishDiagnostics } from "../utils/runtimeDiagnostics";
import { runtimeDiag } from "../extension";
import { UI, TIMEOUTS, TEMP_FILE_PATTERNS, FILE_PATTERNS } from "../utils/constants";
import { UI, TIMEOUTS, TEMP_FILE_PATTERNS } from "../utils/constants";
import { services } from "../services";
import { StepZenResponse, StepZenDiagnostic } from "../types";
import { ValidationError, NetworkError, handleError } from "../errors";
import { ValidationError, handleError } from "../errors";

/* CLEANUP - DELETE WHEN SAFE
// Export utility functions for use in other files
export {
createTempGraphQLFile,
cleanupLater
};
*/

/**
* Creates a temporary GraphQL file with the provided query content
Expand Down Expand Up @@ -93,33 +85,17 @@ function cleanupLater(filePath: string) {
*/
export async function executeStepZenRequest(options: {
queryText?: string;
documentId?: string;
documentContent?: string;
operationName?: string;
varArgs?: string[];
}): Promise<void> {
// Validate options object
if (!options || typeof options !== 'object') {
handleError(new ValidationError("Invalid request options provided", "INVALID_OPTIONS"));
return;
}

const { queryText, documentId, operationName, varArgs = [] } = options;

// Validate at least one of queryText or documentId is provided and valid
if (documentId === undefined && (!queryText || typeof queryText !== 'string')) {
handleError(new ValidationError("Invalid request: either documentId or queryText must be provided", "MISSING_QUERY"));
return;
}

// Validate operationName if provided
if (operationName !== undefined && typeof operationName !== 'string') {
handleError(new ValidationError("Invalid operation name provided", "INVALID_OPERATION_NAME"));
return;
}
const { queryText, documentContent, operationName, varArgs = [] } = options;

// Validate varArgs is an array
if (!Array.isArray(varArgs)) {
handleError(new ValidationError("Invalid variable arguments: expected an array", "INVALID_VAR_ARGS"));
// Validate request options using the request service
try {
services.request.validateRequestOptions({ queryText, documentContent, operationName, varArgs });
} catch (err) {
handleError(err);
return;
}

Expand All @@ -136,90 +112,13 @@ export async function executeStepZenRequest(options: {
const debugLevel = cfg.get<number>("request.debugLevel", 1);

// For persisted documents, we need to make an HTTP request directly
if (documentId) {
if (documentContent) {
try {
// Get StepZen config to build the endpoint URL
const configPath = path.join(projectRoot, FILE_PATTERNS.CONFIG_FILE);
services.logger.debug(`Looking for config file at: ${configPath}`);

// Verify config file exists
if (!fs.existsSync(configPath)) {
handleError(new ValidationError(
`StepZen configuration file not found at: ${configPath}`,
"CONFIG_NOT_FOUND"
));
return;
}

let endpoint: string;
let apiKey: string;
// Load endpoint configuration using the request service
const endpointConfig = await services.request.loadEndpointConfig(projectRoot);

try {
const configContent = fs.readFileSync(configPath, "utf8");

if (!configContent) {
handleError(new ValidationError("StepZen configuration file is empty", "EMPTY_CONFIG"));
return;
}

const config = JSON.parse(configContent);

if (!config || !config.endpoint) {
handleError(new ValidationError("Invalid StepZen configuration: missing endpoint", "MISSING_ENDPOINT"));
return;
}

endpoint = config.endpoint;
apiKey = config.apiKey || "";
} catch (err) {
handleError(new ValidationError(
"Failed to parse StepZen configuration file",
"CONFIG_PARSE_ERROR",
err
));
return;
}

// Parse endpoint to extract account and domain
const endpointParts = endpoint.split("/");
if (endpointParts.length < 2) {
handleError(new ValidationError(
`Invalid StepZen endpoint format: ${endpoint}`,
"INVALID_ENDPOINT_FORMAT"
));
return;
}

// Construct the GraphQL endpoint URL
const graphqlUrl = `https://${endpoint}/graphql`;

// Prepare variables from varArgs
let variables: Record<string, string> = {};
for (let i = 0; i < varArgs.length; i += 2) {
if (varArgs[i] === "--var" && i + 1 < varArgs.length) {
const [name, value] = varArgs[i + 1].split("=");
variables[name] = value;
} else if (varArgs[i] === "--var-file" && i + 1 < varArgs.length) {
try {
const fileContent = fs.readFileSync(varArgs[i + 1], "utf8");
variables = JSON.parse(fileContent);
} catch (err) {
handleError(new ValidationError(
"Failed to read variables file",
"VAR_FILE_READ_ERROR",
err
));
return;
}
}
}

// Prepare the request body
const requestBody = {
documentId,
operationName,
variables
};
// Parse variables using the request service
const { variables } = services.request.parseVariables(varArgs);

// Show a progress notification
await vscode.window.withProgress(
Expand All @@ -229,53 +128,13 @@ export async function executeStepZenRequest(options: {
cancellable: false
},
async () => {
services.logger.info("Making HTTP request to StepZen API for persisted document");
// Use Node.js https module to make the request
const result = await new Promise<StepZenResponse>((resolve, reject) => {
const postData = JSON.stringify(requestBody);

const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
'Authorization': apiKey ? `Apikey ${apiKey}` : '',
'stepzen-debug-level': debugLevel.toString(),
}
};

const req = https.request(graphqlUrl, options, (res) => {
let responseData = '';

res.on('data', (chunk) => {
responseData += chunk;
});

res.on('end', () => {
try {
const json = JSON.parse(responseData);
resolve(json);
} catch (err) {
reject(new ValidationError(
"Failed to parse StepZen response",
"RESPONSE_PARSE_ERROR",
err
));
}
});
});

req.on('error', (err) => {
reject(new NetworkError(
"Failed to connect to StepZen API",
"API_CONNECTION_ERROR",
err
));
});

req.write(postData);
req.end();
});
// Execute the persisted document request using the request service
const result = await services.request.executePersistedDocumentRequest(
endpointConfig,
documentContent,
variables,
operationName
);

// Process results
const rawDiags = (result.extensions?.stepzen?.diagnostics ?? []) as StepZenDiagnostic[];
Expand Down Expand Up @@ -355,37 +214,8 @@ export async function executeStepZenRequest(options: {
},
async () => {
try {
// Parse varArgs into variables object
const variables: Record<string, any> = {};
for (let i = 0; i < varArgs.length; i += 2) {
if (varArgs[i] === "--var" && i + 1 < varArgs.length) {
const [name, value] = varArgs[i + 1].split("=");
if (name && value !== undefined) {
variables[name] = value;
services.logger.debug(`Setting variable ${name}=${value}`);
} else {
services.logger.warn(`Invalid variable format: ${varArgs[i + 1]}`);
}
} else if (varArgs[i] === "--var-file" && i + 1 < varArgs.length) {
try {
const varFilePath = varArgs[i + 1];
services.logger.debug(`Reading variables from file: ${varFilePath}`);
if (!fs.existsSync(varFilePath)) {
throw new ValidationError(`Variables file not found: ${varFilePath}`, "VAR_FILE_NOT_FOUND");
}
const fileContent = fs.readFileSync(varFilePath, "utf8");
const fileVars = JSON.parse(fileContent);
services.logger.debug(`Loaded ${Object.keys(fileVars).length} variables from file`);
Object.assign(variables, fileVars);
} catch (err) {
throw new ValidationError(
"Failed to read variables file",
"VAR_FILE_READ_ERROR",
err
);
}
}
}
// Parse variables using the request service
const { variables } = services.request.parseVariables(varArgs);

// Use the CLI service to execute the request
services.logger.info(`Executing StepZen request${operationName ? ` for operation "${operationName}"` : ' (anonymous operation)'} with debug level ${debugLevel}`);
Expand Down
4 changes: 2 additions & 2 deletions src/commands/runRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,9 @@ export async function runPersisted(documentId: string, operationName: string) {
return; // user cancelled
}

// Execute using document ID approach
// Execute using persisted document approach with the full document content
await executeStepZenRequest({
documentId,
documentContent: content,
operationName,
varArgs
});
Expand Down
86 changes: 85 additions & 1 deletion src/services/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,90 @@ export class StepzenCliService {
throw error;
}
}

/**
* Get the API key from StepZen CLI
*
* @returns Promise resolving to the API key
* @throws CliError if the operation fails
*/
async getApiKey(): Promise<string> {
try {
logger.debug('Retrieving API key from StepZen CLI');

const result = await this.spawnProcessWithOutput(['whoami', '--apikey']);
const apiKey = result.trim();

if (!apiKey) {
throw new CliError("Empty API key returned from StepZen CLI", "EMPTY_API_KEY");
}

logger.debug("Successfully retrieved API key from CLI");
return apiKey;
} catch (err) {
throw new CliError(
"Failed to retrieve API key from StepZen CLI",
"API_KEY_RETRIEVAL_FAILED",
err
);
}
}

/**
* Get the account name from StepZen CLI
*
* @returns Promise resolving to the account name
* @throws CliError if the operation fails
*/
async getAccount(): Promise<string> {
try {
logger.debug('Retrieving account from StepZen CLI');

const result = await this.spawnProcessWithOutput(['whoami', '--account']);
const account = result.trim();

if (!account) {
throw new CliError("Empty account returned from StepZen CLI", "EMPTY_ACCOUNT");
}

logger.debug("Successfully retrieved account from CLI");
return account;
} catch (err) {
throw new CliError(
"Failed to retrieve account from StepZen CLI",
"ACCOUNT_RETRIEVAL_FAILED",
err
);
}
}

/**
* Get the domain from StepZen CLI
*
* @returns Promise resolving to the domain
* @throws CliError if the operation fails
*/
async getDomain(): Promise<string> {
try {
logger.debug('Retrieving domain from StepZen CLI');

const result = await this.spawnProcessWithOutput(['whoami', '--domain']);
const domain = result.trim();

if (!domain) {
throw new CliError("Empty domain returned from StepZen CLI", "EMPTY_DOMAIN");
}

logger.debug("Successfully retrieved domain from CLI");
return domain;
} catch (err) {
throw new CliError(
"Failed to retrieve domain from StepZen CLI",
"DOMAIN_RETRIEVAL_FAILED",
err
);
}
}

// TODO: CLEANUP
// /**
Expand Down Expand Up @@ -228,7 +312,7 @@ export class StepzenCliService {
* @returns Promise resolving to the captured stdout
* @throws CliError if the process fails
*/
private async spawnProcessWithOutput(
public async spawnProcessWithOutput(
args: string[] = [],
options: cp.SpawnOptions = {}
): Promise<string> {
Expand Down
3 changes: 3 additions & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { StepzenCliService } from './cli';
import { Logger, logger } from './logger';
import { ProjectResolver } from './projectResolver';
import { SchemaIndexService } from './SchemaIndexService';
import { RequestService } from './request';

/**
* Service registry for dependency injection of application services
Expand All @@ -12,6 +13,7 @@ export interface ServiceRegistry {
logger: Logger;
projectResolver: ProjectResolver;
schemaIndex: SchemaIndexService;
request: RequestService;
}

/**
Expand All @@ -22,6 +24,7 @@ export const services: ServiceRegistry = {
logger,
projectResolver: new ProjectResolver(logger),
schemaIndex: new SchemaIndexService(),
request: new RequestService(logger),
};

/**
Expand Down
Loading