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
168 changes: 168 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<!--
Copyright IBM Corp. 2025
Assisted by CursorAI
-->

# Schema Indexing Architecture

This document describes the modular architecture of the StepZen VS Code extension's schema indexing system.

## Overview

The schema indexing system has been refactored from a monolithic `stepzenProjectScanner.ts` file into a modular, service-oriented architecture. This improves maintainability, testability, and separation of concerns.

## Architecture Diagram

```
┌─────────────────────────────────────────────────────────────────┐
│ Extension Layer │
├─────────────────────────────────────────────────────────────────┤
│ Commands │ Panels │ Utils │ Language Features │
├─────────────────────────────────────────────────────────────────┤
│ Service Registry │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ SchemaIndexSvc │ │ ProjectResolver │ │ StepzenCliSvc │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Schema Processing Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ SchemaIndexer │ │ SchemaLinker │ │ SchemaParser │ │
│ │ (indexer.ts) │ │ (linker.ts) │ │ (parser.ts) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ GraphQL & File System │
└─────────────────────────────────────────────────────────────────┘
```

## Module Structure

### Service Layer

#### `services/SchemaIndexService.ts`

- **Purpose**: High-level orchestration service for schema indexing
- **Responsibilities**:
- Coordinates the scanning process
- Provides progress notifications to users
- Exposes public API for schema information retrieval
- Manages the lifecycle of indexing operations

#### `services/index.ts`

- **Purpose**: Service registry for dependency injection
- **Responsibilities**:
- Maintains singleton instances of all services
- Provides service override capabilities for testing
- Acts as the central service locator

### Schema Processing Layer

#### `services/schema/indexer.ts`

- **Purpose**: Core indexing logic for GraphQL schema definitions
- **Responsibilities**:
- Builds definition indexes from parsed GraphQL documents
- Tracks root operations (Query, Mutation, Subscription fields)
- Maintains field indexes for all types
- Processes SDL executables and persisted documents
- Manages type relationships and directives

#### `services/schema/linker.ts`

- **Purpose**: SDL include directive traversal
- **Responsibilities**:
- Safely traverses `@sdl(files: [...])` directives
- Prevents infinite loops in file inclusion
- Returns ordered list of all schema files to process

#### `services/schema/parser.ts`

- **Purpose**: GraphQL AST utility functions
- **Responsibilities**:
- Type unwrapping (removing NonNull/List wrappers)
- Full type string generation with nullability
- List type detection
- Scalar type identification

### Migration Complete

The original `utils/stepzenProjectScanner.ts` compatibility layer has been removed. All components now use the new service architecture directly through `services.schemaIndex`.

## Data Flow

1. **Initialization**: Extension activates and creates service instances
2. **Project Detection**: ProjectResolver finds StepZen configuration
3. **Schema Scanning**: SchemaIndexService orchestrates the process:
- SchemaLinker traverses SDL includes to find all files
- Each file is parsed using GraphQL parser
- SchemaIndexer processes each document to build indexes
- SDL executables are scanned for operation metadata
4. **Information Retrieval**: Commands and features query the service for:
- Symbol definitions (go-to-definition)
- Root operations (code lens, request execution)
- Type relationships (visualization)
- Persisted documents (request execution)

## Key Benefits

### Modularity

- Each module has a single, well-defined responsibility
- Dependencies are explicit and injected
- Easy to understand and modify individual components

### Testability

- Services can be mocked independently
- Each module can be unit tested in isolation
- Service registry supports test overrides

### Maintainability

- Clear separation of concerns
- Reduced coupling between components
- Easier to add new features or modify existing ones

### Performance

- Efficient file traversal with loop detection
- Incremental indexing capabilities
- Optimized data structures for fast lookups

## Migration Strategy

The refactoring has been completed in phases:

1. **Phase 1** ✅ **Complete**: New architecture with compatibility layer

- Created modular service architecture
- Maintained backward compatibility
- All tests remained green

2. **Phase 2** ✅ **Complete**: Direct service usage

- Migrated all commands and features to use services directly
- Updated imports across the codebase
- Removed dependency on compatibility layer

3. **Phase 3** ✅ **Complete**: Cleanup
- Removed compatibility layer entirely
- Full migration to service-oriented architecture achieved

