24 GraphQL Integration

GraphQL bietet eine moderne, flexible Alternative zu REST APIs durch seine typisierte Query-Sprache und effiziente Datenabfrage. NestJS integriert GraphQL nahtlos mit TypeScript und bietet sowohl Schema-First als auch Code-First Ansätze. Dieses Kapitel zeigt die umfassende Integration von GraphQL in NestJS-Anwendungen mit fortgeschrittenen Features wie DataLoader, Subscriptions und Authentication.

24.1 GraphQL Setup in NestJS

NestJS unterstützt GraphQL durch das @nestjs/graphql Paket mit Apollo Server Integration und bietet sowohl Schema-First als auch Code-First Entwicklungsansätze.

24.1.1 Grundlegende GraphQL Konfiguration

import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';

@Module({
  imports: [
    // TypeORM Konfiguration
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'password',
      database: 'graphql_demo',
      autoLoadEntities: true,
      synchronize: process.env.NODE_ENV !== 'production',
    }),
    
    // GraphQL Code-First Konfiguration
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
      
      // Development Features
      playground: process.env.NODE_ENV !== 'production',
      introspection: true,
      
      // Context für Authentication
      context: ({ req, res }) => ({
        req,
        res,
        user: req.user,
      }),
      
      // Formatters für bessere Error Handling
      formatError: (error) => {
        console.error('GraphQL Error:', error);
        
        // Don't expose internal errors in production
        if (process.env.NODE_ENV === 'production') {
          return {
            message: error.message,
            code: error.extensions?.code,
            path: error.path,
          };
        }
        
        return error;
      },
      
      // Performance Optimierungen
      includeStacktraceInErrorResponses: process.env.NODE_ENV !== 'production',
      
      // CORS für Frontend Integration
      cors: {
        origin: process.env.FRONTEND_URL || 'http://localhost:3000',
        credentials: true,
      },
      
      // Upload Support
      uploads: {
        maxFileSize: 10000000, // 10MB
        maxFiles: 5,
      },
    }),
  ],
})
export class AppModule {}

24.1.2 Environment-spezifische Konfiguration

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GqlOptionsFactory, GqlModuleOptions } from '@nestjs/graphql';
import { ApolloDriver } from '@nestjs/apollo';
import { join } from 'path';

@Injectable()
export class GraphQLConfigService implements GqlOptionsFactory {
  constructor(private configService: ConfigService) {}

  createGqlOptions(): GqlModuleOptions {
    const isProduction = this.configService.get('NODE_ENV') === 'production';
    const isDevelopment = this.configService.get('NODE_ENV') === 'development';

    return {
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      sortSchema: true,
      
      // Environment-specific settings
      playground: !isProduction,
      introspection: !isProduction,
      debug: isDevelopment,
      
      // Context with enhanced features
      context: ({ req, res, connection }) => {
        // HTTP Request Context
        if (req) {
          return {
            req,
            res,
            user: req.user,
            headers: req.headers,
            correlationId: req.headers['x-correlation-id'],
          };
        }
        
        // WebSocket Context (for subscriptions)
        if (connection) {
          return {
            user: connection.context.user,
            correlationId: connection.context.correlationId,
          };
        }
      },
      
      // Subscription Configuration
      subscriptions: {
        'graphql-ws': {
          onConnect: (context) => {
            const { connectionParams, extra } = context;
            
            // Authentication for WebSocket connections
            return this.authenticateWebSocketConnection(connectionParams, extra);
          },
          onDisconnect: (context) => {
            console.log('WebSocket disconnected:', context);
          },
        },
      },
      
      // Error Handling
      formatError: (error) => this.formatGraphQLError(error),
      
      // Performance Monitoring
      plugins: [
        {
          requestDidStart() {
            return {
              willSendResponse(requestContext) {
                const { request, response } = requestContext;
                
                // Log slow queries
                if (requestContext.metrics?.executionTime > 1000) {
                  console.warn('Slow GraphQL Query:', {
                    query: request.query,
                    variables: request.variables,
                    executionTime: requestContext.metrics.executionTime,
                  });
                }
                
                // Add performance headers
                response.http.setHeader('X-Execution-Time', 
                  requestContext.metrics?.executionTime || 0);
              },
            };
          },
        },
      ],
      
      // Cache Control
      cache: isProduction ? 'bounded' : false,
      
      // Field Resolution
      fieldResolverEnhancers: ['interceptors', 'guards', 'filters'],
    };
  }

  private async authenticateWebSocketConnection(
    connectionParams: any,
    extra: any
  ): Promise<any> {
    try {
      const token = connectionParams?.authorization?.replace('Bearer ', '');
      
      if (!token) {
        throw new Error('No authorization token provided');
      }
      
      // Validate JWT token
      const user = await this.validateJwtToken(token);
      
      return {
        user,
        correlationId: connectionParams?.correlationId || `ws_${Date.now()}`,
      };
    } catch (error) {
      throw new Error('Authentication failed');
    }
  }

  private formatGraphQLError(error: any): any {
    const isProduction = this.configService.get('NODE_ENV') === 'production';
    
    // Log all errors
    console.error('GraphQL Error:', {
      message: error.message,
      path: error.path,
      source: error.source?.body,
      positions: error.positions,
      originalError: error.originalError,
    });
    
    // Production error formatting
    if (isProduction) {
      // Don't expose sensitive information
      return {
        message: error.message,
        code: error.extensions?.code || 'INTERNAL_ERROR',
        path: error.path,
        timestamp: new Date().toISOString(),
      };
    }
    
    // Development error formatting
    return {
      message: error.message,
      code: error.extensions?.code,
      path: error.path,
      locations: error.locations,
      stack: error.stack,
      extensions: error.extensions,
    };
  }

  private async validateJwtToken(token: string): Promise<any> {
    // JWT validation logic
    // This would typically use your authentication service
    return { id: 'user123', email: 'user@example.com' };
  }
}

// Usage with ConfigService
@Module({
  imports: [
    ConfigModule.forRoot(),
    GraphQLModule.forRootAsync({
      driver: ApolloDriver,
      useClass: GraphQLConfigService,
    }),
  ],
})
export class AppModule {}

24.1.3 Federation Setup für Microservices

import { Module } from '@nestjs/common';
import { GraphQLFederationModule } from '@nestjs/graphql';
import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo';

// User Service (Subgraph)
@Module({
  imports: [
    GraphQLFederationModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: join(process.cwd(), 'src/user-schema.gql'),
      federationMetadata: true,
      
      context: ({ req }) => ({
        user: req.user,
        headers: req.headers,
      }),
      
      // Subgraph specific configuration
      buildSchemaOptions: {
        orphanedTypes: [], // Include types that aren't directly referenced
      },
    }),
    UserModule,
  ],
})
export class UserServiceModule {}

// API Gateway (Supergraph)
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
      driver: ApolloGatewayDriver,
      gateway: {
        buildService: ({ url }) => new RemoteGraphQLDataSource({
          url,
          willSendRequest({ request, context }) {
            // Forward authentication headers
            if (context.user) {
              request.http.headers.set('user-id', context.user.id);
              request.http.headers.set('user-roles', JSON.stringify(context.user.roles));
            }
            
            // Forward correlation ID
            if (context.correlationId) {
              request.http.headers.set('x-correlation-id', context.correlationId);
            }
          },
        }),
        
        supergraphSdl: new IntrospectAndCompose({
          subgraphs: [
            { name: 'users', url: 'http://localhost:3001/graphql' },
            { name: 'products', url: 'http://localhost:3002/graphql' },
            { name: 'orders', url: 'http://localhost:3003/graphql' },
          ],
          
          // Polling interval for schema updates
          pollIntervalInMs: 10000,
        }),
      },
      
      context: ({ req }) => ({
        user: req.user,
        correlationId: req.headers['x-correlation-id'],
      }),
      
      // Gateway-specific plugins
      plugins: [
        {
          requestDidStart() {
            return {
              willSendSubgraphRequest(requestContext) {
                const { request, subgraphName } = requestContext;
                
                console.log(`Sending request to ${subgraphName}:`, {
                  query: request.query,
                  variables: request.variables,
                });
              },
            };
          },
        },
      ],
    }),
  ],
})
export class GatewayModule {}

24.2 Resolver-Erstellung

Resolver sind das Herzstück von GraphQL und definieren, wie Felder aufgelöst werden. NestJS bietet typisierte Resolver mit Decorators.

24.2.1 Entity und Object Types

