Idea logo Idea

This tutorial demonstrates how to create a plugin that generates GraphQL type definitions from .idea schema files. The plugin will transform your schema models, types, and enums into proper GraphQL schema definitions.

  1. Overview
  2. Prerequisites
  3. Plugin Structure
  4. Implementation
  5. Schema Configuration
  6. Usage Examples
  7. Advanced Features
  8. Best Practices
  9. Troubleshooting

1. Overview

GraphQL is a query language and runtime for APIs that provides a complete and understandable description of the data in your API. This plugin transforms your .idea schema definitions into comprehensive GraphQL type definitions that enable type-safe API development with excellent tooling support.

This plugin generates GraphQL type definitions from your .idea schema, including:

  • Types: GraphQL object types from schema models
  • Inputs: GraphQL input types for mutations
  • Enums: GraphQL enum types from schema enums
  • Scalars: Custom scalar types when needed
  • Queries and Mutations: Basic CRUD operations

2. Prerequisites

Before implementing the GraphQL schema generator plugin, ensure you have the necessary development environment and knowledge. This section covers the essential requirements for successful plugin creation and GraphQL integration.

  • Node.js 16+ and npm/yarn
  • Basic understanding of GraphQL
  • Familiarity with the @stackpress/idea-transformer library
  • Understanding of .idea schema format

3. Plugin Structure

The plugin structure defines the core architecture and configuration interface for the GraphQL schema generator. This includes the main plugin function, configuration types, and the overall organization of the generated GraphQL schema definitions.

import type { PluginProps } from '@stackpress/idea-transformer/types';
import fs from 'fs/promises';
import path from 'path';

interface GraphQLConfig {
  output: string;
  includeQueries?: boolean;
  includeMutations?: boolean;
  includeSubscriptions?: boolean;
  customScalars?: Record<string, string>;
  generateInputTypes?: boolean;
}

export default async function generateGraphQLSchema(
  props: PluginProps<{ config: GraphQLConfig }>
) {
  const { config, schema, transformer } = props;
  
  // Implementation here...
}

4. Implementation

The implementation section covers the core plugin function and supporting utilities that handle GraphQL schema generation. This includes configuration validation, content generation, file writing, and error handling throughout the generation process.

4.1. Core Plugin Function

The core plugin function serves as the main entry point for GraphQL schema generation. It orchestrates the entire process from configuration validation through content generation to file output, ensuring proper error handling and logging throughout.

export default async function generateGraphQLSchema(
  props: PluginProps<{ config: GraphQLConfig }>
) {
  const { config, schema, transformer } = props;
  
  try {
    // Validate configuration
    if (!config.output) {
      throw new Error('GraphQL plugin requires "output" configuration');
    }
    
    // Generate GraphQL schema
    let schemaContent = '';
    
    // Add custom scalars
    schemaContent += generateCustomScalars(config.customScalars || {});
    
    // Generate enums
    if (schema.enum) {
      schemaContent += generateEnums(schema.enum);
    }
    
    // Generate types
    if (schema.model) {
      schemaContent += generateTypes(schema.model);
      
      if (config.generateInputTypes) {
        schemaContent += generateInputTypes(schema.model);
      }
    }
    
    // Generate custom types
    if (schema.type) {
      schemaContent += generateCustomTypes(schema.type);
    }
    
    // Generate root types
    if (config.includeQueries) {
      schemaContent += generateQueries(schema.model || {});
    }
    
    if (config.includeMutations) {
      schemaContent += generateMutations(schema.model || {});
    }
    
    if (config.includeSubscriptions) {
      schemaContent += generateSubscriptions(schema.model || {});
    }
    
    // Write to output file
    const outputPath = await transformer.loader.absolute(config.output);
    await fs.mkdir(path.dirname(outputPath), { recursive: true });
    await fs.writeFile(outputPath, schemaContent, 'utf8');
    
    console.log(`✅ GraphQL schema generated: ${outputPath}`);
    
  } catch (error) {
    console.error('❌ GraphQL schema generation failed:', error.message);
    throw error;
  }
}

