@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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TestingModuleFactory = exports.createMockRedisClient = exports.createMockRepository = void 0;
|
|
4
|
+
const testing_1 = require("@nestjs/testing");
|
|
5
|
+
const cqrs_1 = require("@nestjs/cqrs");
|
|
6
|
+
const typeorm_1 = require("@nestjs/typeorm");
|
|
7
|
+
const common_1 = require("@nestjs/common");
|
|
8
|
+
const QuanticCommandBus_1 = require("../cqrs/pipeline/QuanticCommandBus");
|
|
9
|
+
const QuanticQueryBus_1 = require("../cqrs/pipeline/QuanticQueryBus");
|
|
10
|
+
const LogBehavior_1 = require("../cqrs/behaviors/LogBehavior");
|
|
11
|
+
const ValidationBehavior_1 = require("../cqrs/behaviors/ValidationBehavior");
|
|
12
|
+
const CacheBehavior_1 = require("../cqrs/behaviors/CacheBehavior");
|
|
13
|
+
const DistributedLockBehavior_1 = require("../cqrs/behaviors/DistributedLockBehavior");
|
|
14
|
+
const TransactionalBehavior_1 = require("../cqrs/behaviors/TransactionalBehavior");
|
|
15
|
+
const PerformanceBehavior_1 = require("../cqrs/behaviors/PerformanceBehavior");
|
|
16
|
+
const MetricsService_1 = require("../metrics/MetricsService");
|
|
17
|
+
const constants_1 = require("../cqrs/constants");
|
|
18
|
+
const mocks_1 = require("./mocks");
|
|
19
|
+
Object.defineProperty(exports, "createMockRepository", { enumerable: true, get: function () { return mocks_1.createMockRepository; } });
|
|
20
|
+
Object.defineProperty(exports, "createMockRedisClient", { enumerable: true, get: function () { return mocks_1.createMockRedisClient; } });
|
|
21
|
+
/**
|
|
22
|
+
* Factory for bootstrapping a NestJS TestingModule preconfigured
|
|
23
|
+
* with mocked repositories, Redis, and optionally the CQRS pipeline.
|
|
24
|
+
*/
|
|
25
|
+
class TestingModuleFactory {
|
|
26
|
+
static create(options = {}) {
|
|
27
|
+
const { providers = [], entities = [], overrides = [], withPipeline = false } = options;
|
|
28
|
+
const mockRepos = entities.map((entity) => ({
|
|
29
|
+
provide: (0, typeorm_1.getRepositoryToken)(entity),
|
|
30
|
+
useValue: (0, mocks_1.createMockRepository)(),
|
|
31
|
+
}));
|
|
32
|
+
const mockRedis = { provide: constants_1.REDIS_CLIENT, useValue: (0, mocks_1.createMockRedisClient)() };
|
|
33
|
+
const pipelineProviders = withPipeline
|
|
34
|
+
? [
|
|
35
|
+
LogBehavior_1.LogBehavior,
|
|
36
|
+
ValidationBehavior_1.ValidationBehavior,
|
|
37
|
+
CacheBehavior_1.CacheBehavior,
|
|
38
|
+
DistributedLockBehavior_1.DistributedLockBehavior,
|
|
39
|
+
TransactionalBehavior_1.TransactionalBehavior,
|
|
40
|
+
PerformanceBehavior_1.PerformanceBehavior,
|
|
41
|
+
MetricsService_1.MetricsService,
|
|
42
|
+
QuanticCommandBus_1.QuanticCommandBus,
|
|
43
|
+
QuanticQueryBus_1.QuanticQueryBus,
|
|
44
|
+
]
|
|
45
|
+
: [];
|
|
46
|
+
let builder = testing_1.Test.createTestingModule({
|
|
47
|
+
imports: [cqrs_1.CqrsModule.forRoot()],
|
|
48
|
+
providers: [
|
|
49
|
+
...mockRepos,
|
|
50
|
+
mockRedis,
|
|
51
|
+
...pipelineProviders,
|
|
52
|
+
...providers,
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
for (const override of overrides) {
|
|
56
|
+
builder = builder.overrideProvider(override.provide).useValue(override.useValue);
|
|
57
|
+
}
|
|
58
|
+
// Silence NestJS logs during tests
|
|
59
|
+
common_1.Logger.overrideLogger(['error']);
|
|
60
|
+
return builder;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.TestingModuleFactory = TestingModuleFactory;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMockRedisClient = exports.createMockRepository = exports.TestingModuleFactory = void 0;
|
|
4
|
+
var TestingModuleFactory_1 = require("./TestingModuleFactory");
|
|
5
|
+
Object.defineProperty(exports, "TestingModuleFactory", { enumerable: true, get: function () { return TestingModuleFactory_1.TestingModuleFactory; } });
|
|
6
|
+
Object.defineProperty(exports, "createMockRepository", { enumerable: true, get: function () { return TestingModuleFactory_1.createMockRepository; } });
|
|
7
|
+
Object.defineProperty(exports, "createMockRedisClient", { enumerable: true, get: function () { return TestingModuleFactory_1.createMockRedisClient; } });
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a mock repository with common TypeORM repository methods stubbed.
|
|
3
|
+
*/
|
|
4
|
+
export declare function createMockRepository<T = any>(): {
|
|
5
|
+
find: jest.Mock<any, any, any>;
|
|
6
|
+
findOne: jest.Mock<any, any, any>;
|
|
7
|
+
findOneBy: jest.Mock<any, any, any>;
|
|
8
|
+
findAndCount: jest.Mock<any, any, any>;
|
|
9
|
+
save: jest.Mock<any, any, any>;
|
|
10
|
+
create: jest.Mock<any, any, any>;
|
|
11
|
+
update: jest.Mock<any, any, any>;
|
|
12
|
+
delete: jest.Mock<any, any, any>;
|
|
13
|
+
remove: jest.Mock<any, any, any>;
|
|
14
|
+
count: jest.Mock<any, any, any>;
|
|
15
|
+
createQueryBuilder: jest.Mock<{
|
|
16
|
+
where: jest.Mock<any, any, any>;
|
|
17
|
+
andWhere: jest.Mock<any, any, any>;
|
|
18
|
+
orderBy: jest.Mock<any, any, any>;
|
|
19
|
+
addOrderBy: jest.Mock<any, any, any>;
|
|
20
|
+
take: jest.Mock<any, any, any>;
|
|
21
|
+
skip: jest.Mock<any, any, any>;
|
|
22
|
+
leftJoinAndSelect: jest.Mock<any, any, any>;
|
|
23
|
+
getMany: jest.Mock<any, any, any>;
|
|
24
|
+
getOne: jest.Mock<any, any, any>;
|
|
25
|
+
getManyAndCount: jest.Mock<any, any, any>;
|
|
26
|
+
}, [], any>;
|
|
27
|
+
manager: {
|
|
28
|
+
transaction: jest.Mock<any, any, any>;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Creates a mock Redis client with common ioredis methods stubbed.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createMockRedisClient(): Record<string, jest.Mock<any, any, any>>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMockRepository = createMockRepository;
|
|
4
|
+
exports.createMockRedisClient = createMockRedisClient;
|
|
5
|
+
/**
|
|
6
|
+
* Creates a mock repository with common TypeORM repository methods stubbed.
|
|
7
|
+
*/
|
|
8
|
+
function createMockRepository() {
|
|
9
|
+
return {
|
|
10
|
+
find: jest.fn(),
|
|
11
|
+
findOne: jest.fn(),
|
|
12
|
+
findOneBy: jest.fn(),
|
|
13
|
+
findAndCount: jest.fn(),
|
|
14
|
+
save: jest.fn().mockImplementation((entity) => Promise.resolve(entity)),
|
|
15
|
+
create: jest.fn().mockImplementation((dto) => dto),
|
|
16
|
+
update: jest.fn(),
|
|
17
|
+
delete: jest.fn(),
|
|
18
|
+
remove: jest.fn(),
|
|
19
|
+
count: jest.fn(),
|
|
20
|
+
createQueryBuilder: jest.fn(() => ({
|
|
21
|
+
where: jest.fn().mockReturnThis(),
|
|
22
|
+
andWhere: jest.fn().mockReturnThis(),
|
|
23
|
+
orderBy: jest.fn().mockReturnThis(),
|
|
24
|
+
addOrderBy: jest.fn().mockReturnThis(),
|
|
25
|
+
take: jest.fn().mockReturnThis(),
|
|
26
|
+
skip: jest.fn().mockReturnThis(),
|
|
27
|
+
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
|
28
|
+
getMany: jest.fn().mockResolvedValue([]),
|
|
29
|
+
getOne: jest.fn().mockResolvedValue(null),
|
|
30
|
+
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
|
31
|
+
})),
|
|
32
|
+
manager: {
|
|
33
|
+
transaction: jest.fn().mockImplementation((cb) => cb({
|
|
34
|
+
save: jest.fn().mockImplementation((entity) => Promise.resolve(entity)),
|
|
35
|
+
findOne: jest.fn(),
|
|
36
|
+
})),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates a mock Redis client with common ioredis methods stubbed.
|
|
42
|
+
*/
|
|
43
|
+
function createMockRedisClient() {
|
|
44
|
+
const client = {
|
|
45
|
+
get: jest.fn().mockResolvedValue(null),
|
|
46
|
+
set: jest.fn().mockResolvedValue('OK'),
|
|
47
|
+
del: jest.fn().mockResolvedValue(1),
|
|
48
|
+
setex: jest.fn().mockResolvedValue('OK'),
|
|
49
|
+
exists: jest.fn().mockResolvedValue(0),
|
|
50
|
+
xadd: jest.fn().mockResolvedValue('1-0'),
|
|
51
|
+
xreadgroup: jest.fn().mockResolvedValue(null),
|
|
52
|
+
xack: jest.fn().mockResolvedValue(1),
|
|
53
|
+
xgroup: jest.fn().mockResolvedValue('OK'),
|
|
54
|
+
quit: jest.fn().mockResolvedValue('OK'),
|
|
55
|
+
disconnect: jest.fn(),
|
|
56
|
+
on: jest.fn().mockReturnThis(),
|
|
57
|
+
duplicate: jest.fn(),
|
|
58
|
+
};
|
|
59
|
+
// duplicate() returns a fresh mock with the same shape
|
|
60
|
+
client.duplicate.mockImplementation(() => createMockRedisClient());
|
|
61
|
+
return client;
|
|
62
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const FEATURE_FLAGS: {
|
|
2
|
+
readonly ENABLE_PREMIUM_MODE: "enable-premium-mode";
|
|
3
|
+
readonly ENABLE_AGENCY_PLAN: "enable-agency-plan";
|
|
4
|
+
readonly ENABLE_BACKLOG_GENERATION: "enable-backlog-generation";
|
|
5
|
+
};
|
|
6
|
+
export type FeatureFlagName = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS];
|
|
7
|
+
//# sourceMappingURL=initial-flags.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FEATURE_FLAGS = void 0;
|
|
4
|
+
exports.FEATURE_FLAGS = {
|
|
5
|
+
ENABLE_PREMIUM_MODE: 'enable-premium-mode',
|
|
6
|
+
ENABLE_AGENCY_PLAN: 'enable-agency-plan',
|
|
7
|
+
ENABLE_BACKLOG_GENERATION: 'enable-backlog-generation',
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=initial-flags.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
export interface UnleashModuleOptions {
|
|
3
|
+
url?: string;
|
|
4
|
+
appName?: string;
|
|
5
|
+
customHeaders?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
export declare class UnleashModule {
|
|
8
|
+
static forRoot(options?: UnleashModuleOptions): DynamicModule;
|
|
9
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var UnleashModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.UnleashModule = void 0;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const unleash_client_1 = require("unleash-client");
|
|
13
|
+
const FeatureFlagBehavior_1 = require("../cqrs/behaviors/FeatureFlagBehavior");
|
|
14
|
+
let UnleashModule = UnleashModule_1 = class UnleashModule {
|
|
15
|
+
static forRoot(options) {
|
|
16
|
+
const unleashProvider = {
|
|
17
|
+
provide: unleash_client_1.Unleash,
|
|
18
|
+
useFactory: () => {
|
|
19
|
+
const logger = new common_1.Logger('UnleashModule');
|
|
20
|
+
const url = options?.url || process.env['UNLEASH_URL'] || 'http://localhost:4242/api';
|
|
21
|
+
const appName = options?.appName || process.env['UNLEASH_APP_NAME'] || 'arex';
|
|
22
|
+
const token = options?.customHeaders?.['Authorization'] ||
|
|
23
|
+
process.env['UNLEASH_API_TOKEN'] ||
|
|
24
|
+
'*:*.unleash-insecure-api-token';
|
|
25
|
+
logger.log(`Connecting to Unleash at ${url}`);
|
|
26
|
+
return (0, unleash_client_1.initialize)({
|
|
27
|
+
url,
|
|
28
|
+
appName,
|
|
29
|
+
customHeaders: {
|
|
30
|
+
Authorization: token,
|
|
31
|
+
},
|
|
32
|
+
refreshInterval: 10000,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
module: UnleashModule_1,
|
|
38
|
+
providers: [unleashProvider, FeatureFlagBehavior_1.FeatureFlagBehavior],
|
|
39
|
+
exports: [unleash_client_1.Unleash, FeatureFlagBehavior_1.FeatureFlagBehavior],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
exports.UnleashModule = UnleashModule;
|
|
44
|
+
exports.UnleashModule = UnleashModule = UnleashModule_1 = __decorate([
|
|
45
|
+
(0, common_1.Global)(),
|
|
46
|
+
(0, common_1.Module)({})
|
|
47
|
+
], UnleashModule);
|
package/package.json
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quanticjs/core",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "NestJS CQRS framework — Result<T>, pipeline behaviors, base entities, multi-tenancy, Redis Streams, observability",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src",
|
|
10
|
+
"tsconfig.json"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/quanticjs/quanticjs-backend.git"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"registry": "https://registry.npmjs.org",
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"lint": "eslint 'src/**/*.ts'",
|
|
24
|
+
"test": "jest --passWithNoTests"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"cockatiel": "^3.2.1",
|
|
28
|
+
"uuid": "^11.1.0",
|
|
29
|
+
"zod": "^3.25.76"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@nestjs/common": "^11.0.0",
|
|
33
|
+
"@nestjs/passport": "^11.0.0",
|
|
34
|
+
"@nestjs/swagger": "^11.0.0",
|
|
35
|
+
"@nestjs/terminus": "^11.0.0",
|
|
36
|
+
"@nestjs/throttler": "^6.0.0",
|
|
37
|
+
"helmet": "^8.0.0",
|
|
38
|
+
"ioredis": "^5.0.0",
|
|
39
|
+
"jwks-rsa": "^4.0.0",
|
|
40
|
+
"nestjs-pino": "^4.0.0",
|
|
41
|
+
"passport": "^0.7.0",
|
|
42
|
+
"passport-jwt": "^4.0.0",
|
|
43
|
+
"pino": "^10.0.0",
|
|
44
|
+
"pino-http": "^11.0.0",
|
|
45
|
+
"pino-seq": "^3.0.0 || ^4.0.0",
|
|
46
|
+
"prom-client": "^15.0.0",
|
|
47
|
+
"typeorm": "^0.3.20",
|
|
48
|
+
"unleash-client": "^6.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"@nestjs/common": {
|
|
52
|
+
"optional": false
|
|
53
|
+
},
|
|
54
|
+
"@nestjs/passport": {
|
|
55
|
+
"optional": false
|
|
56
|
+
},
|
|
57
|
+
"@nestjs/swagger": {
|
|
58
|
+
"optional": false
|
|
59
|
+
},
|
|
60
|
+
"@nestjs/terminus": {
|
|
61
|
+
"optional": false
|
|
62
|
+
},
|
|
63
|
+
"@nestjs/throttler": {
|
|
64
|
+
"optional": false
|
|
65
|
+
},
|
|
66
|
+
"helmet": {
|
|
67
|
+
"optional": false
|
|
68
|
+
},
|
|
69
|
+
"ioredis": {
|
|
70
|
+
"optional": false
|
|
71
|
+
},
|
|
72
|
+
"jwks-rsa": {
|
|
73
|
+
"optional": false
|
|
74
|
+
},
|
|
75
|
+
"nestjs-pino": {
|
|
76
|
+
"optional": false
|
|
77
|
+
},
|
|
78
|
+
"passport": {
|
|
79
|
+
"optional": false
|
|
80
|
+
},
|
|
81
|
+
"passport-jwt": {
|
|
82
|
+
"optional": false
|
|
83
|
+
},
|
|
84
|
+
"pino": {
|
|
85
|
+
"optional": false
|
|
86
|
+
},
|
|
87
|
+
"pino-http": {
|
|
88
|
+
"optional": false
|
|
89
|
+
},
|
|
90
|
+
"pino-seq": {
|
|
91
|
+
"optional": false
|
|
92
|
+
},
|
|
93
|
+
"prom-client": {
|
|
94
|
+
"optional": false
|
|
95
|
+
},
|
|
96
|
+
"typeorm": {
|
|
97
|
+
"optional": false
|
|
98
|
+
},
|
|
99
|
+
"unleash-client": {
|
|
100
|
+
"optional": false
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"devDependencies": {
|
|
104
|
+
"@nestjs/common": "^11.1.19",
|
|
105
|
+
"@nestjs/core": "^11.1.19",
|
|
106
|
+
"@nestjs/cqrs": "^11.0.3",
|
|
107
|
+
"@nestjs/swagger": "^11.4.2",
|
|
108
|
+
"@nestjs/terminus": "^11.1.1",
|
|
109
|
+
"@nestjs/testing": "^11.1.19",
|
|
110
|
+
"@nestjs/throttler": "^6.5.0",
|
|
111
|
+
"@nestjs/typeorm": "^11.0.1",
|
|
112
|
+
"@types/cookie-parser": "^1.4.10",
|
|
113
|
+
"@types/express": "^5.0.6",
|
|
114
|
+
"@types/jest": "^29.5.0",
|
|
115
|
+
"@types/passport-jwt": "^4.0.1",
|
|
116
|
+
"@types/node": "^22.0.0",
|
|
117
|
+
"@types/uuid": "^10.0.0",
|
|
118
|
+
"cookie-parser": "^1.4.7",
|
|
119
|
+
"@nestjs/passport": "^11.0.5",
|
|
120
|
+
"eslint": "^9.0.0",
|
|
121
|
+
"express": "^5.2.1",
|
|
122
|
+
"helmet": "^8.1.0",
|
|
123
|
+
"ioredis": "^5.10.1",
|
|
124
|
+
"jest": "^29.7.0",
|
|
125
|
+
"jwks-rsa": "^4.0.1",
|
|
126
|
+
"passport": "^0.7.0",
|
|
127
|
+
"passport-jwt": "^4.0.1",
|
|
128
|
+
"nestjs-pino": "^4.6.1",
|
|
129
|
+
"pino": "^10.3.1",
|
|
130
|
+
"pino-http": "^11.0.0",
|
|
131
|
+
"pino-seq": "^4.0.0",
|
|
132
|
+
"prom-client": "^15.1.3",
|
|
133
|
+
"reflect-metadata": "^0.2.2",
|
|
134
|
+
"rxjs": "^7.8.2",
|
|
135
|
+
"ts-jest": "^29.2.0",
|
|
136
|
+
"typeorm": "^0.3.29",
|
|
137
|
+
"typescript": "^5.7.0",
|
|
138
|
+
"unleash-client": "^6.10.1"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
2
|
+
import { NestFactory } from '@nestjs/core';
|
|
3
|
+
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
4
|
+
import { Logger } from 'nestjs-pino';
|
|
5
|
+
import helmet from 'helmet';
|
|
6
|
+
import cookieParser from 'cookie-parser';
|
|
7
|
+
import { GlobalExceptionFilter } from '../filters/GlobalExceptionFilter';
|
|
8
|
+
|
|
9
|
+
export interface BootstrapOptions {
|
|
10
|
+
module: any;
|
|
11
|
+
port: number;
|
|
12
|
+
serviceName: string;
|
|
13
|
+
rawBody?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function bootstrapService(options: BootstrapOptions): Promise<INestApplication> {
|
|
17
|
+
const { module, port, serviceName, rawBody } = options;
|
|
18
|
+
|
|
19
|
+
const app = await NestFactory.create(module, {
|
|
20
|
+
bufferLogs: true,
|
|
21
|
+
...(rawBody ? { rawBody: true } : {}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
app.useLogger(app.get(Logger));
|
|
25
|
+
|
|
26
|
+
// Security headers
|
|
27
|
+
app.use(helmet());
|
|
28
|
+
|
|
29
|
+
// Cookie parsing (required for OAuth flows)
|
|
30
|
+
app.use(cookieParser());
|
|
31
|
+
|
|
32
|
+
// CORS
|
|
33
|
+
const allowedOrigins = (process.env.CORS_ORIGINS || 'http://localhost:5173').split(',');
|
|
34
|
+
app.enableCors({
|
|
35
|
+
origin: allowedOrigins,
|
|
36
|
+
credentials: true,
|
|
37
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
38
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-Tenant-ID', 'X-Correlation-ID'],
|
|
39
|
+
exposedHeaders: ['X-Correlation-ID'],
|
|
40
|
+
maxAge: 86400,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Global exception filter
|
|
44
|
+
app.useGlobalFilters(new GlobalExceptionFilter());
|
|
45
|
+
|
|
46
|
+
// Global validation pipe
|
|
47
|
+
app.useGlobalPipes(
|
|
48
|
+
new ValidationPipe({
|
|
49
|
+
whitelist: true,
|
|
50
|
+
forbidNonWhitelisted: true,
|
|
51
|
+
transform: true,
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Swagger — disabled in production
|
|
56
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
57
|
+
const config = new DocumentBuilder()
|
|
58
|
+
.setTitle(`AREX — ${serviceName}`)
|
|
59
|
+
.setDescription(`OpenAPI spec for ${serviceName}`)
|
|
60
|
+
.setVersion('1.0.0')
|
|
61
|
+
.addBearerAuth()
|
|
62
|
+
.build();
|
|
63
|
+
const document = SwaggerModule.createDocument(app, config);
|
|
64
|
+
SwaggerModule.setup('swagger', app, document);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Enable graceful shutdown hooks (SIGTERM, SIGINT)
|
|
68
|
+
app.enableShutdownHooks();
|
|
69
|
+
|
|
70
|
+
await app.listen(port);
|
|
71
|
+
return app;
|
|
72
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { CacheBehavior } from './CacheBehavior';
|
|
2
|
+
import { Result } from '../../result/Result';
|
|
3
|
+
import { Cache } from '../decorators/Cache.decorator';
|
|
4
|
+
|
|
5
|
+
@Cache('test:{id}', { ttlSeconds: 60 })
|
|
6
|
+
class CachedQuery {
|
|
7
|
+
constructor(public readonly id: string) {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class UncachedQuery {
|
|
11
|
+
constructor(public readonly id: string) {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('CacheBehavior', () => {
|
|
15
|
+
let redis: { get: jest.Mock; set: jest.Mock };
|
|
16
|
+
let behavior: CacheBehavior;
|
|
17
|
+
const next = jest.fn();
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
redis = {
|
|
21
|
+
get: jest.fn().mockResolvedValue(null),
|
|
22
|
+
set: jest.fn().mockResolvedValue('OK'),
|
|
23
|
+
};
|
|
24
|
+
behavior = new CacheBehavior(redis as any);
|
|
25
|
+
next.mockReset().mockResolvedValue(Result.success({ data: 'fresh' }));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should skip cache for commands without @Cache decorator', async () => {
|
|
29
|
+
const result = await behavior.execute(new UncachedQuery('1'), next);
|
|
30
|
+
|
|
31
|
+
expect(result.isSuccess).toBe(true);
|
|
32
|
+
expect(redis.get).not.toHaveBeenCalled();
|
|
33
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return cached value on cache hit', async () => {
|
|
37
|
+
redis.get.mockResolvedValue(JSON.stringify({ data: 'cached' }));
|
|
38
|
+
|
|
39
|
+
const result = await behavior.execute(new CachedQuery('abc'), next);
|
|
40
|
+
|
|
41
|
+
expect(result.isSuccess).toBe(true);
|
|
42
|
+
expect(result.value).toEqual({ data: 'cached' });
|
|
43
|
+
expect(next).not.toHaveBeenCalled();
|
|
44
|
+
expect(redis.get).toHaveBeenCalledWith('test:abc');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should call handler and cache result on cache miss', async () => {
|
|
48
|
+
const result = await behavior.execute(new CachedQuery('abc'), next);
|
|
49
|
+
|
|
50
|
+
expect(result.isSuccess).toBe(true);
|
|
51
|
+
expect(result.value).toEqual({ data: 'fresh' });
|
|
52
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(redis.set).toHaveBeenCalledWith('test:abc', JSON.stringify({ data: 'fresh' }), 'EX', 60);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should skip cache when redis is not available', async () => {
|
|
57
|
+
const noCacheBehavior = new CacheBehavior(undefined);
|
|
58
|
+
const result = await noCacheBehavior.execute(new CachedQuery('1'), next);
|
|
59
|
+
|
|
60
|
+
expect(result.isSuccess).toBe(true);
|
|
61
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Injectable, Logger, Optional, Inject } from '@nestjs/common';
|
|
2
|
+
import { getCacheMetadata } from '../decorators/Cache.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 CacheBehavior {
|
|
9
|
+
private readonly logger = new Logger(CacheBehavior.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 = getCacheMetadata(command.constructor);
|
|
17
|
+
|
|
18
|
+
if (!metadata || !this.redis) {
|
|
19
|
+
return next();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cacheKey = this.interpolateKey(metadata.key, command);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const cached = await this.redis.get(cacheKey);
|
|
26
|
+
if (cached) {
|
|
27
|
+
this.logger.debug(`Cache hit: ${cacheKey}`);
|
|
28
|
+
return Result.success(JSON.parse(cached) as T);
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
this.logger.warn(`Cache read failed for key: ${cacheKey}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = await next();
|
|
35
|
+
|
|
36
|
+
if (result.isSuccess && result.value !== undefined) {
|
|
37
|
+
try {
|
|
38
|
+
await this.redis.set(cacheKey, JSON.stringify(result.value), 'EX', metadata.ttlSeconds!);
|
|
39
|
+
this.logger.debug(`Cache set: ${cacheKey} (TTL: ${metadata.ttlSeconds}s)`);
|
|
40
|
+
} catch {
|
|
41
|
+
this.logger.warn(`Cache write failed for key: ${cacheKey}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private interpolateKey(template: string, command: object): string {
|
|
49
|
+
return template.replace(/\{(\w+)\}/g, (_, prop) => {
|
|
50
|
+
const value = (command as Record<string, unknown>)[prop];
|
|
51
|
+
return value != null ? String(value) : '';
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
}
|