improve .env parsing

This commit is contained in:
Paul Coral
2025-08-01 23:18:04 +02:00
parent c3881d7ff7
commit 6050d9d3a3
29 changed files with 455 additions and 98 deletions

View File

@@ -54,3 +54,4 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View File

@@ -18,13 +18,15 @@
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migrate:latest": "ts-node src/migrate.ts"
"migrate:latest": "ts-node src/migrate.ts",
"kysely:codegen": "kysely-codegen --out-file ./src/database/db.d.ts"
},
"dependencies": {
"@my-monorepo/common": "workspace:*",
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"dotenv": "^17.2.1",
"kysely": "^0.28.3",
"pg": "^8.16.3",
"reflect-metadata": "^0.2.2",
@@ -49,6 +51,7 @@
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"jest": "^29.7.0",
"kysely-codegen": "^0.18.5",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",

View File

@@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello')
getHello(): string {
return this.appService.getHello();
}
}

View File

@@ -1,11 +1,13 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthController } from './controllers/auth/auth.controller';
import { AuthController } from './app/auth/auth.controller';
import { DatabaseService } from './database/database.service';
import { AuthService } from './app/auth/auth.service';
import { UserRegistryService } from './app/users/user-registry.service';
import { EnvService } from './env/env.service';
@Module({
imports: [],
controllers: [AppController, AuthController],
providers: [AppService],
controllers: [AuthController],
providers: [DatabaseService, AuthService, UserRegistryService, EnvService],
})
export class AppModule {}

View File

@@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,14 @@
import { Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
@Post('credentials')
// eslint-disable-next-line @typescript-eslint/require-await
async postCredentials() {
return this.authService.verifyCredentials();
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,17 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { UserRegistryService } from '../users/user-registry.service';
@Injectable()
export class AuthService {
constructor(private userRegistry: UserRegistryService) {}
async verifyCredentials() {
const isValidCredential = await this.userRegistry.existsEmailPassword(
'',
'',
);
if (!isValidCredential) {
throw new UnauthorizedException('invalid credentials');
}
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserRegistryService } from './user-registry.service';
describe('UserRegistryService', () => {
let service: UserRegistryService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserRegistryService],
}).compile();
service = module.get<UserRegistryService>(UserRegistryService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../../database/database.service';
import { isDefined } from '@my-monorepo/common';
@Injectable()
export class UserRegistryService {
constructor(private databaseService: DatabaseService) {}
async existsEmailPassword(email: string, hashedPassword: string) {
const id = await this.databaseService.database
.selectFrom('users')
.select('id')
.where((eb) => eb('email', '=', email).and('hash', '=', hashedPassword))
.executeTakeFirst();
return isDefined(id);
}
}

View File

@@ -1,12 +0,0 @@
import { AuthApiService } from '@my-monorepo/common';
import { Controller, Post } from '@nestjs/common';
@Controller(AuthApiService.baseUrl)
export class AuthController implements AuthApiService {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
@Post(AuthApiService.postCredentials)
// eslint-disable-next-line @typescript-eslint/require-await
async postCredentials() {
return 'hello';
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DatabaseService } from './database.service';
describe('DatabaseService', () => {
let service: DatabaseService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DatabaseService],
}).compile();
service = module.get<DatabaseService>(DatabaseService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { Pool } from 'pg';
import { Kysely, PostgresDialect } from 'kysely';
import { DB } from './db';
import { EnvService } from '../env/env.service';
@Injectable()
export class DatabaseService {
database: Kysely<DB>;
constructor(envService: EnvService) {
const config = envService.config;
const dialect = new PostgresDialect({
pool: new Pool({
database: config.DATABASE,
host: config.DATABASE_HOST,
user: config.DATABASE_USER,
password: config.DATABASE_PASSWORD,
port: config.DATABASE_PORT,
max: 10,
}),
});
// Database interface is passed to Kysely's constructor, and from now on, Kysely
// knows your database structure.
// Dialect is passed to Kysely's constructor, and from now on, Kysely knows how
// to communicate with your database.
this.database = new Kysely<DB>({
dialect,
});
}
}

29
apps/backend/src/database/db.d.ts vendored Normal file
View File

@@ -0,0 +1,29 @@
/**
* This file was generated by kysely-codegen.
* Please do not edit it manually.
*/
import type { ColumnType } from "kysely";
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
export interface Users {
created_at: Generated<Timestamp>;
deleted_at: Timestamp | null;
email: string;
first_name: string;
hash: string | null;
id: Generated<Int8>;
last_name: string;
updated_at: Generated<Timestamp>;
}
export interface DB {
users: Users;
}

View File

@@ -1,22 +0,0 @@
import { Pool } from 'pg';
import { Kysely, PostgresDialect } from 'kysely';
const dialect = new PostgresDialect({
// TODO : temporary data, make to .env
pool: new Pool({
database: 'cowsi',
host: 'localhost',
user: 'postgres',
password: 'postgres',
port: 5432,
max: 10,
}),
});
// Database interface is passed to Kysely's constructor, and from now on, Kysely
// knows your database structure.
// Dialect is passed to Kysely's constructor, and from now on, Kysely knows how
// to communicate with your database.
export const db = new Kysely<any>({
dialect,
});

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EnvService } from './env.service';
describe('EnvService', () => {
let service: EnvService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [EnvService],
}).compile();
service = module.get<EnvService>(EnvService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

27
apps/backend/src/env/env.service.ts vendored Normal file
View File

@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import z from 'zod';
import * as dotenv from 'dotenv';
const envParser = z.object({
DATABASE: z.string(),
DATABASE_USER: z.string(),
DATABASE_PASSWORD: z.string(),
DATABASE_HOST: z.string(),
DATABASE_PORT: z.coerce.number(),
});
export type EnvConfig = Readonly<z.infer<typeof envParser>>;
@Injectable()
export class EnvService {
readonly config: EnvConfig;
constructor() {
console.log('paul-debug', dotenv);
const res = dotenv.config();
if (res.error) {
throw res.error;
}
this.config = envParser.parse(res.parsed);
}
}