import { ObjectType, Field, ID, Int, Float } from '@nestjs/graphql';
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';

// User Entity
@Entity()
@ObjectType()
export class User {
  @Field(() => ID)
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Field()
  @Column({ unique: true })
  email: string;

  @Field()
  @Column()
  name: string;

  @Column() // Nicht als GraphQL Field exposed (Password)
  password: string;

  @Field(() => UserRole)
  @Column({ type: 'enum', enum: UserRole, default: UserRole.USER })
  role: UserRole;

  @Field(() => UserStatus)
  @Column({ type: 'enum', enum: UserStatus, default: UserStatus.ACTIVE })
  status: UserStatus;

  @Field(() => Date)
  @CreateDateColumn()
  createdAt: Date;

  @Field(() => Date)
  @UpdateDateColumn()
  updatedAt: Date;

  @Field(() => Date, { nullable: true })
  @Column({ nullable: true })
  lastLoginAt?: Date;

  // Relations
  @Field(() => [Post])
  @OneToMany(() => Post, post => post.author, { lazy: true })
  posts: Post[];

  @Field(() => UserProfile, { nullable: true })
  @OneToOne(() => UserProfile, profile => profile.user, { lazy: true })
  profile?: UserProfile;

  @Field(() => [Order])
  @OneToMany(() => Order, order => order.user, { lazy: true })
  orders: Order[];

  // Virtual Fields (computed properties)
  @Field(() => Int)
  get postCount(): number {
    return this.posts?.length || 0;
  }

  @Field(() => Boolean)
  get isActive(): boolean {
    return this.status === UserStatus.ACTIVE;
  }

  @Field(() => String, { nullable: true })
  get displayName(): string {
    return this.profile?.displayName || this.name;
  }
}

// Enums
import { registerEnumType } from '@nestjs/graphql';

export enum UserRole {
  USER = 'user',
  ADMIN = 'admin',
  MODERATOR = 'moderator',
}

export enum UserStatus {
  ACTIVE = 'active',
  INACTIVE = 'inactive',
  SUSPENDED = 'suspended',
}

registerEnumType(UserRole, {
  name: 'UserRole',
  description: 'User role in the system',
});

registerEnumType(UserStatus, {
  name: 'UserStatus',
  description: 'User account status',
});

// Post Entity
@Entity()
@ObjectType()
export class Post {
  @Field(() => ID)
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Field()
  @Column()
  title: string;

  @Field()
  @Column('text')
  content: string;

  @Field(() => PostStatus)
  @Column({ type: 'enum', enum: PostStatus, default: PostStatus.DRAFT })
  status: PostStatus;

  @Field(() => [String])
  @Column('text', { array: true, default: [] })
  tags: string[];

  @Field(() => Int)
  @Column({ default: 0 })
  viewCount: number;

  @Field(() => Float)
  @Column({ default: 0 })
  rating: number;

  @Field(() => Date)
  @CreateDateColumn()
  createdAt: Date;

  @Field(() => Date)
  @UpdateDateColumn()
  updatedAt: Date;

  @Field(() => Date, { nullable: true })
  @Column({ nullable: true })
  publishedAt?: Date;

  // Relations
  @Field(() => User)
  @ManyToOne(() => User, user => user.posts)
  author: User;

  @Column()
  authorId: string;

  @Field(() => [Comment])
  @OneToMany(() => Comment, comment => comment.post, { lazy: true })
  comments: Comment[];

  @Field(() => [Category])
  @ManyToMany(() => Category, category => category.posts)
  @JoinTable()
  categories: Category[];

  // Virtual fields
  @Field(() => Int)
  get commentCount(): number {
    return this.comments?.length || 0;
  }

  @Field(() => Boolean)
  get isPublished(): boolean {
    return this.status === PostStatus.PUBLISHED && this.publishedAt !== null;
  }
}

export enum PostStatus {
  DRAFT = 'draft',
  PUBLISHED = 'published',
  ARCHIVED = 'archived',
}

registerEnumType(PostStatus, {
  name: 'PostStatus',
  description: 'Post publication status',
});

24.2.2 Input Types und DTOs

import { InputType, Field, ID, Int, PartialType } from '@nestjs/graphql';
import { IsEmail, IsString, MinLength, MaxLength, IsOptional, IsEnum, IsArray } from 'class-validator';

// Create User Input
@InputType()
export class CreateUserInput {
  @Field()
  @IsEmail()
  email: string;

  @Field()
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string;

  @Field()
  @IsString()
  @MinLength(8)
  password: string;

  @Field(() => UserRole, { defaultValue: UserRole.USER })
  @IsOptional()
  @IsEnum(UserRole)
  role?: UserRole;
}

// Update User Input
@InputType()
export class UpdateUserInput extends PartialType(CreateUserInput) {
  @Field(() => ID)
  id: string;

  @Field(() => UserStatus, { nullable: true })
  @IsOptional()
  @IsEnum(UserStatus)
  status?: UserStatus;
}

// Post Input
@InputType()
export class CreatePostInput {
  @Field()
  @IsString()
  @MinLength(5)
  @MaxLength(200)
  title: string;

  @Field()
  @IsString()
  @MinLength(10)
  content: string;

  @Field(() => [String], { defaultValue: [] })
  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  tags?: string[];

  @Field(() => [ID], { defaultValue: [] })
  @IsOptional()
  @IsArray()
  categoryIds?: string[];

  @Field(() => PostStatus, { defaultValue: PostStatus.DRAFT })
  @IsOptional()
  @IsEnum(PostStatus)
  status?: PostStatus;
}

@InputType()
export class UpdatePostInput extends PartialType(CreatePostInput) {
  @Field(() => ID)
  id: string;
}

// Search and Filter Inputs
@InputType()
export class UserFilterInput {
  @Field({ nullable: true })
  @IsOptional()
  @IsString()
  search?: string;

  @Field(() => UserRole, { nullable: true })
  @IsOptional()
  @IsEnum(UserRole)
  role?: UserRole;

  @Field(() => UserStatus, { nullable: true })
  @IsOptional()
  @IsEnum(UserStatus)
  status?: UserStatus;

  @Field(() => Date, { nullable: true })
  @IsOptional()
  createdAfter?: Date;

  @Field(() => Date, { nullable: true })
  @IsOptional()
  createdBefore?: Date;
}

@InputType()
export class PostFilterInput {
  @Field({ nullable: true })
  @IsOptional()
  @IsString()
  search?: string;

  @Field(() => PostStatus, { nullable: true })
  @IsOptional()
  @IsEnum(PostStatus)
  status?: PostStatus;

  @Field(() => [String], { nullable: true })
  @IsOptional()
  @IsArray()
  tags?: string[];

  @Field(() => ID, { nullable: true })
  @IsOptional()
  authorId?: string;

  @Field(() => [ID], { nullable: true })
  @IsOptional()
  @IsArray()
  categoryIds?: string[];

  @Field(() => Float, { nullable: true })
  @IsOptional()
  minRating?: number;
}

// Pagination Input
@InputType()
export class PaginationInput {
  @Field(() => Int, { defaultValue: 1 })
  @IsOptional()
  @Min(1)
  page?: number = 1;

  @Field(() => Int, { defaultValue: 20 })
  @IsOptional()
  @Min(1)
  @Max(100)
  limit?: number = 20;

  @Field({ nullable: true })
  @IsOptional()
  @IsString()
  cursor?: string;
}

// Sort Input
@InputType()
export class SortInput {
  @Field()
  @IsString()
  field: string;

  @Field(() => SortDirection, { defaultValue: SortDirection.ASC })
  @IsOptional()
  @IsEnum(SortDirection)
  direction?: SortDirection = SortDirection.ASC;
}

export enum SortDirection {
  ASC = 'ASC',
  DESC = 'DESC',
}

registerEnumType(SortDirection, {
  name: 'SortDirection',
  description: 'Sort direction for queries',
});

// Complex Query Input
@InputType()
export class PostQueryInput {
  @Field(() => PostFilterInput, { nullable: true })
  @IsOptional()
  filter?: PostFilterInput;

  @Field(() => PaginationInput, { nullable: true })
  @IsOptional()
  pagination?: PaginationInput;

  @Field(() => [SortInput], { nullable: true })
  @IsOptional()
  @IsArray()
  sort?: SortInput[];
}

24.2.3 Comprehensive Resolver Implementation

import { Resolver, Query, Mutation, Args, Context, ResolveField, Parent, Subscription } from '@nestjs/graphql';
import { UseGuards, UseInterceptors, UsePipes } from '@nestjs/common';
import { PubSub } from 'graphql-subscriptions';

