diff --git a/apps/policy-engine/src/cli/command/provision.command.ts b/apps/policy-engine/src/cli/command/provision.command.ts index 3943426f9..6087d57b6 100644 --- a/apps/policy-engine/src/cli/command/provision.command.ts +++ b/apps/policy-engine/src/cli/command/provision.command.ts @@ -1,4 +1,4 @@ -import { ConfigService } from '@nestjs/config' +import { ConfigService } from '@narval/config-module' import { Command, CommandRunner } from 'nest-commander' import { EngineService } from '../../engine/core/service/engine.service' import { ProvisionService } from '../../engine/core/service/provision.service' @@ -12,7 +12,7 @@ export class ProvisionCommand extends CommandRunner { constructor( private provisionService: ProvisionService, private engineService: EngineService, - private configService: ConfigService + private configService: ConfigService ) { super() } @@ -27,7 +27,7 @@ export class ProvisionCommand extends CommandRunner { await this.provisionService.provision() try { - const keyring = this.configService.get('keyring', { infer: true }) + const keyring = this.configService.get('keyring') const engine = await this.engineService.getEngineOrThrow() console.log('Engine ID:', engine.id) diff --git a/apps/policy-engine/src/engine/__test__/e2e/tenant.spec.ts b/apps/policy-engine/src/engine/__test__/e2e/tenant.spec.ts index 780656915..a19cbd120 100644 --- a/apps/policy-engine/src/engine/__test__/e2e/tenant.spec.ts +++ b/apps/policy-engine/src/engine/__test__/e2e/tenant.spec.ts @@ -1,6 +1,6 @@ +import { ConfigModule, ConfigService } from '@narval/config-module' import { EncryptionModuleOptionProvider } from '@narval/encryption-module' import { HttpStatus, INestApplication } from '@nestjs/common' -import { ConfigModule, ConfigService } from '@nestjs/config' import { Test, TestingModule } from '@nestjs/testing' import request from 'supertest' import { v4 as uuid } from 'uuid' @@ -28,7 +28,7 @@ describe('Tenant', () => { let tenantRepository: TenantRepository let tenantService: TenantService let engineService: EngineService - let configService: ConfigService + let configService: ConfigService const adminApiKey = 'test-admin-api-key' @@ -71,12 +71,12 @@ describe('Tenant', () => { tenantService = module.get(TenantService) tenantRepository = module.get(TenantRepository) testPrismaService = module.get(TestPrismaService) - configService = module.get>(ConfigService) + configService = module.get>(ConfigService) await testPrismaService.truncateAll() await engineService.save({ - id: configService.get('engine.id', { infer: true }), + id: configService.get('engine.id'), masterKey: 'unsafe-test-master-key', adminApiKey }) diff --git a/apps/policy-engine/src/engine/core/service/__test__/unit/bootstrap.service.spec.ts b/apps/policy-engine/src/engine/core/service/__test__/unit/bootstrap.service.spec.ts index ccf38f26f..bc28d2e58 100644 --- a/apps/policy-engine/src/engine/core/service/__test__/unit/bootstrap.service.spec.ts +++ b/apps/policy-engine/src/engine/core/service/__test__/unit/bootstrap.service.spec.ts @@ -1,5 +1,5 @@ +import { ConfigModule } from '@narval/config-module' import { EncryptionException, EncryptionService } from '@narval/encryption-module' -import { ConfigModule } from '@nestjs/config' import { Test } from '@nestjs/testing' import { MockProxy, mock } from 'jest-mock-extended' import { EngineService } from '../../../../../engine/core/service/engine.service' diff --git a/apps/policy-engine/src/engine/core/service/engine.service.ts b/apps/policy-engine/src/engine/core/service/engine.service.ts index 5d002b1ae..2a03ab96d 100644 --- a/apps/policy-engine/src/engine/core/service/engine.service.ts +++ b/apps/policy-engine/src/engine/core/service/engine.service.ts @@ -1,5 +1,5 @@ +import { ConfigService } from '@narval/config-module' import { Injectable } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' import { Config } from '../../../policy-engine.config' import { Engine } from '../../../shared/type/domain.type' import { EngineRepository } from '../../persistence/repository/engine.repository' @@ -8,7 +8,7 @@ import { EngineNotProvisionedException } from '../exception/engine-not-provision @Injectable() export class EngineService { constructor( - private configService: ConfigService, + private configService: ConfigService, private engineRepository: EngineRepository ) {} @@ -37,6 +37,6 @@ export class EngineService { } private getId(): string { - return this.configService.get('engine.id', { infer: true }) + return this.configService.get('engine.id') } } diff --git a/apps/policy-engine/src/engine/core/service/evaluation.service.ts b/apps/policy-engine/src/engine/core/service/evaluation.service.ts index 174e84779..d489c7b4e 100644 --- a/apps/policy-engine/src/engine/core/service/evaluation.service.ts +++ b/apps/policy-engine/src/engine/core/service/evaluation.service.ts @@ -1,6 +1,6 @@ +import { ConfigService } from '@narval/config-module' import { EvaluationRequest, EvaluationResponse } from '@narval/policy-engine-shared' import { HttpStatus, Injectable } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' import { resolve } from 'path' import { OpenPolicyAgentEngine } from '../../../open-policy-agent/core/open-policy-agent.engine' import { Config } from '../../../policy-engine.config' @@ -12,7 +12,7 @@ const UNSAFE_ENGINE_PRIVATE_KEY = '0x7cfef3303797cbc7515d9ce22ffe849c701b0f2812f @Injectable() export class EvaluationService { constructor( - private configService: ConfigService, + private configService: ConfigService, private tenantService: TenantService ) {} @@ -44,7 +44,7 @@ export class EvaluationService { entities: entityStore.data, policies: policyStore.data, privateKey: UNSAFE_ENGINE_PRIVATE_KEY, - resourcePath: resolve(this.configService.get('resourcePath', { infer: true })) + resourcePath: resolve(this.configService.get('resourcePath')) }).load() return engine.evaluate(evaluation) diff --git a/apps/policy-engine/src/engine/core/service/provision.service.ts b/apps/policy-engine/src/engine/core/service/provision.service.ts index 13edc28b3..d4512ad22 100644 --- a/apps/policy-engine/src/engine/core/service/provision.service.ts +++ b/apps/policy-engine/src/engine/core/service/provision.service.ts @@ -1,6 +1,6 @@ +import { ConfigService } from '@narval/config-module' import { generateKeyEncryptionKey, generateMasterKey } from '@narval/encryption-module' import { Injectable, Logger } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' import { randomBytes } from 'crypto' import { Config } from '../../../policy-engine.config' import { EngineService } from './engine.service' @@ -10,7 +10,7 @@ export class ProvisionService { private logger = new Logger(ProvisionService.name) constructor( - private configService: ConfigService, + private configService: ConfigService, private engineService: EngineService ) {} @@ -21,11 +21,12 @@ export class ProvisionService { const isFirstTime = engine === null - // IMPORTANT: The order of internal methods call matters. - if (isFirstTime) { + // IMPORTANT: The order of internal methods call matters. await this.createEngine() await this.maybeSetupEncryption() + } else { + this.logger.log('Skip engine provision') } } @@ -46,7 +47,7 @@ export class ProvisionService { return this.logger.log('Skip master key set up because it already exists') } - const keyring = this.configService.get('keyring', { infer: true }) + const keyring = this.configService.get('keyring') if (keyring.type === 'raw') { this.logger.log('Generate and save engine master key') @@ -60,6 +61,6 @@ export class ProvisionService { } private getEngineId(): string { - return this.configService.get('engine.id', { infer: true }) + return this.configService.get('engine.id') } } diff --git a/apps/policy-engine/src/engine/engine.module.ts b/apps/policy-engine/src/engine/engine.module.ts index 4553373eb..463ceed7c 100644 --- a/apps/policy-engine/src/engine/engine.module.ts +++ b/apps/policy-engine/src/engine/engine.module.ts @@ -1,7 +1,7 @@ +import { ConfigModule, ConfigService } from '@narval/config-module' import { EncryptionModule } from '@narval/encryption-module' import { HttpModule } from '@nestjs/axios' import { Module, ValidationPipe } from '@nestjs/common' -import { ConfigModule, ConfigService } from '@nestjs/config' import { APP_PIPE } from '@nestjs/core' import { load } from '../policy-engine.config' import { EncryptionModuleOptionFactory } from '../shared/factory/encryption-module-option.factory' diff --git a/apps/policy-engine/src/main.ts b/apps/policy-engine/src/main.ts index c368c635b..0b2e8d420 100644 --- a/apps/policy-engine/src/main.ts +++ b/apps/policy-engine/src/main.ts @@ -1,5 +1,5 @@ +import { ConfigService } from '@narval/config-module' import { INestApplication, Logger, ValidationPipe } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' import { NestFactory } from '@nestjs/core' import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger' import { lastValueFrom, map, of, switchMap } from 'rxjs' @@ -48,7 +48,7 @@ const withGlobalPipes = (app: INestApplication): INestApplication => { * @returns The modified Nest application instance. */ const withGlobalFilters = - (configService: ConfigService) => + (configService: ConfigService) => (app: INestApplication): INestApplication => { app.useGlobalFilters(new HttpExceptionFilter(configService), new ApplicationExceptionFilter(configService)) @@ -58,8 +58,8 @@ const withGlobalFilters = async function bootstrap() { const logger = new Logger('PolicyEngineBootstrap') const application = await NestFactory.create(PolicyEngineModule, { bodyParser: true }) - const configService = application.get(ConfigService) - const port = configService.get('PORT') + const configService = application.get(ConfigService) + const port = configService.get('port') if (!port) { throw new Error('Missing PORT environment variable') diff --git a/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts b/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts index 6ad3f97d4..c12a12b9f 100644 --- a/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts +++ b/apps/policy-engine/src/open-policy-agent/core/__test__/unit/open-policy-agent.engine.spec.ts @@ -1,3 +1,4 @@ +import { ConfigModule, ConfigService } from '@narval/config-module' import { Action, Criterion, @@ -13,7 +14,7 @@ import { toHex } from '@narval/policy-engine-shared' import { SigningAlg, buildSignerEip191, hash, secp256k1PrivateKeyToJwk, signJwt } from '@narval/signature' -import { ConfigModule, ConfigService, Path, PathValue } from '@nestjs/config' +import { Path, PathValue } from '@nestjs/config' import { Test, TestingModule } from '@nestjs/testing' import { Config, load } from '../../../../policy-engine.config' import { OpenPolicyAgentException } from '../../exception/open-policy-agent.exception' @@ -44,9 +45,9 @@ const getConfig = async

>(propertyPath: P): Promise>(ConfigService) + const service = module.get>(ConfigService) - return service.get(propertyPath, { infer: true }) + return service.get(propertyPath) } describe('OpenPolicyAgentEngine', () => { diff --git a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/rego-transpiler.util.spec.ts b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/rego-transpiler.util.spec.ts index 3f1dce4bd..bd9d669ba 100644 --- a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/rego-transpiler.util.spec.ts +++ b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/rego-transpiler.util.spec.ts @@ -1,3 +1,4 @@ +import { ConfigModule, ConfigService } from '@narval/config-module' import { ApprovalsCriterion, Criterion, @@ -10,7 +11,7 @@ import { ValueOperators, WalletAddressCriterion } from '@narval/policy-engine-shared' -import { ConfigModule, ConfigService, Path, PathValue } from '@nestjs/config' +import { Path, PathValue } from '@nestjs/config' import { Test, TestingModule } from '@nestjs/testing' import { Config, load } from '../../../../../policy-engine.config' import { getRegoRuleTemplatePath, transpile, transpileCriterion, transpileReason } from '../../rego-transpiler.util' @@ -20,9 +21,9 @@ const getConfig = async

>(propertyPath: P): Promise>(ConfigService) + const service = module.get>(ConfigService) - return service.get(propertyPath, { infer: true }) + return service.get(propertyPath) } const getTemplatePath = async () => getRegoRuleTemplatePath(await getConfig('resourcePath')) diff --git a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/wasm-build.util.spec.ts b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/wasm-build.util.spec.ts index 718e9b1df..7ea1b4500 100644 --- a/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/wasm-build.util.spec.ts +++ b/apps/policy-engine/src/open-policy-agent/core/util/__test__/unit/wasm-build.util.spec.ts @@ -1,5 +1,6 @@ +import { ConfigModule, ConfigService } from '@narval/config-module' import { FIXTURE } from '@narval/policy-engine-shared' -import { ConfigModule, ConfigService, Path, PathValue } from '@nestjs/config' +import { Path, PathValue } from '@nestjs/config' import { Test, TestingModule } from '@nestjs/testing' import { loadPolicy } from '@open-policy-agent/opa-wasm' import { existsSync } from 'fs' @@ -22,9 +23,9 @@ const getConfig = async

>(propertyPath: P): Promise>(ConfigService) + const service = module.get>(ConfigService) - return service.get(propertyPath, { infer: true }) + return service.get(propertyPath) } const getTemplatePath = async () => getRegoRuleTemplatePath(await getConfig('resourcePath')) diff --git a/apps/policy-engine/src/policy-engine.module.ts b/apps/policy-engine/src/policy-engine.module.ts index 02f5e6e0d..e2f624fed 100644 --- a/apps/policy-engine/src/policy-engine.module.ts +++ b/apps/policy-engine/src/policy-engine.module.ts @@ -1,9 +1,10 @@ +import { ConfigModule, ConfigService } from '@narval/config-module' import { EncryptionModule } from '@narval/encryption-module' import { Module, OnApplicationBootstrap, ValidationPipe } from '@nestjs/common' -import { ConfigModule, ConfigService } from '@nestjs/config' import { APP_PIPE } from '@nestjs/core' import { BootstrapService } from './engine/core/service/bootstrap.service' import { EngineService } from './engine/core/service/engine.service' +import { ProvisionService } from './engine/core/service/provision.service' import { EngineModule } from './engine/engine.module' import { load } from './policy-engine.config' import { EncryptionModuleOptionFactory } from './shared/factory/encryption-module-option.factory' @@ -32,9 +33,13 @@ import { EncryptionModuleOptionFactory } from './shared/factory/encryption-modul ] }) export class PolicyEngineModule implements OnApplicationBootstrap { - constructor(private bootstrapService: BootstrapService) {} + constructor( + private provisionService: ProvisionService, + private bootstrapService: BootstrapService + ) {} async onApplicationBootstrap() { + await this.provisionService.provision() await this.bootstrapService.boot() } } diff --git a/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts b/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts index 86147ae14..dfc9666c5 100644 --- a/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts +++ b/apps/policy-engine/src/shared/factory/encryption-module-option.factory.ts @@ -1,4 +1,5 @@ import { RawAesKeyringNode } from '@aws-crypto/client-node' +import { ConfigService } from '@narval/config-module' import { EncryptionModuleOption, decryptMasterKey, @@ -7,7 +8,6 @@ import { } from '@narval/encryption-module' import { toBytes } from '@narval/policy-engine-shared' import { Injectable, Logger } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' import { EngineService } from '../../engine/core/service/engine.service' import { Config } from '../../policy-engine.config' import { ENCRYPTION_KEY_NAME, ENCRYPTION_KEY_NAMESPACE, ENCRYPTION_WRAPPING_SUITE } from '../../policy-engine.constant' @@ -18,11 +18,11 @@ export class EncryptionModuleOptionFactory { constructor( private engineService: EngineService, - private configService: ConfigService + private configService: ConfigService ) {} async create(): Promise { - const keyring = this.configService.get('keyring', { infer: true }) + const keyring = this.configService.get('keyring') const engine = await this.engineService.getEngine() // NOTE: An undefined engine at boot time only happens during the diff --git a/apps/policy-engine/src/shared/filter/__test__/unit/application-exception.filter.spec.ts b/apps/policy-engine/src/shared/filter/__test__/unit/application-exception.filter.spec.ts index e27884571..069b34d03 100644 --- a/apps/policy-engine/src/shared/filter/__test__/unit/application-exception.filter.spec.ts +++ b/apps/policy-engine/src/shared/filter/__test__/unit/application-exception.filter.spec.ts @@ -1,6 +1,6 @@ +import { ConfigService } from '@narval/config-module' import { ArgumentsHost, HttpStatus, Logger } from '@nestjs/common' import { HttpArgumentsHost } from '@nestjs/common/interfaces' -import { ConfigService } from '@nestjs/config' import { Response } from 'express' import { mock } from 'jest-mock-extended' import { Config, Env } from '../../../../policy-engine.config' @@ -41,7 +41,7 @@ describe(ApplicationExceptionFilter.name, () => { } const buildConfigServiceMock = (env: Env) => - mock>({ + mock>({ get: jest.fn().mockReturnValue(env) }) diff --git a/apps/policy-engine/src/shared/filter/application-exception.filter.ts b/apps/policy-engine/src/shared/filter/application-exception.filter.ts index f94f56b2d..7cc5fe38f 100644 --- a/apps/policy-engine/src/shared/filter/application-exception.filter.ts +++ b/apps/policy-engine/src/shared/filter/application-exception.filter.ts @@ -1,5 +1,5 @@ +import { ConfigService } from '@narval/config-module' import { ArgumentsHost, Catch, ExceptionFilter, LogLevel, Logger } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' import { Response } from 'express' import { Config, Env } from '../../policy-engine.config' import { ApplicationException } from '../../shared/exception/application.exception' @@ -8,7 +8,7 @@ import { ApplicationException } from '../../shared/exception/application.excepti export class ApplicationExceptionFilter implements ExceptionFilter { private logger = new Logger(ApplicationExceptionFilter.name) - constructor(private configService: ConfigService) {} + constructor(private configService: ConfigService) {} catch(exception: ApplicationException, host: ArgumentsHost) { const ctx = host.switchToHttp() diff --git a/apps/policy-engine/src/shared/filter/http-exception.filter.ts b/apps/policy-engine/src/shared/filter/http-exception.filter.ts index 6fe3ee39a..48a8a499d 100644 --- a/apps/policy-engine/src/shared/filter/http-exception.filter.ts +++ b/apps/policy-engine/src/shared/filter/http-exception.filter.ts @@ -1,5 +1,5 @@ +import { ConfigService } from '@narval/config-module' import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' import { Response } from 'express' import { Config, Env } from '../../policy-engine.config' @@ -7,7 +7,7 @@ import { Config, Env } from '../../policy-engine.config' export class HttpExceptionFilter implements ExceptionFilter { private logger = new Logger(HttpExceptionFilter.name) - constructor(private configService: ConfigService) {} + constructor(private configService: ConfigService) {} catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp() diff --git a/apps/policy-engine/src/shared/module/persistence/service/prisma.service.ts b/apps/policy-engine/src/shared/module/persistence/service/prisma.service.ts index 4034b6c1a..778bea5d3 100644 --- a/apps/policy-engine/src/shared/module/persistence/service/prisma.service.ts +++ b/apps/policy-engine/src/shared/module/persistence/service/prisma.service.ts @@ -1,5 +1,5 @@ +import { ConfigService } from '@narval/config-module' import { Inject, Injectable, Logger, OnApplicationShutdown, OnModuleDestroy, OnModuleInit } from '@nestjs/common' -import { ConfigService } from '@nestjs/config' import { PrismaClient } from '@prisma/client/policy-engine' import { Config } from '../../../../policy-engine.config' @@ -7,8 +7,8 @@ import { Config } from '../../../../policy-engine.config' export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy, OnApplicationShutdown { private logger = new Logger(PrismaService.name) - constructor(@Inject(ConfigService) configService: ConfigService) { - const url = configService.get('database.url', { infer: true }) + constructor(@Inject(ConfigService) configService: ConfigService) { + const url = configService.get('database.url') super({ datasources: { diff --git a/packages/config-module/.eslintrc.json b/packages/config-module/.eslintrc.json new file mode 100644 index 000000000..9d9c0db55 --- /dev/null +++ b/packages/config-module/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/packages/config-module/README.md b/packages/config-module/README.md new file mode 100644 index 000000000..014d62438 --- /dev/null +++ b/packages/config-module/README.md @@ -0,0 +1,7 @@ +# Config Module + +A custom config module built on top of Nest's, better utilizing its options to +achieve more satisfying results in terms of user experience, tailored +specifically for Narval's usage. + +See demo: https://github.com/narval-xyz/armory/pull/184 diff --git a/packages/config-module/jest.config.ts b/packages/config-module/jest.config.ts new file mode 100644 index 000000000..520939c87 --- /dev/null +++ b/packages/config-module/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'config-module', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }] + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/packages/config-module' +} diff --git a/packages/config-module/project.json b/packages/config-module/project.json new file mode 100644 index 000000000..5020c15ab --- /dev/null +++ b/packages/config-module/project.json @@ -0,0 +1,23 @@ +{ + "name": "config-module", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/config-module/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["packages/config-module/**/*.ts"] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/config-module/jest.config.ts" + } + } + }, + "tags": [] +} diff --git a/packages/config-module/src/index.ts b/packages/config-module/src/index.ts new file mode 100644 index 000000000..707972b81 --- /dev/null +++ b/packages/config-module/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/config.module' +export * from './lib/config.service' +export * from './lib/config.type' diff --git a/packages/config-module/src/lib/config.module.ts b/packages/config-module/src/lib/config.module.ts new file mode 100644 index 000000000..a618bbf54 --- /dev/null +++ b/packages/config-module/src/lib/config.module.ts @@ -0,0 +1,22 @@ +import { DynamicModule } from '@nestjs/common' +import { ConfigModule as NestConfigModule } from '@nestjs/config' +import { ConfigService } from './config.service' +import { ConfigModuleOptions } from './config.type' + +export class ConfigModule { + static forRoot(options?: ConfigModuleOptions): DynamicModule { + return { + module: ConfigModule, + global: options?.isGlobal ?? false, + imports: [ + NestConfigModule.forRoot({ + ...options, + // See https://docs.nestjs.com/techniques/configuration#expandable-variables + expandVariables: true + }) + ], + providers: [ConfigService], + exports: [ConfigService] + } + } +} diff --git a/packages/config-module/src/lib/config.service.ts b/packages/config-module/src/lib/config.service.ts new file mode 100644 index 000000000..167c1e8e6 --- /dev/null +++ b/packages/config-module/src/lib/config.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common' +import { ConfigService as NestConfigService, Path, PathValue } from '@nestjs/config' + +/** + * This service extends the NestConfigService to enforce `WasValidated` to be + * true (2nd generic argument). Thus, ensuring `get` method return type will + * always return a value instead of `T | undefined` + * + * See + * - https://docs.nestjs.com/techniques/configuration#using-the-configservice + * - https://docs.nestjs.com/techniques/configuration#custom-configuration-files + * - https://github.com/nestjs/config/blob/8f519ac78f9139e0dd4ee26eb97f73344c0237e8/lib/config.service.ts#L34-L35 + */ +@Injectable() +export class ConfigService extends NestConfigService { + /** + * Override the `get` method to use the configuration schema type to + * autocomplete the property path and always return the right type for it. + * + * This strict typing validation makes impossible to: + * 1. Access a property that does not exist in the configuration schema. + * 2. Cast a property to a different type than the one defined in the + * configuration schema. + */ + override get

>(propertyPath: P): PathValue { + return super.get(propertyPath, { infer: true }) + } +} diff --git a/packages/config-module/src/lib/config.type.ts b/packages/config-module/src/lib/config.type.ts new file mode 100644 index 000000000..30e8df528 --- /dev/null +++ b/packages/config-module/src/lib/config.type.ts @@ -0,0 +1,5 @@ +import { ConfigModuleOptions as NestConfigModuleOptions } from '@nestjs/config' + +export type ConfigModuleOptions = NestConfigModuleOptions & { + load: NestConfigModuleOptions['load'] +} diff --git a/packages/config-module/tsconfig.json b/packages/config-module/tsconfig.json new file mode 100644 index 000000000..f5b85657a --- /dev/null +++ b/packages/config-module/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/config-module/tsconfig.lib.json b/packages/config-module/tsconfig.lib.json new file mode 100644 index 000000000..c297a2487 --- /dev/null +++ b/packages/config-module/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/config-module/tsconfig.spec.json b/packages/config-module/tsconfig.spec.json new file mode 100644 index 000000000..f6d8ffcc9 --- /dev/null +++ b/packages/config-module/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 8b6ad853b..14844b21e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -13,6 +13,7 @@ "noImplicitAny": true, "noImplicitThis": true, "paths": { + "@narval/config-module": ["packages/config-module/src/index.ts"], "@narval/encryption-module": ["packages/encryption-module/src/index.ts"], "@narval/policy-engine-shared": ["packages/policy-engine-shared/src/index.ts"], "@narval/signature": ["packages/signature/src/index.ts"],