diff --git a/.env.sample b/.env.sample index 8ac643e..6c2a63b 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,5 @@ +PROTOCOL=http + GATEWAY_HOST=localhost GATEWAY_PORT=3333 @@ -5,4 +7,7 @@ USER_HOST=localhost USER_PORT=15001 TASK_HOST=localhost -TASK_PORT=15002 \ No newline at end of file +TASK_PORT=15002 + +DATABASE_HOST=127.0.0.1 +DATABASE_PORT=3306 diff --git a/apps/gateway/src/app/app.module.ts b/apps/gateway/src/app/app.module.ts index 72200ac..80c1e35 100644 --- a/apps/gateway/src/app/app.module.ts +++ b/apps/gateway/src/app/app.module.ts @@ -1,20 +1,38 @@ import { IntrospectAndCompose } from '@apollo/gateway'; +import { gatewayConfig, userAppConfig } from '@libs/config'; import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo'; import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; -import { subgraphsConfig } from '@graphql-federation-workspace/applications-config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [ - GraphQLModule.forRoot({ + ConfigModule.forRoot({ + isGlobal: true, + load: [gatewayConfig, userAppConfig], + }), + GraphQLModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], driver: ApolloGatewayDriver, - gateway: { - supergraphSdl: new IntrospectAndCompose({ - subgraphs: subgraphsConfig, - }), + useFactory: (configService: ConfigService) => { + const userAppConfig = configService.get('userApp'); + return { + driver: ApolloGatewayDriver, + gateway: { + supergraphSdl: new IntrospectAndCompose({ + subgraphs: [ + { + name: userAppConfig.name, + url: `${userAppConfig.protocol}://${userAppConfig.host}:${userAppConfig.port}/graphql`, + }, + ], + }), + }, + }; }, }), ], diff --git a/apps/gateway/src/main.ts b/apps/gateway/src/main.ts index 271affc..1e16adf 100644 --- a/apps/gateway/src/main.ts +++ b/apps/gateway/src/main.ts @@ -4,17 +4,21 @@ */ import { Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; -import { gatewayConfig } from '@graphql-federation-workspace/applications-config'; import { AppModule } from './app/app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - const port = gatewayConfig.port; - await app.listen(port); - Logger.log(`🚀 Application is running on: ${gatewayConfig.host}:${port}`); + const configService = app.get(ConfigService); + const config = configService.get('gateway'); + + await app.listen(config.port); + Logger.log( + `🚀 Application is running on: ${config.protocol}://${config.host}:${config.port}`, + ); } bootstrap(); diff --git a/apps/users-application/src/app/app.module.ts b/apps/users-application/src/app/app.module.ts index df8a77d..9d99bd8 100644 --- a/apps/users-application/src/app/app.module.ts +++ b/apps/users-application/src/app/app.module.ts @@ -1,11 +1,19 @@ +import { userAppConfig } from '@libs/config'; import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UsersModule } from '../users/users.module'; @Module({ - imports: [UsersModule], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [userAppConfig], + }), + UsersModule, + ], controllers: [AppController], providers: [AppService], }) diff --git a/apps/users-application/src/main.ts b/apps/users-application/src/main.ts index 720a641..e10bcd8 100644 --- a/apps/users-application/src/main.ts +++ b/apps/users-application/src/main.ts @@ -4,17 +4,21 @@ */ import { Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; -import { userSubGraph } from '@graphql-federation-workspace/applications-config'; import { AppModule } from './app/app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); - const port = userSubGraph.port; - await app.listen(port); - Logger.log(`🚀 Application is running on: ${userSubGraph.host}:${port}`); + const configService = app.get(ConfigService); + const config = configService.get('userApp'); + + await app.listen(config.port); + Logger.log( + ` 🚀 Application is running on: ${config.protocol}://${config.host}:${config.port}`, + ); } bootstrap(); diff --git a/libs/application-config/project.json b/libs/application-config/project.json deleted file mode 100644 index 54220f1..0000000 --- a/libs/application-config/project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "applications-config", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/application-config/src", - "projectType": "library", - "tags": [], - "// targets": "to see all targets run: nx show project applications-config --web", - "targets": {} -} diff --git a/libs/application-config/src/index.ts b/libs/application-config/src/index.ts deleted file mode 100644 index 1721aef..0000000 --- a/libs/application-config/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/applications-config'; -export * from './lib/subgraphs-config'; diff --git a/libs/application-config/src/lib/applications-config.spec.ts b/libs/application-config/src/lib/applications-config.spec.ts deleted file mode 100644 index 6e7389b..0000000 --- a/libs/application-config/src/lib/applications-config.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -describe('Service Configurations', () => { - const originalEnv = process.env; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...originalEnv }; - }); - - afterEach(() => { - process.env = originalEnv; - }); - - it('should use default values when environment variables are not set', () => { - const { - gatewayConfig, - userSubGraph, - taskSubGraph, - } = require('./applications-config'); - - expect(gatewayConfig).toEqual({ - host: 'http://localhost', - name: 'gateway', - port: '3333', - }); - - expect(userSubGraph).toEqual({ - host: 'http://localhost', - name: 'user', - port: '15001', - }); - - expect(taskSubGraph).toEqual({ - host: 'http://localhost', - name: 'task', - port: '15002', - }); - }); - - it('should use environment variables when they are set', () => { - process.env['GATEWAY_HOST'] = 'gateway.example.com'; - process.env['GATEWAY_PORT'] = '4000'; - process.env['USER_HOST'] = 'user.example.com'; - process.env['USER_PORT'] = '5000'; - process.env['TASK_HOST'] = 'task.example.com'; - process.env['TASK_PORT'] = '6000'; - - jest.resetModules(); - - const { - gatewayConfig, - userSubGraph, - taskSubGraph, - } = require('./applications-config'); - - expect(gatewayConfig).toEqual({ - host: 'gateway.example.com', - name: 'gateway', - port: '4000', - }); - - expect(userSubGraph).toEqual({ - host: 'user.example.com', - name: 'user', - port: '5000', - }); - - expect(taskSubGraph).toEqual({ - host: 'task.example.com', - name: 'task', - port: '6000', - }); - }); -}); diff --git a/libs/application-config/src/lib/applications-config.ts b/libs/application-config/src/lib/applications-config.ts deleted file mode 100644 index 63f4909..0000000 --- a/libs/application-config/src/lib/applications-config.ts +++ /dev/null @@ -1,33 +0,0 @@ -interface ApplicationConfig { - host: string; - port: string; - name: string; -} - -const DEFAULT_HOST = 'http://localhost'; - -const DEFAULT_PORT = { - user: '15001', - task: '15002', - gateway: '3333', -}; - -// Gateway -export const gatewayConfig: ApplicationConfig = { - host: process.env['GATEWAY_HOST'] ?? DEFAULT_HOST, - port: process.env['GATEWAY_PORT'] ?? DEFAULT_PORT.gateway, - name: 'gateway', -}; - -// Graphql -export const userSubGraph: ApplicationConfig = { - host: process.env['USER_HOST'] ?? DEFAULT_HOST, - port: process.env['USER_PORT'] ?? DEFAULT_PORT.user, - name: 'user', -}; - -export const taskSubGraph: ApplicationConfig = { - host: process.env['TASK_HOST'] ?? DEFAULT_HOST, - port: process.env['TASK_PORT'] ?? DEFAULT_PORT.task, - name: 'task', -}; diff --git a/libs/application-config/src/lib/subgraphs-config.spec.ts b/libs/application-config/src/lib/subgraphs-config.spec.ts deleted file mode 100644 index 4782a64..0000000 --- a/libs/application-config/src/lib/subgraphs-config.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { subgraphsConfig } from './subgraphs-config'; -import { userSubGraph } from './applications-config'; - -describe('subgraphsConfig', () => { - it('should have the correct configuration for the user subgraph', () => { - expect(subgraphsConfig).toHaveLength(1); - expect(subgraphsConfig[0]).toEqual({ - name: userSubGraph.name, - url: `${userSubGraph.host}:${userSubGraph.port}/graphql`, - }); - }); -}); diff --git a/libs/application-config/src/lib/subgraphs-config.ts b/libs/application-config/src/lib/subgraphs-config.ts deleted file mode 100644 index 4c59dd4..0000000 --- a/libs/application-config/src/lib/subgraphs-config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ServiceEndpointDefinition } from '@apollo/gateway'; - -import { userSubGraph } from './applications-config'; - -export const subgraphsConfig: ServiceEndpointDefinition[] = [ - { - name: userSubGraph.name, - url: `${userSubGraph.host}:${userSubGraph.port}/graphql`, - }, -]; diff --git a/libs/application-config/README.md b/libs/config/README.md similarity index 100% rename from libs/application-config/README.md rename to libs/config/README.md diff --git a/libs/application-config/eslint.config.js b/libs/config/eslint.config.js similarity index 100% rename from libs/application-config/eslint.config.js rename to libs/config/eslint.config.js diff --git a/libs/application-config/jest.config.ts b/libs/config/jest.config.ts similarity index 69% rename from libs/application-config/jest.config.ts rename to libs/config/jest.config.ts index a7ffe20..bb3bf75 100644 --- a/libs/application-config/jest.config.ts +++ b/libs/config/jest.config.ts @@ -1,10 +1,10 @@ export default { - displayName: 'applications-config', + displayName: 'config', preset: '../../jest.preset.js', testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], }, moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/libs/application-config', + coverageDirectory: '../../coverage/libs/config', }; diff --git a/libs/config/project.json b/libs/config/project.json new file mode 100644 index 0000000..281a94f --- /dev/null +++ b/libs/config/project.json @@ -0,0 +1,9 @@ +{ + "name": "config", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/config/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project config --web", + "targets": {} +} diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts new file mode 100644 index 0000000..e41b14c --- /dev/null +++ b/libs/config/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/applications.config'; +export * from './lib/database.config'; diff --git a/libs/config/src/lib/applications.config.spec.ts b/libs/config/src/lib/applications.config.spec.ts new file mode 100644 index 0000000..d7f9532 --- /dev/null +++ b/libs/config/src/lib/applications.config.spec.ts @@ -0,0 +1,61 @@ +import { gatewayConfig, userAppConfig } from './applications.config'; + +describe('Config Tests', () => { + describe('gatewayConfig', () => { + it('should return default gateway config when no environment variables are set', () => { + const config = gatewayConfig(); + expect(config).toEqual({ + protocol: 'http', + host: 'localhost', + port: 3333, + }); + }); + + it('should return gateway config with environment variables', () => { + process.env['PROTOCOL'] = 'https'; + process.env['GATEWAY_HOST'] = 'gateway.example.com'; + process.env['GATEWAY_PORT'] = '4444'; + + const config = gatewayConfig(); + expect(config).toEqual({ + protocol: 'https', + host: 'gateway.example.com', + port: 4444, + }); + + delete process.env['PROTOCOL']; + delete process.env['GATEWAY_HOST']; + delete process.env['GATEWAY_PORT']; + }); + }); + + describe('userAppConfig', () => { + it('should return default user app config when no environment variables are set', () => { + const config = userAppConfig(); + expect(config).toEqual({ + protocol: 'http', + host: 'localhost', + port: 15001, + name: 'user', + }); + }); + + it('should return user app config with environment variables', () => { + process.env['PROTOCOL'] = 'https'; + process.env['USER_HOST'] = 'user.example.com'; + process.env['USER_PORT'] = '5555'; + + const config = userAppConfig(); + expect(config).toEqual({ + protocol: 'https', + host: 'user.example.com', + port: 5555, + name: 'user', + }); + + delete process.env['PROTOCOL']; + delete process.env['USER_HOST']; + delete process.env['USER_PORT']; + }); + }); +}); \ No newline at end of file diff --git a/libs/config/src/lib/applications.config.ts b/libs/config/src/lib/applications.config.ts new file mode 100644 index 0000000..8c0c2ea --- /dev/null +++ b/libs/config/src/lib/applications.config.ts @@ -0,0 +1,39 @@ +import { registerAs } from '@nestjs/config'; + +interface ServiceConfig { + protocol: string; + host: string; + port: number; + name?: string; +} + +const DEFAULT_PROTOCOL = 'http' as const; +const DEFAULT_HOST = 'localhost' as const; +const DEFAULT_PORT = { + user: 15001, + task: 15002, + gateway: 3333, +} as const; + +export const gatewayConfig = registerAs( + 'gateway', + (): ServiceConfig => ({ + protocol: process.env['PROTOCOL'] ?? DEFAULT_PROTOCOL, + host: process.env['GATEWAY_HOST'] ?? DEFAULT_HOST, + port: process.env['GATEWAY_PORT'] + ? Number(process.env['GATEWAY_PORT']) + : DEFAULT_PORT.gateway, + }), +); + +export const userAppConfig = registerAs( + 'userApp', + (): ServiceConfig => ({ + protocol: process.env['PROTOCOL'] ?? DEFAULT_PROTOCOL, + host: process.env['USER_HOST'] ?? DEFAULT_HOST, + port: process.env['USER_PORT'] + ? Number(process.env['USER_PORT']) + : DEFAULT_PORT.user, + name: 'user', + }), +); diff --git a/libs/config/src/lib/database.config.spec.ts b/libs/config/src/lib/database.config.spec.ts new file mode 100644 index 0000000..123b341 --- /dev/null +++ b/libs/config/src/lib/database.config.spec.ts @@ -0,0 +1,32 @@ +import { databaseConfig } from './database.config'; + +describe('databaseConfig', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); // Clears the cache + process.env = { ...OLD_ENV }; // Make a copy of the old environment + }); + + afterAll(() => { + process.env = OLD_ENV; // Restore old environment + }); + + it('should return the correct database host and port from environment variables', () => { + process.env['DATABASE_HOST'] = 'localhost'; + process.env['DATABASE_PORT'] = '3306'; + + const config = databaseConfig(); + expect(config.host).toBe('localhost'); + expect(config.port).toBe(3306); + }); + + it('should return the default port if DATABASE_PORT is not set', () => { + process.env['DATABASE_HOST'] = 'localhost'; + delete process.env['DATABASE_PORT']; + + const config = databaseConfig(); + expect(config.host).toBe('localhost'); + expect(config.port).toBe(5432); + }); +}); \ No newline at end of file diff --git a/libs/config/src/lib/database.config.ts b/libs/config/src/lib/database.config.ts new file mode 100644 index 0000000..647a408 --- /dev/null +++ b/libs/config/src/lib/database.config.ts @@ -0,0 +1,18 @@ +import { registerAs } from '@nestjs/config'; +import { z } from 'zod'; + +const databaseSchema = z.object({ + host: z.string().min(1), + port: z.coerce.number().int().min(1).max(65535), +}); + +export type DatabaseConfig = z.infer; + +export const databaseConfig = registerAs('database', () => { + const config = { + host: process.env['DATABASE_HOST'], + port: process.env['DATABASE_PORT'] || 5432, + }; + + return databaseSchema.parse(config); +}); diff --git a/libs/application-config/tsconfig.json b/libs/config/tsconfig.json similarity index 100% rename from libs/application-config/tsconfig.json rename to libs/config/tsconfig.json diff --git a/libs/application-config/tsconfig.lib.json b/libs/config/tsconfig.lib.json similarity index 100% rename from libs/application-config/tsconfig.lib.json rename to libs/config/tsconfig.lib.json diff --git a/libs/application-config/tsconfig.spec.json b/libs/config/tsconfig.spec.json similarity index 100% rename from libs/application-config/tsconfig.spec.json rename to libs/config/tsconfig.spec.json diff --git a/package.json b/package.json index 59c504b..aead434 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@apollo/subgraph": "^2.5.5", "@nestjs/apollo": "^12.2.1", "@nestjs/common": "^10.4.7", + "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.7", "@nestjs/graphql": "^12.2.1", "@nestjs/platform-express": "^10.4.7", @@ -25,7 +26,8 @@ "graphql-tools": "^9.0.2", "inlineTrace": "link:@apollo/server/plugin/inlineTrace", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.14.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eeee887..54934f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@nestjs/common': specifier: ^10.4.7 version: 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/config': + specifier: ^3.3.0 + version: 3.3.0(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1) '@nestjs/core': specifier: ^10.4.7 version: 10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -50,6 +53,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.1 + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@eslint/js': specifier: ^9.14.0 @@ -1279,6 +1285,12 @@ packages: class-validator: optional: true + '@nestjs/config@3.3.0': + resolution: {integrity: sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + rxjs: ^7.1.0 + '@nestjs/core@10.4.7': resolution: {integrity: sha512-AIpQzW/vGGqSLkKvll1R7uaSNv99AxZI2EFyVJPNGDgFsfXaohfV1Ukl6f+s75Km+6Fj/7aNl80EqzNWQCS8Ig==} peerDependencies: @@ -2892,6 +2904,10 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + dotenv-expand@11.0.6: resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==} engines: {node: '>=12'} @@ -5911,6 +5927,9 @@ packages: zen-observable@0.8.15: resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + snapshots: '@adobe/css-tools@4.3.3': {} @@ -7459,6 +7478,14 @@ snapshots: tslib: 2.7.0 uid: 2.0.2 + '@nestjs/config@3.3.0(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1) + dotenv: 16.4.5 + dotenv-expand: 10.0.0 + lodash: 4.17.21 + rxjs: 7.8.1 + '@nestjs/core@10.4.7(@nestjs/common@10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.4.7(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -9439,6 +9466,8 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv-expand@10.0.0: {} + dotenv-expand@11.0.6: dependencies: dotenv: 16.4.5 @@ -12749,3 +12778,5 @@ snapshots: zen-observable@0.8.15: optional: true + + zod@3.23.8: {} diff --git a/tsconfig.base.json b/tsconfig.base.json index db56ae1..91bb2ed 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,8 +15,8 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@graphql-federation-workspace/applications-config": [ - "libs/application-config/src/index.ts" + "@libs/config": [ + "libs/config/src/index.ts" ], "@prompt/user-prompt-log-entity": [ "libs/prompt/domain/user-prompt-log-entity/src/index.ts"