@Resolver(() => User)
export class UserResolver {
  private pubSub = new PubSub();

  constructor(
    private userService: UserService,
    private postService: PostService,
    private dataLoaderService: DataLoaderService,
  ) {}

  // Queries
  @Query(() => [User])
  @UseGuards(GraphQLAuthGuard)
  @UseInterceptors(LoggingInterceptor)
  async users(
    @Args('filter', { nullable: true }) filter?: UserFilterInput,
    @Args('pagination', { nullable: true }) pagination?: PaginationInput,
    @Args('sort', { nullable: true }) sort?: SortInput[],
    @Context() context?: any,
  ): Promise<User[]> {
    const queryOptions = {
      filter: filter || {},
      pagination: pagination || { page: 1, limit: 20 },
      sort: sort || [{ field: 'createdAt', direction: SortDirection.DESC }],
    };

    return await this.userService.findMany(queryOptions);
  }

  @Query(() => User, { nullable: true })
  @UseGuards(GraphQLAuthGuard)
  async user(
    @Args('id', { type: () => ID }) id: string,
    @Context() context?: any,
  ): Promise<User | null> {
    const user = await this.userService.findById(id);
    
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    // Authorization check
    if (!this.canAccessUser(context.user, user)) {
      throw new ForbiddenException('Access denied');
    }

    return user;
  }

  @Query(() => User)
  @UseGuards(GraphQLAuthGuard)
  async me(@Context() context: any): Promise<User> {
    return await this.userService.findById(context.user.id);
  }

  // Mutations
  @Mutation(() => User)
  @UseGuards(GraphQLAuthGuard, AdminGuard)
  @UsePipes(ValidationPipe)
  async createUser(
    @Args('input') input: CreateUserInput,
    @Context() context: any,
  ): Promise<User> {
    const user = await this.userService.create(input);
    
    // Publish subscription event
    this.pubSub.publish('userCreated', { userCreated: user });
    
    return user;
  }

  @Mutation(() => User)
  @UseGuards(GraphQLAuthGuard)
  async updateUser(
    @Args('input') input: UpdateUserInput,
    @Context() context: any,
  ): Promise<User> {
    // Authorization check
    if (!this.canModifyUser(context.user, input.id)) {
      throw new ForbiddenException('Access denied');
    }

    const user = await this.userService.update(input.id, input);
    
    // Publish subscription event
    this.pubSub.publish('userUpdated', { userUpdated: user });
    
    return user;
  }

  @Mutation(() => Boolean)
  @UseGuards(GraphQLAuthGuard, AdminGuard)
  async deleteUser(
    @Args('id', { type: () => ID }) id: string,
    @Context() context: any,
  ): Promise<boolean> {
    const success = await this.userService.delete(id);
    
    if (success) {
      this.pubSub.publish('userDeleted', { userDeleted: { id } });
    }
    
    return success;
  }

  // Field Resolvers
  @ResolveField(() => [Post])
  async posts(
    @Parent() user: User,
    @Args('filter', { nullable: true }) filter?: PostFilterInput,
    @Args('pagination', { nullable: true }) pagination?: PaginationInput,
  ): Promise<Post[]> {
    // Use DataLoader to prevent N+1 queries
    return await this.dataLoaderService.postsByUserIdLoader.load({
      userId: user.id,
      filter,
      pagination,
    });
  }

  @ResolveField(() => UserProfile, { nullable: true })
  async profile(@Parent() user: User): Promise<UserProfile | null> {
    return await this.dataLoaderService.userProfileLoader.load(user.id);
  }

  @ResolveField(() => Int)
  async postCount(@Parent() user: User): Promise<number> {
    return await this.dataLoaderService.userPostCountLoader.load(user.id);
  }

  @ResolveField(() => [User])
  async followers(
    @Parent() user: User,
    @Args('pagination', { nullable: true }) pagination?: PaginationInput,
  ): Promise<User[]> {
    return await this.userService.getFollowers(user.id, pagination);
  }

  @ResolveField(() => [User])
  async following(
    @Parent() user: User,
    @Args('pagination', { nullable: true }) pagination?: PaginationInput,
  ): Promise<User[]> {
    return await this.userService.getFollowing(user.id, pagination);
  }

  // Subscriptions
  @Subscription(() => User, {
    filter: (payload, variables, context) => {
      // Only send updates for users the subscriber can access
      return context.user && context.user.role === 'admin';
    },
  })
  @UseGuards(GraphQLAuthGuard)
  userCreated() {
    return this.pubSub.asyncIterator('userCreated');
  }

  @Subscription(() => User, {
    filter: (payload, variables, context) => {
      // Users can subscribe to their own updates
      return payload.userUpdated.id === context.user.id || context.user.role === 'admin';
    },
  })
  @UseGuards(GraphQLAuthGuard)
  userUpdated() {
    return this.pubSub.asyncIterator('userUpdated');
  }

  // Helper methods
  private canAccessUser(currentUser: any, targetUser: User): boolean {
    // Admins can access all users
    if (currentUser.role === 'admin') {
      return true;
    }
    
    // Users can access their own profile
    if (currentUser.id === targetUser.id) {
      return true;
    }
    
    // Users can access public profiles
    return targetUser.status === UserStatus.ACTIVE;
  }

  private canModifyUser(currentUser: any, targetUserId: string): boolean {
    // Admins can modify all users
    if (currentUser.role === 'admin') {
      return true;
    }
    
    // Users can only modify their own profile
    return currentUser.id === targetUserId;
  }
}

// Post Resolver
@Resolver(() => Post)
export class PostResolver {
  private pubSub = new PubSub();

  constructor(
    private postService: PostService,
    private userService: UserService,
    private dataLoaderService: DataLoaderService,
  ) {}

  @Query(() => [Post])
  async posts(
    @Args('query', { nullable: true }) query?: PostQueryInput,
    @Context() context?: any,
  ): Promise<Post[]> {
    return await this.postService.findMany(query || {});
  }

  @Query(() => Post, { nullable: true })
  async post(
    @Args('id', { type: () => ID }) id: string,
    @Context() context?: any,
  ): Promise<Post | null> {
    const post = await this.postService.findById(id);
    
    if (!post) {
      return null;
    }

    // Increment view count
    await this.postService.incrementViewCount(id);
    
    return post;
  }

  @Mutation(() => Post)
  @UseGuards(GraphQLAuthGuard)
  async createPost(
    @Args('input') input: CreatePostInput,
    @Context() context: any,
  ): Promise<Post> {
    const post = await this.postService.create({
      ...input,
      authorId: context.user.id,
    });
    
    this.pubSub.publish('postCreated', { postCreated: post });
    
    return post;
  }

  @Mutation(() => Post)
  @UseGuards(GraphQLAuthGuard)
  async updatePost(
    @Args('input') input: UpdatePostInput,
    @Context() context: any,
  ): Promise<Post> {
    const post = await this.postService.findById(input.id);
    
    if (!post) {
      throw new NotFoundException(`Post with ID ${input.id} not found`);
    }

    // Authorization check
    if (!this.canModifyPost(context.user, post)) {
      throw new ForbiddenException('Access denied');
    }

    const updatedPost = await this.postService.update(input.id, input);
    
    this.pubSub.publish('postUpdated', { postUpdated: updatedPost });
    
    return updatedPost;
  }

  @Mutation(() => Boolean)
  @UseGuards(GraphQLAuthGuard)
  async deletePost(
    @Args('id', { type: () => ID }) id: string,
    @Context() context: any,
  ): Promise<boolean> {
    const post = await this.postService.findById(id);
    
    if (!post) {
      throw new NotFoundException(`Post with ID ${id} not found`);
    }

    if (!this.canModifyPost(context.user, post)) {
      throw new ForbiddenException('Access denied');
    }

    const success = await this.postService.delete(id);
    
    if (success) {
      this.pubSub.publish('postDeleted', { postDeleted: { id } });
    }
    
    return success;
  }

  // Field Resolvers
  @ResolveField(() => User)
  async author(@Parent() post: Post): Promise<User> {
    return await this.dataLoaderService.userLoader.load(post.authorId);
  }

  @ResolveField(() => [Comment])
  async comments(
    @Parent() post: Post,
    @Args('pagination', { nullable: true }) pagination?: PaginationInput,
  ): Promise<Comment[]> {
    return await this.dataLoaderService.commentsByPostIdLoader.load({
      postId: post.id,
      pagination,
    });
  }

