import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm'; import { IdentityApplicationModule } from '@task-stream/api/identity/application'; import { IdentityInfrastructureModule } from '@task-stream/api/identity/infrastructure'; import { AuthRequest, IdentityPresentersModule, WsAuthenticator, } from '@task-stream/api/identity/presenters'; import { SharedModule } from '@task-stream/api/shared/application'; import { SharedInfrastructureModule } from '@task-stream/api/shared/infrastructure'; import { TrackerApplicationModule } from '@task-stream/api/tracker/application'; import { TrackerInfrastructureModule } from '@task-stream/api/tracker/infrastructure'; import { TrackerPresentersModule } from '@task-stream/api/tracker/presenters'; import { join } from 'node:path'; import { configSchema } from './config.schema'; import { HealthController } from './health.controller'; /* * Composition root. Each bounded context is assembled outside-in: * presenters (GraphQL/HTTP) → application (services, CQRS handlers) → infrastructure (adapters) */ const identityApplication = IdentityApplicationModule.withInfrastructure( IdentityInfrastructureModule, ); const identity = IdentityPresentersModule.withApplication(identityApplication); // The tracker's user directory asks the identity context (IdentityService): // the same module, so one identity context serves both. const tracker = TrackerPresentersModule.withApplication( TrackerApplicationModule.withInfrastructure( TrackerInfrastructureModule.withIdentity(identityApplication), ), ); @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, validationSchema: configSchema, cache: true, }), // Write model: PostgreSQL (event store + lookup tables) TypeOrmModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ type: 'postgres', url: config.getOrThrow('POSTGRES_URL'), autoLoadEntities: true, synchronize: config.get('POSTGRES_SYNCHRONIZE'), // uuid ids come from gen_random_uuid() (core since PostgreSQL 13). uuidExtension: 'pgcrypto', }), }), // Read model: MongoDB MongooseModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ uri: config.getOrThrow('MONGO_URL'), }), }), // Event store (PostgreSQL) → Redis stream → events bridge, AggregateRehydrator SharedModule.withInfrastructure( SharedInfrastructureModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ redisUrl: config.getOrThrow('REDIS_URL'), streamKey: config.getOrThrow('EVENT_STREAM_KEY'), maxLength: config.getOrThrow('EVENT_STREAM_MAX_LENGTH'), eventsBridgeEnabled: config.getOrThrow( 'EVENTS_BRIDGE_ENABLED', ), }), }), ), // GraphQL + realtime subscriptions over graphql-ws GraphQLModule.forRootAsync({ driver: ApolloDriver, imports: [identity], inject: [WsAuthenticator, ConfigService], useFactory: (ws: WsAuthenticator, config: ConfigService) => ({ // Writes the SDL for frontend codegen; empty = keep the schema in memory. autoSchemaFile: config.get('GRAPHQL_SCHEMA_FILE') ? join( process.cwd(), config.get('GRAPHQL_SCHEMA_FILE') as string, ) : true, sortSchema: true, // GraphiQL on GET /graphql (send the Pireon token in the Headers tab). graphiql: config.get('GRAPHQL_PLAYGROUND') ? { url: '/graphql' } : false, playground: false, includeStacktraceInErrorResponses: config.get('NODE_ENV') === 'development', introspection: config.get('GRAPHQL_PLAYGROUND'), subscriptions: { 'graphql-ws': { path: '/graphql', // connection_init payload: { "authorization": "Bearer " } onConnect: async (ctx) => { try { (ctx.extra as { auth?: AuthRequest }).auth = await ws.authenticate(ctx.connectionParams); return true; } catch { return false; // closes the socket with 4403 Forbidden } }, }, }, context: ({ req, extra, }: { req?: AuthRequest; extra?: { auth?: AuthRequest }; }) => ({ req: extra?.auth ?? req, }), }), }), identity, tracker, ], controllers: [HealthController], }) export class AppModule {}