@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.
Files changed (181) hide show
  1. package/dist/bootstrap/bootstrapService.d.ts +8 -0
  2. package/dist/bootstrap/bootstrapService.js +58 -0
  3. package/dist/cqrs/PipelineExecutor.d.ts +31 -0
  4. package/dist/cqrs/PipelineExecutor.js +81 -0
  5. package/dist/cqrs/behaviors/CacheBehavior.d.ts +9 -0
  6. package/dist/cqrs/behaviors/CacheBehavior.js +68 -0
  7. package/dist/cqrs/behaviors/DistributedLockBehavior.d.ts +12 -0
  8. package/dist/cqrs/behaviors/DistributedLockBehavior.js +90 -0
  9. package/dist/cqrs/behaviors/FeatureFlagBehavior.d.ts +8 -0
  10. package/dist/cqrs/behaviors/FeatureFlagBehavior.js +58 -0
  11. package/dist/cqrs/behaviors/InvalidateCacheBehavior.d.ts +9 -0
  12. package/dist/cqrs/behaviors/InvalidateCacheBehavior.js +61 -0
  13. package/dist/cqrs/behaviors/LogBehavior.d.ts +9 -0
  14. package/dist/cqrs/behaviors/LogBehavior.js +125 -0
  15. package/dist/cqrs/behaviors/PerformanceBehavior.d.ts +12 -0
  16. package/dist/cqrs/behaviors/PerformanceBehavior.js +56 -0
  17. package/dist/cqrs/behaviors/TransactionalBehavior.d.ts +18 -0
  18. package/dist/cqrs/behaviors/TransactionalBehavior.js +77 -0
  19. package/dist/cqrs/behaviors/ValidationBehavior.d.ts +4 -0
  20. package/dist/cqrs/behaviors/ValidationBehavior.js +33 -0
  21. package/dist/cqrs/behaviors/WorkflowBehavior.d.ts +8 -0
  22. package/dist/cqrs/behaviors/WorkflowBehavior.js +61 -0
  23. package/dist/cqrs/constants.d.ts +2 -0
  24. package/dist/cqrs/constants.js +5 -0
  25. package/dist/cqrs/decorators/Cache.decorator.d.ts +13 -0
  26. package/dist/cqrs/decorators/Cache.decorator.js +18 -0
  27. package/dist/cqrs/decorators/DistributedLock.decorator.d.ts +15 -0
  28. package/dist/cqrs/decorators/DistributedLock.decorator.js +23 -0
  29. package/dist/cqrs/decorators/FeatureFlag.decorator.d.ts +9 -0
  30. package/dist/cqrs/decorators/FeatureFlag.decorator.js +14 -0
  31. package/dist/cqrs/decorators/InvalidateCache.decorator.d.ts +6 -0
  32. package/dist/cqrs/decorators/InvalidateCache.decorator.js +14 -0
  33. package/dist/cqrs/decorators/IsolatedTransaction.decorator.d.ts +14 -0
  34. package/dist/cqrs/decorators/IsolatedTransaction.decorator.js +25 -0
  35. package/dist/cqrs/decorators/Log.decorator.d.ts +11 -0
  36. package/dist/cqrs/decorators/Log.decorator.js +18 -0
  37. package/dist/cqrs/decorators/Validate.decorator.d.ts +24 -0
  38. package/dist/cqrs/decorators/Validate.decorator.js +37 -0
  39. package/dist/cqrs/decorators/Workflow.decorator.d.ts +8 -0
  40. package/dist/cqrs/decorators/Workflow.decorator.js +14 -0
  41. package/dist/cqrs/interfaces/WorkflowEngine.d.ts +14 -0
  42. package/dist/cqrs/interfaces/WorkflowEngine.js +4 -0
  43. package/dist/cqrs/pipeline/QuanticCommandBus.d.ts +37 -0
  44. package/dist/cqrs/pipeline/QuanticCommandBus.js +99 -0
  45. package/dist/cqrs/pipeline/QuanticQueryBus.d.ts +28 -0
  46. package/dist/cqrs/pipeline/QuanticQueryBus.js +78 -0
  47. package/dist/cqrs/pipeline/runPipeline.d.ts +3 -0
  48. package/dist/cqrs/pipeline/runPipeline.js +12 -0
  49. package/dist/cqrs/transaction/TransactionContext.d.ts +18 -0
  50. package/dist/cqrs/transaction/TransactionContext.js +26 -0
  51. package/dist/cqrs/transaction/getTransactionalRepo.d.ts +16 -0
  52. package/dist/cqrs/transaction/getTransactionalRepo.js +22 -0
  53. package/dist/cqrs/validation/ICommandValidator.d.ts +48 -0
  54. package/dist/cqrs/validation/ICommandValidator.js +21 -0
  55. package/dist/entities/BaseEntity.d.ts +5 -0
  56. package/dist/entities/BaseEntity.js +31 -0
  57. package/dist/entities/TenantBaseEntity.d.ts +4 -0
  58. package/dist/entities/TenantBaseEntity.js +22 -0
  59. package/dist/events/DomainEvent.d.ts +14 -0
  60. package/dist/events/DomainEvent.js +27 -0
  61. package/dist/events/OutboxEvent.entity.d.ts +18 -0
  62. package/dist/events/OutboxEvent.entity.js +87 -0
  63. package/dist/events/OutboxPublisherService.d.ts +14 -0
  64. package/dist/events/OutboxPublisherService.js +104 -0
  65. package/dist/events/RedisStreamConsumer.d.ts +43 -0
  66. package/dist/events/RedisStreamConsumer.js +158 -0
  67. package/dist/events/RedisStreamPublisher.d.ts +9 -0
  68. package/dist/events/RedisStreamPublisher.js +60 -0
  69. package/dist/filters/GlobalExceptionFilter.d.ts +11 -0
  70. package/dist/filters/GlobalExceptionFilter.js +102 -0
  71. package/dist/guards/JwtAuthGuard.d.ts +10 -0
  72. package/dist/guards/JwtAuthGuard.js +46 -0
  73. package/dist/guards/JwtStrategy.d.ts +22 -0
  74. package/dist/guards/JwtStrategy.js +47 -0
  75. package/dist/guards/RolesGuard.d.ts +8 -0
  76. package/dist/guards/RolesGuard.js +52 -0
  77. package/dist/index.d.ts +69 -0
  78. package/dist/index.js +146 -0
  79. package/dist/interceptors/ResultInterceptor.d.ts +7 -0
  80. package/dist/interceptors/ResultInterceptor.js +88 -0
  81. package/dist/lifecycle/GracefulShutdownService.d.ts +24 -0
  82. package/dist/lifecycle/GracefulShutdownService.js +93 -0
  83. package/dist/logging/pino-config.d.ts +35 -0
  84. package/dist/logging/pino-config.js +79 -0
  85. package/dist/metrics/MetricsController.d.ts +7 -0
  86. package/dist/metrics/MetricsController.js +42 -0
  87. package/dist/metrics/MetricsService.d.ts +13 -0
  88. package/dist/metrics/MetricsService.js +58 -0
  89. package/dist/middleware/CorrelationIdMiddleware.d.ts +4 -0
  90. package/dist/middleware/CorrelationIdMiddleware.js +33 -0
  91. package/dist/middleware/CorrelationStore.d.ts +11 -0
  92. package/dist/middleware/CorrelationStore.js +9 -0
  93. package/dist/middleware/TenantContextMiddleware.d.ts +7 -0
  94. package/dist/middleware/TenantContextMiddleware.js +27 -0
  95. package/dist/middleware/TenantStore.d.ts +9 -0
  96. package/dist/middleware/TenantStore.js +9 -0
  97. package/dist/redis/redis.module.d.ts +8 -0
  98. package/dist/redis/redis.module.js +49 -0
  99. package/dist/resilience/CircuitBreakerFactory.d.ts +12 -0
  100. package/dist/resilience/CircuitBreakerFactory.js +22 -0
  101. package/dist/result/Result.d.ts +26 -0
  102. package/dist/result/Result.js +62 -0
  103. package/dist/shared-kernel.module.d.ts +13 -0
  104. package/dist/shared-kernel.module.js +87 -0
  105. package/dist/subscribers/TenantSubscriber.d.ts +20 -0
  106. package/dist/subscribers/TenantSubscriber.js +52 -0
  107. package/dist/testing/TestingModuleFactory.d.ts +23 -0
  108. package/dist/testing/TestingModuleFactory.js +63 -0
  109. package/dist/testing/index.d.ts +2 -0
  110. package/dist/testing/index.js +7 -0
  111. package/dist/testing/mocks.d.ts +34 -0
  112. package/dist/testing/mocks.js +62 -0
  113. package/dist/unleash/initial-flags.d.ts +7 -0
  114. package/dist/unleash/initial-flags.js +9 -0
  115. package/dist/unleash/unleash.module.d.ts +9 -0
  116. package/dist/unleash/unleash.module.js +47 -0
  117. package/package.json +140 -0
  118. package/src/bootstrap/bootstrapService.ts +72 -0
  119. package/src/cqrs/behaviors/CacheBehavior.spec.ts +63 -0
  120. package/src/cqrs/behaviors/CacheBehavior.ts +54 -0
  121. package/src/cqrs/behaviors/DistributedLockBehavior.ts +88 -0
  122. package/src/cqrs/behaviors/FeatureFlagBehavior.ts +46 -0
  123. package/src/cqrs/behaviors/InvalidateCacheBehavior.spec.ts +89 -0
  124. package/src/cqrs/behaviors/InvalidateCacheBehavior.ts +50 -0
  125. package/src/cqrs/behaviors/LogBehavior.spec.ts +55 -0
  126. package/src/cqrs/behaviors/LogBehavior.ts +121 -0
  127. package/src/cqrs/behaviors/PerformanceBehavior.spec.ts +48 -0
  128. package/src/cqrs/behaviors/PerformanceBehavior.ts +43 -0
  129. package/src/cqrs/behaviors/TransactionalBehavior.ts +64 -0
  130. package/src/cqrs/behaviors/ValidationBehavior.spec.ts +114 -0
  131. package/src/cqrs/behaviors/ValidationBehavior.ts +29 -0
  132. package/src/cqrs/behaviors/WorkflowBehavior.spec.ts +97 -0
  133. package/src/cqrs/behaviors/WorkflowBehavior.ts +62 -0
  134. package/src/cqrs/constants.ts +2 -0
  135. package/src/cqrs/decorators/Cache.decorator.ts +24 -0
  136. package/src/cqrs/decorators/DistributedLock.decorator.ts +34 -0
  137. package/src/cqrs/decorators/FeatureFlag.decorator.ts +23 -0
  138. package/src/cqrs/decorators/InvalidateCache.decorator.spec.ts +20 -0
  139. package/src/cqrs/decorators/InvalidateCache.decorator.ts +17 -0
  140. package/src/cqrs/decorators/IsolatedTransaction.decorator.ts +24 -0
  141. package/src/cqrs/decorators/Log.decorator.ts +22 -0
  142. package/src/cqrs/decorators/Validate.decorator.ts +39 -0
  143. package/src/cqrs/decorators/Workflow.decorator.ts +22 -0
  144. package/src/cqrs/interfaces/WorkflowEngine.ts +19 -0
  145. package/src/cqrs/pipeline/QuanticCommandBus.ts +69 -0
  146. package/src/cqrs/pipeline/QuanticQueryBus.ts +56 -0
  147. package/src/cqrs/pipeline/runPipeline.ts +22 -0
  148. package/src/cqrs/transaction/TransactionContext.ts +26 -0
  149. package/src/cqrs/transaction/getTransactionalRepo.ts +23 -0
  150. package/src/cqrs/validation/ICommandValidator.ts +55 -0
  151. package/src/entities/BaseEntity.ts +16 -0
  152. package/src/entities/TenantBaseEntity.ts +7 -0
  153. package/src/events/DomainEvent.ts +27 -0
  154. package/src/events/OutboxEvent.entity.ts +56 -0
  155. package/src/events/OutboxPublisherService.ts +94 -0
  156. package/src/events/RedisStreamConsumer.ts +172 -0
  157. package/src/events/RedisStreamPublisher.ts +54 -0
  158. package/src/filters/GlobalExceptionFilter.ts +125 -0
  159. package/src/guards/JwtAuthGuard.ts +29 -0
  160. package/src/guards/JwtStrategy.ts +41 -0
  161. package/src/guards/RolesGuard.ts +39 -0
  162. package/src/index.ts +118 -0
  163. package/src/interceptors/ResultInterceptor.ts +93 -0
  164. package/src/lifecycle/GracefulShutdownService.ts +77 -0
  165. package/src/logging/pino-config.ts +80 -0
  166. package/src/metrics/MetricsController.ts +17 -0
  167. package/src/metrics/MetricsService.ts +55 -0
  168. package/src/middleware/CorrelationIdMiddleware.ts +27 -0
  169. package/src/middleware/CorrelationStore.ts +13 -0
  170. package/src/middleware/TenantContextMiddleware.ts +21 -0
  171. package/src/middleware/TenantStore.ts +11 -0
  172. package/src/redis/redis.module.ts +41 -0
  173. package/src/resilience/CircuitBreakerFactory.ts +33 -0
  174. package/src/result/Result.ts +66 -0
  175. package/src/shared-kernel.module.ts +87 -0
  176. package/src/subscribers/TenantSubscriber.ts +47 -0
  177. package/src/testing/TestingModuleFactory.ts +78 -0
  178. package/src/testing/index.ts +2 -0
  179. package/src/testing/mocks.ts +59 -0
  180. package/src/unleash/unleash.module.ts +45 -0
  181. package/tsconfig.json +22 -0