  @ResolveField(() => [Category])
  async categories(@Parent() post: Post): Promise<Category[]> {
    return await this.dataLoaderService.categoriesByPostIdLoader.load(post.id);
  }

  private canModifyPost(user: any, post: Post): boolean {
    return user.role === 'admin' || user.id === post.authorId;
  }
}

24.3 Schema First vs Code First

NestJS unterstützt beide Ansätze für GraphQL-Schema-Definition mit unterschiedlichen Vor- und Nachteilen.

24.3.1 Code First Approach

Der Code First Ansatz generiert das GraphQL-Schema automatisch aus TypeScript-Klassen und Decorators.

// Code First - Entity Definition
import { ObjectType, Field, ID, Int, registerEnumType } from '@nestjs/graphql';

@ObjectType()
export class Book {
  @Field(() => ID)
  id: string;

  @Field()
  title: string;

  @Field()
  description: string;

  @Field(() => Int)
  pages: number;

  @Field(() => BookStatus)
  status: BookStatus;

  @Field(() => [String])
  genres: string[];

  @Field(() => Author)
  author: Author;

  @Field(() => [Review])
  reviews: Review[];

  @Field(() => Float, { nullable: true })
  averageRating?: number;

  @Field(() => Date)
  publishedAt: Date;

  @Field(() => Date)
  createdAt: Date;

  @Field(() => Date)
  updatedAt: Date;
}

@ObjectType()
export class Author {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;

  @Field({ nullable: true })
  biography?: string;

  @Field(() => Date, { nullable: true })
  birthDate?: Date;

  @Field(() => [Book])
  books: Book[];

  @Field(() => [Award])
  awards: Award[];
}

@ObjectType()
export class Review {
  @Field(() => ID)
  id: string;

  @Field(() => Int, { description: 'Rating from 1 to 5' })
  rating: number;

  @Field()
  comment: string;

  @Field(() => User)
  reviewer: User;

  @Field(() => Book)
  book: Book;

  @Field(() => Date)
  createdAt: Date;
}

export enum BookStatus {
  DRAFT = 'draft',
  PUBLISHED = 'published',
  OUT_OF_PRINT = 'out_of_print',
}

registerEnumType(BookStatus, {
  name: 'BookStatus',
  description: 'Book publication status',
});

// Input Types
@InputType()
export class CreateBookInput {
  @Field()
  @IsString()
  @MinLength(1)
  @MaxLength(200)
  title: string;

  @Field()
  @IsString()
  @MinLength(10)
  description: string;

  @Field(() => Int)
  @IsNumber()
  @Min(1)
  pages: number;

  @Field(() => [String])
  @IsArray()
  @IsString({ each: true })
  genres: string[];

  @Field(() => ID)
  @IsString()
  authorId: string;

  @Field(() => Date, { nullable: true })
  @IsOptional()
  @IsDate()
  publishedAt?: Date;
}

// Custom Scalars
import { Scalar, CustomScalar } from '@nestjs/graphql';
import { Kind, ValueNode } from 'graphql';

@Scalar('DateTime', () => Date)
export class DateTimeScalar implements CustomScalar<number, Date> {
  description = 'Date custom scalar type';

  parseValue(value: number): Date {
    return new Date(value); // Value from the client
  }

  serialize(value: Date): number {
    return value.getTime(); // Value sent to the client
  }

  parseLiteral(ast: ValueNode): Date {
    if (ast.kind === Kind.INT) {
      return new Date(ast.value);
    }
    return null;
  }
}

// Union Types
import { createUnionType } from '@nestjs/graphql';

export const SearchResult = createUnionType({
  name: 'SearchResult',
  types: () => [Book, Author] as const,
  resolveType(value) {
    if (value.title) {
      return Book;
    }
    if (value.name) {
      return Author;
    }
    return null;
  },
});

// Interface Types
import { InterfaceType } from '@nestjs/graphql';

@InterfaceType()
export abstract class Node {
  @Field(() => ID)
  id: string;
}

@ObjectType({ implements: () => [Node] })
export class BookNode extends Node {
  @Field()
  title: string;
  
  // ... other book fields
}

// Advanced Field Configuration
@ObjectType()
export class AdvancedBook {
  @Field(() => ID)
  id: string;

  @Field()
  title: string;

  // Computed field with custom complexity
  @Field(() => Float, {
    nullable: true,
    description: 'Average rating calculated from all reviews',
    complexity: 5, // Query complexity points
  })
  averageRating?: number;

  // Field with custom middleware
  @Field(() => String, {
    middleware: [LoggingFieldMiddleware],
  })
  sensitiveData: string;

  // Deprecated field
  @Field({
    deprecationReason: 'Use newField instead',
  })
  oldField: string;

  @Field()
  newField: string;
}

24.3.2 Schema First Approach

Der Schema First Ansatz definiert das GraphQL-Schema in .graphql Dateien und generiert TypeScript-Interfaces.

