@quanticjs/core 1.1.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/QuanticCoreModule.d.ts +4 -0
  2. package/dist/QuanticCoreModule.js +53 -0
  3. package/dist/cqrs/behaviors/LogBehavior.d.ts +4 -1
  4. package/dist/cqrs/behaviors/LogBehavior.js +3 -0
  5. package/dist/cqrs/behaviors/TransactionalBehavior.d.ts +4 -11
  6. package/dist/cqrs/behaviors/TransactionalBehavior.js +3 -12
  7. package/dist/cqrs/behaviors/ValidationBehavior.d.ts +4 -1
  8. package/dist/cqrs/behaviors/ValidationBehavior.js +3 -0
  9. package/dist/cqrs/pipeline/QuanticCommandBus.d.ts +3 -28
  10. package/dist/cqrs/pipeline/QuanticCommandBus.js +11 -59
  11. package/dist/cqrs/pipeline/QuanticQueryBus.d.ts +3 -19
  12. package/dist/cqrs/pipeline/QuanticQueryBus.js +11 -38
  13. package/dist/cqrs/pipeline/behavior-order.d.ts +11 -0
  14. package/dist/cqrs/pipeline/behavior-order.js +14 -0
  15. package/dist/cqrs/pipeline/types.d.ts +8 -0
  16. package/dist/cqrs/pipeline/types.js +4 -0
  17. package/dist/index.d.ts +6 -22
  18. package/dist/index.js +13 -44
  19. package/package.json +26 -80
  20. package/dist/cqrs/PipelineExecutor.d.ts +0 -31
  21. package/dist/cqrs/PipelineExecutor.js +0 -81
  22. package/dist/cqrs/behaviors/CacheBehavior.d.ts +0 -9
  23. package/dist/cqrs/behaviors/CacheBehavior.js +0 -68
  24. package/dist/cqrs/behaviors/DistributedLockBehavior.d.ts +0 -12
  25. package/dist/cqrs/behaviors/DistributedLockBehavior.js +0 -90
  26. package/dist/cqrs/behaviors/FeatureFlagBehavior.d.ts +0 -8
  27. package/dist/cqrs/behaviors/FeatureFlagBehavior.js +0 -58
  28. package/dist/cqrs/behaviors/InvalidateCacheBehavior.d.ts +0 -9
  29. package/dist/cqrs/behaviors/InvalidateCacheBehavior.js +0 -61
  30. package/dist/cqrs/behaviors/PerformanceBehavior.d.ts +0 -12
  31. package/dist/cqrs/behaviors/PerformanceBehavior.js +0 -56
  32. package/dist/cqrs/behaviors/WorkflowBehavior.d.ts +0 -8
  33. package/dist/cqrs/behaviors/WorkflowBehavior.js +0 -61
  34. package/dist/events/DomainEvent.d.ts +0 -14
  35. package/dist/events/DomainEvent.js +0 -27
  36. package/dist/events/OutboxEvent.entity.d.ts +0 -18
  37. package/dist/events/OutboxEvent.entity.js +0 -87
  38. package/dist/events/OutboxPublisherService.d.ts +0 -14
  39. package/dist/events/OutboxPublisherService.js +0 -104
  40. package/dist/events/RedisStreamConsumer.d.ts +0 -43
  41. package/dist/events/RedisStreamConsumer.js +0 -158
  42. package/dist/events/RedisStreamPublisher.d.ts +0 -9
  43. package/dist/events/RedisStreamPublisher.js +0 -60
  44. package/dist/metrics/MetricsController.d.ts +0 -7
  45. package/dist/metrics/MetricsController.js +0 -42
  46. package/dist/metrics/MetricsService.d.ts +0 -13
  47. package/dist/metrics/MetricsService.js +0 -58
  48. package/dist/redis/redis.module.d.ts +0 -8
  49. package/dist/redis/redis.module.js +0 -49
  50. package/dist/shared-kernel.module.d.ts +0 -13
  51. package/dist/shared-kernel.module.js +0 -87
  52. package/dist/testing/TestingModuleFactory.d.ts +0 -23
  53. package/dist/testing/TestingModuleFactory.js +0 -63
  54. package/dist/testing/index.d.ts +0 -2
  55. package/dist/testing/index.js +0 -7
  56. package/dist/unleash/initial-flags.d.ts +0 -7
  57. package/dist/unleash/initial-flags.js +0 -9
  58. package/dist/unleash/unleash.module.d.ts +0 -9
  59. package/dist/unleash/unleash.module.js +0 -47
  60. package/src/bootstrap/bootstrapService.ts +0 -72
  61. package/src/cqrs/behaviors/CacheBehavior.spec.ts +0 -63
  62. package/src/cqrs/behaviors/CacheBehavior.ts +0 -54
  63. package/src/cqrs/behaviors/DistributedLockBehavior.ts +0 -88
  64. package/src/cqrs/behaviors/FeatureFlagBehavior.ts +0 -46
  65. package/src/cqrs/behaviors/InvalidateCacheBehavior.spec.ts +0 -89
  66. package/src/cqrs/behaviors/InvalidateCacheBehavior.ts +0 -50
  67. package/src/cqrs/behaviors/LogBehavior.spec.ts +0 -55
  68. package/src/cqrs/behaviors/LogBehavior.ts +0 -121
  69. package/src/cqrs/behaviors/PerformanceBehavior.spec.ts +0 -48
  70. package/src/cqrs/behaviors/PerformanceBehavior.ts +0 -43
  71. package/src/cqrs/behaviors/TransactionalBehavior.ts +0 -64
  72. package/src/cqrs/behaviors/ValidationBehavior.spec.ts +0 -114
  73. package/src/cqrs/behaviors/ValidationBehavior.ts +0 -29
  74. package/src/cqrs/behaviors/WorkflowBehavior.spec.ts +0 -97
  75. package/src/cqrs/behaviors/WorkflowBehavior.ts +0 -62
  76. package/src/cqrs/constants.ts +0 -2
  77. package/src/cqrs/decorators/Cache.decorator.ts +0 -24
  78. package/src/cqrs/decorators/DistributedLock.decorator.ts +0 -34
  79. package/src/cqrs/decorators/FeatureFlag.decorator.ts +0 -23
  80. package/src/cqrs/decorators/InvalidateCache.decorator.spec.ts +0 -20
  81. package/src/cqrs/decorators/InvalidateCache.decorator.ts +0 -17
  82. package/src/cqrs/decorators/IsolatedTransaction.decorator.ts +0 -24
  83. package/src/cqrs/decorators/Log.decorator.ts +0 -22
  84. package/src/cqrs/decorators/Validate.decorator.ts +0 -39
  85. package/src/cqrs/decorators/Workflow.decorator.ts +0 -22
  86. package/src/cqrs/interfaces/WorkflowEngine.ts +0 -19
  87. package/src/cqrs/pipeline/QuanticCommandBus.ts +0 -69
  88. package/src/cqrs/pipeline/QuanticQueryBus.ts +0 -56
  89. package/src/cqrs/pipeline/runPipeline.ts +0 -22
  90. package/src/cqrs/transaction/TransactionContext.ts +0 -26
  91. package/src/cqrs/transaction/getTransactionalRepo.ts +0 -23
  92. package/src/cqrs/validation/ICommandValidator.ts +0 -55
  93. package/src/entities/BaseEntity.ts +0 -16
  94. package/src/entities/TenantBaseEntity.ts +0 -7
  95. package/src/events/DomainEvent.ts +0 -27
  96. package/src/events/OutboxEvent.entity.ts +0 -56
  97. package/src/events/OutboxPublisherService.ts +0 -94
  98. package/src/events/RedisStreamConsumer.ts +0 -172
  99. package/src/events/RedisStreamPublisher.ts +0 -54
  100. package/src/filters/GlobalExceptionFilter.ts +0 -125
  101. package/src/guards/JwtAuthGuard.ts +0 -29
  102. package/src/guards/JwtStrategy.ts +0 -41
  103. package/src/guards/RolesGuard.ts +0 -39
  104. package/src/index.ts +0 -118
  105. package/src/interceptors/ResultInterceptor.ts +0 -93
  106. package/src/lifecycle/GracefulShutdownService.ts +0 -77
  107. package/src/logging/pino-config.ts +0 -80
  108. package/src/metrics/MetricsController.ts +0 -17
  109. package/src/metrics/MetricsService.ts +0 -55
  110. package/src/middleware/CorrelationIdMiddleware.ts +0 -27
  111. package/src/middleware/CorrelationStore.ts +0 -13
  112. package/src/middleware/TenantContextMiddleware.ts +0 -21
  113. package/src/middleware/TenantStore.ts +0 -11
  114. package/src/redis/redis.module.ts +0 -41
  115. package/src/resilience/CircuitBreakerFactory.ts +0 -33
  116. package/src/result/Result.ts +0 -66
  117. package/src/shared-kernel.module.ts +0 -87
  118. package/src/subscribers/TenantSubscriber.ts +0 -47
  119. package/src/testing/TestingModuleFactory.ts +0 -78
  120. package/src/testing/index.ts +0 -2
  121. package/src/testing/mocks.ts +0 -59
  122. package/src/unleash/unleash.module.ts +0 -45
  123. package/tsconfig.json +0 -22
