@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,88 +0,0 @@
1
- import { Injectable, Logger, Optional, Inject } from '@nestjs/common';
2
- import { getDistributedLockMetadata } from '../decorators/DistributedLock.decorator';
3
- import { Result, ErrorType } from '../../result/Result';
4
- import { REDIS_CLIENT } from '../constants';
5
- import type { Redis } from 'ioredis';
6
- import { v4 as uuidv4 } from 'uuid';
7
-
8
- @Injectable()
9
- export class DistributedLockBehavior {
10
- private readonly logger = new Logger(DistributedLockBehavior.name);
11
-
12
- constructor(
13
- @Optional() @Inject(REDIS_CLIENT) private readonly redis?: Redis,
14
- ) {}
15
-
16
- async execute<T>(command: object, next: () => Promise<Result<T>>): Promise<Result<T>> {
17
- const metadata = getDistributedLockMetadata(command.constructor);
18
-
19
- if (!metadata || !this.redis) {
20
- return next();
21
- }
22
-
23
- const lockKey = `lock:${this.interpolateKey(metadata.key, command)}`;
24
- const lockValue = uuidv4();
25
- const acquired = await this.tryAcquire(
26
- lockKey,
27
- lockValue,
28
- metadata.lockTtlSeconds!,
29
- metadata.acquireTimeoutSeconds!,
30
- );
31
-
32
- if (!acquired) {
33
- this.logger.warn(`Failed to acquire lock: ${lockKey}`);
34
- return Result.failure<T>(ErrorType.Conflict, `Resource is locked: ${metadata.key}`);
35
- }
36
-
37
- try {
38
- return await next();
39
- } finally {
40
- await this.release(lockKey, lockValue);
41
- }
42
- }
43
-
44
- private async tryAcquire(
45
- key: string,
46
- value: string,
47
- ttlSeconds: number,
48
- timeoutSeconds: number,
49
- ): Promise<boolean> {
50
- const deadline = Date.now() + timeoutSeconds * 1000;
51
- const retryDelay = 50;
52
-
53
- while (Date.now() < deadline) {
54
- const result = await this.redis!.set(key, value, 'EX', ttlSeconds, 'NX');
55
- if (result === 'OK') return true;
56
- await this.sleep(retryDelay);
57
- }
58
-
59
- return false;
60
- }
61
-
62
- private async release(key: string, value: string): Promise<void> {
63
- // Lua script ensures only the owner releases the lock
64
- const script = `
65
- if redis.call("get", KEYS[1]) == ARGV[1] then
66
- return redis.call("del", KEYS[1])
67
- else
68
- return 0
69
- end
70
- `;
71
- try {
72
- await this.redis!.eval(script, 1, key, value);
73
- } catch {
74
- this.logger.warn(`Failed to release lock: ${key}`);
75
- }
76
- }
77
-
78
- private sleep(ms: number): Promise<void> {
79
- return new Promise((resolve) => setTimeout(resolve, ms));
80
- }
81
-
82
- private interpolateKey(template: string, command: object): string {
83
- return template.replace(/\{(\w+)\}/g, (_, prop) => {
84
- const value = (command as Record<string, unknown>)[prop];
85
- return value != null ? String(value) : '';
86
- });
87
- }
88
- }
@@ -1,46 +0,0 @@
1
- import { Injectable, Optional, Logger } from '@nestjs/common';
2
- import { Unleash } from 'unleash-client';
3
- import { getFeatureFlagMetadata } from '../decorators/FeatureFlag.decorator';
4
- import { Result, ErrorType } from '../../result/Result';
5
-
6
- @Injectable()
7
- export class FeatureFlagBehavior {
8
- private readonly logger = new Logger(FeatureFlagBehavior.name);
9
-
10
- constructor(@Optional() private readonly unleash?: Unleash) {}
11
-
12
- async execute<T>(command: object, next: () => Promise<Result<T>>): Promise<Result<T>> {
13
- const metadata = getFeatureFlagMetadata(command.constructor);
14
-
15
- if (!metadata) {
16
- return next();
17
- }
18
-
19
- if (!this.unleash) {
20
- this.logger.debug('Unleash not configured, skipping feature flag check');
21
- return next();
22
- }
23
-
24
- const { flagName, fallback, defaultValue } = metadata;
25
- const isEnabled = this.unleash.isEnabled(flagName);
26
-
27
- if (isEnabled) {
28
- return next();
29
- }
30
-
31
- this.logger.warn(`Feature flag "${flagName}" is disabled, applying fallback: ${fallback}`);
32
-
33
- switch (fallback) {
34
- case 'skip':
35
- return Result.success(undefined as unknown as T);
36
- case 'default':
37
- return Result.success(defaultValue as T);
38
- case 'throw':
39
- default:
40
- return Result.failure<T>(
41
- ErrorType.Forbidden,
42
- `Feature "${flagName}" is currently disabled`,
43
- );
44
- }
45
- }
46
- }
@@ -1,89 +0,0 @@
1
- import { InvalidateCacheBehavior } from './InvalidateCacheBehavior';
2
- import { Result, ErrorType } from '../../result/Result';
3
- import { InvalidateCache } from '../decorators/InvalidateCache.decorator';
4
-
5
- @InvalidateCache(['user:{userId}', 'users:list'])
6
- class UpdateUserCommand {
7
- constructor(
8
- public readonly userId: string,
9
- public readonly name: string,
10
- ) {}
11
- }
12
-
13
- @InvalidateCache([])
14
- class EmptyKeysCommand {}
15
-
16
- class PlainCommand {
17
- constructor(public readonly id: string) {}
18
- }
19
-
20
- describe('InvalidateCacheBehavior', () => {
21
- let redis: { del: jest.Mock };
22
- let behavior: InvalidateCacheBehavior;
23
- const next = jest.fn();
24
-
25
- beforeEach(() => {
26
- redis = {
27
- del: jest.fn().mockResolvedValue(1),
28
- };
29
- behavior = new InvalidateCacheBehavior(redis as any);
30
- next.mockReset().mockResolvedValue(Result.success({ updated: true }));
31
- });
32
-
33
- it('should skip when command has no @InvalidateCache decorator', async () => {
34
- const result = await behavior.execute(new PlainCommand('1'), next);
35
-
36
- expect(result.isSuccess).toBe(true);
37
- expect(next).toHaveBeenCalledTimes(1);
38
- expect(redis.del).not.toHaveBeenCalled();
39
- });
40
-
41
- it('should skip when Redis is unavailable', async () => {
42
- const noRedisBehavior = new InvalidateCacheBehavior(undefined);
43
- const result = await noRedisBehavior.execute(new UpdateUserCommand('123', 'Alice'), next);
44
-
45
- expect(result.isSuccess).toBe(true);
46
- expect(next).toHaveBeenCalledTimes(1);
47
- });
48
-
49
- it('should invalidate cache on successful result', async () => {
50
- const result = await behavior.execute(new UpdateUserCommand('123', 'Alice'), next);
51
-
52
- expect(result.isSuccess).toBe(true);
53
- expect(next).toHaveBeenCalledTimes(1);
54
- expect(redis.del).toHaveBeenCalledWith('user:123', 'users:list');
55
- });
56
-
57
- it('should skip invalidation on failure result', async () => {
58
- next.mockResolvedValue(Result.failure(ErrorType.InternalError, 'something broke'));
59
-
60
- const result = await behavior.execute(new UpdateUserCommand('123', 'Alice'), next);
61
-
62
- expect(result.isSuccess).toBe(false);
63
- expect(redis.del).not.toHaveBeenCalled();
64
- });
65
-
66
- it('should interpolate key templates with command properties', async () => {
67
- const result = await behavior.execute(new UpdateUserCommand('456', 'Bob'), next);
68
-
69
- expect(result.isSuccess).toBe(true);
70
- expect(redis.del).toHaveBeenCalledWith('user:456', 'users:list');
71
- });
72
-
73
- it('should return successful result even when redis.del() throws', async () => {
74
- redis.del.mockRejectedValue(new Error('Connection lost'));
75
-
76
- const result = await behavior.execute(new UpdateUserCommand('123', 'Alice'), next);
77
-
78
- expect(result.isSuccess).toBe(true);
79
- expect(result.value).toEqual({ updated: true });
80
- });
81
-
82
- it('should skip redis.del() when keys array is empty', async () => {
83
- const result = await behavior.execute(new EmptyKeysCommand(), next);
84
-
85
- expect(result.isSuccess).toBe(true);
86
- expect(next).toHaveBeenCalledTimes(1);
87
- expect(redis.del).not.toHaveBeenCalled();
88
- });
89
- });
@@ -1,50 +0,0 @@
1
- import { Injectable, Logger, Optional, Inject } from '@nestjs/common';
2
- import { getInvalidateCacheMetadata } from '../decorators/InvalidateCache.decorator';
3
- import { Result } from '../../result/Result';
4
- import { REDIS_CLIENT } from '../constants';
5
- import type { Redis } from 'ioredis';
6
-
7
- @Injectable()
8
- export class InvalidateCacheBehavior {
9
- private readonly logger = new Logger(InvalidateCacheBehavior.name);
10
-
11
- constructor(
12
- @Optional() @Inject(REDIS_CLIENT) private readonly redis?: Redis,
13
- ) {}
14
-
15
- async execute<T>(command: object, next: () => Promise<Result<T>>): Promise<Result<T>> {
16
- const metadata = getInvalidateCacheMetadata(command.constructor);
17
-
18
- if (!metadata || !this.redis) {
19
- return next();
20
- }
21
-
22
- const result = await next();
23
-
24
- if (!result.isSuccess) {
25
- return result;
26
- }
27
-
28
- const resolvedKeys = metadata.keys.map((key) => this.interpolateKey(key, command));
29
-
30
- if (resolvedKeys.length === 0) {
31
- return result;
32
- }
33
-
34
- try {
35
- await this.redis.del(...resolvedKeys);
36
- this.logger.debug(`Cache invalidated: ${resolvedKeys.join(', ')}`);
37
- } catch {
38
- this.logger.warn(`Cache invalidation failed for keys: ${resolvedKeys.join(', ')}`);
39
- }
40
-
41
- return result;
42
- }
43
-
44
- private interpolateKey(template: string, command: object): string {
45
- return template.replace(/\{(\w+)\}/g, (_, prop) => {
46
- const value = (command as Record<string, unknown>)[prop];
47
- return value != null ? String(value) : '';
48
- });
49
- }
50
- }
@@ -1,55 +0,0 @@
1
- import { LogBehavior } from './LogBehavior';
2
- import { Result } from '../../result/Result';
3
-
4
- describe('LogBehavior', () => {
5
- let behavior: LogBehavior;
6
-
7
- beforeEach(() => {
8
- behavior = new LogBehavior();
9
- });
10
-
11
- it('should pass through successful results', async () => {
12
- const next = jest.fn().mockResolvedValue(Result.success({ id: '1' }));
13
- const command = new (class TestCommand { buildId = 'b1'; })();
14
-
15
- const result = await behavior.execute(command, next);
16
-
17
- expect(result.isSuccess).toBe(true);
18
- expect(next).toHaveBeenCalledTimes(1);
19
- });
20
-
21
- it('should pass through failed results', async () => {
22
- const next = jest.fn().mockResolvedValue(Result.notFound('not found'));
23
-
24
- const result = await behavior.execute({}, next);
25
-
26
- expect(result.isSuccess).toBe(false);
27
- });
28
-
29
- it('should catch exceptions and return Result.failure', async () => {
30
- const next = jest.fn().mockRejectedValue(new Error('boom'));
31
-
32
- const result = await behavior.execute({}, next);
33
-
34
- expect(result.isSuccess).toBe(false);
35
- expect(result.errorMessage).toBe('boom');
36
- });
37
-
38
- it('should mask PII fields in payload', async () => {
39
- const next = jest.fn().mockResolvedValue(Result.success('ok'));
40
- const command = {
41
- email: 'john@example.com',
42
- accessToken: 'secret-token',
43
- brdText: 'A'.repeat(100),
44
- name: 'visible',
45
- };
46
-
47
- // The behavior masks internally for logging. We verify it doesn't affect the original.
48
- await behavior.execute(command, next);
49
-
50
- // Original command should be unmodified
51
- expect(command.email).toBe('john@example.com');
52
- expect(command.accessToken).toBe('secret-token');
53
- expect(command.brdText).toBe('A'.repeat(100));
54
- });
55
- });
@@ -1,121 +0,0 @@
1
- import { Injectable, Logger } from '@nestjs/common';
2
- import { Result, ErrorType } from '../../result/Result';
3
- import { correlationStore } from '../../middleware/CorrelationStore';
4
-
5
- const PII_FIELDS = new Set(['email', 'githubAccessToken', 'accessToken', 'token', 'secretKey', 'password']);
6
- const MAX_STRING_LENGTH = 200;
7
- const MAX_ARRAY_LENGTH = 5;
8
- const MAX_DEPTH = 2;
9
-
10
- @Injectable()
11
- export class LogBehavior {
12
- private readonly logger = new Logger('PipelineBehavior');
13
-
14
- async execute<T>(command: object, next: () => Promise<Result<T>>): Promise<Result<T>> {
15
- const commandName = command.constructor.name;
16
- const startTime = Date.now();
17
- const ctx = correlationStore.getStore();
18
-
19
- const logContext = {
20
- command: commandName,
21
- correlationId: ctx?.correlationId,
22
- userId: ctx?.userId,
23
- orgId: ctx?.organizationId,
24
- ...(this.extractBuildId(command)),
25
- payload: this.maskPayload(command),
26
- };
27
-
28
- try {
29
- const result = await next();
30
- const durationMs = Date.now() - startTime;
31
-
32
- if (result.isSuccess) {
33
- this.logger.log({
34
- msg: `${commandName} completed`,
35
- ...logContext,
36
- durationMs,
37
- result: 'success',
38
- });
39
- } else {
40
- this.logger.warn({
41
- msg: `${commandName} completed`,
42
- ...logContext,
43
- durationMs,
44
- result: 'failure',
45
- errorType: result.errorType ?? 'Unknown',
46
- errorMessage: result.errorMessage,
47
- });
48
- }
49
-
50
- return result;
51
- } catch (error) {
52
- const durationMs = Date.now() - startTime;
53
- const err = error instanceof Error ? error : new Error(String(error));
54
- this.logger.error({
55
- msg: `${commandName} failed`,
56
- ...logContext,
57
- durationMs,
58
- result: 'exception',
59
- error: err.message,
60
- stack: err.stack,
61
- });
62
- return Result.failure(ErrorType.InternalError, err.message);
63
- }
64
- }
65
-
66
- private maskPayload(command: object): Record<string, unknown> {
67
- const excludeFields = this.getExcludeFields(command);
68
- return this.sanitize(command, 0, excludeFields) as Record<string, unknown>;
69
- }
70
-
71
- private getExcludeFields(command: object): Set<string> {
72
- const ctor = command.constructor as { logExclude?: string[] };
73
- return new Set(ctor.logExclude ?? []);
74
- }
75
-
76
- private sanitize(value: unknown, depth: number, excludeFields?: Set<string>): unknown {
77
- if (value === null || value === undefined) return value;
78
-
79
- if (typeof value === 'string') {
80
- return value.length > MAX_STRING_LENGTH
81
- ? `${value.substring(0, MAX_STRING_LENGTH)}... (${value.length} chars)`
82
- : value;
83
- }
84
-
85
- if (typeof value !== 'object') return value;
86
-
87
- if (depth >= MAX_DEPTH) return '[nested]';
88
-
89
- if (Array.isArray(value)) {
90
- if (value.length > MAX_ARRAY_LENGTH) {
91
- return [
92
- ...value.slice(0, MAX_ARRAY_LENGTH).map((v) => this.sanitize(v, depth + 1)),
93
- `... +${value.length - MAX_ARRAY_LENGTH} more`,
94
- ];
95
- }
96
- return value.map((v) => this.sanitize(v, depth + 1));
97
- }
98
-
99
- const masked: Record<string, unknown> = {};
100
- for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
101
- if (excludeFields?.has(key)) {
102
- masked[key] = '[excluded]';
103
- } else if (PII_FIELDS.has(key)) {
104
- if (key === 'email' && typeof val === 'string') {
105
- const [local, domain] = val.split('@');
106
- masked[key] = domain ? `${local?.[0]}***@${domain}` : '[REDACTED]';
107
- } else {
108
- masked[key] = '[REDACTED]';
109
- }
110
- } else {
111
- masked[key] = this.sanitize(val, depth + 1);
112
- }
113
- }
114
- return masked;
115
- }
116
-
117
- private extractBuildId(command: object): { buildId?: string } {
118
- const buildId = (command as Record<string, unknown>)['buildId'];
119
- return typeof buildId === 'string' ? { buildId } : {};
120
- }
121
- }
@@ -1,48 +0,0 @@
1
- import { PerformanceBehavior } from './PerformanceBehavior';
2
- import { Result } from '../../result/Result';
3
-
4
- describe('PerformanceBehavior', () => {
5
- it('should pass through results and record metrics', async () => {
6
- const observe = jest.fn();
7
- const metrics = {
8
- handlerDuration: {
9
- labels: jest.fn().mockReturnValue({ observe }),
10
- },
11
- };
12
- const behavior = new PerformanceBehavior(metrics as any);
13
- const next = jest.fn().mockResolvedValue(Result.success('ok'));
14
-
15
- class TestCommand {}
16
- const result = await behavior.execute(new TestCommand(), next);
17
-
18
- expect(result.isSuccess).toBe(true);
19
- expect(metrics.handlerDuration.labels).toHaveBeenCalledWith('TestCommand', 'success');
20
- expect(observe).toHaveBeenCalled();
21
- });
22
-
23
- it('should record failure label for failed results', async () => {
24
- const observe = jest.fn();
25
- const metrics = {
26
- handlerDuration: {
27
- labels: jest.fn().mockReturnValue({ observe }),
28
- },
29
- };
30
- const behavior = new PerformanceBehavior(metrics as any);
31
- const next = jest.fn().mockResolvedValue(Result.notFound('nope'));
32
-
33
- class FailCommand {}
34
- await behavior.execute(new FailCommand(), next);
35
-
36
- expect(metrics.handlerDuration.labels).toHaveBeenCalledWith('FailCommand', 'failure');
37
- });
38
-
39
- it('should work without metrics service', async () => {
40
- const behavior = new PerformanceBehavior(undefined);
41
- const next = jest.fn().mockResolvedValue(Result.success('ok'));
42
-
43
- class NoMetrics {}
44
- const result = await behavior.execute(new NoMetrics(), next);
45
-
46
- expect(result.isSuccess).toBe(true);
47
- });
48
- });
@@ -1,43 +0,0 @@
1
- import { Injectable, Logger, Optional } from '@nestjs/common';
2
- import { Result } from '../../result/Result';
3
- import { MetricsService } from '../../metrics/MetricsService';
4
-
5
- const SLOW_THRESHOLD_MS = 500;
6
-
7
- /**
8
- * PerformanceBehavior logs a warning when a handler exceeds 500ms
9
- * and records handler duration in Prometheus histogram.
10
- */
11
- @Injectable()
12
- export class PerformanceBehavior {
13
- private readonly logger = new Logger('PerformanceBehavior');
14
-
15
- constructor(@Optional() private readonly metrics?: MetricsService) {}
16
-
17
- async execute<T>(command: object, next: () => Promise<Result<T>>): Promise<Result<T>> {
18
- const handlerName = command.constructor.name;
19
- const startTime = Date.now();
20
-
21
- const result = await next();
22
-
23
- const durationMs = Date.now() - startTime;
24
- const durationSec = durationMs / 1000;
25
- const resultLabel = result.isSuccess ? 'success' : 'failure';
26
-
27
- // Record metrics
28
- this.metrics?.handlerDuration
29
- .labels(handlerName, resultLabel)
30
- .observe(durationSec);
31
-
32
- // Warn on slow handlers
33
- if (durationMs > SLOW_THRESHOLD_MS) {
34
- this.logger.warn({
35
- msg: `Slow handler detected: ${handlerName} took ${durationMs}ms`,
36
- handler: handlerName,
37
- durationMs,
38
- });
39
- }
40
-
41
- return result;
42
- }
43
- }
@@ -1,64 +0,0 @@
1
- import { Injectable, Logger, Optional } from '@nestjs/common';
2
- import { DataSource } from 'typeorm';
3
- import { Result, ErrorType } from '../../result/Result';
4
- import { TransactionContext } from '../transaction/TransactionContext';
5
- import { isIsolatedTransaction } from '../decorators/IsolatedTransaction.decorator';
6
-
7
- /**
8
- * TransactionalBehavior — UnitOfWork pattern via AsyncLocalStorage.
9
- *
10
- * Every command is transactional by default (no decorator needed):
11
- * - If no ambient transaction exists → CREATE one, own commit/rollback
12
- * - If an ambient transaction exists → JOIN it (pass through, outer scope owns lifecycle)
13
- * - If command is @IsolatedTransaction() → always CREATE a new one, even if context exists
14
- *
15
- * Queries skip this behavior entirely (separate pipeline chain in QuanticQueryBus).
16
- */
17
- @Injectable()
18
- export class TransactionalBehavior {
19
- private readonly logger = new Logger(TransactionalBehavior.name);
20
-
21
- constructor(@Optional() private readonly dataSource?: DataSource) {}
22
-
23
- async execute<T>(command: object, next: () => Promise<Result<T>>): Promise<Result<T>> {
24
- if (!this.dataSource) {
25
- return next();
26
- }
27
-
28
- const existing = TransactionContext.get();
29
- const isolated = isIsolatedTransaction(command.constructor);
30
-
31
- // JOIN — ambient transaction exists and command does not demand isolation
32
- if (existing && !isolated) {
33
- return next();
34
- }
35
-
36
- // CREATE — we are the outermost scope (or isolated); we own the lifecycle
37
- const queryRunner = this.dataSource.createQueryRunner();
38
- await queryRunner.connect();
39
- await queryRunner.startTransaction();
40
-
41
- try {
42
- const result = await TransactionContext.run(queryRunner, () => next());
43
-
44
- if (result.isSuccess) {
45
- await queryRunner.commitTransaction();
46
- } else {
47
- await queryRunner.rollbackTransaction();
48
- this.logger.warn(
49
- `Transaction rolled back for ${command.constructor.name}: ${result.errorMessage}`,
50
- );
51
- }
52
-
53
- return result;
54
- } catch (error) {
55
- await queryRunner.rollbackTransaction();
56
- return Result.failure<T>(
57
- ErrorType.InternalError,
58
- `Transaction failed for ${command.constructor.name}: ${(error as Error).message}`,
59
- );
60
- } finally {
61
- await queryRunner.release();
62
- }
63
- }
64
- }