Merge pull request #95 from BRAVO68WEB/feat/zod-env-config

feat: use zod to read and parse config
This commit is contained in:
Jyotirmoy Bandyopadhayaya 2023-06-23 13:24:30 +05:30 committed by GitHub
commit 2572a63314
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 73 additions and 121 deletions

View File

@ -1,44 +1,30 @@
import fs from 'fs';
import { parse as parseFile } from 'envfile';
import {
IConfigClass,
IConfigStore,
IConfigKeys,
} from '../interfaces/config.interface';
import { z } from 'zod';
// TODO: Use zod to validate the config
export default class ConfigStoreFactory implements IConfigClass {
public configStoreType: IConfigStore;
constructor(isProd = false) {
if (isProd) {
this.configStoreType = 'production';
} else {
this.configStoreType = 'development';
}
}
public async getConfigStore(): Promise<Partial<IConfigKeys>> {
if (this.configStoreType === 'development') {
const envContent = await fs.readFileSync(`./.env`, 'utf8');
const env: Partial<IConfigKeys> = await parseFile(envContent);
return env;
} else {
let reqEnvContent: any = await fs.readFileSync('./.env.example', 'utf8');
reqEnvContent = reqEnvContent.replaceAll('=', '');
reqEnvContent = reqEnvContent.split('\n');
const missingKeys: string[] = [];
const env: Partial<IConfigKeys> = {};
for (const line of reqEnvContent) {
if (!process.env[line]) {
missingKeys.push(line);
} else env[line] = process.env[line];
}
if (missingKeys.length > 0) {
throw new Error(`Missing keys: ${missingKeys}`);
}
return env;
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
// eslint-disable-next-line
interface ProcessEnv extends z.infer<typeof ZodEnvironmentVariables> {}
}
}
const ZodEnvironmentVariables = z.object({
PORT: z.string(),
NODE_ENV: z.string(),
HASURA_GRAPHQL_ADMIN_SECRET: z.string(),
HASURA_GRAPHQL_ENDPOINT: z.string(),
CACHE_ENV: z.string(),
REDIS_URL: z.string(),
R2_CLIENT_ID: z.string(),
R2_CLIENT_SECRET: z.string(),
R2_BUCKET_NAME: z.string(),
R2_BUCKET_REGION: z.string(),
R2_BUCKET_ENDPOINT: z.string(),
R2_BUCKET_URL: z.string(),
R2_BUCKET_FOLDER: z.string(),
MASTER_KEY: z.string(),
});
ZodEnvironmentVariables.parse(process.env);
console.log('✅ Environment variables verified!');

View File

@ -4,7 +4,6 @@ import {
S3ClientConfig,
DeleteObjectCommand,
} from '@aws-sdk/client-s3';
import { configKeys } from '..';
import { IUploadStrategy, UploaderRep } from '../interfaces/upload.interface';
export default class UploadStrategy implements IUploadStrategy {
@ -19,12 +18,12 @@ export default class UploadStrategy implements IUploadStrategy {
UploadStrategy._s3Opts = options;
const s3ClientOpts: S3ClientConfig = {
region: configKeys.R2_BUCKET_REGION || '',
endpoint: configKeys.R2_BUCKET_ENDPOINT || '',
region: process.env.R2_BUCKET_REGION || '',
endpoint: process.env.R2_BUCKET_ENDPOINT || '',
forcePathStyle: true,
credentials: {
accessKeyId: configKeys.R2_CLIENT_ID || '',
secretAccessKey: configKeys.R2_CLIENT_SECRET || '',
accessKeyId: process.env.R2_CLIENT_ID || '',
secretAccessKey: process.env.R2_CLIENT_SECRET || '',
},
};
const client = new S3Client(s3ClientOpts);
@ -48,9 +47,9 @@ export default class UploadStrategy implements IUploadStrategy {
};
await UploadStrategy._s3Client.send(new PutObjectCommand(uploadParams));
return {
url: configKeys.R2_BUCKET_URL as string,
bucket_name: configKeys.R2_BUCKET_NAME as string,
folder: configKeys.R2_BUCKET_FOLDER as string,
url: process.env.R2_BUCKET_URL as string,
bucket_name: process.env.R2_BUCKET_NAME as string,
folder: process.env.R2_BUCKET_FOLDER as string,
};
}
@ -63,9 +62,9 @@ export default class UploadStrategy implements IUploadStrategy {
await UploadStrategy._s3Client.send(new DeleteObjectCommand(deleteParams));
return {
url: configKeys.R2_BUCKET_URL as string,
bucket_name: configKeys.R2_BUCKET_NAME as string,
folder: configKeys.R2_BUCKET_FOLDER as string,
url: process.env.R2_BUCKET_URL as string,
bucket_name: process.env.R2_BUCKET_NAME as string,
folder: process.env.R2_BUCKET_FOLDER as string,
};
}
}

View File

@ -1,6 +1,5 @@
import * as redis from 'redis';
import NodeCache from 'node-cache';
import { configKeys } from '..';
import { logger } from '../libs';
export type CacheEnvironment = 'inmemory' | 'redis';
@ -18,7 +17,7 @@ export default class CacheClient {
}
static init(forceEnv?: CacheEnvironment) {
const env = forceEnv || configKeys.CACHE_ENV || 'inmemory';
const env = forceEnv || process.env.CACHE_ENV || 'inmemory';
if (!['inmemory', 'redis'].includes(env))
throw new Error(
@ -28,7 +27,7 @@ export default class CacheClient {
this._clientMode = env as CacheEnvironment;
const redisUrl = configKeys.REDIS_URL || '';
const redisUrl = process.env.REDIS_URL || '';
if (env === 'redis') {
this._redisClient = redis.createClient({

View File

@ -1,5 +1,4 @@
import { GraphQLClient } from 'graphql-request';
import { configKeys } from '..';
import axios from 'axios';
import { logger } from '../libs';
@ -8,9 +7,9 @@ export let client = new GraphQLClient('');
export const hgqlInit = async () => {
logger.info('🚀 GraphQL Client Initialized');
let HASURA_URL: string = configKeys.HASURA_GRAPHQL_ENDPOINT || '';
let HASURA_URL: string = process.env.HASURA_GRAPHQL_ENDPOINT || '';
HASURA_URL += HASURA_URL.endsWith('/') ? 'v1/graphql' : '/v1/graphql';
const HASURA_ADMIN: string = configKeys.HASURA_GRAPHQL_ADMIN_SECRET || '';
const HASURA_ADMIN: string = process.env.HASURA_GRAPHQL_ADMIN_SECRET || '';
client = new GraphQLClient(HASURA_URL, {
headers: {
@ -179,9 +178,9 @@ export const hgqlInit = async () => {
const config = {
method: 'post',
url: configKeys.HASURA_GRAPHQL_ENDPOINT + '/v1/metadata',
url: process.env.HASURA_GRAPHQL_ENDPOINT + '/v1/metadata',
headers: {
'x-hasura-admin-secret': configKeys.HASURA_GRAPHQL_ADMIN_SECRET,
'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET,
},
};

View File

@ -8,7 +8,7 @@ import ratelimiter from 'express-rate-limit';
import { hgqlInit } from './helpers';
import { errorHandler, notFoundHandler } from './libs';
import pkg from './package.json' assert { type: 'json' };
import configStore from './configs';
import './configs';
import CacheClient, { CacheEnvironment } from './helpers/cache.factory';
import URLStoreController from './controllers/urlstore.controller';
import ConfigService from './services/config.service';
@ -19,17 +19,15 @@ logger.info('🚀 @' + pkg.author.name + '/' + pkg.name, 'v' + pkg.version);
const isDev: boolean = process.env.NODE_ENV == 'production';
logger.info(isDev ? '🚀 Production Mode' : '🚀 Development Mode');
const configs = new configStore(isDev);
const configKeys = await configs.getConfigStore();
const urlStoreController = new URLStoreController();
const logStream = new LogStream();
logger.info(`🔑 Master Key ${configKeys.MASTER_KEY}`);
logger.info(`🔑 Master Key ${process.env.MASTER_KEY}`);
import routes from './routes';
hgqlInit();
CacheClient.init(configKeys.CACHE_ENV as CacheEnvironment);
CacheClient.init(process.env.CACHE_ENV as CacheEnvironment);
app.use(cors());
app.use(helmet());
@ -65,10 +63,10 @@ app.get('/:urlKey', urlStoreController.get);
app.use(notFoundHandler);
app.use(errorHandler);
app.listen(configKeys.PORT, async () => {
logger.info(`🚂 Server running on port ${configKeys.PORT}`);
app.listen(process.env.PORT, async () => {
logger.info(`🚂 Server running on port ${process.env.PORT}`);
const { initConfig } = new ConfigService();
await initConfig();
});
export { configKeys, logger };
export { logger };

View File

@ -3,27 +3,6 @@ import { ModRequest } from '../types';
type IConfigStore = 'development' | 'production';
export interface IConfigKeys {
PORT: string | number;
NODE_ENV: string;
HASURA_GRAPHQL_ADMIN_SECRET: string;
HASURA_GRAPHQL_ENDPOINT: string;
CACHE_ENV: string;
REDIS_URL: string;
R2_CLIENT_ID: string;
R2_CLIENT_SECRET: string;
R2_BUCKET_NAME: string;
R2_BUCKET_REGION: string;
R2_BUCKET_ENDPOINT: string;
R2_BUCKET_URL: string;
R2_BUCKET_FOLDER: string;
MASTER_KEY: string;
}
export interface IConfigClass {
getConfigStore(): Promise<Partial<IConfigKeys>>;
}
export interface IConfigController {
getAllConfig(
req: ModRequest,

View File

@ -2,7 +2,6 @@ import { NextFunction, Request, Response } from 'express';
import Joi from 'joi';
import { CustomError, NotFoundError } from './error';
import { pick } from './utilities';
import { configKeys } from '..';
export const errorHandler = async (
err: any,
@ -21,7 +20,7 @@ export const errorHandler = async (
message: err.message,
error: true,
data: null,
error_stack: configKeys.NODE_ENV === 'production' ? undefined : err.stack,
error_stack: process.env.NODE_ENV === 'production' ? undefined : err.stack,
});
};

View File

@ -17,7 +17,6 @@
"axios": "^1.2.1",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"envfile": "^6.18.0",
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"form-data": "^4.0.0",
@ -34,7 +33,8 @@
"redis": "^4.6.7",
"sanitize-filename": "^1.6.3",
"sharp": "^0.32.1",
"winston": "^3.9.0"
"winston": "^3.9.0",
"zod": "^3.21.4"
},
"scripts": {
"dev": "concurrently \"npm run dev:express\" \"npm run dev:hasura\"",

View File

@ -1,6 +1,5 @@
import { gql } from 'graphql-request';
import { client } from '../helpers';
import { configKeys } from '..';
import { IAPIKeyService } from '../interfaces/apikey.interface';
import { encapDataKeys } from '../libs';
import { Apikeys, Apikeys_Mutation_Response } from '../graphql/types';
@ -52,7 +51,7 @@ export default class APIKeyService implements IAPIKeyService {
}
public async generateS(masterKey: string): Promise<Apikeys> {
if (masterKey !== configKeys.MASTER_KEY)
if (masterKey !== process.env.MASTER_KEY)
throw new Error('Invalid master key');
const query = gql`
mutation generateAPIKey {
@ -67,7 +66,7 @@ export default class APIKeyService implements IAPIKeyService {
}
public async deleteS(apikeyID: string, masterKey: string): Promise<number> {
if (masterKey !== configKeys.MASTER_KEY)
if (masterKey !== process.env.MASTER_KEY)
throw new Error('Invalid master key');
const query = gql`
mutation deleteAPIKey($apikeyID: uuid!) {
@ -86,7 +85,7 @@ export default class APIKeyService implements IAPIKeyService {
}
public async listS(masterKey: string): Promise<encapDataKey[]> {
if (masterKey !== configKeys.MASTER_KEY)
if (masterKey !== process.env.MASTER_KEY)
throw new Error('Invalid master key');
const query = gql`
query listAPIKeys {

View File

@ -1,5 +1,4 @@
import UploaderService from '../data/uploader.service';
import { configKeys } from '..';
import { gql } from 'graphql-request';
import { client } from '../helpers';
import sharp from 'sharp';
@ -19,13 +18,13 @@ export default class Uploader implements IUploaderService {
configService: ConfigService;
constructor() {
this.uploaderService = new UploaderService(configKeys.R2_BUCKET_NAME);
this.uploaderService = new UploaderService(process.env.R2_BUCKET_NAME);
this.configService = new ConfigService();
}
public uploadS = async (file: any, meta: UserMeta): Promise<Uploads> => {
await this.uploaderService.uploadFile(
configKeys.R2_BUCKET_FOLDER as string,
process.env.R2_BUCKET_FOLDER as string,
file.newName,
file.buffer,
file.mimetype,
@ -44,11 +43,11 @@ export default class Uploader implements IUploaderService {
`;
const urlObj = {
url:
configKeys.R2_BUCKET_URL +
process.env.R2_BUCKET_URL +
'/' +
configKeys.R2_BUCKET_NAME +
process.env.R2_BUCKET_NAME +
'/' +
configKeys.R2_BUCKET_FOLDER +
process.env.R2_BUCKET_FOLDER +
'/' +
file.newName,
};
@ -71,7 +70,7 @@ export default class Uploader implements IUploaderService {
await image.toFormat('jpeg');
const buffer = await image.toBuffer();
await this.uploaderService.uploadFile(
configKeys.R2_BUCKET_FOLDER as string,
process.env.R2_BUCKET_FOLDER as string,
file.newName,
buffer,
file.mimetype,
@ -90,11 +89,11 @@ export default class Uploader implements IUploaderService {
`;
const urlObj = {
url:
configKeys.R2_BUCKET_URL +
process.env.R2_BUCKET_URL +
'/' +
configKeys.R2_BUCKET_NAME +
process.env.R2_BUCKET_NAME +
'/' +
configKeys.R2_BUCKET_FOLDER +
process.env.R2_BUCKET_FOLDER +
'/' +
file.newName,
};
@ -123,7 +122,7 @@ export default class Uploader implements IUploaderService {
await image.toFormat('jpeg');
const buffer = await image.toBuffer();
await this.uploaderService.uploadFile(
configKeys.R2_BUCKET_FOLDER as string,
process.env.R2_BUCKET_FOLDER as string,
filename,
buffer,
'image/jpeg',
@ -144,11 +143,11 @@ export default class Uploader implements IUploaderService {
const urlObj = {
url:
configKeys.R2_BUCKET_URL +
process.env.R2_BUCKET_URL +
'/' +
configKeys.R2_BUCKET_NAME +
process.env.R2_BUCKET_NAME +
'/' +
configKeys.R2_BUCKET_FOLDER +
process.env.R2_BUCKET_FOLDER +
'/' +
filename,
};
@ -202,7 +201,7 @@ export default class Uploader implements IUploaderService {
let filename = await this.downloadFile(url);
filename = sanitize(filename);
await this.uploaderService.uploadFile(
configKeys.R2_BUCKET_FOLDER as string,
process.env.R2_BUCKET_FOLDER as string,
filename,
fs.readFileSync(`uploads/${filename}`),
'application/octet-stream',
@ -223,11 +222,11 @@ export default class Uploader implements IUploaderService {
const urlObj = {
url:
configKeys.R2_BUCKET_URL +
process.env.R2_BUCKET_URL +
'/' +
configKeys.R2_BUCKET_NAME +
process.env.R2_BUCKET_NAME +
'/' +
configKeys.R2_BUCKET_FOLDER +
process.env.R2_BUCKET_FOLDER +
'/' +
filename,
};
@ -325,7 +324,7 @@ export default class Uploader implements IUploaderService {
const filename = data.delete_uploads_by_pk.upload_url.split('/').pop()!;
await this.uploaderService.deleteFile(
configKeys.R2_BUCKET_FOLDER as string,
process.env.R2_BUCKET_FOLDER as string,
filename
);

View File

@ -8170,11 +8170,6 @@ entities@^2.0.0:
resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz"
integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
envfile@^6.18.0:
version "6.18.0"
resolved "https://registry.npmjs.org/envfile/-/envfile-6.18.0.tgz"
integrity sha512-IsYv64dtlNXTm4huvCBpbXsdZQurYUju9WoYCkSj+SDYpO3v4/dq346QsCnNZ3JcnWw0G3E6+saVkVtmPw98Gg==
envinfo@^7.7.3:
version "7.8.1"
resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz"