From 83a13a9d40d3dcb630d97bb44b59d5602f1a617c Mon Sep 17 00:00:00 2001 From: emmanuelonah Date: Sat, 4 Jan 2025 14:54:56 +0100 Subject: [PATCH] feat: add the create get and patch action for influencer resource --- server/.env.example | 3 +- server/.prettierrc | 2 +- server/README.md | 1 + server/package.json | 4 +- server/src/db/mongo/index.helper.ts | 2 +- server/src/db/seed/index.seed.ts | 18 +++- server/src/db/seed/influencers.mock.json | 87 ++++++++++++++++++- server/src/index.ts | 3 +- .../error-handler/index.middleware.ts | 7 +- server/src/plugins/index.ts | 1 + server/src/plugins/mongo-id-normalize.test.ts | 6 +- .../influencer/dto/create-influencer.dto.ts | 74 +++++++++++++++- .../influencer/dto/patch-influencer.dto.ts | 50 ++++++++++- .../src/routes/influencer/index.controller.ts | 11 ++- server/src/routes/influencer/index.d.ts | 1 - server/src/routes/influencer/index.model.ts | 25 +++++- server/src/routes/influencer/index.route.ts | 6 ++ server/src/routes/influencer/index.schema.ts | 81 ++++++++++++++++- server/src/routes/influencer/index.service.ts | 1 - .../src/routes/influencer/index.services.ts | 48 ++++++++++ server/src/routes/influencer/index.test.ts | 83 ++++++++++++++++++ server/src/routes/influencer/index.types.ts | 36 ++++++++ .../services/api-response/index.service.ts | 4 +- .../src/services/api-response/index.test.ts | 6 +- server/src/utils/env-vars.test.ts | 4 +- server/src/utils/env-vars.util.ts | 4 +- server/yarn.lock | 10 +++ 27 files changed, 542 insertions(+), 36 deletions(-) create mode 100644 server/src/plugins/index.ts delete mode 100644 server/src/routes/influencer/index.d.ts delete mode 100644 server/src/routes/influencer/index.service.ts create mode 100644 server/src/routes/influencer/index.services.ts create mode 100644 server/src/routes/influencer/index.test.ts create mode 100644 server/src/routes/influencer/index.types.ts diff --git a/server/.env.example b/server/.env.example index d979adc..35b0c16 100644 --- a/server/.env.example +++ b/server/.env.example @@ -3,4 +3,5 @@ ADCASH_INFLUENCER_MANAGER_SERVER_URL= ADCASH_INFLUENCER_MANAGER_SERVER_PORT= ADCASH_INFLUENCER_MANAGER_SERVER_MONGO_DB_URI= ADCASH_INFLUENCER_MANAGER_CLIENT_URL= -ADCASH_INFLUENCER_MANAGER_CLIENT_PORT= \ No newline at end of file +ADCASH_INFLUENCER_MANAGER_CLIENT_PORT= +NODE_ENV= "it is recommended to use 'development' for local development" diff --git a/server/.prettierrc b/server/.prettierrc index 0c45137..141d99b 100644 --- a/server/.prettierrc +++ b/server/.prettierrc @@ -3,5 +3,5 @@ "tabWidth": 4, "semi": true, "singleQuote": true, - "printWidth": 100 + "printWidth": 120 } diff --git a/server/README.md b/server/README.md index ac9911f..7ea7d97 100644 --- a/server/README.md +++ b/server/README.md @@ -21,6 +21,7 @@ ADCASH_INFLUENCER_MANAGER_SERVER_PORT= ADCASH_INFLUENCER_MANAGER_SERVER_MONGO_DB_URI= ADCASH_INFLUENCER_MANAGER_CLIENT_URL= ADCASH_INFLUENCER_MANAGER_CLIENT_PORT= +NODE_ENV= ``` ## Scripts diff --git a/server/package.json b/server/package.json index d51f2fb..8201396 100644 --- a/server/package.json +++ b/server/package.json @@ -25,6 +25,7 @@ "prepare": "cd .. && husky install ./server/.husky" }, "dependencies": { + "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cors": "^2.8.5", "dotenv": "^16.4.7", @@ -33,7 +34,8 @@ "express-rate-limit": "^7.0.0", "helmet": "^8.0.0", "mongoose": "^8.9.3", - "morgan": "^1.10.0" + "morgan": "^1.10.0", + "reflect-metadata": "^0.2.2" }, "devDependencies": { "@types/cors": "^2.8.14", diff --git a/server/src/db/mongo/index.helper.ts b/server/src/db/mongo/index.helper.ts index 0d330e1..96035e4 100644 --- a/server/src/db/mongo/index.helper.ts +++ b/server/src/db/mongo/index.helper.ts @@ -1,7 +1,7 @@ import { Schema } from 'mongoose'; class MongoHelpers { - public _idToId = (schema: Schema) => { + public _idToId = (schema: Schema) => { schema.virtual('id').get(function () { return (this as any)._id.toHexString(); }); diff --git a/server/src/db/seed/index.seed.ts b/server/src/db/seed/index.seed.ts index ff73dac..144a5cd 100644 --- a/server/src/db/seed/index.seed.ts +++ b/server/src/db/seed/index.seed.ts @@ -1,7 +1,23 @@ +import mockedInfluences from './influencers.mock.json'; + +import { envVars } from '../../utils'; +import { InfluencerModel } from '../../routes/influencer/index.model'; +import { InfluencerRequest } from '../../routes/influencer/index.types'; + export class Seeder { public static async run() { await Seeder.seedInfluencers(); } - private static async seedInfluencers() {} + private static async seedInfluencers() { + if (envVars.NODE_ENV != 'development') return; + + const influencersResponse = await InfluencerModel.all(); + + if (!influencersResponse.length) { + for (const influencer of mockedInfluences) { + await InfluencerModel.create(influencer as InfluencerRequest); + } + } + } } diff --git a/server/src/db/seed/influencers.mock.json b/server/src/db/seed/influencers.mock.json index fe51488..4945ff0 100644 --- a/server/src/db/seed/influencers.mock.json +++ b/server/src/db/seed/influencers.mock.json @@ -1 +1,86 @@ -[] +[ + { + "firstName": "Alice", + "lastName": "Smith", + "socialMediaHandles": [ + { + "type": "instagram", + "userName": "alice_smith" + }, + { + "type": "tiktok", + "userName": "alice_tiktok" + } + ], + "manager": { + "id": "m1", + "imgUrl": "https://example.com/images/m1.jpg", + "firstName": "Bob", + "lastName": "Johnson", + "email": "bob.johnson@example.com" + } + }, + { + "firstName": "John", + "lastName": "Doe", + "socialMediaHandles": [ + { + "type": "instagram", + "userName": "john_doe" + }, + { + "type": "tiktok", + "userName": "john_doe_tiktok" + } + ], + "manager": { + "id": "m2", + "imgUrl": null, + "firstName": "Sarah", + "lastName": "Connor", + "email": null + } + }, + { + "firstName": "Emily", + "lastName": "Davis", + "socialMediaHandles": [ + { + "type": "instagram", + "userName": "emily_davis" + }, + { + "type": "tiktok", + "userName": "emily_tiktok" + } + ], + "manager": { + "id": "m3", + "imgUrl": "https://example.com/images/m3.jpg", + "firstName": "Michael", + "lastName": "Smith", + "email": "michael.smith@example.com" + } + }, + { + "firstName": "Chris", + "lastName": "Brown", + "socialMediaHandles": [ + { + "type": "instagram", + "userName": "chris_brown" + }, + { + "type": "tiktok", + "userName": "chris_brown_tiktok" + } + ], + "manager": { + "id": "m4", + "imgUrl": null, + "firstName": "Jessica", + "lastName": "Taylor", + "email": null + } + } +] diff --git a/server/src/index.ts b/server/src/index.ts index a02cde1..bbaf335 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,4 +1,5 @@ import 'colors'; +import 'reflect-metadata'; import fs from 'fs'; import path from 'path'; @@ -10,7 +11,7 @@ import { connectDb } from './db/index.db'; import { envVars } from './utils'; async function startServer() { - await connectDb(async () => { + await connectDb(() => { const httpServer = https.createServer( { key: fs.readFileSync(path.join(__dirname, '..', 'private', 'key.pem')), diff --git a/server/src/middlewares/error-handler/index.middleware.ts b/server/src/middlewares/error-handler/index.middleware.ts index 5336957..efa32ca 100644 --- a/server/src/middlewares/error-handler/index.middleware.ts +++ b/server/src/middlewares/error-handler/index.middleware.ts @@ -2,12 +2,7 @@ import { Request, Response, NextFunction } from 'express'; import { HttpException } from '../../services/http-exception/index.service'; -export function errorHandler( - err: HttpException, - _req: Request, - res: Response, - _next: NextFunction -) { +export function errorHandler(err: HttpException, _req: Request, res: Response, _next: NextFunction) { console.error(err); console.error(err.name); console.error(Object.keys(err)); diff --git a/server/src/plugins/index.ts b/server/src/plugins/index.ts new file mode 100644 index 0000000..3128cb9 --- /dev/null +++ b/server/src/plugins/index.ts @@ -0,0 +1 @@ +export * from './mongo-id-normalize.plugin'; diff --git a/server/src/plugins/mongo-id-normalize.test.ts b/server/src/plugins/mongo-id-normalize.test.ts index e689f62..8877f54 100644 --- a/server/src/plugins/mongo-id-normalize.test.ts +++ b/server/src/plugins/mongo-id-normalize.test.ts @@ -15,11 +15,7 @@ describe.skip('_idToId', () => { _id: 'mockObjectId', toJSON: () => ({ _id: 'mockObjectId' }), } as any; - const transformedDoc = (schema as any).options.toJSON.transform.call( - doc, - doc, - doc.toJSON() - ); + const transformedDoc = (schema as any).options.toJSON.transform.call(doc, doc, doc.toJSON()); expect(transformedDoc._id).toBeUndefined(); expect(transformedDoc.id).toBe('mockObjectId'); diff --git a/server/src/routes/influencer/dto/create-influencer.dto.ts b/server/src/routes/influencer/dto/create-influencer.dto.ts index cb0ff5c..e192d54 100644 --- a/server/src/routes/influencer/dto/create-influencer.dto.ts +++ b/server/src/routes/influencer/dto/create-influencer.dto.ts @@ -1 +1,73 @@ -export {}; +/* eslint-disable no-unused-vars */ + +import { + IsString, + IsNotEmpty, + IsArray, + ValidateNested, + IsOptional, + IsEmail, + MaxLength, + IsEnum, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export enum SocialMediaType { + Instagram = 'instagram', + TikTok = 'tiktok', +} + +class SocialMediaHandleDto { + @IsEnum(SocialMediaType, { message: 'Social media handle must be either instagram or tiktok' }) + @IsNotEmpty({ message: 'Social media handle type is required' }) + type: SocialMediaType; + + @IsString() + @IsNotEmpty({ message: 'Social media handle username is required' }) + userName: string; +} + +class ManagerDto { + @IsString() + @IsNotEmpty({ message: 'Manager ID is required' }) + id: string; + + @IsOptional() + @IsString() + imgUrl?: string; + + @IsString() + @IsNotEmpty({ message: 'Manager first name is required' }) + @MaxLength(50, { message: 'Manager first name cannot exceed 50 characters' }) + firstName: string; + + @IsString() + @IsNotEmpty({ message: 'Manager last name is required' }) + @MaxLength(50, { message: 'Manager last name cannot exceed 50 characters' }) + lastName: string; + + @IsOptional() + @IsEmail({}, { message: 'Invalid email format' }) + email?: string; +} + +export class CreateInfluencerDto { + @IsString() + @IsNotEmpty({ message: 'First name is required' }) + @MaxLength(50, { message: 'First name cannot exceed 50 characters' }) + firstName: string; + + @IsString() + @IsNotEmpty({ message: 'Last name is required' }) + @MaxLength(50, { message: 'Last name cannot exceed 50 characters' }) + lastName: string; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SocialMediaHandleDto) + socialMediaHandles: SocialMediaHandleDto[]; + + @ValidateNested() + @Type(() => ManagerDto) + manager: ManagerDto; +} diff --git a/server/src/routes/influencer/dto/patch-influencer.dto.ts b/server/src/routes/influencer/dto/patch-influencer.dto.ts index cb0ff5c..610a27c 100644 --- a/server/src/routes/influencer/dto/patch-influencer.dto.ts +++ b/server/src/routes/influencer/dto/patch-influencer.dto.ts @@ -1 +1,49 @@ -export {}; +import { Type } from 'class-transformer'; +import { IsOptional, ValidateNested, IsString, IsNotEmpty, MaxLength } from 'class-validator'; + +class ManagerDto { + @IsString() + @IsNotEmpty({ message: 'Manager ID is required' }) + id: string; + + @IsOptional() + @IsString() + imgUrl?: string; + + @IsString() + @IsNotEmpty({ message: 'Manager first name is required' }) + @MaxLength(50, { message: 'Manager first name cannot exceed 50 characters' }) + firstName: string; + + @IsString() + @IsNotEmpty({ message: 'Manager last name is required' }) + @MaxLength(50, { message: 'Manager last name cannot exceed 50 characters' }) + lastName: string; + + @IsOptional() + @IsString() + email?: string; +} + +export class PatchInfluencerDto { + @ValidateNested() + @Type(() => ManagerDto) + manager: ManagerDto; + + @IsOptional() + @IsString() + @MaxLength(50, { message: 'First name cannot exceed 50 characters' }) + firstName?: string; + + @IsOptional() + @IsString() + @MaxLength(50, { message: 'Last name cannot exceed 50 characters' }) + lastName?: string; + + @IsOptional() + @ValidateNested({ each: true }) + socialMediaHandles?: Array<{ + type: 'instagram' | 'tiktok'; + userName: string; + }>; +} diff --git a/server/src/routes/influencer/index.controller.ts b/server/src/routes/influencer/index.controller.ts index cb0ff5c..c24de9d 100644 --- a/server/src/routes/influencer/index.controller.ts +++ b/server/src/routes/influencer/index.controller.ts @@ -1 +1,10 @@ -export {}; +import { asyncHandler } from '../../middlewares'; +import { InfluencerServices } from './index.services'; + +export class InfluencerController { + public static httpCreateInfluencer = asyncHandler(InfluencerServices.createInfluencer); + + public static httpGetInfluencers = asyncHandler(InfluencerServices.getInfluencers); + + public static httpPatchInfluencer = asyncHandler(InfluencerServices.patchInfluencer); +} diff --git a/server/src/routes/influencer/index.d.ts b/server/src/routes/influencer/index.d.ts deleted file mode 100644 index cb0ff5c..0000000 --- a/server/src/routes/influencer/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/server/src/routes/influencer/index.model.ts b/server/src/routes/influencer/index.model.ts index cb0ff5c..59ac789 100644 --- a/server/src/routes/influencer/index.model.ts +++ b/server/src/routes/influencer/index.model.ts @@ -1 +1,24 @@ -export {}; +import { Influencer } from './index.schema'; +import { InfluencerRequest, InfluencerQueryParams } from './index.types'; + +export class InfluencerModel { + public static create(influencer: InfluencerRequest) { + return Influencer.create(influencer); + } + + public static all() { + return Influencer.find().lean().exec(); + } + + public static getByNameOrManager({ firstName, lastName, managerId }: InfluencerQueryParams) { + const query: any = {}; + if (firstName) query.firstName = firstName; + if (lastName) query.lastName = lastName; + if (managerId) query['manager.id'] = managerId; + return Influencer.find(query).lean().exec(); + } + + public static update(id: string, influencer: Partial) { + return Influencer.findByIdAndUpdate(id, influencer, { new: true, runValidators: true }).lean().exec(); + } +} diff --git a/server/src/routes/influencer/index.route.ts b/server/src/routes/influencer/index.route.ts index c028ba7..e2935bb 100644 --- a/server/src/routes/influencer/index.route.ts +++ b/server/src/routes/influencer/index.route.ts @@ -1,5 +1,11 @@ import { Router } from 'express'; +import { InfluencerController } from './index.controller'; + const influencerRouter = Router(); +influencerRouter.post('/', InfluencerController.httpCreateInfluencer); +influencerRouter.get('/', InfluencerController.httpGetInfluencers); +influencerRouter.patch('/:id', InfluencerController.httpPatchInfluencer); + export { influencerRouter }; diff --git a/server/src/routes/influencer/index.schema.ts b/server/src/routes/influencer/index.schema.ts index cb0ff5c..3c7b82a 100644 --- a/server/src/routes/influencer/index.schema.ts +++ b/server/src/routes/influencer/index.schema.ts @@ -1 +1,80 @@ -export {}; +import mongoose, { Model } from 'mongoose'; + +import { _idToId } from '../../plugins'; +import { InfluencerResponse } from './index.types'; +import { HttpException } from '../../services/http-exception/index.service'; + +const influencerSchema = new mongoose.Schema({ + firstName: { + type: String, + required: [true, 'First name is required'], + maxlength: [50, 'First name cannot exceed 50 characters'], + }, + lastName: { + type: String, + required: [true, 'Last name is required'], + maxlength: [50, 'Last name cannot exceed 50 characters'], + }, + socialMediaHandles: { + type: [ + { + type: { + type: String, + enum: { + values: ['instagram', 'tiktok'], + message: 'Social media handle must be either instagram or tiktok', + }, + required: [true, 'Social media handle type is required'], + }, + userName: { + type: String, + required: [true, 'Social media handle username is required'], + }, + }, + ], + required: [true, 'Social media handles are required'], + }, + manager: { + id: { + type: String, + required: [true, 'Manager ID is required'], + }, + imgUrl: { + type: String, + default: null, + }, + firstName: { + type: String, + required: [true, 'Manager first name is required'], + maxlength: [50, 'Manager first name cannot exceed 50 characters'], + }, + lastName: { + type: String, + required: [true, 'Manager last name is required'], + maxlength: [50, 'Manager last name cannot exceed 50 characters'], + }, + email: { + type: String, + default: null, + }, + }, +}); + +influencerSchema.plugin(_idToId); + +influencerSchema.pre('save', async function (next) { + const handles = this.socialMediaHandles; + const InfluencerModel = this.constructor as Model; + const duplicateUsernames = await InfluencerModel.find({ + _id: { $ne: this._id }, + 'socialMediaHandles.userName': { $in: handles.map((h) => h.userName) }, + }); + + if (duplicateUsernames.length > 0) { + return next(new HttpException(409, 'Some social media usernames already exist.')); + } + + return next(); +}); + +export const Influencer = mongoose.model('Influencer', influencerSchema); diff --git a/server/src/routes/influencer/index.service.ts b/server/src/routes/influencer/index.service.ts deleted file mode 100644 index cb0ff5c..0000000 --- a/server/src/routes/influencer/index.service.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/server/src/routes/influencer/index.services.ts b/server/src/routes/influencer/index.services.ts new file mode 100644 index 0000000..c818d30 --- /dev/null +++ b/server/src/routes/influencer/index.services.ts @@ -0,0 +1,48 @@ +import { Request, Response, NextFunction } from 'express'; + +import { ApiResponse } from '../../services'; +import { InfluencerModel } from './index.model'; +import { initDto } from '../../utils/init-dto.util'; +import { InfluencerQueryParams } from './index.types'; +import { PatchInfluencerDto } from './dto/patch-influencer.dto'; +import { CreateInfluencerDto } from './dto/create-influencer.dto'; +import { HttpException } from '../../services/http-exception/index.service'; + +export class InfluencerServices { + public static createInfluencer = async (req: Request, res: Response, next: NextFunction) => { + try { + await initDto(CreateInfluencerDto, req.body); + const createdInfluencer = await InfluencerModel.create(req.body); + return res.status(200).json(ApiResponse.success(createdInfluencer)); + } catch (error) { + return next(new HttpException(error.statusCode, error.message)); + } + }; + + public static getInfluencers = async ( + req: Request<{}, {}, {}, InfluencerQueryParams>, + res: Response, + next: NextFunction + ) => { + try { + const { firstName, lastName, managerId } = req.query; + const shouldGetByNameOrManager = firstName || lastName || managerId; + const influencers = shouldGetByNameOrManager + ? await InfluencerModel.getByNameOrManager({ firstName, lastName, managerId }) + : await InfluencerModel.all(); + return res.status(200).json(ApiResponse.success(influencers)); + } catch (error) { + return next(new HttpException(error.statusCode, error.message)); + } + }; + + public static patchInfluencer = async (req: Request, res: Response, next: NextFunction) => { + try { + await initDto(PatchInfluencerDto, req.body.manager); + const updatedInfluencer = await InfluencerModel.update(req.params.id, req.body); + return res.status(200).json(ApiResponse.success(updatedInfluencer)); + } catch (error) { + return next(new HttpException(error.statusCode, error.message)); + } + }; +} diff --git a/server/src/routes/influencer/index.test.ts b/server/src/routes/influencer/index.test.ts new file mode 100644 index 0000000..d6c0363 --- /dev/null +++ b/server/src/routes/influencer/index.test.ts @@ -0,0 +1,83 @@ +import 'reflect-metadata'; + +import express from 'express'; +import request from 'supertest'; + +import { ApiResponse } from '../../services'; +import { InfluencerModel } from './index.model'; +import { influencerRouter } from './index.route'; +import { initDto } from '../../utils/init-dto.util'; + +const app = express(); + +app.use(express.json()); +app.use('/influencers', influencerRouter); + +jest.mock('./index.model'); +jest.mock('../../utils/init-dto.util'); + +const influencer = { + firstName: 'John', + lastName: 'Doe', + socialMediaHandles: [ + { + type: 'instagram', + userName: 'john_doe', + }, + ], + manager: { + id: '1', + firstName: 'Jane', + lastName: 'Doe', + }, +}; + +describe('Influencer Routes', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should create an influencer', async () => { + (initDto as jest.Mock).mockResolvedValueOnce(undefined); + (InfluencerModel.create as jest.Mock).mockResolvedValueOnce(influencer); + + const response = await request(app) + .post('/influencers') + .send({ + firstName: 'John', + lastName: 'Doe', + socialMediaHandles: [{ type: 'instagram', userName: 'john_doe' }], + manager: { id: '1', firstName: 'Jane', lastName: 'Doe' }, + }); + + expect(response.status).toBe(200); + expect(response.body).toEqual(ApiResponse.success(influencer)); + }); + + it('should get all influencers', async () => { + (InfluencerModel.all as jest.Mock).mockResolvedValueOnce([influencer]); + + const response = await request(app).get('/influencers'); + + expect(response.status).toBe(200); + expect(response.body).toEqual(ApiResponse.success([influencer])); + }); + + it('should update an influencer', async () => { + (initDto as jest.Mock).mockResolvedValueOnce(undefined); + (InfluencerModel.update as jest.Mock).mockResolvedValueOnce(influencer); + + const response = await request(app) + .patch('/influencers/1') + .send({ + manager: { + id: '1', + firstName: 'Emmanuel', + lastName: 'Onah', + }, + }); + + expect(response.status).toBe(200); + expect(response.body).toEqual(ApiResponse.success(influencer)); + }); +}); diff --git a/server/src/routes/influencer/index.types.ts b/server/src/routes/influencer/index.types.ts new file mode 100644 index 0000000..55cde34 --- /dev/null +++ b/server/src/routes/influencer/index.types.ts @@ -0,0 +1,36 @@ +import { Document } from 'mongoose'; + +export type InfluencerQueryParams = Partial<{ + firstName: string; + lastName: string; + managerId: string; +}>; + +export type SocialMediaHandle = { + type: 'instagram' | 'tiktok'; + userName: string; +}; + +export type Manager = { + id: string; + imgUrl: string | null; + firstName: string; + lastName: string; + email: string | null; +}; + +type Influencer = { + firstName: string; + lastName: string; + socialMediaHandles: SocialMediaHandle[]; + /** + * Because manager is not a requirement, i will create a list of dummy managers on the client side + * and the user will select one of them. In a real world scenario, the manager would be a separate + * document/table and the influencer would have a reference to the manager using reference id/fk. + */ + manager: Manager; +}; + +export interface InfluencerRequest extends Influencer {} + +export interface InfluencerResponse extends Influencer, Document {} diff --git a/server/src/services/api-response/index.service.ts b/server/src/services/api-response/index.service.ts index f38f6a4..69fbd8c 100644 --- a/server/src/services/api-response/index.service.ts +++ b/server/src/services/api-response/index.service.ts @@ -8,11 +8,11 @@ export interface SuccessType { export interface ErrorType extends HttpException {} export class ApiResponse { - public error(statusCode: number, message: string) { + public static error(statusCode: number, message: string) { return new HttpException(statusCode, message); } - public success>(data: D) { + public static success>(data: D) { return { success: true, data }; } } diff --git a/server/src/services/api-response/index.test.ts b/server/src/services/api-response/index.test.ts index 8d38a79..f13c5cb 100644 --- a/server/src/services/api-response/index.test.ts +++ b/server/src/services/api-response/index.test.ts @@ -1,17 +1,15 @@ import { ApiResponse } from './index.service'; describe('response', () => { - const apiResponse = new ApiResponse(); - it('should return success response', () => { - expect(apiResponse.success({ name: 'Foo Bar Baz' })).toMatchObject({ + expect(ApiResponse.success({ name: 'Foo Bar Baz' })).toMatchObject({ success: true, data: { name: 'Foo Bar Baz' }, }); }); it('should return error response', () => { - expect(apiResponse.error(400, 'Invalid email')).toMatchObject({ + expect(ApiResponse.error(400, 'Invalid email')).toMatchObject({ success: false, statusCode: 400, message: 'Invalid email', diff --git a/server/src/utils/env-vars.test.ts b/server/src/utils/env-vars.test.ts index 0df100c..b8d0ba6 100644 --- a/server/src/utils/env-vars.test.ts +++ b/server/src/utils/env-vars.test.ts @@ -2,8 +2,6 @@ import { envVars } from './env-vars.util'; describe('envVars', () => { it('should return configuration object', () => { - expect(typeof envVars.appName).toBe('string'); - expect(typeof envVars.serverPort).toBe('number'); - expect(typeof envVars.serverUrl).toBe('string'); + expect(Object.keys(envVars).length).toBe(7); }); }); diff --git a/server/src/utils/env-vars.util.ts b/server/src/utils/env-vars.util.ts index 25b83bd..c75ce70 100644 --- a/server/src/utils/env-vars.util.ts +++ b/server/src/utils/env-vars.util.ts @@ -6,9 +6,9 @@ env.config(); export const envVars = { appName: envVar.get('ADCASH_INFLUENCER_MANAGER_APP_NAME').asString(), serverPort: envVar.get('ADCASH_INFLUENCER_MANAGER_SERVER_PORT').asPortNumber(), - serverUrl: - envVar.get('ADCASH_INFLUENCER_MANAGER_SERVER_URL').asUrlString() || 'http://localhost:8080', + serverUrl: envVar.get('ADCASH_INFLUENCER_MANAGER_SERVER_URL').asUrlString(), mongoDbUri: envVar.get('ADCASH_INFLUENCER_MANAGER_SERVER_MONGO_DB_URI').asString(), clientPort: envVar.get('ADCASH_INFLUENCER_MANAGER_CLIENT_PORT').asPortNumber(), clientUrl: envVar.get('ADCASH_INFLUENCER_MANAGER_CLIENT_URL').asUrlString(), + NODE_ENV: envVar.get('ADCASH_INFLUENCER_MANAGER_CLIENT_URL').asUrlString(), }; diff --git a/server/yarn.lock b/server/yarn.lock index 650cd5b..7365500 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1463,6 +1463,11 @@ cjs-module-lexer@^1.0.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== +class-transformer@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + class-validator@^0.14.1: version "0.14.1" resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.14.1.tgz#ff2411ed8134e9d76acfeb14872884448be98110" @@ -3977,6 +3982,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +reflect-metadata@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9"