4.2. Type Mapping Functions

Type mapping functions handle the conversion of .idea schema types to their GraphQL equivalents. These functions ensure proper type safety and handle complex scenarios like arrays, required fields, and custom scalar types.

function mapSchemaTypeToGraphQL(schemaType: string, customScalars: Record<string, string> = {}): string {
  // Check for custom scalar mappings first
  if (customScalars[schemaType]) {
    return customScalars[schemaType];
  }
  
  // Standard type mappings
  const typeMap: Record<string, string> = {
    'String': 'String',
    'Number': 'Float',
    'Integer': 'Int',
    'Boolean': 'Boolean',
    'Date': 'DateTime',
    'JSON': 'JSON',
    'ID': 'ID'
  };
  
  return typeMap[schemaType] || schemaType;
}

function formatFieldType(column: any, customScalars: Record<string, string> = {}): string {
  let type = mapSchemaTypeToGraphQL(column.type, customScalars);
  
  // Handle arrays
  if (column.multiple) {
    type = `[${type}]`;
  }
  
  // Handle required fields
  if (column.required) {
    type += '!';
  }
  
  return type;
}

4.3. Schema Generation Functions

Schema generation functions create specific parts of the GraphQL schema including custom scalars, enums, types, input types, and root operation types. These functions handle proper GraphQL syntax construction and type relationships.

function generateCustomScalars(customScalars: Record<string, string>): string {
  if (Object.keys(customScalars).length === 0) {
    return `# Custom Scalars
scalar DateTime
scalar JSON

`;
  }
  
  let content = '# Custom Scalars\n';
  content += 'scalar DateTime\n';
  content += 'scalar JSON\n';
  
  for (const [name, description] of Object.entries(customScalars)) {
    content += `scalar ${name}\n`;
  }
  
  return content + '\n';
}

function generateEnums(enums: Record<string, any>): string {
  let content = '# Enums\n';
  
  for (const [enumName, enumDef] of Object.entries(enums)) {
    content += `enum ${enumName} {\n`;
    
    for (const [key, value] of Object.entries(enumDef)) {
      content += `  ${key}\n`;
    }
    
    content += '}\n\n';
  }
  
  return content;
}

function generateTypes(models: Record<string, any>): string {
  let content = '# Types\n';
  
  for (const [modelName, model] of Object.entries(models)) {
    content += `type ${modelName} {\n`;
    
    for (const column of model.columns || []) {
      const fieldType = formatFieldType(column);
      content += `  ${column.name}: ${fieldType}\n`;
    }
    
    content += '}\n\n';
  }
  
  return content;
}

function generateInputTypes(models: Record<string, any>): string {
  let content = '# Input Types\n';
  
  for (const [modelName, model] of Object.entries(models)) {
    // Create input type
    content += `input ${modelName}Input {\n`;
    
    for (const column of model.columns || []) {
      // Skip auto-generated fields like ID for input types
      if (column.attributes?.id) continue;
      
      let fieldType = formatFieldType(column);
      // Remove required constraint for input types (make them optional)
      fieldType = fieldType.replace('!', '');
      
      content += `  ${column.name}: ${fieldType}\n`;
    }
    
    content += '}\n\n';
    
    // Create update input type
    content += `input ${modelName}UpdateInput {\n`;
    
    for (const column of model.columns || []) {
      let fieldType = formatFieldType(column);
      // All fields are optional in update input
      fieldType = fieldType.replace('!', '');
      
      content += `  ${column.name}: ${fieldType}\n`;
    }
    
    content += '}\n\n';
  }
  
  return content;
}

function generateCustomTypes(types: Record<string, any>): string {
  let content = '# Custom Types\n';
  
  for (const [typeName, typeDef] of Object.entries(types)) {
    content += `type ${typeName} {\n`;
    
    for (const column of typeDef.columns || []) {
      const fieldType = formatFieldType(column);
      content += `  ${column.name}: ${fieldType}\n`;
    }
    
    content += '}\n\n';
  }
  
  return content;
}

function generateQueries(models: Record<string, any>): string {
  let content = '# Queries\ntype Query {\n';
  
  for (const [modelName, model] of Object.entries(models)) {
    const lowerName = modelName.toLowerCase();
    
    // Get single item
    content += `  ${lowerName}(id: ID!): ${modelName}\n`;
    
    // Get multiple items
    content += `  ${lowerName}s(limit: Int, offset: Int): [${modelName}]\n`;
  }
  
  content += '}\n\n';
  return content;
}

function generateMutations(models: Record<string, any>): string {
  let content = '# Mutations\ntype Mutation {\n';
  
  for (const [modelName, model] of Object.entries(models)) {
    const lowerName = modelName.toLowerCase();
    
    // Create
    content += `  create${modelName}(input: ${modelName}Input!): ${modelName}\n`;
    
    // Update
    content += `  update${modelName}(id: ID!, input: ${modelName}UpdateInput!): ${modelName}\n`;
    
    // Delete
    content += `  delete${modelName}(id: ID!): Boolean\n`;
  }
  
  content += '}\n\n';
  return content;
}

function generateSubscriptions(models: Record<string, any>): string {
  let content = '# Subscriptions\ntype Subscription {\n';
  
  for (const [modelName, model] of Object.entries(models)) {
    const lowerName = modelName.toLowerCase();
    
    // Subscribe to changes
    content += `  ${lowerName}Created: ${modelName}\n`;
    content += `  ${lowerName}Updated: ${modelName}\n`;
    content += `  ${lowerName}Deleted: ID\n`;
  }
  
  content += '}\n\n';
  return content;
}

5. Schema Configuration

Schema configuration demonstrates how to integrate the GraphQL schema generator into your .idea schema files. This section covers plugin configuration options and their effects on the generated GraphQL schema definitions.

Add the GraphQL plugin to your .idea schema file:

plugin "./plugins/graphql-schema.js" {
  output "./generated/schema.graphql"
  includeQueries true
  includeMutations true
  includeSubscriptions false
  generateInputTypes true
  customScalars {
    Email "String"
    URL "String"
    PhoneNumber "String"
  }
}

Configuration Options

Configuration options control how GraphQL schema definitions are generated, including operation types, input generation, and custom scalar handling. Understanding these options helps you customize the plugin to meet your specific GraphQL requirements.

OptionTypeDefaultDescription
includeQueriesbooleanfalseGenerate Query type with CRUD operations
includeMutationsbooleanfalseGenerate Mutation type with CRUD operations
includeSubscriptionsbooleanfalseGenerate Subscription type for real-time updates
generateInputTypesbooleantrueGenerate input types for mutations
customScalarsobject{}Custom scalar type mappings

6. Usage Examples

Usage examples demonstrate practical applications of the GraphQL schema generator with real-world scenarios. These examples show how to configure the plugin for different use cases and how the generated GraphQL schemas integrate into development workflows.

6.1. Basic Schema

A basic schema example shows the fundamental structure needed to generate GraphQL type definitions. This includes model definitions with proper attributes, enum declarations, and plugin configuration that produces comprehensive GraphQL schemas.

enum UserRole {
  ADMIN "admin"
  USER "user"
  GUEST "guest"
}

model User {
  id String @id @default("nanoid()")
  email String @unique @required
  name String @required
  role UserRole @default("USER")
  active Boolean @default(true)
  createdAt Date @default("now()")
}

plugin "./plugins/graphql-schema.js" {
  output "./schema.graphql"
  includeQueries true
  includeMutations true
}

6.2. Generated Output

The generated output demonstrates the GraphQL schema produced by the plugin from the basic schema example. This shows how schema definitions are transformed into proper GraphQL type definitions with full type safety and operation support.

