@quanticjs/core 1.1.1
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.
- package/dist/bootstrap/bootstrapService.d.ts +8 -0
- package/dist/bootstrap/bootstrapService.js +58 -0
- package/dist/cqrs/PipelineExecutor.d.ts +31 -0
- package/dist/cqrs/PipelineExecutor.js +81 -0
- package/dist/cqrs/behaviors/CacheBehavior.d.ts +9 -0
- package/dist/cqrs/behaviors/CacheBehavior.js +68 -0
- package/dist/cqrs/behaviors/DistributedLockBehavior.d.ts +12 -0
- package/dist/cqrs/behaviors/DistributedLockBehavior.js +90 -0
- package/dist/cqrs/behaviors/FeatureFlagBehavior.d.ts +8 -0
- package/dist/cqrs/behaviors/FeatureFlagBehavior.js +58 -0
- package/dist/cqrs/behaviors/InvalidateCacheBehavior.d.ts +9 -0
- package/dist/cqrs/behaviors/InvalidateCacheBehavior.js +61 -0
- package/dist/cqrs/behaviors/LogBehavior.d.ts +9 -0
- package/dist/cqrs/behaviors/LogBehavior.js +125 -0
- package/dist/cqrs/behaviors/PerformanceBehavior.d.ts +12 -0
- package/dist/cqrs/behaviors/PerformanceBehavior.js +56 -0
- package/dist/cqrs/behaviors/TransactionalBehavior.d.ts +18 -0
- package/dist/cqrs/behaviors/TransactionalBehavior.js +77 -0
- package/dist/cqrs/behaviors/ValidationBehavior.d.ts +4 -0
- package/dist/cqrs/behaviors/ValidationBehavior.js +33 -0
- package/dist/cqrs/behaviors/WorkflowBehavior.d.ts +8 -0
- package/dist/cqrs/behaviors/WorkflowBehavior.js +61 -0
- package/dist/cqrs/constants.d.ts +2 -0
- package/dist/cqrs/constants.js +5 -0
- package/dist/cqrs/decorators/Cache.decorator.d.ts +13 -0
- package/dist/cqrs/decorators/Cache.decorator.js +18 -0
- package/dist/cqrs/decorators/DistributedLock.decorator.d.ts +15 -0
- package/dist/cqrs/decorators/DistributedLock.decorator.js +23 -0
- package/dist/cqrs/decorators/FeatureFlag.decorator.d.ts +9 -0
- package/dist/cqrs/decorators/FeatureFlag.decorator.js +14 -0
- package/dist/cqrs/decorators/InvalidateCache.decorator.d.ts +6 -0
- package/dist/cqrs/decorators/InvalidateCache.decorator.js +14 -0
- package/dist/cqrs/decorators/IsolatedTransaction.decorator.d.ts +14 -0
- package/dist/cqrs/decorators/IsolatedTransaction.decorator.js +25 -0
- package/dist/cqrs/decorators/Log.decorator.d.ts +11 -0
- package/dist/cqrs/decorators/Log.decorator.js +18 -0
- package/dist/cqrs/decorators/Validate.decorator.d.ts +24 -0
- package/dist/cqrs/decorators/Validate.decorator.js +37 -0
- package/dist/cqrs/decorators/Workflow.decorator.d.ts +8 -0
- package/dist/cqrs/decorators/Workflow.decorator.js +14 -0
- package/dist/cqrs/interfaces/WorkflowEngine.d.ts +14 -0
- package/dist/cqrs/interfaces/WorkflowEngine.js +4 -0
- package/dist/cqrs/pipeline/QuanticCommandBus.d.ts +37 -0
- package/dist/cqrs/pipeline/QuanticCommandBus.js +99 -0
- package/dist/cqrs/pipeline/QuanticQueryBus.d.ts +28 -0
- package/dist/cqrs/pipeline/QuanticQueryBus.js +78 -0
- package/dist/cqrs/pipeline/runPipeline.d.ts +3 -0
- package/dist/cqrs/pipeline/runPipeline.js +12 -0
- package/dist/cqrs/transaction/TransactionContext.d.ts +18 -0
- package/dist/cqrs/transaction/TransactionContext.js +26 -0
- package/dist/cqrs/transaction/getTransactionalRepo.d.ts +16 -0
- package/dist/cqrs/transaction/getTransactionalRepo.js +22 -0
- package/dist/cqrs/validation/ICommandValidator.d.ts +48 -0
- package/dist/cqrs/validation/ICommandValidator.js +21 -0
- package/dist/entities/BaseEntity.d.ts +5 -0
- package/dist/entities/BaseEntity.js +31 -0
- package/dist/entities/TenantBaseEntity.d.ts +4 -0
- package/dist/entities/TenantBaseEntity.js +22 -0
- package/dist/events/DomainEvent.d.ts +14 -0
- package/dist/events/DomainEvent.js +27 -0
- package/dist/events/OutboxEvent.entity.d.ts +18 -0
- package/dist/events/OutboxEvent.entity.js +87 -0
- package/dist/events/OutboxPublisherService.d.ts +14 -0
- package/dist/events/OutboxPublisherService.js +104 -0
- package/dist/events/RedisStreamConsumer.d.ts +43 -0
- package/dist/events/RedisStreamConsumer.js +158 -0
- package/dist/events/RedisStreamPublisher.d.ts +9 -0
- package/dist/events/RedisStreamPublisher.js +60 -0
- package/dist/filters/GlobalExceptionFilter.d.ts +11 -0
- package/dist/filters/GlobalExceptionFilter.js +102 -0
- package/dist/guards/JwtAuthGuard.d.ts +10 -0
- package/dist/guards/JwtAuthGuard.js +46 -0
- package/dist/guards/JwtStrategy.d.ts +22 -0
- package/dist/guards/JwtStrategy.js +47 -0
- package/dist/guards/RolesGuard.d.ts +8 -0
- package/dist/guards/RolesGuard.js +52 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +146 -0
- package/dist/interceptors/ResultInterceptor.d.ts +7 -0
- package/dist/interceptors/ResultInterceptor.js +88 -0
- package/dist/lifecycle/GracefulShutdownService.d.ts +24 -0
- package/dist/lifecycle/GracefulShutdownService.js +93 -0
- package/dist/logging/pino-config.d.ts +35 -0
- package/dist/logging/pino-config.js +79 -0
- package/dist/metrics/MetricsController.d.ts +7 -0
- package/dist/metrics/MetricsController.js +42 -0
- package/dist/metrics/MetricsService.d.ts +13 -0
- package/dist/metrics/MetricsService.js +58 -0
- package/dist/middleware/CorrelationIdMiddleware.d.ts +4 -0
- package/dist/middleware/CorrelationIdMiddleware.js +33 -0
- package/dist/middleware/CorrelationStore.d.ts +11 -0
- package/dist/middleware/CorrelationStore.js +9 -0
- package/dist/middleware/TenantContextMiddleware.d.ts +7 -0
- package/dist/middleware/TenantContextMiddleware.js +27 -0
- package/dist/middleware/TenantStore.d.ts +9 -0
- package/dist/middleware/TenantStore.js +9 -0
- package/dist/redis/redis.module.d.ts +8 -0
- package/dist/redis/redis.module.js +49 -0
- package/dist/resilience/CircuitBreakerFactory.d.ts +12 -0
- package/dist/resilience/CircuitBreakerFactory.js +22 -0
- package/dist/result/Result.d.ts +26 -0
- package/dist/result/Result.js +62 -0
- package/dist/shared-kernel.module.d.ts +13 -0
- package/dist/shared-kernel.module.js +87 -0
- package/dist/subscribers/TenantSubscriber.d.ts +20 -0
- package/dist/subscribers/TenantSubscriber.js +52 -0
- package/dist/testing/TestingModuleFactory.d.ts +23 -0
- package/dist/testing/TestingModuleFactory.js +63 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/mocks.d.ts +34 -0
- package/dist/testing/mocks.js +62 -0
- package/dist/unleash/initial-flags.d.ts +7 -0
- package/dist/unleash/initial-flags.js +9 -0
- package/dist/unleash/unleash.module.d.ts +9 -0
- package/dist/unleash/unleash.module.js +47 -0
- package/package.json +140 -0
- package/src/bootstrap/bootstrapService.ts +72 -0
- package/src/cqrs/behaviors/CacheBehavior.spec.ts +63 -0
- package/src/cqrs/behaviors/CacheBehavior.ts +54 -0
- package/src/cqrs/behaviors/DistributedLockBehavior.ts +88 -0
- package/src/cqrs/behaviors/FeatureFlagBehavior.ts +46 -0
- package/src/cqrs/behaviors/InvalidateCacheBehavior.spec.ts +89 -0
- package/src/cqrs/behaviors/InvalidateCacheBehavior.ts +50 -0
- package/src/cqrs/behaviors/LogBehavior.spec.ts +55 -0
- package/src/cqrs/behaviors/LogBehavior.ts +121 -0
- package/src/cqrs/behaviors/PerformanceBehavior.spec.ts +48 -0
- package/src/cqrs/behaviors/PerformanceBehavior.ts +43 -0
- package/src/cqrs/behaviors/TransactionalBehavior.ts +64 -0
- package/src/cqrs/behaviors/ValidationBehavior.spec.ts +114 -0
- package/src/cqrs/behaviors/ValidationBehavior.ts +29 -0
- package/src/cqrs/behaviors/WorkflowBehavior.spec.ts +97 -0
- package/src/cqrs/behaviors/WorkflowBehavior.ts +62 -0
- package/src/cqrs/constants.ts +2 -0
- package/src/cqrs/decorators/Cache.decorator.ts +24 -0
- package/src/cqrs/decorators/DistributedLock.decorator.ts +34 -0
- package/src/cqrs/decorators/FeatureFlag.decorator.ts +23 -0
- package/src/cqrs/decorators/InvalidateCache.decorator.spec.ts +20 -0
- package/src/cqrs/decorators/InvalidateCache.decorator.ts +17 -0
- package/src/cqrs/decorators/IsolatedTransaction.decorator.ts +24 -0
- package/src/cqrs/decorators/Log.decorator.ts +22 -0
- package/src/cqrs/decorators/Validate.decorator.ts +39 -0
- package/src/cqrs/decorators/Workflow.decorator.ts +22 -0
- package/src/cqrs/interfaces/WorkflowEngine.ts +19 -0
- package/src/cqrs/pipeline/QuanticCommandBus.ts +69 -0
- package/src/cqrs/pipeline/QuanticQueryBus.ts +56 -0
- package/src/cqrs/pipeline/runPipeline.ts +22 -0
- package/src/cqrs/transaction/TransactionContext.ts +26 -0
- package/src/cqrs/transaction/getTransactionalRepo.ts +23 -0
- package/src/cqrs/validation/ICommandValidator.ts +55 -0
- package/src/entities/BaseEntity.ts +16 -0
- package/src/entities/TenantBaseEntity.ts +7 -0
- package/src/events/DomainEvent.ts +27 -0
- package/src/events/OutboxEvent.entity.ts +56 -0
- package/src/events/OutboxPublisherService.ts +94 -0
- package/src/events/RedisStreamConsumer.ts +172 -0
- package/src/events/RedisStreamPublisher.ts +54 -0
- package/src/filters/GlobalExceptionFilter.ts +125 -0
- package/src/guards/JwtAuthGuard.ts +29 -0
- package/src/guards/JwtStrategy.ts +41 -0
- package/src/guards/RolesGuard.ts +39 -0
- package/src/index.ts +118 -0
- package/src/interceptors/ResultInterceptor.ts +93 -0
- package/src/lifecycle/GracefulShutdownService.ts +77 -0
- package/src/logging/pino-config.ts +80 -0
- package/src/metrics/MetricsController.ts +17 -0
- package/src/metrics/MetricsService.ts +55 -0
- package/src/middleware/CorrelationIdMiddleware.ts +27 -0
- package/src/middleware/CorrelationStore.ts +13 -0
- package/src/middleware/TenantContextMiddleware.ts +21 -0
- package/src/middleware/TenantStore.ts +11 -0
- package/src/redis/redis.module.ts +41 -0
- package/src/resilience/CircuitBreakerFactory.ts +33 -0
- package/src/result/Result.ts +66 -0
- package/src/shared-kernel.module.ts +87 -0
- package/src/subscribers/TenantSubscriber.ts +47 -0
- package/src/testing/TestingModuleFactory.ts +78 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/mocks.ts +59 -0
- package/src/unleash/unleash.module.ts +45 -0
- package/tsconfig.json +22 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { correlationStore } from './CorrelationStore';
|
|
4
|
+
|
|
5
|
+
const CORRELATION_ID_HEADER = 'X-Correlation-ID';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class CorrelationIdMiddleware implements NestMiddleware {
|
|
9
|
+
use(req: any, res: any, next: () => void): void {
|
|
10
|
+
const correlationId =
|
|
11
|
+
(req.headers[CORRELATION_ID_HEADER.toLowerCase()] as string) || uuidv4();
|
|
12
|
+
|
|
13
|
+
req.correlationId = correlationId;
|
|
14
|
+
req.headers[CORRELATION_ID_HEADER.toLowerCase()] = correlationId;
|
|
15
|
+
res.setHeader(CORRELATION_ID_HEADER, correlationId);
|
|
16
|
+
|
|
17
|
+
const storeData = {
|
|
18
|
+
correlationId,
|
|
19
|
+
userId: req.user?.keycloakId,
|
|
20
|
+
organizationId: req.user?.organizationId,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
correlationStore.run(storeData, () => {
|
|
24
|
+
next();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
export interface CorrelationStoreData {
|
|
4
|
+
correlationId: string;
|
|
5
|
+
userId?: string;
|
|
6
|
+
organizationId?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Request-scoped async local storage for correlation context.
|
|
11
|
+
* Set by CorrelationIdMiddleware, read by LogBehavior and error filters.
|
|
12
|
+
*/
|
|
13
|
+
export const correlationStore = new AsyncLocalStorage<CorrelationStoreData>();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
2
|
+
import { tenantStore } from './TenantStore';
|
|
3
|
+
|
|
4
|
+
export interface TenantContext {
|
|
5
|
+
organizationId: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class TenantContextMiddleware implements NestMiddleware {
|
|
10
|
+
use(req: any, _res: any, next: () => void): void {
|
|
11
|
+
// Extract org_id from JWT payload (set by JwtAuthGuard)
|
|
12
|
+
const orgId = req.user?.organizationId || req.headers['x-tenant-id'] || null;
|
|
13
|
+
req.tenantId = orgId;
|
|
14
|
+
req.tenantContext = { organizationId: orgId } as TenantContext;
|
|
15
|
+
|
|
16
|
+
// Store in AsyncLocalStorage so TenantSubscriber can access it
|
|
17
|
+
tenantStore.run({ organizationId: orgId }, () => {
|
|
18
|
+
next();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
export interface TenantStoreData {
|
|
4
|
+
organizationId: string | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Request-scoped async local storage for tenant context.
|
|
9
|
+
* Set by TenantContextMiddleware, read by TenantSubscriber.
|
|
10
|
+
*/
|
|
11
|
+
export const tenantStore = new AsyncLocalStorage<TenantStoreData>();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DynamicModule, Global, Logger, Module } from '@nestjs/common';
|
|
2
|
+
import Redis from 'ioredis';
|
|
3
|
+
import { REDIS_CLIENT } from '../cqrs/constants';
|
|
4
|
+
|
|
5
|
+
export interface RedisModuleOptions {
|
|
6
|
+
url?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Global()
|
|
10
|
+
@Module({})
|
|
11
|
+
export class RedisModule {
|
|
12
|
+
private static readonly logger = new Logger('RedisModule');
|
|
13
|
+
|
|
14
|
+
static forRoot(options: RedisModuleOptions = {}): DynamicModule {
|
|
15
|
+
const redisProvider = {
|
|
16
|
+
provide: REDIS_CLIENT,
|
|
17
|
+
useFactory: () => {
|
|
18
|
+
const url = options.url || process.env.REDIS_URL || 'redis://localhost:6379';
|
|
19
|
+
const client = new Redis(url, {
|
|
20
|
+
maxRetriesPerRequest: 3,
|
|
21
|
+
retryStrategy(times: number) {
|
|
22
|
+
const delay = Math.min(times * 200, 5000);
|
|
23
|
+
RedisModule.logger.warn(`Redis reconnecting (attempt ${times}, delay ${delay}ms)`);
|
|
24
|
+
return delay;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
client.on('connect', () => RedisModule.logger.log('Redis connected'));
|
|
29
|
+
client.on('error', (err) => RedisModule.logger.error('Redis error', err.message));
|
|
30
|
+
|
|
31
|
+
return client;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
module: RedisModule,
|
|
37
|
+
providers: [redisProvider],
|
|
38
|
+
exports: [REDIS_CLIENT],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { circuitBreaker, ConsecutiveBreaker, handleAll, retry, wrap, ExponentialBackoff } from 'cockatiel';
|
|
2
|
+
|
|
3
|
+
export interface CircuitBreakerOptions {
|
|
4
|
+
halfOpenAfterMs?: number;
|
|
5
|
+
consecutiveFailures?: number;
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a resilience policy combining retry + circuit breaker.
|
|
11
|
+
* Usage:
|
|
12
|
+
* const policy = createCircuitBreaker({ consecutiveFailures: 5 });
|
|
13
|
+
* const result = await policy.execute(() => httpCall());
|
|
14
|
+
*/
|
|
15
|
+
export function createCircuitBreaker(options: CircuitBreakerOptions = {}) {
|
|
16
|
+
const {
|
|
17
|
+
halfOpenAfterMs = 30_000,
|
|
18
|
+
consecutiveFailures = 5,
|
|
19
|
+
maxRetries = 2,
|
|
20
|
+
} = options;
|
|
21
|
+
|
|
22
|
+
const retryPolicy = retry(handleAll, {
|
|
23
|
+
maxAttempts: maxRetries,
|
|
24
|
+
backoff: new ExponentialBackoff(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const breaker = circuitBreaker(handleAll, {
|
|
28
|
+
halfOpenAfter: halfOpenAfterMs,
|
|
29
|
+
breaker: new ConsecutiveBreaker(consecutiveFailures),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return wrap(retryPolicy, breaker);
|
|
33
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export enum ErrorType {
|
|
2
|
+
NotFound = 'NOT_FOUND',
|
|
3
|
+
Forbidden = 'FORBIDDEN',
|
|
4
|
+
Conflict = 'CONFLICT',
|
|
5
|
+
ValidationError = 'VALIDATION_ERROR',
|
|
6
|
+
InternalError = 'INTERNAL_ERROR',
|
|
7
|
+
Unauthorized = 'UNAUTHORIZED',
|
|
8
|
+
UnprocessableEntity = 'UNPROCESSABLE_ENTITY',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class Result<T> {
|
|
12
|
+
private constructor(
|
|
13
|
+
public readonly isSuccess: boolean,
|
|
14
|
+
public readonly value?: T,
|
|
15
|
+
public readonly errorType?: ErrorType,
|
|
16
|
+
public readonly errorMessage?: string,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
static success<T>(value: T): Result<T> {
|
|
20
|
+
return new Result<T>(true, value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static failure<T>(errorType: ErrorType, message: string): Result<T> {
|
|
24
|
+
return new Result<T>(false, undefined, errorType, message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static notFound<T>(message: string): Result<T> {
|
|
28
|
+
return Result.failure<T>(ErrorType.NotFound, message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static forbidden<T>(message: string): Result<T> {
|
|
32
|
+
return Result.failure<T>(ErrorType.Forbidden, message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static conflict<T>(message: string): Result<T> {
|
|
36
|
+
return Result.failure<T>(ErrorType.Conflict, message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static validationError<T>(message: string): Result<T> {
|
|
40
|
+
return Result.failure<T>(ErrorType.ValidationError, message);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static unauthorized<T>(message: string): Result<T> {
|
|
44
|
+
return Result.failure<T>(ErrorType.Unauthorized, message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static unprocessableEntity<T>(message: string): Result<T> {
|
|
48
|
+
return Result.failure<T>(ErrorType.UnprocessableEntity, message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
unwrap(): T {
|
|
52
|
+
if (!this.isSuccess || this.value === undefined) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Cannot unwrap failed Result: ${this.errorType} - ${this.errorMessage}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return this.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
map<U>(fn: (value: T) => U): Result<U> {
|
|
61
|
+
if (this.isSuccess && this.value !== undefined) {
|
|
62
|
+
return Result.success(fn(this.value));
|
|
63
|
+
}
|
|
64
|
+
return Result.failure<U>(this.errorType!, this.errorMessage!);
|
|
65
|
+
}
|
|
66
|
+
}
|