# schema.graphql
type Book {
  id: ID!
  title: String!
  description: String!
  pages: Int!
  status: BookStatus!
  genres: [String!]!
  author: Author!
  reviews: [Review!]!
  averageRating: Float
  publishedAt: DateTime!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type Author {
  id: ID!
  name: String!
  biography: String
  birthDate: DateTime
  books: [Book!]!
  awards: [Award!]!
}

type Review {
  id: ID!
  rating: Int!
  comment: String!
  reviewer: User!
  book: Book!
  createdAt: DateTime!
}

enum BookStatus {
  DRAFT
  PUBLISHED
  OUT_OF_PRINT
}

input CreateBookInput {
  title: String!
  description: String!
  pages: Int!
  genres: [String!]!
  authorId: ID!
  publishedAt: DateTime
}

input BookFilterInput {
  search: String
  status: BookStatus
  genres: [String!]
  authorId: ID
  minPages: Int
  maxPages: Int
  minRating: Float
  publishedAfter: DateTime
  publishedBefore: DateTime
}

type Query {
  books(filter: BookFilterInput, pagination: PaginationInput): [Book!]!
  book(id: ID!): Book
  authors(filter: AuthorFilterInput): [Author!]!
  author(id: ID!): Author
  searchBooks(query: String!): [SearchResult!]!
}

type Mutation {
  createBook(input: CreateBookInput!): Book!
  updateBook(id: ID!, input: UpdateBookInput!): Book!
  deleteBook(id: ID!): Boolean!
  
  createAuthor(input: CreateAuthorInput!): Author!
  updateAuthor(id: ID!, input: UpdateAuthorInput!): Author!
  
  createReview(input: CreateReviewInput!): Review!
  updateReview(id: ID!, input: UpdateReviewInput!): Review!
  deleteReview(id: ID!): Boolean!
}

type Subscription {
  bookCreated: Book!
  bookUpdated: Book!
  bookDeleted: BookDeletedPayload!
  
  reviewAdded(bookId: ID!): Review!
}

# Custom scalars
scalar DateTime
scalar JSON

# Union types
union SearchResult = Book | Author

# Interface types
interface Node {
  id: ID!
}

# Directives
directive @auth(requires: UserRole = USER) on FIELD_DEFINITION
directive @rateLimit(max: Int!, window: Int!) on FIELD_DEFINITION
// Schema First Resolver Implementation
import { Resolver, Query, Mutation, Args, Context, ResolveField, Parent } from '@nestjs/graphql';

// Generated types from schema.graphql
export interface Book {
  id: string;
  title: string;
  description: string;
  pages: number;
  status: BookStatus;
  genres: string[];
  authorId: string;
  publishedAt: Date;
  createdAt: Date;
  updatedAt: Date;
}

export interface CreateBookInput {
  title: string;
  description: string;
  pages: number;
  genres: string[];
  authorId: string;
  publishedAt?: Date;
}

@Resolver('Book')
export class BookResolver {
  constructor(
    private bookService: BookService,
    private authorService: AuthorService,
    private reviewService: ReviewService,
  ) {}

  @Query('books')
  async getBooks(
    @Args('filter') filter?: BookFilterInput,
    @Args('pagination') pagination?: PaginationInput,
  ): Promise<Book[]> {
    return await this.bookService.findMany({ filter, pagination });
  }

  @Query('book')
  async getBook(@Args('id') id: string): Promise<Book | null> {
    return await this.bookService.findById(id);
  }

  @Mutation('createBook')
  async createBook(
    @Args('input') input: CreateBookInput,
    @Context() context: any,
  ): Promise<Book> {
    return await this.bookService.create(input);
  }

  @ResolveField('author')
  async getAuthor(@Parent() book: Book): Promise<Author> {
    return await this.authorService.findById(book.authorId);
  }

  @ResolveField('reviews')
  async getReviews(@Parent() book: Book): Promise<Review[]> {
    return await this.reviewService.findByBookId(book.id);
  }

  @ResolveField('averageRating')
  async getAverageRating(@Parent() book: Book): Promise<number | null> {
    return await this.reviewService.calculateAverageRating(book.id);
  }
}

// Code Generation Configuration
// graphql-code-generator.yml
overwrite: true
schema: "src/**/*.graphql"
generates:
  src/graphql/generated.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
    config:
      contextType: "../types#GraphQLContext"
      mappers:
        Book: "../models#BookModel"
        Author: "../models#AuthorModel"
        Review: "../models#ReviewModel"

24.3.3 Hybrid Approach

// Combining both approaches
@Module({
  imports: [
    // Schema First für externe APIs
    GraphQLModule.forFeature({
      typePaths: ['./**/*.external.graphql'],
      resolvers: [ExternalApiResolver],
    }),
    
    // Code First für interne Entities
    GraphQLModule.forFeature({
      autoSchemaFile: 'src/internal-schema.gql',
      resolvers: [UserResolver, PostResolver],
    }),
  ],
})
export class HybridGraphQLModule {}

// Schema Stitching für mehrere Schemas
import { mergeSchemas } from '@graphql-tools/schema';

export const createStitchedSchema = () => {
  return mergeSchemas({
    schemas: [
      internalSchema,
      externalApiSchema,
    ],
    resolvers: {
      // Cross-schema resolvers
      User: {
        externalData: {
          fragment: 'fragment UserFragment on User { id }',
          resolve(user, args, context, info) {
            return context.externalApiLoader.load(user.id);
          },
        },
      },
    },
  });
};

24.4 DataLoader für N+1 Problem

DataLoader löst das N+1-Query-Problem durch Batch-Loading und Caching von Datenbankzugriffen.

24.4.1 DataLoader Implementation

import DataLoader from 'dataloader';
import { Injectable, Scope } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';

@Injectable({ scope: Scope.REQUEST })
export class DataLoaderService {
  constructor(
    @InjectRepository(User) private userRepository: Repository<User>,
    @InjectRepository(Post) private postRepository: Repository<Post>,
    @InjectRepository(Comment) private commentRepository: Repository<Comment>,
    @InjectRepository(Category) private categoryRepository: Repository<Category>,
    @InjectRepository(UserProfile) private userProfileRepository: Repository<UserProfile>,
  ) {}

  // User Loader
  readonly userLoader = new DataLoader<string, User>(
    async (userIds: readonly string[]) => {
      const users = await this.userRepository.find({
        where: { id: In(userIds as string[]) },
      });

      // Return users in the same order as requested IDs
      return userIds.map(id => users.find(user => user.id === id) || null);
    },
    {
      cache: true,
      maxBatchSize: 100,
      batchScheduleFn: callback => setTimeout(callback, 1), // Batch within 1ms
    }
  );

  // Posts by User ID Loader
  readonly postsByUserIdLoader = new DataLoader<
    { userId: string; filter?: any; pagination?: any },
    Post[]
  >(
    async (keys) => {
      const results = await Promise.all(
        keys.map(async ({ userId, filter, pagination }) => {
          const queryBuilder = this.postRepository
            .createQueryBuilder('post')
            .where('post.authorId = :userId', { userId });

          // Apply filters
          if (filter?.status) {
            queryBuilder.andWhere('post.status = :status', { status: filter.status });
          }

          if (filter?.search) {
            queryBuilder.andWhere(
              '(post.title ILIKE :search OR post.content ILIKE :search)',
              { search: `%${filter.search}%` }
            );
          }

          // Apply pagination
          if (pagination?.limit) {
            queryBuilder.limit(pagination.limit);
          }

          if (pagination?.page && pagination?.limit) {
            queryBuilder.offset((pagination.page - 1) * pagination.limit);
          }

          return await queryBuilder.getMany();
        })
      );

      return results;
    },
    {
      cache: false, // Don't cache filtered results
      maxBatchSize: 50,
    }
  );

  // Comments by Post ID Loader
  readonly commentsByPostIdLoader = new DataLoader<
    { postId: string; pagination?: any },
    Comment[]
  >(
    async (keys) => {
      // Group by postId for efficient querying
      const postIds = [...new Set(keys.map(k => k.postId))];
      
      const comments = await this.commentRepository.find({
        where: { postId: In(postIds) },
        order: { createdAt: 'DESC' },
        relations: ['author'],
      });

      // Group comments by postId
      const commentsByPostId = new Map<string, Comment[]>();
      comments.forEach(comment => {
        if (!commentsByPostId.has(comment.postId)) {
          commentsByPostId.set(comment.postId, []);
        }
        commentsByPostId.get(comment.postId)!.push(comment);
      });

      // Return comments for each requested key with pagination
      return keys.map(({ postId, pagination }) => {
        const postComments = commentsByPostId.get(postId) || [];
        
        if (pagination?.limit) {
          const offset = pagination.page ? (pagination.page - 1) * pagination.limit : 0;
          return postComments.slice(offset, offset + pagination.limit);
        }
        
        return postComments;
      });
    },
    {
      cache: true,
      maxBatchSize: 100,
    }
  );

  // User Profile Loader
  readonly userProfileLoader = new DataLoader<string, UserProfile | null>(
    async (userIds: readonly string[]) => {
      const profiles = await this.userProfileRepository.find({
        where: { userId: In(userIds as string[]) },
      });

      return userIds.map(userId => 
        profiles.find(profile => profile.userId === userId) || null
      );
    },
    {
      cache: true,
      maxBatchSize: 100,
    }
  );

  // Categories by Post ID Loader (Many-to-Many)
  readonly categoriesByPostIdLoader = new DataLoader<string, Category[]>(
    async (postIds: readonly string[]) => {
      const query = `
        SELECT p.id as "postId", c.*
        FROM post p
        JOIN post_categories pc ON p.id = pc."postId"
        JOIN category c ON pc."categoryId" = c.id
        WHERE p.id = ANY($1)
      `;

      const results = await this.categoryRepository.query(query, [postIds as string[]]);
      
      // Group categories by postId
      const categoriesByPostId = new Map<string, Category[]>();
      results.forEach(row => {
        const { postId, ...category } = row;
        if (!categoriesByPostId.has(postId)) {
          categoriesByPostId.set(postId, []);
        }
        categoriesByPostId.get(postId)!.push(category);
      });

      return postIds.map(postId => categoriesByPostId.get(postId) || []);
    },
    {
      cache: true,
      maxBatchSize: 100,
    }
  );

  // Count Loaders
  readonly userPostCountLoader = new DataLoader<string, number>(
    async (userIds: readonly string[]) => {
      const counts = await this.postRepository
        .createQueryBuilder('post')
        .select('post.authorId', 'authorId')
        .addSelect('COUNT(*)', 'count')
        .where('post.authorId IN (:...userIds)', { userIds: userIds as string[] })
        .groupBy('post.authorId')
        .getRawMany();

      const countMap = new Map(counts.map(c => [c.authorId, parseInt(c.count)]));
      
      return userIds.map(userId => countMap.get(userId) || 0);
    },
    {
      cache: true,
      maxBatchSize: 100,
    }
  );

  readonly postCommentCountLoader = new DataLoader<string, number>(
    async (postIds: readonly string[]) => {
      const counts = await this.commentRepository
        .createQueryBuilder('comment')
        .select('comment.postId', 'postId')
        .addSelect('COUNT(*)', 'count')
        .where('comment.postId IN (:...postIds)', { postIds: postIds as string[] })
        .groupBy('comment.postId')
        .getRawMany();

      const countMap = new Map(counts.map(c => [c.postId, parseInt(c.count)]));
      
      return postIds.map(postId => countMap.get(postId) || 0);
    },
    {
      cache: true,
      maxBatchSize: 100,
    }
  );

  // Generic Loader Factory
  createLoader<K, V>(
    batchLoadFn: (keys: readonly K[]) => Promise<V[]>,
    options?: DataLoader.Options<K, V>
  ): DataLoader<K, V> {
    return new DataLoader(batchLoadFn, {
      cache: true,
      maxBatchSize: 100,
      ...options,
    });
  }

  // Priming functions (preload known data)
  primeUser(user: User): void {
    this.userLoader.prime(user.id, user);
  }

  primeUserProfile(profile: UserProfile): void {
    this.userProfileLoader.prime(profile.userId, profile);
  }

  // Cache clearing functions
  clearUserCache(userId: string): void {
    this.userLoader.clear(userId);
    this.userProfileLoader.clear(userId);
    this.postsByUserIdLoader.clearAll(); // Clear related caches
    this.userPostCountLoader.clear(userId);
  }

  clearPostCache(postId: string): void {
    this.commentsByPostIdLoader.clearAll(); // Clear filtered caches
    this.postCommentCountLoader.clear(postId);
    this.categoriesByPostIdLoader.clear(postId);
  }

  // Clear all caches (useful for testing)
  clearAllCaches(): void {
    this.userLoader.clearAll();
    this.postsByUserIdLoader.clearAll();
    this.commentsByPostIdLoader.clearAll();
    this.userProfileLoader.clearAll();
    this.categoriesByPostIdLoader.clearAll();
    this.userPostCountLoader.clearAll();
    this.postCommentCountLoader.clearAll();
  }
}

24.4.2 Advanced DataLoader Patterns

// Conditional DataLoader
@Injectable({ scope: Scope.REQUEST })
export class ConditionalDataLoaderService {
  constructor(
    private dataLoaderService: DataLoaderService,
    @Inject(REQUEST) private request: any,
  ) {}

  // Context-aware loader
  readonly contextualUserLoader = new DataLoader<string, User>(
    async (userIds: readonly string[]) => {
      const currentUser = this.request.user;
      
      // Admins can see all users
      if (currentUser?.role === 'admin') {
        return this.dataLoaderService.userLoader.loadMany(userIds as string[]);
      }
      
      // Regular users can only see active users
      const users = await this.userRepository.find({
        where: { 
          id: In(userIds as string[]),
          status: UserStatus.ACTIVE,
        },
      });

      return userIds.map(id => 
        users.find(user => user.id === id) || new Error('User not found or not accessible')
      );
    }
  );

  // Cached computed fields
  readonly userFullNameLoader = new DataLoader<string, string>(
    async (userIds: readonly string[]) => {
      const users = await this.dataLoaderService.userLoader.loadMany(userIds as string[]);
      const profiles = await this.dataLoaderService.userProfileLoader.loadMany(userIds as string[]);
      
      return userIds.map((userId, index) => {
        const user = users[index] as User;
        const profile = profiles[index] as UserProfile;
        
        if (profile?.displayName) {
          return profile.displayName;
        }
        
        return user?.name || 'Unknown User';
      });
    },
    {
      cache: true,
      maxBatchSize: 100,
    }
  );

  // Aggregation loader
  readonly userStatsLoader = new DataLoader<string, UserStats>(
    async (userIds: readonly string[]) => {
      const [postCounts, commentCounts, followerCounts] = await Promise.all([
        this.dataLoaderService.userPostCountLoader.loadMany(userIds as string[]),
        this.getUserCommentCounts(userIds as string[]),
        this.getUserFollowerCounts(userIds as string[]),
      ]);

      return userIds.map((userId, index) => ({
        userId,
        postCount: postCounts[index] as number || 0,
        commentCount: commentCounts[index] || 0,
        followerCount: followerCounts[index] || 0,
      }));
    }
  );

  private async getUserCommentCounts(userIds: string[]): Promise<number[]> {
    const counts = await this.commentRepository
      .createQueryBuilder('comment')
      .select('comment.authorId', 'authorId')
      .addSelect('COUNT(*)', 'count')
      .where('comment.authorId IN (:...userIds)', { userIds })
      .groupBy('comment.authorId')
      .getRawMany();

    const countMap = new Map(counts.map(c => [c.authorId, parseInt(c.count)]));
    return userIds.map(userId => countMap.get(userId) || 0);
  }

  private async getUserFollowerCounts(userIds: string[]): Promise<number[]> {
    // Implementation for follower counts
    return userIds.map(() => 0); // Placeholder
  }
}

interface UserStats {
  userId: string;
  postCount: number;
  commentCount: number;
  followerCount: number;
}

// DataLoader with Redis Caching
@Injectable({ scope: Scope.REQUEST })
export class RedisDataLoaderService {
  constructor(
    @Inject('REDIS_CLIENT') private redis: Redis,
    private dataLoaderService: DataLoaderService,
  ) {}

  readonly persistentUserLoader = new DataLoader<string, User>(
    async (userIds: readonly string[]) => {
      const cacheKeys = userIds.map(id => `user:${id}`);
      const cached = await this.redis.mget(...cacheKeys);
      
      const uncachedIds: string[] = [];
      const results: (User | null)[] = [];
      
      userIds.forEach((id, index) => {
        if (cached[index]) {
          results[index] = JSON.parse(cached[index]);
        } else {
          uncachedIds.push(id);
          results[index] = null;
        }
      });

      // Fetch uncached users
      if (uncachedIds.length > 0) {
        const uncachedUsers = await this.dataLoaderService.userLoader.loadMany(uncachedIds);
        
        // Cache the results
        const pipeline = this.redis.pipeline();
        uncachedUsers.forEach((user, index) => {
          if (user && !(user instanceof Error)) {
            const cacheKey = `user:${uncachedIds[index]}`;
            pipeline.setex(cacheKey, 300, JSON.stringify(user)); // 5 minutes cache
          }
        });
        await pipeline.exec();

        // Merge results
        let uncachedIndex = 0;
        results.forEach((result, index) => {
          if (result === null) {
            results[index] = uncachedUsers[uncachedIndex] as User;
            uncachedIndex++;
          }
        });
      }

      return results;
    },
    {
      cache: false, // Don't use in-memory cache since we have Redis
      maxBatchSize: 100,
    }
  );
}

24.5 Subscriptions

GraphQL Subscriptions ermöglichen Real-time Updates über WebSocket-Verbindungen.

24.5.1 Basic Subscription Setup

import { Resolver, Subscription, Mutation, Args, Context } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { UseGuards } from '@nestjs/common';

@Resolver()
export class SubscriptionResolver {
  constructor(private pubSub: PubSub) {}

  // Basic Subscription
  @Subscription(() => Post, {
    name: 'postAdded',
  })
  @UseGuards(GraphQLAuthGuard)
  postAdded() {
    return this.pubSub.asyncIterator('postAdded');
  }

  // Filtered Subscription
  @Subscription(() => Comment, {
    name: 'commentAdded',
    filter: (payload, variables, context) => {
      // Only send comments for the specified post
      return payload.commentAdded.postId === variables.postId;
    },
  })
  @UseGuards(GraphQLAuthGuard)
  commentAdded(@Args('postId') postId: string) {
    return this.pubSub.asyncIterator('commentAdded');
  }

  // Conditional Subscription based on user role
  @Subscription(() => User, {
    name: 'userStatusChanged',
    filter: (payload, variables, context) => {
      // Only admins can subscribe to user status changes
      return context.user?.role === 'admin';
    },
  })
  @UseGuards(GraphQLAuthGuard, AdminGuard)
  userStatusChanged() {
    return this.pubSub.asyncIterator('userStatusChanged');
  }

  // Subscription with dynamic topic
  @Subscription(() => Notification, {
    name: 'notificationReceived',
    resolve: (payload) => {
      // Transform payload before sending to client
      return {
        ...payload.notificationReceived,
        isNew: true,
        receivedAt: new Date(),
      };
    },
    filter: (payload, variables, context) => {
      // Users can only receive their own notifications
      return payload.notificationReceived.userId === context.user.id;
    },
  })
  @UseGuards(GraphQLAuthGuard)
  notificationReceived() {
    return this.pubSub.asyncIterator('notificationReceived');
  }

  // Triggering subscription events
  @Mutation(() => Post)
  @UseGuards(GraphQLAuthGuard)
  async createPost(
    @Args('input') input: CreatePostInput,
    @Context() context: any,
  ): Promise<Post> {
    const post = await this.postService.create({
      ...input,
      authorId: context.user.id,
    });

    // Trigger subscription
    await this.pubSub.publish('postAdded', { postAdded: post });

    return post;
  }

  @Mutation(() => Comment)
  @UseGuards(GraphQLAuthGuard)
  async addComment(
    @Args('input') input: CreateCommentInput,
    @Context() context: any,
  ): Promise<Comment> {
    const comment = await this.commentService.create({
      ...input,
      authorId: context.user.id,
    });

    // Trigger subscription with post-specific event
    await this.pubSub.publish('commentAdded', { commentAdded: comment });

    // Also trigger general notification
    await this.pubSub.publish('notificationReceived', {
      notificationReceived: {
        id: `notif_${Date.now()}`,
        type: 'COMMENT_ADDED',
        userId: comment.post.authorId, // Notify post author
        message: `${context.user.name} commented on your post`,
        data: { commentId: comment.id, postId: comment.postId },
      },
    });

    return comment;
  }
}

24.5.2 Advanced Subscription Patterns

// Redis PubSub for Scalable Subscriptions
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

@Injectable()
export class PubSubService {
  private pubSub: RedisPubSub;

  constructor() {
    const options = {
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
      retryDelayOnFailover: 100,
      enableOfflineQueue: false,
      lazyConnect: true,
    };

    this.pubSub = new RedisPubSub({
      publisher: new Redis(options),
      subscriber: new Redis(options),
    });
  }

  async publish(triggerName: string, payload: any): Promise<void> {
    await this.pubSub.publish(triggerName, payload);
  }

  asyncIterator<T>(triggers: string | string[]): AsyncIterator<T> {
    return this.pubSub.asyncIterator<T>(triggers);
  }

  // Typed publish methods
  async publishPostAdded(post: Post): Promise<void> {
    await this.publish('postAdded', { postAdded: post });
  }

  async publishCommentAdded(comment: Comment): Promise<void> {
    await this.publish('commentAdded', { commentAdded: comment });
  }

  async publishUserStatusChanged(user: User): Promise<void> {
    await this.publish('userStatusChanged', { userStatusChanged: user });
  }

  async publishNotification(notification: Notification): Promise<void> {
    await this.publish(`notification:${notification.userId}`, {
      notificationReceived: notification,
    });
  }
}

// Real-time Chat System
@Resolver()
export class ChatSubscriptionResolver {
  constructor(private pubSubService: PubSubService) {}

  @Subscription(() => ChatMessage, {
    name: 'messageAdded',
    filter: (payload, variables, context) => {
      const { roomId } = variables;
      const message = payload.messageAdded;
      
      // Check if user is member of the room
      return this.isUserInRoom(context.user.id, roomId) && 
             message.roomId === roomId;
    },
  })
  @UseGuards(GraphQLAuthGuard)
  messageAdded(@Args('roomId') roomId: string) {
    return this.pubSubService.asyncIterator(`chat:${roomId}`);
  }

  @Subscription(() => TypingIndicator, {
    name: 'userTyping',
    filter: (payload, variables, context) => {
      const { roomId } = variables;
      const typing = payload.userTyping;
      
      // Don't send typing indicator to the user who is typing
      return typing.roomId === roomId && 
             typing.userId !== context.user.id;
    },
  })
  @UseGuards(GraphQLAuthGuard)
  userTyping(@Args('roomId') roomId: string) {
    return this.pubSubService.asyncIterator(`typing:${roomId}`);
  }

  @Subscription(() => UserPresence, {
    name: 'userPresenceChanged',
    filter: (payload, variables, context) => {
      const { roomId } = variables;
      return payload.userPresenceChanged.roomId === roomId;
    },
  })
  @UseGuards(GraphQLAuthGuard)
  userPresenceChanged(@Args('roomId') roomId: string) {
    return this.pubSubService.asyncIterator(`presence:${roomId}`);
  }

  @Mutation(() => ChatMessage)
  @UseGuards(GraphQLAuthGuard)
  async sendMessage(
    @Args('input') input: SendMessageInput,
    @Context() context: any,
  ): Promise<ChatMessage> {
    const message = await this.chatService.sendMessage({
      ...input,
      senderId: context.user.id,
    });

    // Publish to room subscribers
    await this.pubSubService.publish(`chat:${input.roomId}`, {
      messageAdded: message,
    });

    return message;
  }

  @Mutation(() => Boolean)
  @UseGuards(GraphQLAuthGuard)
  async startTyping(
    @Args('roomId') roomId: string,
    @Context() context: any,
  ): Promise<boolean> {
    await this.pubSubService.publish(`typing:${roomId}`, {
      userTyping: {
        userId: context.user.id,
        roomId,
        isTyping: true,
        timestamp: new Date(),
      },
    });

    return true;
  }

  @Mutation(() => Boolean)
  @UseGuards(GraphQLAuthGuard)
  async stopTyping(
    @Args('roomId') roomId: string,
    @Context() context: any,
  ): Promise<boolean> {
    await this.pubSubService.publish(`typing:${roomId}`, {
      userTyping: {
        userId: context.user.id,
        roomId,
        isTyping: false,
        timestamp: new Date(),
      },
    });

    return true;
  }

  private async isUserInRoom(userId: string, roomId: string): Promise<boolean> {
    // Implementation to check room membership
    return true; // Placeholder
  }
}

// Live Data Subscription
@Resolver()
export class LiveDataResolver {
  constructor(private pubSubService: PubSubService) {}

  @Subscription(() => SystemMetrics, {
    name: 'systemMetricsUpdated',
    filter: (payload, variables, context) => {
      // Only admins can subscribe to system metrics
      return context.user?.role === 'admin';
    },
  })
  @UseGuards(GraphQLAuthGuard, AdminGuard)
  systemMetricsUpdated() {
    return this.pubSubService.asyncIterator('systemMetrics');
  }

  @Subscription(() => OrderStatus, {
    name: 'orderStatusChanged',
    filter: (payload, variables, context) => {
      const order = payload.orderStatusChanged;
      // Users can only subscribe to their own orders
      return order.userId === context.user.id;
    },
  })
  @UseGuards(GraphQLAuthGuard)
  orderStatusChanged() {
    return this.pubSubService.asyncIterator('orderStatus');
  }

  // Progress tracking subscription
  @Subscription(() => ProgressUpdate, {
    name: 'progressUpdated',
    filter: (payload, variables, context) => {
      const progress = payload.progressUpdated;
      return progress.userId === context.user.id && 
             progress.taskId === variables.taskId;
    },
  })
  @UseGuards(GraphQLAuthGuard)
  progressUpdated(@Args('taskId') taskId: string) {
    return this.pubSubService.asyncIterator(`progress:${taskId}`);
  }
}

// Subscription Types
@ObjectType()
export class ChatMessage {
  @Field(() => ID)
  id: string;

  @Field()
  content: string;

  @Field(() => User)
  sender: User;

  @Field()
  roomId: string;

  @Field(() => Date)
  createdAt: Date;

  @Field(() => MessageType)
  type: MessageType;
}

@ObjectType()
export class TypingIndicator {
  @Field()
  userId: string;

  @Field()
  roomId: string;

  @Field()
  isTyping: boolean;

  @Field(() => Date)
  timestamp: Date;
}

@ObjectType()
export class UserPresence {
  @Field()
  userId: string;

  @Field()
  roomId: string;

  @Field(() => PresenceStatus)
  status: PresenceStatus;

  @Field(() => Date)
  lastSeen: Date;
}

@ObjectType()
export class SystemMetrics {
  @Field(() => Float)
  cpuUsage: number;

  @Field(() => Float)
  memoryUsage: number;

  @Field(() => Int)
  activeConnections: number;

  @Field(() => Date)
  timestamp: Date;
}

@ObjectType()
export class ProgressUpdate {
  @Field()
  taskId: string;

  @Field()
  userId: string;

  @Field(() => Float)
  percentage: number;

  @Field()
  status: string;

  @Field({ nullable: true })
  message?: string;

  @Field(() => Date)
  timestamp: Date;
}

enum MessageType {
  TEXT = 'text',
  IMAGE = 'image',
  FILE = 'file',
  SYSTEM = 'system',
}

enum PresenceStatus {
  ONLINE = 'online',
  OFFLINE = 'offline',
  AWAY = 'away',
  BUSY = 'busy',
}

registerEnumType(MessageType, { name: 'MessageType' });
registerEnumType(PresenceStatus, { name: 'PresenceStatus' });

24.6 Authentication in GraphQL

GraphQL Authentication erfordert besondere Aufmerksamkeit, da sowohl HTTP- als auch WebSocket-Verbindungen unterstützt werden müssen.

24.6.1 JWT Authentication Implementation

// GraphQL Auth Guard
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class GraphQLAuthGuard extends AuthGuard('jwt') {
  getRequest(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    const request = ctx.getContext().req;
    
    // For subscriptions, the context might be different
    if (!request) {
      const connection = ctx.getContext().connection;
      return connection?.context || {};
    }
    
    return request;
  }

  handleRequest(err: any, user: any, info: any, context: ExecutionContext) {
    if (err || !user) {
      throw new UnauthorizedException('Authentication failed');
    }
    
    // Add user to GraphQL context
    const gqlContext = GqlExecutionContext.create(context).getContext();
    gqlContext.user = user;
    
    return user;
  }
}

// Role-based Guards
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get<string[]>('roles', context.getHandler());
    
    if (!roles) {
      return true;
    }

    const ctx = GqlExecutionContext.create(context);
    const user = ctx.getContext().user;

    if (!user) {
      return false;
    }

    return roles.some(role => user.roles?.includes(role));
  }
}

