first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
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 identity = IdentityPresentersModule.withApplication(
IdentityApplicationModule.withInfrastructure(IdentityInfrastructureModule),
);
const tracker = TrackerPresentersModule.withApplication(
TrackerApplicationModule.withInfrastructure(TrackerInfrastructureModule),
);
@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<string>('POSTGRES_URL'),
autoLoadEntities: true,
synchronize: config.get<boolean>('POSTGRES_SYNCHRONIZE'),
}),
}),
// Read model: MongoDB
MongooseModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
uri: config.getOrThrow<string>('MONGO_URL'),
}),
}),
// Event store (PostgreSQL) → Redis stream → events bridge, AggregateRehydrator
SharedModule.withInfrastructure(
SharedInfrastructureModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
redisUrl: config.getOrThrow<string>('REDIS_URL'),
streamKey: config.getOrThrow<string>('EVENT_STREAM_KEY'),
maxLength: config.getOrThrow<number>('EVENT_STREAM_MAX_LENGTH'),
eventsBridgeEnabled: config.getOrThrow<boolean>(
'EVENTS_BRIDGE_ENABLED',
),
}),
}),
),
// GraphQL + realtime subscriptions over graphql-ws
GraphQLModule.forRootAsync<ApolloDriverConfig>({
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<string>('GRAPHQL_SCHEMA_FILE')
? join(
process.cwd(),
config.get<string>('GRAPHQL_SCHEMA_FILE') as string,
)
: true,
sortSchema: true,
// GraphiQL on GET /graphql (send the Pireon token in the Headers tab).
graphiql: config.get<boolean>('GRAPHQL_PLAYGROUND')
? { url: '/graphql' }
: false,
playground: false,
includeStacktraceInErrorResponses:
config.get('NODE_ENV') === 'development',
introspection: config.get<boolean>('GRAPHQL_PLAYGROUND'),
subscriptions: {
'graphql-ws': {
path: '/graphql',
// connection_init payload: { "authorization": "Bearer <pireon token>" }
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 {}

View File

@@ -0,0 +1,36 @@
import * as Joi from 'joi';
export const configSchema = Joi.object({
PORT: Joi.number().default(3000),
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
// Write model
POSTGRES_URL: Joi.string().uri().required(),
POSTGRES_SYNCHRONIZE: Joi.boolean().default(false),
// Read model
MONGO_URL: Joi.string().uri().required(),
// Event stream + GraphQL pub/sub + session cache
REDIS_URL: Joi.string().uri().required(),
EVENT_STREAM_KEY: Joi.string().default('task-stream:events'),
EVENT_STREAM_MAX_LENGTH: Joi.number().default(1_000_000),
// Consume the Redis stream and run @EventsHandler projections on this instance.
EVENTS_BRIDGE_ENABLED: Joi.boolean().default(true),
// Pireon identity server
PIREON_BASE_URL: Joi.string().uri().default('https://oauth.pireon.dev'),
PIREON_WEBHOOK_SECRET: Joi.string().allow('').optional(),
// OAuth client of task-stream in Pireon, used by the web app to sign in.
PIREON_CLIENT_ID: Joi.string().allow('').optional(),
PIREON_CLIENT_SECRET: Joi.string().allow('').optional(),
PIREON_SCOPES: Joi.string().default('users:read'),
PIREON_PASSWORD_GRANT: Joi.boolean().default(true),
PIREON_TIMEOUT_MS: Joi.number().default(5000),
AUTH_SESSION_TTL_SECONDS: Joi.number().min(0).default(60),
GRAPHQL_PLAYGROUND: Joi.boolean().default(true),
GRAPHQL_SCHEMA_FILE: Joi.string().allow('').default('apps/api/schema.gql'),
});

View File

@@ -0,0 +1,39 @@
import {
Controller,
Get,
Inject,
ServiceUnavailableException,
} from '@nestjs/common';
import { InjectConnection } from '@nestjs/mongoose';
import { Public } from '@task-stream/api/identity/presenters';
import { REDIS_CLIENT } from '@task-stream/api/shared/infrastructure';
import Redis from 'ioredis';
import { Connection } from 'mongoose';
import { DataSource } from 'typeorm';
@Controller('health')
export class HealthController {
constructor(
private readonly postgres: DataSource,
@InjectConnection() private readonly mongo: Connection,
@Inject(REDIS_CLIENT) private readonly redis: Redis,
) {}
@Public()
@Get()
async check() {
const checks = await Promise.allSettled([
this.postgres.query('SELECT 1'),
this.mongo.db?.admin().ping(),
this.redis.ping(),
]);
const [postgres, mongo, redis] = checks.map((c) =>
c.status === 'fulfilled' ? 'up' : 'down',
);
const result = { status: 'ok', postgres, mongo, redis };
if (checks.some((c) => c.status === 'rejected')) {
throw new ServiceUnavailableException({ ...result, status: 'degraded' });
}
return result;
}
}