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
75 changes: 41 additions & 34 deletions src/commands/executeStepZenRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ import { ValidationError, handleError } from "../errors";
* @param options.documentId Optional document ID for persisted document requests
* @param options.operationName Optional name of the operation to execute
* @param options.varArgs Optional variable arguments (--var, --var-file)
* @param options.auth Optional authorization info (admin or jwt)
* @returns Promise that resolves when execution completes
*/
export async function executeStepZenRequest(options: {
queryText?: string;
documentContent?: string;
operationName?: string;
varArgs?: string[];
auth?: { type: 'admin' | 'jwt', jwt?: string };
}): Promise<void> {
const { queryText, documentContent, operationName, varArgs = [] } = options;
const { queryText, documentContent, operationName, varArgs = [], auth } = options;

// Validate request options using the request service
try {
Expand All @@ -53,15 +55,33 @@ export async function executeStepZenRequest(options: {
const cfg = vscode.workspace.getConfiguration("stepzen");
const debugLevel = cfg.get<number>("request.debugLevel", 1);

// Prepare headers based on auth selection
let customHeaders: Record<string, string> = {};
let adminKey: string | undefined;
if (auth?.type === 'jwt') {
// Always need the admin key for debug header
adminKey = await services.request.getApiKey();
customHeaders = {
'Authorization': `Bearer ${auth.jwt}`,
'StepZen-Debug-Authorization': `apikey ${adminKey}`,
'stepzen-debug-level': String(debugLevel),
};
} else {
// Default: admin key in Authorization
adminKey = await services.request.getApiKey();
customHeaders = {
'Authorization': `Apikey ${adminKey}`,
'stepzen-debug-level': String(debugLevel),
};
}

// For persisted documents, we need to make an HTTP request directly
if (documentContent) {
try {
// Load endpoint configuration using the request service
const endpointConfig = await services.request.loadEndpointConfig(projectRoot);

// Parse variables using the request service
const { variables } = services.request.parseVariables(varArgs);

// Show a progress notification
await vscode.window.withProgress(
{
Expand All @@ -70,25 +90,23 @@ export async function executeStepZenRequest(options: {
cancellable: false
},
async () => {
// Execute the persisted document request using the request service
// Execute the persisted document request using the request service, passing custom headers
const result = await services.request.executePersistedDocumentRequest(
endpointConfig,
documentContent,
variables,
operationName
);

operationName,
customHeaders
);
// Process results
const rawDiags = (result.extensions?.stepzen?.diagnostics ?? []) as StepZenDiagnostic[];
services.logger.info("Processing diagnostics for persisted operation...");
const summaries = summariseDiagnostics(rawDiags);
publishDiagnostics(summaries, runtimeDiag);

services.logger.info("Persisted document request completed successfully");
await openResultsPanel(result);
}
);

return;
} catch (err: unknown) {
handleError(err);
Expand All @@ -104,48 +122,41 @@ export async function executeStepZenRequest(options: {

// Create a temp file for the query, which we'll need for Terminal mode
let tmpFile: string | undefined;

try {
// Terminal output mode with debug level 0
if (debugLevel === 0) {
try {
tmpFile = createTempGraphQLFile(queryText);
const term = vscode.window.createTerminal(UI.TERMINAL_NAME);
term.show();

// Build CLI command for terminal
const parts = [
"stepzen request",
`--file "${tmpFile}"`,
`--file \"${tmpFile}\"`,
];

// Add operation name if specified
// Log operation name
if (operationName) {
services.logger.info(`Using specified operation: "${operationName}"`);
services.logger.info(`Using specified operation: \"${operationName}\"`);
} else {
services.logger.debug('No operation name specified, letting StepZen select the default operation');
}

// Add debug level header - properly escape quotes for shell
parts.push('--header', `"stepzen-debug-level: ${debugLevel}"`);

// Add custom headers
for (const [key, value] of Object.entries(customHeaders)) {
parts.push('--header', `\"${key}: ${value}\"`);
}
// Add variable arguments
parts.push(...varArgs);

const cmd = parts.filter(Boolean).join(" ");
services.logger.info(`Executing StepZen request in terminal${operationName ? ` for operation "${operationName}"` : ' (anonymous operation)'}`);
services.logger.info(`Executing StepZen request in terminal${operationName ? ` for operation \"${operationName}\"` : ' (anonymous operation)'}`);
services.logger.debug(`Terminal command: ${cmd}`);
term.sendText(`cd "${projectRoot}" && ${cmd}`);

term.sendText(`cd \"${projectRoot}\" && ${cmd}`);
// Cleanup temp file later
cleanupLater(tmpFile);
} catch (err) {
handleError(err);
}
return;
}

// JSON result mode with progress notification
services.logger.info("Executing StepZen request with CLI service...");
await vscode.window.withProgress(
Expand All @@ -158,17 +169,15 @@ export async function executeStepZenRequest(options: {
try {
// 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}`);
services.logger.debug(`Calling CLI service with request${operationName ? ` for operation "${operationName}"` : ' (no operation specified)'}`);
const stdout = await services.cli.request(queryText, variables, operationName, debugLevel);
// Use the CLI service to execute the request, passing custom headers
services.logger.info(`Executing StepZen request${operationName ? ` for operation \"${operationName}\"` : ' (anonymous operation)'} with debug level ${debugLevel}`);
services.logger.debug(`Calling CLI service with request${operationName ? ` for operation \"${operationName}\"` : ' (no operation specified)'}`);
const stdout = await services.cli.request(queryText, variables, operationName, debugLevel, customHeaders);
services.logger.debug("Received response from StepZen CLI service");

let json: StepZenResponse;
try {
// Parse the response as JSON
services.logger.debug(`Parsing JSON response${operationName ? ` for operation "${operationName}"` : ''}`);
services.logger.debug(`Parsing JSON response${operationName ? ` for operation \"${operationName}\"` : ''}`);
json = JSON.parse(stdout) as StepZenResponse;
} catch (parseErr) {
throw new ValidationError(
Expand All @@ -177,15 +186,13 @@ export async function executeStepZenRequest(options: {
parseErr
);
}

// Process results
const rawDiags = (json.extensions?.stepzen?.diagnostics ?? []) as StepZenDiagnostic[];
services.logger.info("Processing diagnostics for file-based request...");
const summaries = summariseDiagnostics(rawDiags);
publishDiagnostics(summaries, runtimeDiag);

await openResultsPanel(json);
services.logger.info(`StepZen request completed successfully${operationName ? ` for operation "${operationName}"` : ''}`);
services.logger.info(`StepZen request completed successfully${operationName ? ` for operation \"${operationName}\"` : ''}`);
} catch (err) {
handleError(err);
// Clear any partial results
Expand Down
115 changes: 88 additions & 27 deletions src/commands/runRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,46 @@ async function collectVariableArgs(query: string, chosenOp?: string): Promise<st
}
}

// Key for persisting last auth selection and JWT
const JWT_STATE_KEY = 'stepzen.lastJwtToken';

/**
* Prompts the user to select the authorization type and (if needed) enter a JWT.
* Always prompts for the type, but prepopulates JWT with last-used value.
* Returns an object with the selected type and token (if JWT).
*/
async function promptForAuthorization(context: vscode.ExtensionContext): Promise<{ type: 'admin' | 'jwt', jwt?: string } | undefined> {
// Retrieve last JWT
const lastJwt = context.globalState.get<string>(JWT_STATE_KEY);

const options = [
{ label: 'Default (Admin Key)', value: 'admin', description: 'Use your StepZen admin API key (default)' },
{ label: 'Bearer Token (JWT)', value: 'jwt', description: 'Use a Bearer JWT for Authorization header' },
];

const pick = await vscode.window.showQuickPick(options, {
placeHolder: 'Select authorization type for this request',
canPickMany: false,
ignoreFocusOut: true,
});
if (!pick) {return undefined;}

if (pick.value === 'jwt') {
const jwt = await vscode.window.showInputBox({
prompt: 'Enter your Bearer JWT',
value: lastJwt || '',
password: true,
ignoreFocusOut: true,
});
if (!jwt) {return undefined;}
// Persist JWT
context.globalState.update(JWT_STATE_KEY, jwt);
return { type: 'jwt', jwt };
} else {
return { type: 'admin' };
}
}

/* -------------------------------------------------------------
* Common execution function with support for persisted documents
* ------------------------------------------------------------*/
Expand All @@ -137,7 +177,7 @@ async function collectVariableArgs(query: string, chosenOp?: string): Promise<st
* Runs the GraphQL request in the active editor
* If multiple operations are found, prompts the user to select one
*/
export async function runGraphQLRequest() {
export async function runGraphQLRequest(context: vscode.ExtensionContext) {
try {
services.logger.info("Starting Run GraphQL Request command");

Expand Down Expand Up @@ -193,11 +233,19 @@ export async function runGraphQLRequest() {
return; // user cancelled
}

// Execute using file-based approach
// Prompt for authorization type and JWT
const auth = await promptForAuthorization(context);
if (!auth) {
services.logger.info("Run GraphQL Request cancelled by user during auth selection");
return;
}

// Execute using file-based approach, passing auth info (to be used in next step)
await executeStepZenRequest({
queryText: query,
operationName,
varArgs
varArgs,
auth // <-- pass auth info for header construction
});

services.logger.info("Run GraphQL Request completed successfully");
Expand All @@ -210,9 +258,10 @@ export async function runGraphQLRequest() {
* Executes a specific operation from a GraphQL file
* Used by the "▶ Run" codelens button in the editor
*
* @param context The extension context
* @param operation The operation entry to run
*/
export async function runOperation(operation: OperationEntry) {
export async function runOperation(context: vscode.ExtensionContext, operation: OperationEntry) {
// Check workspace trust first
if (!vscode.workspace.isTrusted) {
vscode.window.showWarningMessage(MESSAGES.GRAPHQL_OPERATIONS_NOT_AVAILABLE_UNTRUSTED);
Expand All @@ -239,27 +288,33 @@ export async function runOperation(operation: OperationEntry) {
}
const content = document.getText();

// Validate operation range
if (!operation.range || typeof operation.range.start !== 'number' || typeof operation.range.end !== 'number') {
vscode.window.showErrorMessage(MESSAGES.INVALID_OPERATION_RANGE);
return;
}
// Validate operation range
if (!operation.range || typeof operation.range.start !== 'number' || typeof operation.range.end !== 'number') {
vscode.window.showErrorMessage(MESSAGES.INVALID_OPERATION_RANGE);
return;
}

// Extract just this operation's text based on range
const operationText = content.substring(operation.range.start, operation.range.end);

// Collect variable args for the operation
const varArgs = await collectVariableArgs(operationText, operation.name);
if (varArgs === undefined) {
return; // user cancelled
}

// Execute using file-based approach
await executeStepZenRequest({
queryText: operationText,
operationName: operation.name,
varArgs
});
// Extract just this operation's text based on range
const operationText = content.substring(operation.range.start, operation.range.end);

// Collect variable args for the operation
const varArgs = await collectVariableArgs(operationText, operation.name);
if (varArgs === undefined) {
return; // user cancelled
}

// Prompt for authorization type and JWT
const auth = await promptForAuthorization(context);
if (!auth) {
return;
}
// Execute using file-based approach
await executeStepZenRequest({
queryText: operationText,
operationName: operation.name,
varArgs,
auth
});
} catch (error: unknown) {
handleError(error);
}
Expand All @@ -269,10 +324,11 @@ export async function runOperation(operation: OperationEntry) {
* Executes a persisted operation using its document ID
* Used by the "▶ Run (persisted)" codelens button in the editor
*
* @param context The extension context
* @param documentId The persisted document ID
* @param operationName The name of the operation within the document
*/
export async function runPersisted(documentId: string, operationName: string) {
export async function runPersisted(context: vscode.ExtensionContext, documentId: string, operationName: string) {
// Check workspace trust first
if (!vscode.workspace.isTrusted) {
vscode.window.showWarningMessage(MESSAGES.PERSISTED_OPERATIONS_NOT_AVAILABLE_UNTRUSTED);
Expand Down Expand Up @@ -344,12 +400,17 @@ export async function runPersisted(documentId: string, operationName: string) {
if (varArgs === undefined) {
return; // user cancelled
}

// Prompt for authorization type and JWT
const auth = await promptForAuthorization(context);
if (!auth) {
return;
}
// Execute using persisted document approach with the full document content
await executeStepZenRequest({
documentContent: content,
operationName,
varArgs
varArgs,
auth
});
} catch (error: unknown) {
handleError(error);
Expand Down
10 changes: 4 additions & 6 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { safeRegisterCommand } from "./utils/safeRegisterCommand";
// Removed import - now using services.schemaIndex directly
import { StepZenCodeLensProvider } from "./utils/codelensProvider";
import { services } from "./services";
import { runGraphQLRequest } from "./commands/runRequest";


let stepzenTerminal: vscode.Terminal | undefined;
Expand Down Expand Up @@ -213,10 +214,7 @@ export async function activate(context: vscode.ExtensionContext) {
const { deployStepZen } = await import("./commands/deploy.js");
return deployStepZen();
}),
safeRegisterCommand(COMMANDS.RUN_REQUEST, async () => {
const { runGraphQLRequest } = await import("./commands/runRequest.js");
return runGraphQLRequest();
}),
safeRegisterCommand(COMMANDS.RUN_REQUEST, () => runGraphQLRequest(context)),
safeRegisterCommand(COMMANDS.OPEN_EXPLORER, async () => {
const { openQueryExplorer } = await import("./commands/openExplorer.js");
return openQueryExplorer(context);
Expand Down Expand Up @@ -260,11 +258,11 @@ export async function activate(context: vscode.ExtensionContext) {
}),
safeRegisterCommand(COMMANDS.RUN_OPERATION, async (...args: unknown[]) => {
const { runOperation } = await import("./commands/runRequest.js");
return runOperation(args[0] as any);
return runOperation(context, args[0] as any);
}),
safeRegisterCommand(COMMANDS.RUN_PERSISTED, async (...args: unknown[]) => {
const { runPersisted } = await import("./commands/runRequest.js");
return runPersisted(args[0] as string, args[1] as string);
return runPersisted(context, args[0] as string, args[1] as string);
}),
safeRegisterCommand(COMMANDS.CLEAR_RESULTS, async () => {
const { clearResults } = await import("./commands/runRequest.js");
Expand Down
Loading