// Custom Decorators
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

export const CurrentUser = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const ctx = GqlExecutionContext.create(context);
    return ctx.getContext().user;
  },
);

// Resource-based Authorization Guard
@Injectable()
export class ResourceAuthGuard implements CanActivate {
  constructor(
    private userService: UserService,
    private postService: PostService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const ctx = GqlExecutionContext.create(context);
    const args = ctx.getArgs();
    const user = ctx.getContext().user;
    
    if (!user) {
      return false;
    }

    // For update/delete operations, check ownership
    if (args.id || args.input?.id) {
      const resourceId = args.id || args.input.id;
      const handlerName = context.getHandler().name;
      
      if (handlerName.includes('Post')) {
        return await this.canAccessPost(user, resourceId);
      }
      
      if (handlerName.includes('User')) {
        return await this.canAccessUser(user, resourceId);
      }
    }

    return true;
  }

  private async canAccessPost(user: any, postId: string): Promise<boolean> {
    if (user.role === 'admin') {
      return true;
    }

    const post = await this.postService.findById(postId);
    return post?.authorId === user.id;
  }

  private async canAccessUser(user: any, userId: string): Promise<boolean> {
    if (user.role === 'admin') {
      return true;
    }

    return user.id === userId;
  }
}

// Authentication Resolver
@Resolver()
export class AuthResolver {
  constructor(
    private authService: AuthService,
    private userService: UserService,
  ) {}