@@ -0,0 +1,97 @@
1
+ import { WorkflowBehavior } from './WorkflowBehavior';
2
+ import { Workflow } from '../decorators/Workflow.decorator';
3
+ import { Result } from '../../result/Result';
4
+ import { WorkflowEngine, WorkflowStartResult } from '../interfaces/WorkflowEngine';
5
+
6
+ const mockEngine: jest.Mocked<WorkflowEngine> = {
7
+ startProcess: jest.fn(),
8
+ signalProcess: jest.fn(),
9
+ abortProcess: jest.fn(),
10
+ };
11
+
12
+ @Workflow('onboarding-process')
13
+ class DecoratedCommand {
14
+ constructor(public readonly userId: string) {}
15
+ }
16
+
17
+ @Workflow('guarded-process', { fallback: 'skip' })
18
+ class SkipFallbackCommand {
19
+ constructor(public readonly id: string) {}
20
+ }
21
+
22
+ class PlainCommand {
23
+ constructor(public readonly id: string) {}
24
+ }
25
+
26
+ describe('WorkflowBehavior', () => {
27
+ let behavior: WorkflowBehavior;
28
+ let next: jest.Mock;
29
+
30
+ beforeEach(() => {
31
+ jest.clearAllMocks();
32
+ behavior = new WorkflowBehavior(mockEngine);
33
+ next = jest.fn().mockResolvedValue(Result.success({ done: true }));
34
+ });
35
+
36
+ it('passes through commands without @Workflow decorator', async () => {
37
+ const cmd = new PlainCommand('123');
38
+ const result = await behavior.execute(cmd, next);
39
+
40
+ expect(next).toHaveBeenCalled();
41
+ expect(result.isSuccess).toBe(true);
42
+ expect(mockEngine.startProcess).not.toHaveBeenCalled();
43
+ });
44
+
45
+ it('short-circuits decorated commands and starts a process', async () => {
46
+ const startResult: WorkflowStartResult = {
47
+ workflowInstanceId: 'wf-1',
48
+ processInstanceId: 'pi-1',
49
+ status: 'STARTED',
50
+ };
51
+ mockEngine.startProcess.mockResolvedValue(startResult);
52
+
53
+ const cmd = new DecoratedCommand('user-42');
54
+ const result = await behavior.execute(cmd, next);
55
+
56
+ expect(next).not.toHaveBeenCalled();
57
+ expect(mockEngine.startProcess).toHaveBeenCalledWith(
58
+ 'onboarding-process',
59
+ cmd,
60
+ { commandType: 'DecoratedCommand' },
61
+ );
62
+ expect(result.isSuccess).toBe(true);
63
+ expect(result.value).toEqual(startResult);
64
+ });
65
+
66
+ it('passes through when no WorkflowEngine is injected', async () => {
67
+ const behaviorNoEngine = new WorkflowBehavior(undefined);
68
+ const cmd = new DecoratedCommand('user-42');
69
+
70
+ const result = await behaviorNoEngine.execute(cmd, next);
71
+
72
+ expect(next).toHaveBeenCalled();
73
+ expect(result.isSuccess).toBe(true);
74
+ });
75
+
76
+ it('applies fallback throw on engine error', async () => {
77
+ mockEngine.startProcess.mockRejectedValue(new Error('connection refused'));
78
+
79
+ const cmd = new DecoratedCommand('user-42');
80
+ const result = await behavior.execute(cmd, next);
81
+
82
+ expect(next).not.toHaveBeenCalled();
83
+ expect(result.isSuccess).toBe(false);
84
+ expect(result.errorMessage).toContain('Workflow start failed');
85
+ expect(result.errorMessage).toContain('connection refused');
86
+ });
87
+
88
+ it('applies fallback skip on engine error (calls next)', async () => {
89
+ mockEngine.startProcess.mockRejectedValue(new Error('timeout'));
90
+
91
+ const cmd = new SkipFallbackCommand('123');
92
+ const result = await behavior.execute(cmd, next);
93
+
94
+ expect(next).toHaveBeenCalled();
95
+ expect(result.isSuccess).toBe(true);
96
+ });
97
+ });
@@ -0,0 +1,62 @@
1
+ import { Injectable, Optional, Inject, Logger } from '@nestjs/common';
2
+ import { getWorkflowMetadata } from '../decorators/Workflow.decorator';
3
+ import { WORKFLOW_ENGINE, WorkflowEngine } from '../interfaces/WorkflowEngine';
4
+ import { Result, ErrorType } from '../../result/Result';
5
+
6
+ @Injectable()
7
+ export class WorkflowBehavior {
8
+ private readonly logger = new Logger(WorkflowBehavior.name);
9
+
10
+ constructor(
11
+ @Optional() @Inject(WORKFLOW_ENGINE)
12
+ private readonly engine?: WorkflowEngine,
13
+ ) {}
14
+
15
+ async execute<T>(
16
+ command: object,
17
+ next: () => Promise<Result<T>>,
18
+ ): Promise<Result<T>> {
19
+ const metadata = getWorkflowMetadata(command.constructor);
20
+
21
+ if (!metadata) {
22
+ return next();
23
+ }
24
+
25
+ if (!this.engine) {
26
+ this.logger.debug('No WorkflowEngine provided, skipping workflow behavior');
27
+ return next();
28
+ }
29
+
30
+ try {
31
+ const result = await this.engine.startProcess(
32
+ metadata.processDefinitionId,
33
+ command,
34
+ { commandType: command.constructor.name },
35
+ );
36
+ return Result.success(result as unknown as T);
37
+ } catch (error: any) {
38
+ const fallback = metadata.fallback ?? 'throw';
39
+
40
+ this.logger.warn(
41
+ `Workflow start failed for "${metadata.processDefinitionId}", applying fallback: ${fallback}`,
42
+ error.message,
43
+ );
44
+
45
+ switch (fallback) {
46
+ case 'skip':
47
+ return next();
48
+ case 'queue':
49
+ return Result.failure<T>(
50
+ ErrorType.InternalError,
51
+ `Workflow queuing not available without companion package: ${error.message}`,
52
+ );
53
+ case 'throw':
54
+ default:
55
+ return Result.failure<T>(
56
+ ErrorType.InternalError,
57
+ `Workflow start failed: ${error.message}`,
58
+ );
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,2 @@
1
+ /** Injection token for the Redis client (ioredis) */
2
+ export const REDIS_CLIENT = 'REDIS_CLIENT';
@@ -0,0 +1,24 @@
1
+ import 'reflect-metadata';
2
+
3
+ const CACHE_KEY = 'arex:cache';
4
+
5
+ export interface CacheOptions {
6
+ /** Cache key template — use {propName} for interpolation, e.g. 'user:{id}' */
7
+ key: string;
8
+ /** TTL in seconds (default: 60) */
9
+ ttlSeconds?: number;
10
+ }
11
+
12
+ /**
13
+ * @Cache('key:{prop}', { ttlSeconds: 60 }) — caches query results in Redis.
14
+ * Reads from cache on hit; sets on miss with TTL.
15
+ */
16
+ export function Cache(key: string, options: Omit<CacheOptions, 'key'> = {}): ClassDecorator {
17
+ return (target: object) => {
18
+ Reflect.defineMetadata(CACHE_KEY, { key, ttlSeconds: 60, ...options }, target);
19
+ };
20
+ }
21
+
22
+ export function getCacheMetadata(target: object): CacheOptions | undefined {
23
+ return Reflect.getMetadata(CACHE_KEY, target);
24
+ }
@@ -0,0 +1,34 @@
1
+ import 'reflect-metadata';
2
+
3
+ const LOCK_KEY = 'arex:distributed-lock';
4
+
5
+ export interface DistributedLockOptions {
6
+ /** Lock key template — use {propName} for interpolation */
7
+ key: string;
8
+ /** Max time to wait for lock acquisition in seconds (default: 5) */
9
+ acquireTimeoutSeconds?: number;
10
+ /** Lock auto-release time in seconds (default: 30) */
11
+ lockTtlSeconds?: number;
12
+ }
13
+
14
+ /**
15
+ * @DistributedLock('key:{prop}') — acquires a Redis distributed lock before execution.
16
+ * Throws ConflictException on timeout.
17
+ */
18
+ export function DistributedLock(
19
+ key: string,
20
+ options: Omit<DistributedLockOptions, 'key'> = {},
21
+ ): ClassDecorator {
22
+ return (target: object) => {
23
+ Reflect.defineMetadata(LOCK_KEY, {
24
+ key,
25
+ acquireTimeoutSeconds: 5,
26
+ lockTtlSeconds: 30,
27
+ ...options,
28
+ }, target);
29
+ };
30
+ }
31
+
32
+ export function getDistributedLockMetadata(target: object): DistributedLockOptions | undefined {
33
+ return Reflect.getMetadata(LOCK_KEY, target);
34
+ }
@@ -0,0 +1,23 @@
1
+ import 'reflect-metadata';
2
+
3
+ const FEATURE_FLAG_KEY = 'arex:feature-flag';
4
+
5
+ export interface FeatureFlagOptions {
6
+ fallback?: 'throw' | 'skip' | 'default';
7
+ defaultValue?: unknown;
8
+ }
9
+
10
+ export function FeatureFlag(
11
+ flagName: string,
12
+ options: FeatureFlagOptions = { fallback: 'throw' },
13
+ ): ClassDecorator {
14
+ return (target: object) => {
15
+ Reflect.defineMetadata(FEATURE_FLAG_KEY, { flagName, ...options }, target);
16
+ };
17
+ }
18
+
19
+ export function getFeatureFlagMetadata(
20
+ target: object,
21
+ ): { flagName: string } & FeatureFlagOptions | undefined {
22
+ return Reflect.getMetadata(FEATURE_FLAG_KEY, target);
23
+ }
@@ -0,0 +1,20 @@
1
+ import { InvalidateCache, getInvalidateCacheMetadata } from './InvalidateCache.decorator';
2
+
3
+ @InvalidateCache(['user:{userId}', 'users:list'])
4
+ class DecoratedCommand {}
5
+
6
+ class PlainCommand {}
7
+
8
+ describe('InvalidateCache decorator', () => {
9
+ it('should store metadata on decorated class', () => {
10
+ const metadata = getInvalidateCacheMetadata(DecoratedCommand);
11
+
12
+ expect(metadata).toEqual({ keys: ['user:{userId}', 'users:list'] });
13
+ });
14
+
15
+ it('should return undefined for undecorated class', () => {
16
+ const metadata = getInvalidateCacheMetadata(PlainCommand);
17
+
18
+ expect(metadata).toBeUndefined();
19
+ });
20
+ });
@@ -0,0 +1,17 @@
1
+ import 'reflect-metadata';
2
+
3
+ const INVALIDATE_CACHE_KEY = 'arex:invalidate-cache';
4
+
5
+ export interface InvalidateCacheOptions {
6
+ keys: string[];
7
+ }
8
+
9
+ export function InvalidateCache(keys: string[]): ClassDecorator {
10
+ return (target: object) => {
11
+ Reflect.defineMetadata(INVALIDATE_CACHE_KEY, { keys }, target);
12
+ };
13
+ }
14
+
15
+ export function getInvalidateCacheMetadata(target: object): InvalidateCacheOptions | undefined {
16
+ return Reflect.getMetadata(INVALIDATE_CACHE_KEY, target);
17
+ }
@@ -0,0 +1,24 @@
1
+ import 'reflect-metadata';
2
+
3
+ const ISOLATED_TRANSACTION_KEY = 'arex:isolated-transaction';
4
+
5
+ /**
6
+ * @IsolatedTransaction() — forces a command to run in its own dedicated transaction,
7
+ * even when an outer transaction scope already exists.
8
+ *
9
+ * Use for operations that must commit independently of the parent flow:
10
+ * - Audit logging (must persist even if the business operation rolls back)
11
+ * - Notifications / side-effects that should not be lost on rollback
12
+ *
13
+ * By default all commands share the ambient transaction (UnitOfWork pattern).
14
+ * This decorator opts out of that behavior.
15
+ */
16
+ export function IsolatedTransaction(): ClassDecorator {
17
+ return (target: object) => {
18
+ Reflect.defineMetadata(ISOLATED_TRANSACTION_KEY, true, target);
19
+ };
20
+ }
21
+
22
+ export function isIsolatedTransaction(target: object): boolean {
23
+ return Reflect.getMetadata(ISOLATED_TRANSACTION_KEY, target) === true;
24
+ }
@@ -0,0 +1,22 @@
1
+ import 'reflect-metadata';
2
+
3
+ const LOG_KEY = 'arex:log';
4
+
5
+ export interface LogOptions {
6
+ /** Whether to log the command/query payload (default: true) */
7
+ logPayload?: boolean;
8
+ }
9
+
10
+ /**
11
+ * @Log() — marks a command/query for automatic entry/exit logging.
12
+ * Applied globally by QuanticCommandBus/QuanticQueryBus when no explicit decorator is present.
13
+ */
14
+ export function Log(options: LogOptions = {}): ClassDecorator {
15
+ return (target: object) => {
16
+ Reflect.defineMetadata(LOG_KEY, { logPayload: true, ...options }, target);
17
+ };
18
+ }
19
+
20
+ export function getLogMetadata(target: object): LogOptions | undefined {
21
+ return Reflect.getMetadata(LOG_KEY, target);
22
+ }
@@ -0,0 +1,39 @@
1
+ import 'reflect-metadata';
2
+ import type { ICommandValidator } from '../validation/ICommandValidator';
3
+
4
+ const VALIDATE_KEY = 'arex:validate';
5
+ const VALIDATOR_CLASS_KEY = 'arex:validator-class';
6
+
7
+ type ValidatorClass = new () => ICommandValidator;
8
+
9
+ /**
10
+ * @Validate(ValidatorClass) — links a command/query to its Zod-based validator.
11
+ *
12
+ * The ValidationBehavior instantiates the validator and calls validate()
13
+ * before the handler runs. Returns Result.validationError() on failure.
14
+ *
15
+ * Usage:
16
+ * ```typescript
17
+ * @Validate(BlockUserValidator)
18
+ * export class BlockUserCommand {
19
+ * constructor(
20
+ * public readonly blockerId: string,
21
+ * public readonly blockedUserId: string,
22
+ * ) {}
23
+ * }
24
+ * ```
25
+ */
26
+ export function Validate(validatorClass: ValidatorClass): ClassDecorator {
27
+ return (target: object) => {
28
+ Reflect.defineMetadata(VALIDATE_KEY, true, target);
29
+ Reflect.defineMetadata(VALIDATOR_CLASS_KEY, validatorClass, target);
30
+ };
31
+ }
32
+
33
+ export function shouldValidate(target: object): boolean {
34
+ return Reflect.getMetadata(VALIDATE_KEY, target) === true;
35
+ }
36
+
37
+ export function getValidatorClass(target: object): ValidatorClass | undefined {
38
+ return Reflect.getMetadata(VALIDATOR_CLASS_KEY, target);
39
+ }
@@ -0,0 +1,22 @@
1
+ import 'reflect-metadata';
2
+
3
+ const WORKFLOW_KEY = 'arex:workflow';
4
+
5
+ export interface WorkflowOptions {
6
+ fallback?: 'throw' | 'skip' | 'queue';
7
+ }
8
+
9
+ export function Workflow(
10
+ processDefinitionId: string,
11
+ options?: WorkflowOptions,
12
+ ): ClassDecorator {
13
+ return (target: object) => {
14
+ Reflect.defineMetadata(WORKFLOW_KEY, { processDefinitionId, ...options }, target);
15
+ };
16
+ }
17
+
18
+ export function getWorkflowMetadata(
19
+ target: object,
20
+ ): { processDefinitionId: string } & WorkflowOptions | undefined {
21
+ return Reflect.getMetadata(WORKFLOW_KEY, target);
22
+ }
@@ -0,0 +1,19 @@
1
+ export const WORKFLOW_ENGINE = Symbol('WORKFLOW_ENGINE');
2
+
3
+ export interface WorkflowStartResult {
4
+ workflowInstanceId: string;
5
+ processInstanceId: string;
6
+ status: 'STARTED';
7
+ }
8
+
9
+ export interface WorkflowEngine {
10
+ startProcess(
11
+ processDefinitionId: string,
12
+ command: object,
13
+ metadata: { commandType: string; correlationId?: string },
14
+ ): Promise<WorkflowStartResult>;
15
+
16
+ signalProcess(processInstanceId: string, signal: string, data?: unknown): Promise<void>;
17
+
18
+ abortProcess(processInstanceId: string): Promise<void>;
19
+ }
@@ -0,0 +1,69 @@
1
+ import { Inject, Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common';
2
+ import { CommandBus } from '@nestjs/cqrs';
3
+ import { Result } from '../../result/Result';
4
+ import { LogBehavior } from '../behaviors/LogBehavior';
5
+ import { FeatureFlagBehavior } from '../behaviors/FeatureFlagBehavior';
6
+ import { ValidationBehavior } from '../behaviors/ValidationBehavior';
7
+ import { CacheBehavior } from '../behaviors/CacheBehavior';
8
+ import { DistributedLockBehavior } from '../behaviors/DistributedLockBehavior';
9
+ import { TransactionalBehavior } from '../behaviors/TransactionalBehavior';
10
+ import { PerformanceBehavior } from '../behaviors/PerformanceBehavior';
11
+ import { WorkflowBehavior } from '../behaviors/WorkflowBehavior';
12
+ import { InvalidateCacheBehavior } from '../behaviors/InvalidateCacheBehavior';
13
+ import { BehaviorFn, runPipeline } from './runPipeline';
14
+
15
+ /**
16
+ * QuanticCommandBus — wraps the @nestjs/cqrs CommandBus with a behavior pipeline.
17
+ *
18
+ * Pipeline order:
19
+ * InvalidateCache → Log → Performance → FeatureFlag → Validate → Workflow → Cache → DistributedLock → Transactional → Handler
20
+ *
21
+ * Controllers inject the standard CommandBus from @nestjs/cqrs.
22
+ * This class patches its execute() in onModuleInit so the pipeline is transparent.
23
+ */
24
+ @Injectable()
25
+ export class QuanticCommandBus implements OnModuleInit {
26
+ private readonly logger = new Logger(QuanticCommandBus.name);
27
+ private behaviors: BehaviorFn[] = [];
28
+ private originalExecute!: CommandBus['execute'];
29
+
30
+ constructor(
31
+ private readonly commandBus: CommandBus,
32
+ private readonly logBehavior: LogBehavior,
33
+ private readonly performanceBehavior: PerformanceBehavior,
34
+ @Optional() @Inject(FeatureFlagBehavior) private readonly featureFlagBehavior: FeatureFlagBehavior | undefined,
35
+ private readonly validationBehavior: ValidationBehavior,
36
+ @Optional() @Inject(WorkflowBehavior) private readonly workflowBehavior: WorkflowBehavior | undefined,
37
+ private readonly cacheBehavior: CacheBehavior,
38
+ private readonly distributedLockBehavior: DistributedLockBehavior,
39
+ private readonly transactionalBehavior: TransactionalBehavior,
40
+ private readonly invalidateCacheBehavior: InvalidateCacheBehavior,
41
+ ) {}
42
+
43
+ onModuleInit() {
44
+ const optional = (behavior: { execute: BehaviorFn } | undefined): BehaviorFn[] =>
45
+ behavior ? [(cmd, next) => behavior.execute(cmd, next)] : [];
46
+
47
+ this.behaviors = [
48
+ (cmd, next) => this.invalidateCacheBehavior.execute(cmd, next),
49
+ (cmd, next) => this.logBehavior.execute(cmd, next),
50
+ (cmd, next) => this.performanceBehavior.execute(cmd, next),
51
+ ...optional(this.featureFlagBehavior),
52
+ (cmd, next) => this.validationBehavior.execute(cmd, next),
53
+ ...optional(this.workflowBehavior),
54
+ (cmd, next) => this.cacheBehavior.execute(cmd, next),
55
+ (cmd, next) => this.distributedLockBehavior.execute(cmd, next),
56
+ (cmd, next) => this.transactionalBehavior.execute(cmd, next),
57
+ ];
58
+
59
+ this.originalExecute = this.commandBus.execute.bind(this.commandBus);
60
+
61
+ const self = this;
62
+ this.commandBus.execute = <T>(command: object): Promise<T> => {
63
+ const handler = () => self.originalExecute(command) as Promise<Result<T>>;
64
+ return runPipeline(command, handler, self.behaviors) as Promise<T>;
65
+ };
66
+
67
+ this.logger.log(`Command pipeline initialized — ${this.behaviors.length} behaviors`);
68
+ }
69
+ }
@@ -0,0 +1,56 @@
1
+ import { Inject, Injectable, Logger, OnModuleInit, Optional } from '@nestjs/common';
2
+ import { QueryBus } from '@nestjs/cqrs';
3
+ import { Result } from '../../result/Result';
4
+ import { LogBehavior } from '../behaviors/LogBehavior';
5
+ import { FeatureFlagBehavior } from '../behaviors/FeatureFlagBehavior';
6
+ import { ValidationBehavior } from '../behaviors/ValidationBehavior';
7
+ import { CacheBehavior } from '../behaviors/CacheBehavior';
8
+ import { PerformanceBehavior } from '../behaviors/PerformanceBehavior';
9
+ import { BehaviorFn, runPipeline } from './runPipeline';
10
+
11
+ /**
12
+ * QuanticQueryBus — wraps the @nestjs/cqrs QueryBus with a behavior pipeline.
13
+ *
14
+ * Pipeline order:
15
+ * Log → Performance → FeatureFlag → Validate → Cache → Handler
16
+ *
17
+ * Queries skip Workflow, DistributedLock, and Transactional — they are read-only.
18
+ */
19
+ @Injectable()
20
+ export class QuanticQueryBus implements OnModuleInit {
21
+ private readonly logger = new Logger(QuanticQueryBus.name);
22
+ private behaviors: BehaviorFn[] = [];
23
+ private originalExecute!: QueryBus['execute'];
24
+
25
+ constructor(
26
+ private readonly queryBus: QueryBus,
27
+ private readonly logBehavior: LogBehavior,
28
+ private readonly performanceBehavior: PerformanceBehavior,
29
+ @Optional() @Inject(FeatureFlagBehavior) private readonly featureFlagBehavior: FeatureFlagBehavior | undefined,
30
+ private readonly validationBehavior: ValidationBehavior,
31
+ private readonly cacheBehavior: CacheBehavior,
32
+ ) {}
33
+
34
+ onModuleInit() {
35
+ const optional = (behavior: { execute: BehaviorFn } | undefined): BehaviorFn[] =>
36
+ behavior ? [(cmd, next) => behavior.execute(cmd, next)] : [];
37
+
38
+ this.behaviors = [
39
+ (cmd, next) => this.logBehavior.execute(cmd, next),
40
+ (cmd, next) => this.performanceBehavior.execute(cmd, next),
41
+ ...optional(this.featureFlagBehavior),
42
+ (cmd, next) => this.validationBehavior.execute(cmd, next),
43
+ (cmd, next) => this.cacheBehavior.execute(cmd, next),
44
+ ];
45
+
46
+ this.originalExecute = this.queryBus.execute.bind(this.queryBus);
47
+
48
+ const self = this;
49
+ this.queryBus.execute = <T>(query: object): Promise<T> => {
50
+ const handler = () => self.originalExecute(query) as Promise<Result<T>>;
51
+ return runPipeline(query, handler, self.behaviors) as Promise<T>;
52
+ };
53
+
54
+ this.logger.log(`Query pipeline initialized — ${this.behaviors.length} behaviors`);
55
+ }
56
+ }
@@ -0,0 +1,22 @@
1
+ import { Result } from '../../result/Result';
2
+
3
+ export type BehaviorFn = <T>(
4
+ command: object,
5
+ next: () => Promise<Result<T>>,
6
+ ) => Promise<Result<T>>;
7
+
8
+ export function runPipeline<T>(
9
+ command: object,
10
+ handler: () => Promise<Result<T>>,
11
+ behaviors: BehaviorFn[],
12
+ ): Promise<Result<T>> {
13
+ let next = handler;
14
+
15
+ for (let i = behaviors.length - 1; i >= 0; i--) {
16
+ const behavior = behaviors[i]!;
17
+ const currentNext = next;
18
+ next = () => behavior(command, currentNext);
19
+ }
20
+
21
+ return next();
22
+ }
@@ -0,0 +1,26 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ import type { QueryRunner } from 'typeorm';
3
+
4
+ /**
5
+ * Ambient transaction context using AsyncLocalStorage.
6
+ *
7
+ * Node.js is single-threaded — each async chain (HTTP request, Bull job,
8
+ * event consumer) gets its own isolated storage. Two concurrent requests
9
+ * never share a QueryRunner.
10
+ *
11
+ * The TransactionalBehavior creates the context for the outermost command;
12
+ * nested commands join automatically.
13
+ */
14
+ export class TransactionContext {
15
+ private static readonly storage = new AsyncLocalStorage<QueryRunner>();
16
+
17
+ /** Returns the active QueryRunner for this async scope, or undefined. */
18
+ static get(): QueryRunner | undefined {
19
+ return this.storage.getStore();
20
+ }
21
+
22
+ /** Runs `fn` with `queryRunner` as the ambient transaction for all nested calls. */
23
+ static run<T>(queryRunner: QueryRunner, fn: () => Promise<T>): Promise<T> {
24
+ return this.storage.run(queryRunner, fn);
25
+ }
26
+ }
@@ -0,0 +1,23 @@
1
+ import type { Repository, ObjectLiteral, EntityTarget } from 'typeorm';
2
+ import { TransactionContext } from './TransactionContext';
3
+
4
+ /**
5
+ * Returns a repository bound to the ambient transaction's QueryRunner,
6
+ * or the original repository if no transaction context exists.
7
+ *
8
+ * Usage in command handlers:
9
+ * ```typescript
10
+ * async execute(cmd: CreateUserCommand): Promise<Result<UserDto>> {
11
+ * const repo = getTransactionalRepo(this.userRepo);
12
+ * const user = repo.create({ email: cmd.email });
13
+ * await repo.save(user);
14
+ * return Result.success(toDto(user));
15
+ * }
16
+ * ```
17
+ */
18
+ export function getTransactionalRepo<T extends ObjectLiteral>(
19
+ repo: Repository<T>,
20
+ ): Repository<T> {
21
+ const qr = TransactionContext.get();
22
+ return qr ? qr.manager.getRepository(repo.target as EntityTarget<T>) : repo;
23
+ }
@@ -0,0 +1,55 @@
1
+ import { Result } from '../../result/Result';
2
+
3
+ /**
4
+ * Interface for Zod-based command/query validators.
5
+ *
6
+ * Each command that needs validation gets a separate validator class
7
+ * with a Zod schema. The `@Validate(ValidatorClass)` decorator links
8
+ * the command to its validator.
9
+ *
10
+ * Usage:
11
+ * ```typescript
12
+ * export class BlockUserValidator implements ICommandValidator<BlockUserCommand> {
13
+ * schema = z.object({
14
+ * blockerId: z.string().uuid(),
15
+ * blockedUserId: z.string().uuid(),
16
+ * }).refine(d => d.blockerId !== d.blockedUserId, 'Cannot block yourself');
17
+ *
18
+ * validate(command: BlockUserCommand): Result<void> {
19
+ * return validateCommand(this.schema, command);
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+ export interface ICommandValidator<T = any> {
25
+ validate(command: T): Result<void>;
26
+ }
27
+
28
+ /**
29
+ * A schema that can safeParse — matches Zod's ZodType without importing it.
30
+ * This avoids Zod v3/v4 compatibility issues across packages.
31
+ */
32
+ interface SafeParseable {
33
+ safeParse(data: unknown): { success: true } | { success: false; error: { issues: Array<{ path: PropertyKey[]; message: string }> } };
34
+ }
35
+
36
+ /**
37
+ * Helper: run a Zod schema against a command and return Result<void>.
38
+ * Formats all Zod issues into a single error message.
39
+ */
40
+ export function validateCommand(schema: SafeParseable, command: unknown): Result<void> {
41
+ const result = schema.safeParse(command);
42
+
43
+ if (result.success) {
44
+ return Result.success(undefined);
45
+ }
46
+
47
+ const messages = result.error.issues
48
+ .map((issue) => {
49
+ const path = issue.path.length > 0 ? `${issue.path.map(String).join('.')}: ` : '';
50
+ return `${path}${issue.message}`;
51
+ })
52
+ .join('; ');
53
+
54
+ return Result.validationError(messages);
55
+ }