@@ -1,125 +0,0 @@
1
- import {
2
- ExceptionFilter,
3
- Catch,
4
- ArgumentsHost,
5
- HttpException,
6
- Logger,
7
- } from '@nestjs/common';
8
- import { ThrottlerException } from '@nestjs/throttler';
9
- import { Request, Response } from 'express';
10
- import { correlationStore } from '../middleware/CorrelationStore';
11
-
12
- interface ProblemDetails {
13
- type: string;
14
- title: string;
15
- status: number;
16
- detail?: string;
17
- instance?: string;
18
- correlationId?: string;
19
- retryAfter?: number;
20
- errors?: Array<{ field?: string; message: string }>;
21
- stack?: string;
22
- }
23
-
24
- /**
25
- * Global exception filter for framework-level exceptions (guards, pipes, throttler).
26
- * Result<T> errors are handled by ResultInterceptor — they never reach this filter.
27
- */
28
- @Catch()
29
- export class GlobalExceptionFilter implements ExceptionFilter {
30
- private readonly logger = new Logger(GlobalExceptionFilter.name);
31
- private readonly isProduction = process.env.NODE_ENV === 'production';
32
-
33
- catch(exception: unknown, host: ArgumentsHost): void {
34
- const ctx = host.switchToHttp();
35
- const request = ctx.getRequest<Request>();
36
- const response = ctx.getResponse<Response>();
37
- const correlationId = correlationStore.getStore()?.correlationId;
38
-
39
- if (response.headersSent) return;
40
-
41
- if (correlationId) {
42
- response.setHeader('X-Correlation-ID', correlationId);
43
- }
44
-
45
- const problem = this.toProblemDetails(exception, correlationId, request.originalUrl);
46
-
47
- if (problem.retryAfter) {
48
- response.setHeader('Retry-After', String(problem.retryAfter));
49
- }
50
-
51
- this.logger.error({
52
- msg: `${problem.status} ${problem.title}`,
53
- correlationId,
54
- status: problem.status,
55
- detail: problem.detail,
56
- });
57
-
58
- response
59
- .status(problem.status)
60
- .setHeader('Content-Type', 'application/problem+json')
61
- .json(problem);
62
- }
63
-
64
- private toProblemDetails(
65
- exception: unknown,
66
- correlationId?: string,
67
- instance?: string,
68
- ): ProblemDetails {
69
- // ThrottlerException → 429
70
- if (exception instanceof ThrottlerException) {
71
- return {
72
- type: 'https://arex.dev/errors/RATE_LIMITED',
73
- title: 'Too Many Requests',
74
- status: 429,
75
- detail: 'Rate limit exceeded. Please retry later.',
76
- instance,
77
- correlationId,
78
- retryAfter: 60,
79
- };
80
- }
81
-
82
- // NestJS HttpException (guards, pipes, etc.)
83
- if (exception instanceof HttpException) {
84
- const status = exception.getStatus();
85
- const exceptionResponse = exception.getResponse();
86
- const detail =
87
- typeof exceptionResponse === 'string'
88
- ? exceptionResponse
89
- : (exceptionResponse as Record<string, unknown>).message;
90
-
91
- if (status === 400 && Array.isArray(detail)) {
92
- return {
93
- type: 'https://arex.dev/errors/VALIDATION_ERROR',
94
- title: 'Validation Error',
95
- status: 400,
96
- detail: 'One or more validation errors occurred.',
97
- instance,
98
- correlationId,
99
- errors: (detail as string[]).map((msg) => ({ message: msg })),
100
- };
101
- }
102
-
103
- return {
104
- type: `https://arex.dev/errors/HTTP_${status}`,
105
- title: exception.name,
106
- status,
107
- detail: typeof detail === 'string' ? detail : JSON.stringify(detail),
108
- instance,
109
- correlationId,
110
- };
111
- }
112
-
113
- // Unhandled errors (middleware crashes, etc.)
114
- const error = exception instanceof Error ? exception : new Error(String(exception));
115
- return {
116
- type: 'https://arex.dev/errors/INTERNAL_ERROR',
117
- title: 'Internal Server Error',
118
- status: 500,
119
- detail: this.isProduction ? 'An unexpected error occurred.' : error.message,
120
- instance,
121
- correlationId,
122
- ...(this.isProduction ? {} : { stack: error.stack }),
123
- };
124
- }
125
- }
@@ -1,29 +0,0 @@
1
- import { ExecutionContext, Injectable } from '@nestjs/common';
2
- import { Reflector } from '@nestjs/core';
3
- import { AuthGuard } from '@nestjs/passport';
4
-
5
- const IS_PUBLIC_KEY = 'isPublic';
6
-
7
- export const Public = () => (target: object, _key?: string | symbol, descriptor?: PropertyDescriptor) => {
8
- if (descriptor) {
9
- Reflect.defineMetadata(IS_PUBLIC_KEY, true, descriptor.value!);
10
- } else {
11
- Reflect.defineMetadata(IS_PUBLIC_KEY, true, target);
12
- }
13
- };
14
-
15
- @Injectable()
16
- export class JwtAuthGuard extends AuthGuard('jwt') {
17
- constructor(private readonly reflector: Reflector) {
18
- super();
19
- }
20
-
21
- canActivate(context: ExecutionContext) {
22
- const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
23
- context.getHandler(),
24
- context.getClass(),
25
- ]);
26
- if (isPublic) return true;
27
- return super.canActivate(context);
28
- }
29
- }
@@ -1,41 +0,0 @@
1
- import { Injectable } from '@nestjs/common';
2
- import { PassportStrategy } from '@nestjs/passport';
3
- import { Strategy, ExtractJwt } from 'passport-jwt';
4
- import { passportJwtSecret } from 'jwks-rsa';
5
-
6
- export interface JwtPayload {
7
- sub: string;
8
- email: string;
9
- realm_access?: { roles: string[] };
10
- preferred_username?: string;
11
- }
12
-
13
- @Injectable()
14
- export class JwtStrategy extends PassportStrategy(Strategy) {
15
- constructor() {
16
- const internalUrl = process.env.KEYCLOAK_INTERNAL_URL || process.env.KEYCLOAK_URL || 'http://localhost:8080';
17
- const publicUrl = process.env.KEYCLOAK_URL || 'http://localhost:8080';
18
- const realm = process.env.KEYCLOAK_REALM || 'arex';
19
-
20
- super({
21
- jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
22
- secretOrKeyProvider: passportJwtSecret({
23
- jwksUri: `${internalUrl}/realms/${realm}/protocol/openid-connect/certs`,
24
- cache: true,
25
- cacheMaxAge: 600_000,
26
- rateLimit: true,
27
- }),
28
- issuer: `${publicUrl}/realms/${realm}`,
29
- algorithms: ['RS256'],
30
- });
31
- }
32
-
33
- validate(payload: JwtPayload) {
34
- return {
35
- keycloakId: payload.sub,
36
- email: payload.email,
37
- roles: payload.realm_access?.roles || [],
38
- username: payload.preferred_username,
39
- };
40
- }
41
- }
@@ -1,39 +0,0 @@
1
- import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
2
- import { Reflector } from '@nestjs/core';
3
-
4
- const ROLES_KEY = 'roles';
5
-
6
- export const Roles = (...roles: string[]): ClassDecorator & MethodDecorator =>
7
- (target: object, _key?: string | symbol, descriptor?: PropertyDescriptor) => {
8
- if (descriptor) {
9
- Reflect.defineMetadata(ROLES_KEY, roles, descriptor.value!);
10
- } else {
11
- Reflect.defineMetadata(ROLES_KEY, roles, target);
12
- }
13
- };
14
-
15
- @Injectable()
16
- export class RolesGuard implements CanActivate {
17
- constructor(private readonly reflector: Reflector) {}
18
-
19
- canActivate(context: ExecutionContext): boolean {
20
- const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
21
- context.getHandler(),
22
- context.getClass(),
23
- ]);
24
-
25
- if (!requiredRoles || requiredRoles.length === 0) return true;
26
-
27
- const request = context.switchToHttp().getRequest();
28
- const userRoles: string[] = request.user?.roles || [];
29
-
30
- const hasRole = requiredRoles.some((role) => userRoles.includes(role));
31
- if (!hasRole) {
32
- throw new ForbiddenException({
33
- error: { code: 'FORBIDDEN', message: 'Insufficient permissions' },
34
- });
35
- }
36
-
37
- return true;
38
- }
39
- }
package/src/index.ts DELETED
@@ -1,118 +0,0 @@
1
- // Result
2
- export { Result, ErrorType } from './result/Result';
3
-
4
- // Entities
5
- export { BaseEntity } from './entities/BaseEntity';
6
- export { TenantBaseEntity } from './entities/TenantBaseEntity';
7
-
8
- // Middleware
9
- export { CorrelationIdMiddleware } from './middleware/CorrelationIdMiddleware';
10
- export { correlationStore } from './middleware/CorrelationStore';
11
- export type { CorrelationStoreData } from './middleware/CorrelationStore';
12
- export { TenantContextMiddleware, TenantContext } from './middleware/TenantContextMiddleware';
13
- export { tenantStore } from './middleware/TenantStore';
14
- export type { TenantStoreData } from './middleware/TenantStore';
15
-
16
- // Filters
17
- export { GlobalExceptionFilter } from './filters/GlobalExceptionFilter';
18
-
19
- // Interceptors
20
- export { ResultInterceptor } from './interceptors/ResultInterceptor';
21
-
22
- // Guards
23
- export { JwtAuthGuard, Public } from './guards/JwtAuthGuard';
24
- export { JwtStrategy, JwtPayload } from './guards/JwtStrategy';
25
- export { RolesGuard, Roles } from './guards/RolesGuard';
26
-
27
- // Subscribers
28
- export { TenantSubscriber } from './subscribers/TenantSubscriber';
29
-
30
- // Resilience
31
- export { createCircuitBreaker } from './resilience/CircuitBreakerFactory';
32
- export type { CircuitBreakerOptions } from './resilience/CircuitBreakerFactory';
33
-
34
- // Bootstrap
35
- export { bootstrapService } from './bootstrap/bootstrapService';
36
- export type { BootstrapOptions } from './bootstrap/bootstrapService';
37
-
38
- // Logging
39
- export { createPinoConfig } from './logging/pino-config';
40
-
41
- // CQRS Constants
42
- export { REDIS_CLIENT } from './cqrs/constants';
43
-
44
- // CQRS Decorators
45
- export { FeatureFlag, getFeatureFlagMetadata } from './cqrs/decorators/FeatureFlag.decorator';
46
- export type { FeatureFlagOptions } from './cqrs/decorators/FeatureFlag.decorator';
47
- export { Log, getLogMetadata } from './cqrs/decorators/Log.decorator';
48
- export type { LogOptions } from './cqrs/decorators/Log.decorator';
49
- export { Validate, shouldValidate, getValidatorClass } from './cqrs/decorators/Validate.decorator';
50
-
51
- // Validation
52
- export type { ICommandValidator } from './cqrs/validation/ICommandValidator';
53
- export { validateCommand } from './cqrs/validation/ICommandValidator';
54
- export { Cache, getCacheMetadata } from './cqrs/decorators/Cache.decorator';
55
- export type { CacheOptions } from './cqrs/decorators/Cache.decorator';
56
- export { InvalidateCache, getInvalidateCacheMetadata } from './cqrs/decorators/InvalidateCache.decorator';
57
- export type { InvalidateCacheOptions } from './cqrs/decorators/InvalidateCache.decorator';
58
- export { DistributedLock, getDistributedLockMetadata } from './cqrs/decorators/DistributedLock.decorator';
59
- export type { DistributedLockOptions } from './cqrs/decorators/DistributedLock.decorator';
60
- export { IsolatedTransaction, isIsolatedTransaction } from './cqrs/decorators/IsolatedTransaction.decorator';
61
- export { Workflow, getWorkflowMetadata } from './cqrs/decorators/Workflow.decorator';
62
- export type { WorkflowOptions } from './cqrs/decorators/Workflow.decorator';
63
-
64
- // Workflow Engine Interface
65
- export { WORKFLOW_ENGINE } from './cqrs/interfaces/WorkflowEngine';
66
- export type { WorkflowEngine, WorkflowStartResult } from './cqrs/interfaces/WorkflowEngine';
67
-
68
- // Transaction (UnitOfWork)
69
- export { TransactionContext } from './cqrs/transaction/TransactionContext';
70
- export { getTransactionalRepo } from './cqrs/transaction/getTransactionalRepo';
71
-
72
- // Metrics
73
- export { MetricsService } from './metrics/MetricsService';
74
- export { MetricsController } from './metrics/MetricsController';
75
-
76
- // CQRS Behaviors
77
- export { LogBehavior } from './cqrs/behaviors/LogBehavior';
78
- export { FeatureFlagBehavior } from './cqrs/behaviors/FeatureFlagBehavior';
79
- export { ValidationBehavior } from './cqrs/behaviors/ValidationBehavior';
80
- export { CacheBehavior } from './cqrs/behaviors/CacheBehavior';
81
- export { InvalidateCacheBehavior } from './cqrs/behaviors/InvalidateCacheBehavior';
82
- export { DistributedLockBehavior } from './cqrs/behaviors/DistributedLockBehavior';
83
- export { TransactionalBehavior } from './cqrs/behaviors/TransactionalBehavior';
84
- export { PerformanceBehavior } from './cqrs/behaviors/PerformanceBehavior';
85
- export { WorkflowBehavior } from './cqrs/behaviors/WorkflowBehavior';
86
-
87
- // CQRS Pipeline
88
- export { QuanticCommandBus } from './cqrs/pipeline/QuanticCommandBus';
89
- export { QuanticQueryBus } from './cqrs/pipeline/QuanticQueryBus';
90
- export { type BehaviorFn } from './cqrs/pipeline/runPipeline';
91
-
92
- // Redis
93
- export { RedisModule } from './redis/redis.module';
94
- export type { RedisModuleOptions } from './redis/redis.module';
95
-
96
- // Events
97
- export { DomainEvent } from './events/DomainEvent';
98
- export type { DomainEventPayload } from './events/DomainEvent';
99
- export { OutboxEvent, OutboxEventStatus } from './events/OutboxEvent.entity';
100
- export { RedisStreamPublisher } from './events/RedisStreamPublisher';
101
- export { RedisStreamConsumer } from './events/RedisStreamConsumer';
102
- export { OutboxPublisherService } from './events/OutboxPublisherService';
103
-
104
- // Unleash
105
- export { UnleashModule } from './unleash/unleash.module';
106
- export type { UnleashModuleOptions } from './unleash/unleash.module';
107
-
108
- // Lifecycle
109
- export { GracefulShutdownService } from './lifecycle/GracefulShutdownService';
110
-
111
- // QuanticModule
112
- export { QuanticModule, SharedKernelModule } from './shared-kernel.module';
113
- export type { QuanticModuleOptions, SharedKernelModuleOptions } from './shared-kernel.module';
114
-
115
- // Testing mock helpers (safe for prod — no @nestjs/testing dependency)
116
- export { createMockRepository, createMockRedisClient } from './testing/mocks';
117
-
118
-
@@ -1,93 +0,0 @@
1
- import {
2
- Injectable,
3
- NestInterceptor,
4
- ExecutionContext,
5
- CallHandler,
6
- Logger,
7
- } from '@nestjs/common';
8
- import { Request, Response } from 'express';
9
- import { Observable, switchMap, catchError, of, EMPTY } from 'rxjs';
10
- import { Result, ErrorType } from '../result/Result';
11
- import { correlationStore } from '../middleware/CorrelationStore';
12
-
13
- const ERROR_TYPE_TO_STATUS: Record<string, number> = {
14
- [ErrorType.NotFound]: 404,
15
- [ErrorType.Forbidden]: 403,
16
- [ErrorType.Unauthorized]: 401,
17
- [ErrorType.Conflict]: 409,
18
- [ErrorType.ValidationError]: 400,
19
- [ErrorType.UnprocessableEntity]: 422,
20
- [ErrorType.InternalError]: 500,
21
- };
22
-
23
- const ERROR_TYPE_TO_TITLE: Record<string, string> = {
24
- [ErrorType.NotFound]: 'Not Found',
25
- [ErrorType.Forbidden]: 'Forbidden',
26
- [ErrorType.Unauthorized]: 'Unauthorized',
27
- [ErrorType.Conflict]: 'Conflict',
28
- [ErrorType.ValidationError]: 'Validation Error',
29
- [ErrorType.UnprocessableEntity]: 'Unprocessable Entity',
30
- [ErrorType.InternalError]: 'Internal Server Error',
31
- };
32
-
33
- @Injectable()
34
- export class ResultInterceptor implements NestInterceptor {
35
- private readonly logger = new Logger(ResultInterceptor.name);
36
-
37
- intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
38
- return next.handle().pipe(
39
- switchMap((data) => {
40
- if (data instanceof Result && !data.isSuccess) {
41
- this.sendResultError(context, data);
42
- return EMPTY;
43
- }
44
- return of(data);
45
- }),
46
- catchError((error) => {
47
- if (error instanceof Result) {
48
- this.sendResultError(context, error);
49
- return EMPTY;
50
- }
51
- const err = error instanceof Error ? error : new Error(String(error));
52
- this.logger.error({
53
- msg: 'Unhandled exception before CQRS pipeline',
54
- error: err.message,
55
- stack: err.stack,
56
- });
57
- this.sendResultError(context, Result.failure(ErrorType.InternalError, err.message));
58
- return EMPTY;
59
- }),
60
- );
61
- }
62
-
63
- private sendResultError(context: ExecutionContext, result: Result<unknown>): void {
64
- const request = context.switchToHttp().getRequest<Request>();
65
- const response = context.switchToHttp().getResponse<Response>();
66
- const correlationId = correlationStore.getStore()?.correlationId;
67
- const status = ERROR_TYPE_TO_STATUS[result.errorType!] ?? 500;
68
- const title = ERROR_TYPE_TO_TITLE[result.errorType!] ?? 'Unknown Error';
69
-
70
- if (correlationId) {
71
- response.setHeader('X-Correlation-ID', correlationId);
72
- }
73
-
74
- this.logger.warn({
75
- msg: `${status} ${result.errorType}`,
76
- correlationId,
77
- status,
78
- detail: result.errorMessage,
79
- });
80
-
81
- response
82
- .status(status)
83
- .setHeader('Content-Type', 'application/problem+json')
84
- .json({
85
- type: `https://arex.dev/errors/${result.errorType}`,
86
- title,
87
- status,
88
- detail: result.errorMessage,
89
- instance: request.originalUrl,
90
- correlationId,
91
- });
92
- }
93
- }
@@ -1,77 +0,0 @@
1
- import { Injectable, Logger, OnModuleDestroy, Optional, Inject } from '@nestjs/common';
2
- import { DataSource } from 'typeorm';
3
- import { REDIS_CLIENT } from '../cqrs/constants';
4
- import type { Redis } from 'ioredis';
5
-
6
- const SHUTDOWN_TIMEOUT_MS = 30_000;
7
-
8
- /**
9
- * GracefulShutdownService ensures clean resource teardown on SIGTERM/SIGINT.
10
- *
11
- * Shutdown order:
12
- * 1. Wait for in-flight work (subclasses can override `drainWork`)
13
- * 2. Close database connections
14
- * 3. Quit Redis
15
- */
16
- @Injectable()
17
- export class GracefulShutdownService implements OnModuleDestroy {
18
- private readonly logger = new Logger(GracefulShutdownService.name);
19
-
20
- constructor(
21
- @Optional() private readonly dataSource?: DataSource,
22
- @Optional() @Inject(REDIS_CLIENT) private readonly redis?: Redis,
23
- ) {}
24
-
25
- async onModuleDestroy(): Promise<void> {
26
- this.logger.log('Graceful shutdown initiated...');
27
-
28
- // 1. Drain in-flight work with timeout
29
- try {
30
- await this.withTimeout(this.drainWork(), SHUTDOWN_TIMEOUT_MS, 'Work drain');
31
- } catch (err) {
32
- this.logger.warn(`Work drain timeout or error: ${(err as Error).message}`);
33
- }
34
-
35
- // 2. Close database connections
36
- if (this.dataSource?.isInitialized) {
37
- try {
38
- await this.dataSource.destroy();
39
- this.logger.log('Database connections closed');
40
- } catch (err) {
41
- this.logger.warn(`Database close error: ${(err as Error).message}`);
42
- }
43
- }
44
-
45
- // 3. Quit Redis
46
- if (this.redis) {
47
- try {
48
- await this.redis.quit();
49
- this.logger.log('Redis connection closed');
50
- } catch (err) {
51
- this.logger.warn(`Redis quit error: ${(err as Error).message}`);
52
- }
53
- }
54
-
55
- this.logger.log('Graceful shutdown complete');
56
- }
57
-
58
- /**
59
- * Override in service-specific subclasses to drain Bull queues,
60
- * close Socket.IO servers, etc.
61
- */
62
- protected async drainWork(): Promise<void> {
63
- // Base implementation — no-op
64
- }
65
-
66
- private withTimeout(promise: Promise<void>, ms: number, label: string): Promise<void> {
67
- return new Promise((resolve, reject) => {
68
- const timer = setTimeout(() => {
69
- reject(new Error(`${label} timed out after ${ms}ms`));
70
- }, ms);
71
-
72
- promise
73
- .then(() => { clearTimeout(timer); resolve(); })
74
- .catch((err) => { clearTimeout(timer); reject(err); });
75
- });
76
- }
77
- }
@@ -1,80 +0,0 @@
1
- function canResolvePinoSeq(): boolean {
2
- try {
3
- require.resolve('pino-seq');
4
- return true;
5
- } catch {
6
- return false;
7
- }
8
- }
9
-
10
- function buildTransport(logLevel: string) {
11
- const seqUrl = process.env['SEQ_URL'];
12
- const useSeq = seqUrl && canResolvePinoSeq();
13
-
14
- if (process.env['NODE_ENV'] === 'production' && useSeq) {
15
- return {
16
- target: 'pino-seq',
17
- options: { serverUrl: seqUrl, logOtherAs: 'Verbose' },
18
- };
19
- }
20
-
21
- const targets: any[] = [
22
- { target: 'pino/file', options: { destination: 1 }, level: logLevel },
23
- ];
24
-
25
- if (useSeq) {
26
- targets.push({
27
- target: 'pino-seq',
28
- options: { serverUrl: seqUrl, logOtherAs: 'Verbose' },
29
- level: logLevel,
30
- });
31
- }
32
-
33
- return { targets };
34
- }
35
-
36
- export function createPinoConfig(serviceName: string) {
37
- const logLevel = process.env['LOG_LEVEL'] || 'info';
38
-
39
- return {
40
- pinoHttp: {
41
- level: logLevel,
42
- transport: buildTransport(logLevel),
43
- serializers: {
44
- req(req: any) {
45
- return {
46
- id: req.id,
47
- method: req.method,
48
- url: req.url,
49
- correlationId: req.raw?.correlationId || req.headers?.['x-correlation-id'],
50
- };
51
- },
52
- res(res: any) {
53
- return {
54
- statusCode: res.statusCode,
55
- };
56
- },
57
- email(value: string) {
58
- if (!value || typeof value !== 'string') return value;
59
- const [local, domain] = value.split('@');
60
- if (!domain) return value;
61
- return `${local[0]}***@${domain}`;
62
- },
63
- token(_value: string) {
64
- return '[REDACTED]';
65
- },
66
- brd(value: string) {
67
- if (!value || typeof value !== 'string') return value;
68
- return value.length > 50 ? `${value.substring(0, 50)}...` : value;
69
- },
70
- },
71
- customProps(req: any) {
72
- return {
73
- correlationId:
74
- req.correlationId || req.headers?.['x-correlation-id'],
75
- service: serviceName,
76
- };
77
- },
78
- },
79
- };
80
- }
@@ -1,17 +0,0 @@
1
- import { Controller, Get, Res } from '@nestjs/common';
2
- import { Response } from 'express';
3
- import { Public } from '../guards/JwtAuthGuard';
4
- import { MetricsService } from './MetricsService';
5
-
6
- @Public()
7
- @Controller()
8
- export class MetricsController {
9
- constructor(private readonly metrics: MetricsService) {}
10
-
11
- @Get('metrics')
12
- async getMetrics(@Res() res: Response) {
13
- const metricsOutput = await this.metrics.getMetrics();
14
- res.set('Content-Type', this.metrics.getContentType());
15
- res.end(metricsOutput);
16
- }
17
- }
@@ -1,55 +0,0 @@
1
- import { Injectable, OnModuleInit } from '@nestjs/common';
2
- import { Registry, Counter, Histogram, Gauge, collectDefaultMetrics } from 'prom-client';
3
-
4
- @Injectable()
5
- export class MetricsService implements OnModuleInit {
6
- readonly registry = new Registry();
7
-
8
- readonly buildTotal = new Counter({
9
- name: 'arex_build_total',
10
- help: 'Total number of builds',
11
- labelNames: ['status', 'tech_stack'] as const,
12
- registers: [this.registry],
13
- });
14
-
15
- readonly stageDuration = new Histogram({
16
- name: 'arex_stage_duration_seconds',
17
- help: 'Duration of pipeline stages in seconds',
18
- labelNames: ['stage_type', 'status'] as const,
19
- buckets: [0.5, 1, 2, 5, 10, 30, 60, 120, 300],
20
- registers: [this.registry],
21
- });
22
-
23
- readonly promptCacheHits = new Counter({
24
- name: 'arex_prompt_cache_hits_total',
25
- help: 'Total prompt cache hits',
26
- registers: [this.registry],
27
- });
28
-
29
- readonly queueDepth = new Gauge({
30
- name: 'arex_queue_depth',
31
- help: 'Current depth of Bull job queue',
32
- labelNames: ['queue'] as const,
33
- registers: [this.registry],
34
- });
35
-
36
- readonly handlerDuration = new Histogram({
37
- name: 'arex_handler_duration_seconds',
38
- help: 'Duration of command/query handlers in seconds',
39
- labelNames: ['handler', 'result'] as const,
40
- buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2, 5],
41
- registers: [this.registry],
42
- });
43
-
44
- onModuleInit() {
45
- collectDefaultMetrics({ register: this.registry });
46
- }
47
-
48
- async getMetrics(): Promise<string> {
49
- return this.registry.metrics();
50
- }
51
-
52
- getContentType(): string {
53
- return this.registry.contentType;
54
- }
55
- }