  @Mutation(() => AuthPayload)
  async login(
    @Args('input') input: LoginInput,
    @Context() context: any,
  ): Promise<AuthPayload> {
    const { user, accessToken, refreshToken } = await this.authService.login(
      input.email,
      input.password,
    );

    // Set HTTP-only cookie for refresh token
    context.res.cookie('refreshToken', refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    });

    return {
      user,
      accessToken,
      expiresIn: 900, // 15 minutes
    };
  }

  @Mutation(() => AuthPayload)
  async refreshToken(@Context() context: any): Promise<AuthPayload> {
    const refreshToken = context.req.cookies?.refreshToken;
    
    if (!refreshToken) {
      throw new UnauthorizedException('Refresh token not found');
    }

    const { user, accessToken, refreshToken: newRefreshToken } = 
      await this.authService.refreshToken(refreshToken);

    // Update refresh token cookie
    context.res.cookie('refreshToken', newRefreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 7 * 24 * 60 * 60 * 1000,
    });

    return {
      user,
      accessToken,
      expiresIn: 900,
    };
  }

  @Mutation(() => Boolean)
  @UseGuards(GraphQLAuthGuard)
  async logout(@Context() context: any): Promise<boolean> {
    // Clear refresh token cookie
    context.res.clearCookie('refreshToken');
    
    // Optionally blacklist the access token
    await this.authService.blacklistToken(context.user.token);

    return true;
  }

  @Query(() => User)
  @UseGuards(GraphQLAuthGuard)
  async me(@CurrentUser() user: any): Promise<User> {
    return await this.userService.findById(user.id);
  }

  @Mutation(() => User)
  async register(
    @Args('input') input: RegisterInput,
  ): Promise<User> {
    return await this.authService.register(input);
  }

  @Mutation(() => Boolean)
  async forgotPassword(
    @Args('email') email: string,
  ): Promise<boolean> {
    await this.authService.sendPasswordResetEmail(email);
    return true;
  }

  @Mutation(() => Boolean)
  async resetPassword(
    @Args('input') input: ResetPasswordInput,
  ): Promise<boolean> {
    await this.authService.resetPassword(input.token, input.newPassword);
    return true;
  }
}