# Custom Scalars
scalar DateTime
scalar JSON

# Enums
enum UserRole {
  ADMIN
  USER
  GUEST
}

# Types
type User {
  id: ID!
  email: String!
  name: String!
  role: UserRole!
  active: Boolean!
  createdAt: DateTime!
}

# Input Types
input UserInput {
  email: String
  name: String
  role: UserRole
  active: Boolean
}

input UserUpdateInput {
  email: String
  name: String
  role: UserRole
  active: Boolean
  createdAt: DateTime
}

# Queries
type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User]
}

# Mutations
type Mutation {
  createUser(input: UserInput!): User
  updateUser(id: ID!, input: UserUpdateInput!): User
  deleteUser(id: ID!): Boolean
}

7. Advanced Features

Advanced features extend the basic GraphQL schema generation with sophisticated type handling, relationship management, directive support, and custom scalar types. These features enable production-ready GraphQL schemas that handle complex requirements.

7.1. Custom Scalar Types

Custom scalar types allow you to define specialized data types that map to specific validation or formatting requirements. This feature enables the creation of domain-specific types that enhance type safety and API clarity.

// In your plugin configuration
customScalars: {
  Email: "String",
  URL: "String", 
  PhoneNumber: "String",
  BigInt: "String"
}

7.2. Relationship Handling

Relationship handling manages references between different types and models in your schema. This ensures that type relationships are properly represented in the generated GraphQL schema with correct type references and nullability handling.

function handleRelationships(column: any, models: Record<string, any>): string {
  // Check if the column type is another model
  if (models[column.type]) {
    let type = column.type;
    
    if (column.multiple) {
      type = `[${type}]`;
    }
    
    if (column.required) {
      type += '!';
    }
    
    return type;
  }
  
  return formatFieldType(column);
}

7.3. Directive Support

Directive support enables the addition of GraphQL directives to fields and types, providing metadata and behavior hints for GraphQL servers and tools. This feature enhances schema expressiveness and enables advanced GraphQL features.

function generateDirectives(column: any): string {
  const directives: string[] = [];
  
  if (column.attributes?.unique) {
    directives.push('@unique');
  }
  
  if (column.attributes?.deprecated) {
    directives.push('@deprecated(reason: "Use alternative field")');
  }
  
  return directives.length > 0 ? ` ${directives.join(' ')}` : '';
}

8. Best Practices

Best practices ensure your generated GraphQL schemas are maintainable, performant, and follow GraphQL conventions. These guidelines cover type safety, error handling, configuration validation, and performance optimization.

8.1. Type Safety

Type safety is crucial for preventing runtime errors and ensuring reliable GraphQL schema generation. Always validate input data and use proper TypeScript types throughout the plugin implementation to ensure consistent output.

interface GraphQLColumn {
  name: string;
  type: string;
  required: boolean;
  multiple: boolean;
  attributes?: Record<string, any>;
}

function validateColumn(column: any): column is GraphQLColumn {
  return (
    typeof column.name === 'string' &&
    typeof column.type === 'string' &&
    typeof column.required === 'boolean'
  );
}

8.2. Error Handling

Proper error handling ensures that schema generation failures provide clear, actionable feedback to developers. Implement comprehensive error handling patterns and meaningful error messages to improve the debugging experience.

function generateTypes(models: Record<string, any>): string {
  try {
    let content = '# Types\n';
    
    for (const [modelName, model] of Object.entries(models)) {
      if (!model.columns || !Array.isArray(model.columns)) {
        console.warn(`⚠️  Model ${modelName} has no valid columns`);
        continue;
      }
      
      content += generateSingleType(modelName, model);
    }
    
    return content;
  } catch (error) {
    throw new Error(`Failed to generate GraphQL types: ${error.message}`);
  }
}

8.3. Configuration Validation

Configuration validation ensures that plugin settings are correct and complete before schema generation begins. This prevents runtime errors and provides early feedback about configuration issues.