## Testing Strategy

- **Unit Tests**: Each module tested independently with mocked dependencies
- **Integration Tests**: Service interactions tested with real implementations
- **Compatibility Tests**: Ensure backward compatibility is maintained
- **Performance Tests**: Verify indexing performance with large schemas

## Error Handling

- Consistent error handling across all modules
- Graceful degradation when files are missing or malformed
- Detailed logging for debugging and troubleshooting
- User-friendly progress notifications and error messages

---

_Portions of the Content may be generated with the assistance of CursorAI_
6 changes: 3 additions & 3 deletions src/commands/addMaterializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import * as vscode from 'vscode';
import { getRootOperationsMap } from '../utils/stepzenProjectScanner';
// Removed import - now using services.schemaIndex directly
import { services } from '../services';
import { handleError } from '../errors';

Expand Down Expand Up @@ -50,7 +50,7 @@ export async function addMaterializer() {
const baseType = declaredType.replace(/[[\]!]/g, ''); // Remove [] and ! characters

// Find matching root operations (queries) that return the same type
const ops = Object.entries(getRootOperationsMap()).filter(([_opName, info]) => {
const ops = Object.entries(services.schemaIndex.getRootOperations()).filter(([_opName, info]) => {
return info.returnType === baseType && info.isList === isList;
});

Expand Down Expand Up @@ -79,7 +79,7 @@ export async function addMaterializer() {
services.logger.info(`Selected operation: ${chosen}`);

// Get argument names
const argNames = getRootOperationsMap()[chosen].args;
const argNames = services.schemaIndex.getRootOperations()[chosen].args;

// Build directive snippet with proper indentation
const indentUnit = editor.options.insertSpaces
Expand Down
13 changes: 4 additions & 9 deletions src/commands/generateOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs";
import {
getRootOperationsMap,
getFieldIndex,
scanStepZenProject,
ArgInfo,
} from "../utils/stepzenProjectScanner";
import type { ArgInfo } from "../services/schema/indexer";
import { resolveStepZenProjectRoot } from "../utils/stepzenProject";
import { services } from "../services";
import { FILE_PATTERNS, GRAPHQL, MESSAGES, TEMP_FILE_PATTERNS } from "../utils/constants";
Expand Down Expand Up @@ -58,11 +53,11 @@ export async function generateOperations() {
}

// Scan the project to get the latest schema information
await scanStepZenProject(indexPath);
await services.schemaIndex.scan(indexPath);

// Get the root operations and field index
const rootOps = getRootOperationsMap();
const fieldIdx = getFieldIndex();
const rootOps = services.schemaIndex.getRootOperations();
const fieldIdx = services.schemaIndex.getFieldIndex();

// Debug logging
services.logger.debug(
Expand Down
4 changes: 2 additions & 2 deletions src/commands/goToDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import * as vscode from "vscode";
import { findDefinition } from "../utils/stepzenProjectScanner";
// Removed import - now using services.schemaIndex directly
import { services } from "../services";
import { handleError } from "../errors";

Expand Down Expand Up @@ -38,7 +38,7 @@ export async function goToDefinition() {
const token = document.getText(wordRange);
services.logger.info(`Searching for definition of symbol: "${token}"`);

const locations = findDefinition(token);
const locations = services.schemaIndex.findDefinition(token);
if (!locations || locations.length === 0) {
vscode.window.showWarningMessage(`No definition found for "${token}".`);
services.logger.warn(`No definition found for "${token}".`);
Expand Down
4 changes: 2 additions & 2 deletions src/commands/runRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as vscode from "vscode";
import { parse, NamedTypeNode, OperationDefinitionNode } from "graphql";
import { clearResultsPanel } from "../panels/resultsPanel";
import { runtimeDiag } from "../extension";
import { getPersistedDocMap, OperationEntry } from "../utils/stepzenProjectScanner";
import type { OperationEntry } from "../services/schema/indexer";
import { services } from "../services";
// Import executeStepZenRequest from separate file
import { executeStepZenRequest } from "./executeStepZenRequest";
Expand Down Expand Up @@ -316,7 +316,7 @@ export async function runPersisted(documentId: string, operationName: string) {
return;
}

const persistedDocMap = getPersistedDocMap();
const persistedDocMap = services.schemaIndex.getPersistedDocMap();
if (!persistedDocMap) {
vscode.window.showErrorMessage("Persisted document map is not available");
return;
Expand Down
10 changes: 5 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as fs from "fs";
import { StepZenError, handleError } from "./errors";
import { UI, FILE_PATTERNS, COMMANDS, CONFIG_KEYS, MESSAGES, LANGUAGE_IDS } from "./utils/constants";
import { safeRegisterCommand } from "./utils/safeRegisterCommand";
import { scanStepZenProject, computeHash } from "./utils/stepzenProjectScanner";
// Removed import - now using services.schemaIndex directly
import { StepZenCodeLensProvider } from "./utils/codelensProvider";
import { services } from "./services";

Expand Down Expand Up @@ -95,9 +95,9 @@ async function initialiseFor(folder: vscode.WorkspaceFolder) {
);

const fullSDL = contents.join('\n');
lastSchemaHash = computeHash(fullSDL);
lastSchemaHash = services.schemaIndex.computeHash(fullSDL);

await scanStepZenProject(indexPath);
await services.schemaIndex.scan(indexPath);
} catch (err) {
const error = new StepZenError(
"Initial project scan failed",
Expand Down Expand Up @@ -144,7 +144,7 @@ async function initialiseFor(folder: vscode.WorkspaceFolder) {
);

const fullSDL = contents.join('\n');
const currentHash = computeHash(fullSDL);
const currentHash = services.schemaIndex.computeHash(fullSDL);

// Skip parsing if schema hasn't changed
if (lastSchemaHash && lastSchemaHash === currentHash) {
Expand All @@ -153,7 +153,7 @@ async function initialiseFor(folder: vscode.WorkspaceFolder) {
}

services.logger.info(`Rescanning project after change in ${uri.fsPath}`);
await scanStepZenProject(indexPath);
await services.schemaIndex.scan(indexPath);

// Update hash after successful scan
lastSchemaHash = currentHash;
Expand Down
18 changes: 7 additions & 11 deletions src/panels/schemaVisualizerPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@
// src/panels/schemaVisualizerPanel.ts
import * as vscode from "vscode";
import { Uri } from "vscode";
import {
getFieldIndex,
getTypeDirectives,
getTypeRelationships,
scanStepZenProject,
import type {
FieldInfo,
DirectiveInfo,
TypeRelationship,
} from "../utils/stepzenProjectScanner";
} from "../services/schema/indexer";
import { services } from "../services";
import { resolveStepZenProjectRoot } from "../utils/stepzenProject";
import * as path from "path";
Expand Down Expand Up @@ -206,7 +202,7 @@ export async function openSchemaVisualizerPanel(
* @returns true if schema data was successfully loaded, false otherwise
*/
async function ensureSchemaDataLoaded(): Promise<boolean> {
const fieldIndex = getFieldIndex();
const fieldIndex = services.schemaIndex.getFieldIndex();

// If we already have schema data, return true
if (Object.keys(fieldIndex).length > 0) {
Expand Down Expand Up @@ -248,7 +244,7 @@ async function ensureSchemaDataLoaded(): Promise<boolean> {

// Scan the project
services.logger.info(`Scanning StepZen project at ${indexPath}`);
await scanStepZenProject(indexPath);
await services.schemaIndex.scan(indexPath);
services.logger.debug("Schema scan completed successfully");
return true;
} catch (error) {
Expand All @@ -264,9 +260,9 @@ async function ensureSchemaDataLoaded(): Promise<boolean> {
* @returns A complete schema model for visualization
*/
function buildSchemaModel(): SchemaVisualizerModel {
const fieldIndex = getFieldIndex();
const typeDirectives = getTypeDirectives();
const relationships = getTypeRelationships();
const fieldIndex = services.schemaIndex.getFieldIndex();
const typeDirectives = services.schemaIndex.getTypeDirectives();
const relationships = services.schemaIndex.getTypeRelationships();

const model: SchemaVisualizerModel = {
types: {},
Expand Down
Loading