// Protected Resolvers with Authorization
@Resolver(() => Post)
export class PostResolver {
  constructor(
    private postService: PostService,
    private userService: UserService,
  ) {}

  @Query(() => [Post])
  async posts(
    @Args('filter', { nullable: true }) filter?: PostFilterInput,
  ): Promise<Post[]> {
    // Public endpoint - no authentication required
    return await this.postService.findPublished(filter);
  }

  @Query(() => [Post])
  @UseGuards(GraphQLAuthGuard)
  async myPosts(
    @CurrentUser() user: any,
    @Args('filter', { nullable: true }) filter?: PostFilterInput,
  ): Promise<Post[]> {
    return await this.postService.findByAuthor(user.id, filter);
  }

  @Mutation(() => Post)
  @UseGuards(GraphQLAuthGuard)
  @Roles('author', 'admin')
  async createPost(
    @Args('input') input: CreatePostInput,
    @CurrentUser() user: any,
  ): Promise<Post> {
    return await this.postService.create({
      ...input,
      authorId: user.id,
    });
  }

  @Mutation(() => Post)
  @UseGuards(GraphQLAuthGuard, ResourceAuthGuard)
  async updatePost(
    @Args('input') input: UpdatePostInput,
    @CurrentUser() user: any,
  ): Promise<Post> {
    // ResourceAuthGuard ensures user can modify this post
    return await this.postService.update(input.id, input);
  }

  @Mutation(() => Boolean)
  @UseGuards(GraphQLAuthGuard, ResourceAuthGuard)
  async deletePost(
    @Args('id') id: string,
    @CurrentUser() user: any,
  ): Promise<boolean> {
    return await this.postService.delete(id);
  }

  @Mutation(() => Post)
  @UseGuards(GraphQLAuthGuard)
  @Roles('admin', 'moderator')
  async moderatePost(
    @Args('input') input: ModeratePostInput,
    @CurrentUser() user: any,
  ): Promise<Post> {
    return await this.postService.moderate(input.postId, input.action, user.id);
  }
}

// Authentication Types
@ObjectType()
export class AuthPayload {
  @Field(() => User)
  user: User;

  @Field()
  accessToken: string;

  @Field(() => Int)
  expiresIn: number;
}

@InputType()
export class LoginInput {
  @Field()
  @IsEmail()
  email: string;

  @Field()
  @IsString()
  password: string;

  @Field({ defaultValue: false })
  @IsOptional()
  @IsBoolean()
  rememberMe?: boolean;
}

@InputType()
export class RegisterInput {
  @Field()
  @IsEmail()
  email: string;

  @Field()
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  name: string;

  @Field()
  @IsString()
  @MinLength(8)
  password: string;

  @Field()
  @IsString()
  @MinLength(8)
  confirmPassword: string;
}

@InputType()
export class ResetPasswordInput {
  @Field()
  @IsString()
  token: string;

  @Field()
  @IsString()
  @MinLength(8)
  newPassword: string;
}

// Field-level Authorization
@Resolver(() => User)
export class UserResolver {
  @ResolveField(() => String, { nullable: true })
  async email(
    @Parent() user: User,
    @CurrentUser() currentUser?: any,
  ): Promise<string | null> {
    // Only return email for the user themselves or admins
    if (currentUser?.id === user.id || currentUser?.role === 'admin') {
      return user.email;
    }
    return null;
  }

  @ResolveField(() => [Order])
  @UseGuards(GraphQLAuthGuard)
  async orders(
    @Parent() user: User,
    @CurrentUser() currentUser: any,
  ): Promise<Order[]> {
    // Users can only see their own orders
    if (currentUser.id !== user.id && currentUser.role !== 'admin') {
      throw new ForbiddenException('Access denied');
    }

    return await this.orderService.findByUserId(user.id);
  }
}

GraphQL Integration in NestJS bietet eine mächtige und flexible Alternative zu REST APIs. Die typisierte Natur von GraphQL kombiniert mit NestJS’s TypeScript-Support ermöglicht es, sichere, performante und wartbare APIs zu entwickeln. Durch die Verwendung von DataLoader, Subscriptions und robusten Authentication-Mechanismen können auch komplexe Real-time-Anwendungen effizient umgesetzt werden.