function validateConfig(config: any): asserts config is GraphQLConfig {
  if (!config.output || typeof config.output !== 'string') {
    throw new Error('GraphQL plugin requires "output" configuration as string');
  }
  
  if (config.customScalars && typeof config.customScalars !== 'object') {
    throw new Error('customScalars must be an object');
  }
}

8.4. Performance Optimization

Performance optimization techniques help maintain reasonable generation times when working with large schemas. Implement caching strategies and efficient algorithms to ensure the plugin scales well with complex type hierarchies.

// Cache type mappings
const typeCache = new Map<string, string>();

function getCachedType(schemaType: string, customScalars: Record<string, string>): string {
  const cacheKey = `${schemaType}:${JSON.stringify(customScalars)}`;
  
  if (typeCache.has(cacheKey)) {
    return typeCache.get(cacheKey)!;
  }
  
  const mappedType = mapSchemaTypeToGraphQL(schemaType, customScalars);
  typeCache.set(cacheKey, mappedType);
  
  return mappedType;
}

9. Troubleshooting

This section addresses common issues encountered when generating GraphQL schemas and provides solutions for debugging and resolving problems. Understanding these troubleshooting techniques helps ensure reliable schema generation.

9.1. Common Issues

Common issues include invalid GraphQL identifiers, circular dependencies, and missing required fields. These problems typically arise from schema complexity or naming conflicts that can be resolved with proper validation and sanitization.

9.1.1. Invalid GraphQL Names

Invalid GraphQL names occur when schema identifiers contain characters that are not valid in GraphQL. The plugin should validate and sanitize names to ensure they conform to GraphQL naming conventions.

   function sanitizeGraphQLName(name: string): string {
     // GraphQL names must match /^[_A-Za-z][_0-9A-Za-z]*$/
     return name.replace(/[^_A-Za-z0-9]/g, '_').replace(/^[0-9]/, '_%samp;');
   }

9.1.2. Circular Dependencies

Circular dependencies can cause infinite loops during generation or invalid GraphQL schemas. Detecting and handling these scenarios is essential for robust schema generation, especially with complex type relationships.

   function detectCircularDependencies(models: Record<string, any>): string[] {
     const visited = new Set<string>();
     const recursionStack = new Set<string>();
     const cycles: string[] = [];
     
     // Implementation for cycle detection...
     
     return cycles;
   }

9.1.3. Missing Required Fields

Missing required fields can result in invalid GraphQL types that fail validation. Ensure all models have proper field definitions and handle edge cases where schema definitions might be incomplete.

   function validateRequiredFields(model: any): void {
     if (!model.columns || model.columns.length === 0) {
       throw new Error(`Model must have at least one column`);
     }
   }

9.2. Debugging Tips

Debugging tips help identify and resolve issues during GraphQL schema generation. These techniques provide visibility into the generation process and help diagnose problems with schema logic or output formatting.

9.2.1. Enable Verbose Logging

Verbose logging provides detailed information about the schema generation process, helping identify where issues occur and what data is being processed at each step.

   const DEBUG = process.env.DEBUG === 'true';
   
   function debugLog(message: string, data?: any) {
     if (DEBUG) {
       console.log(`[GraphQL Plugin] ${message}`, data || '');
     }
   }

9.2.2. Validate Generated Schema

Validating the generated GraphQL schema ensures that the output is syntactically correct and will work with GraphQL servers and tools. This validation step catches generation errors before deployment.

   import { buildSchema } from 'graphql';
   
   function validateGeneratedSchema(schemaContent: string): void {
     try {
       buildSchema(schemaContent);
       console.log('✅ Generated GraphQL schema is valid');
     } catch (error) {
       throw new Error(`Invalid GraphQL schema: ${error.message}`);
     }
   }

This tutorial provides a comprehensive foundation for creating GraphQL schema generators from .idea files. The generated schemas can be used with any GraphQL server implementation like Apollo Server, GraphQL Yoga, or others.