mirror of https://github.com/BRAVO68WEB/shx.git
Merge pull request #95 from BRAVO68WEB/feat/zod-env-config
feat: use zod to read and parse config
This commit is contained in:
commit
2572a63314
|
@ -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!');
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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\"",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue