diff --git a/README.md b/README.md index 772447f61..9f56c86e1 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ Before you begin, ensure you have the following installed on your machine: - [NestJs](https://docs.nestjs.com) (NestJS' Documentation) - [Git](https://git-scm.com/) +## Setup Guide + #### Detailed guide on setting and starting the Application + - [Setup Guide](setup-guide.md) + ## Contribution Guide ## Getting Started diff --git a/db/migrations/1721761856231-migration.ts b/db/migrations/1721761856231-migration.ts new file mode 100644 index 000000000..dbfaeafc5 --- /dev/null +++ b/db/migrations/1721761856231-migration.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class Migration1721761856231 implements MigrationInterface { + name = 'Migration1721761856231'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."user_user_type_enum" AS ENUM('super_admin', 'admin', 'vendor')`); + await queryRunner.query( + `CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "first_name" character varying NOT NULL, "last_name" character varying NOT NULL, "email" character varying NOT NULL, "password" character varying NOT NULL, "is_active" boolean, "attempts_left" integer, "time_left" integer, "user_type" "public"."user_user_type_enum" NOT NULL DEFAULT 'vendor', CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "organisation_preference" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "value" character varying NOT NULL, "organisationId" uuid NOT NULL, CONSTRAINT "PK_3149ecbe39a50d9b76f09b9dd44" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `CREATE TABLE "organisation" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "description" text NOT NULL, "email" character varying NOT NULL, "industry" character varying NOT NULL, "type" character varying NOT NULL, "country" character varying NOT NULL, "address" text NOT NULL, "state" character varying NOT NULL, "isDeleted" boolean NOT NULL DEFAULT false, "ownerId" uuid NOT NULL, "creatorId" uuid NOT NULL, CONSTRAINT "UQ_a795e00e9d60fc3c2683caac33b" UNIQUE ("email"), CONSTRAINT "PK_c725ae234ef1b74cce43d2d00c1" PRIMARY KEY ("id"))` + ); + await queryRunner.query( + `ALTER TABLE "organisation_preference" ADD CONSTRAINT "FK_2de786f3f89e650581916d67f86" FOREIGN KEY ("organisationId") REFERENCES "organisation"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "organisation" ADD CONSTRAINT "FK_d8df3e440ba45237db29bae7631" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "organisation" ADD CONSTRAINT "FK_87890d319ae77ea7ae5ec2586df" FOREIGN KEY ("creatorId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "organisation" DROP CONSTRAINT "FK_87890d319ae77ea7ae5ec2586df"`); + await queryRunner.query(`ALTER TABLE "organisation" DROP CONSTRAINT "FK_d8df3e440ba45237db29bae7631"`); + await queryRunner.query(`ALTER TABLE "organisation_preference" DROP CONSTRAINT "FK_2de786f3f89e650581916d67f86"`); + await queryRunner.query(`DROP TABLE "organisation"`); + await queryRunner.query(`DROP TABLE "organisation_preference"`); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`DROP TYPE "public"."user_user_type_enum"`); + } +} diff --git a/package-lock.json b/package-lock.json index af20e84ae..e9343dac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "passport-jwt": "^4.0.1", "pg": "^8.12.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", "typeorm": "^0.3.20", "typeorm-extension": "^3.5.1", "types-joi": "^2.1.0" @@ -50,6 +49,7 @@ "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5.0.0", "husky": "^9.0.11", "jest": "^29.5.0", @@ -61,7 +61,7 @@ "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" + "typescript": "^5.5.4" } }, "node_modules/@ampproject/remapping": { @@ -5854,6 +5854,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -14790,9 +14807,9 @@ "integrity": "sha512-wdvZWNhDx9syXdes3V+YH0KLRNiwGsg7itbjL27truN1Av3YvnJDc3HGs9kbTpfzi3vV2q0scM2y6iSkm9nriQ==" }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "devOptional": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 38c90d3fc..2649d2485 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "passport-jwt": "^4.0.1", "pg": "^8.12.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", "typeorm": "^0.3.20", "typeorm-extension": "^3.5.1", "types-joi": "^2.1.0" @@ -84,7 +83,7 @@ "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3" + "typescript": "^5.5.4" }, "jest": { "moduleFileExtensions": [ diff --git a/setup-guide.md b/setup-guide.md new file mode 100644 index 000000000..f32fba764 --- /dev/null +++ b/setup-guide.md @@ -0,0 +1,283 @@ +# Quick Start Guide + +## How To Start Up The Application + +### Cloning the repository +- Fork and clone the repository +```bash +git clone https://github.com/hngprojects/hng_boilerplate_nestjs.git +cd hng_boilerplate_nestjs +git checkout chore/database-setup +``` + +### Setting Up the Environment + +### Prerequisites: + + - #### Ensure you have NodeJs and Npm installed. + - You can download and install Node.js from [nodejs.org](https://nodejs.org/). + - Check your installation here: + ```bash + node -v + npm -v + ``` + - #### Ensure you have NestJs installed + - Install the NestJS CLI globally using npm: + + ``` + npm install -g @nestjs/cli + ``` + - Check your installation + ``` + nest -v + ``` + +### Database Setup +- #### Ensure PostgreSQL is installed and running. + +- #### Update the .env file with your database credentials + ``` + PROFILE=local + NODE_ENV=development + PORT=3008 + DB_TYPE=dt_type + DB_USERNAME=your_username + DB_PASSWORD=your_password + DB_HOST=localhost + DB_DATABASE=db_name + DB_ENTITIES=dist/**/*.entity{.ts,.js} + DB_MIGRATIONS=dist/db/migrations/*{.ts,.js} + ``` + +### Running The Application Locally + - Install all dependencies + ``` + npm install + ``` + - Start the application in dev mode + ``` + npm run start:dev + ``` + +### Create And Apply Migration Files +- #### Migration files should be placed in the `db/migration` directory. +- #### Generate migration files automatically with typeorm + ``` + npm run migration:generate + ``` +- #### To manually create migration file + ``` + npm run migration:create + ``` +- #### Run migration + ``` + npm run migration:run + ``` + + + #### Create Sample Data + - The seed data is run once when app starts + +## Testing the Endpoint + +### You can test the endpoint using curl or Postman +- Using Curl +``` +curl -X GET http://localhost:3008/api/v1/users/ +``` + +- Using Postman + * Open Postman. + * Create a new GET request to http://localhost:3008/api/v1/users. + * Send the request and verify the response. +- Expected Response +```json + { + "id": "d6aa5dc9-a1c3-4516-ad4e-31d169893510", + "first_name": "John", + "last_name": "Smith", + "email": "john.smith@example.com", + "password": "$2b$10$lOsaGJVjYxxZsVQ2WNsiwe./MEu.MEp2QiXKAS1FwP3gQtctOM2tG", + "is_active": null, + "attempts_left": null, + "time_left": null, + "created_at": "2024-07-20T13:08:28.273Z", + "updated_at": "2024-07-20T13:08:28.523Z", + "profile": { + "id": "6f8984f8-682a-4bb9-a618-6b14ea109bd3", + "username": "johnsmith", + "bio": "bio data", + "phone": "1234567890", + "avatar_image": "image.png" + }, + "products": [ + { + "id": "61c30739-1b7e-447a-b58e-3b05e0e7c0d3", + "product_name": "Product 1", + "product_price": 100, + "description": "Description 1" + } + ], + "organisations": [ + { + "org_id": "c52d7c25-8632-4b08-91d1-1f9a837f1861", + "org_name": "Org 1", + "description": "Description 1" + }, + { + "org_id": "4b42ea8e-e1e2-407e-88cb-9893c05a4167", + "org_name": "Org 2", + "description": "Description 2" + } + ] + } +``` + +- There are 2 default users created when you start the application. +- To get the sample data, use this route after starting the application: +``` +http://localhost:3008/api/v1/seed/users +``` +- Also, you can get the users by signing in to your database and query the users table using: +`SELECT * FROM users;`# Quick Start Guide + +## How To Start Up The Application + +### Cloning the repository +- Fork and clone the repository +```bash +git clone https://github.com/hngprojects/hng_boilerplate_nestjs.git +cd hng_boilerplate_nestjs +git checkout chore/database-setup +``` + +### Setting Up the Environment + +### Prerequisites: + + - #### Ensure you have NodeJs and Npm installed. + - You can download and install Node.js from [nodejs.org](https://nodejs.org/). + - Check your installation here: + ```bash + node -v + npm -v + ``` + - #### Ensure you have NestJs installed + - Install the NestJS CLI globally using npm: + + ``` + npm install -g @nestjs/cli + ``` + - Check your installation + ``` + nest -v + ``` + +### Database Setup +- #### Ensure PostgreSQL is installed and running. + +- #### Update the .env file with your database credentials + ``` + PROFILE=local + NODE_ENV=development + PORT=3008 + DB_TYPE=dt_type + DB_USERNAME=your_username + DB_PASSWORD=your_password + DB_HOST=localhost + DB_DATABASE=db_name + DB_ENTITIES=dist/**/*.entity{.ts,.js} + DB_MIGRATIONS=dist/db/migrations/*{.ts,.js} + ``` + +### Running The Application Locally + - Install all dependencies + ``` + npm install + ``` + - Start the application in dev mode + ``` + npm run start:dev + ``` + +### Create And Apply Migration Files +- #### Migration files should be placed in the `db/migration` directory. +- #### Generate migration files automatically with typeorm + ``` + npm run migration:generate + ``` +- #### To manually create migration file + ``` + npm run migration:create + ``` +- #### Run migration + ``` + npm run migration:run + ``` + + + #### Create Sample Data + - The seed data is run once when app starts + +## Testing the Endpoint + +### You can test the endpoint using curl or Postman +- Using Curl +``` +curl -X GET http://localhost:3008/api/v1/users/ +``` + +- Using Postman + * Open Postman. + * Create a new GET request to http://localhost:3008/api/v1/users. + * Send the request and verify the response. +- Expected Response +```json + { + "id": "d6aa5dc9-a1c3-4516-ad4e-31d169893510", + "first_name": "John", + "last_name": "Smith", + "email": "john.smith@example.com", + "password": "$2b$10$lOsaGJVjYxxZsVQ2WNsiwe./MEu.MEp2QiXKAS1FwP3gQtctOM2tG", + "is_active": null, + "attempts_left": null, + "time_left": null, + "created_at": "2024-07-20T13:08:28.273Z", + "updated_at": "2024-07-20T13:08:28.523Z", + "profile": { + "id": "6f8984f8-682a-4bb9-a618-6b14ea109bd3", + "username": "johnsmith", + "bio": "bio data", + "phone": "1234567890", + "avatar_image": "image.png" + }, + "products": [ + { + "id": "61c30739-1b7e-447a-b58e-3b05e0e7c0d3", + "product_name": "Product 1", + "product_price": 100, + "description": "Description 1" + } + ], + "organisations": [ + { + "org_id": "c52d7c25-8632-4b08-91d1-1f9a837f1861", + "org_name": "Org 1", + "description": "Description 1" + }, + { + "org_id": "4b42ea8e-e1e2-407e-88cb-9893c05a4167", + "org_name": "Org 2", + "description": "Description 2" + } + ] + } +``` + +- There are 2 default users created when you start the application. +- To get the sample data, use this route after starting the application: +``` +http://localhost:3008/api/v1/seed/users +``` +- Also, you can get the users by signing in to your database and query the users table using: +`SELECT * FROM users;` \ No newline at end of file diff --git a/src/database/seeding/seeding.service.ts b/src/database/seeding/seeding.service.ts index 40bcab8f4..7481d8398 100644 --- a/src/database/seeding/seeding.service.ts +++ b/src/database/seeding/seeding.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { User } from '../../modules/user/entities/user.entity'; +import { Organisation } from '../../modules/organisations/entities/organisations.entity'; @Injectable() export class SeedingService { @@ -8,6 +9,7 @@ export class SeedingService { async seedDatabase() { const userRepository = this.dataSource.getRepository(User); + const organisationRepository = this.dataSource.getRepository(Organisation); try { const existingUsers = await userRepository.count(); @@ -41,6 +43,40 @@ export class SeedingService { throw new Error('Failed to create all users'); } + const or1 = organisationRepository.create({ + name: 'Org 1', + description: 'Description 1', + email: 'test1@email.com', + industry: 'industry1', + type: 'type1', + country: 'country1', + state: 'state1', + address: 'address1', + owner: savedUsers[0], + creator: savedUsers[0], + isDeleted: false, + }); + + const or2 = organisationRepository.create({ + name: 'Org 2', + description: 'Description 2', + email: 'test2@email.com', + industry: 'industry2', + type: 'type2', + country: 'country2', + state: 'state2', + address: 'address2', + owner: savedUsers[0], + creator: savedUsers[0], + isDeleted: false, + }); + + await organisationRepository.save([or1, or2]); + const savedOrganisations = await organisationRepository.find(); + if (savedOrganisations.length !== 2) { + throw new Error('Failed to create all organisations'); + } + await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); @@ -55,7 +91,7 @@ export class SeedingService { async getUsers(): Promise { try { - return this.dataSource.getRepository(User).find({ relations: ['profile', 'products', 'organisations'] }); + return this.dataSource.getRepository(User).find({ relations: ['organisations'] }); } catch (error) { console.error('Error fetching users:', error.message); throw error; diff --git a/src/main.ts b/src/main.ts index 77596ba8a..701b8ee0a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { ConfigService } from '@nestjs/config'; import { DataSource } from 'typeorm'; import dataSource, { initializeDataSource } from './database/data-source'; import { SeedingService } from './database/seeding/seeding.service'; +import { ResponseInterceptor } from './shared/inteceptors/response.interceptor'; async function bootstrap() { const app = await NestFactory.create(AppModule, { bufferLogs: true }); @@ -29,7 +30,8 @@ async function bootstrap() { app.enable('trust proxy'); app.useLogger(logger); app.enableCors(); - app.setGlobalPrefix('api/v1', { exclude: ["/", "health"] }); + app.setGlobalPrefix('api/v1', { exclude: ['/', 'health'] }); + app.useGlobalInterceptors(new ResponseInterceptor()); // TODO: set options for swagger docs const options = new DocumentBuilder() diff --git a/src/modules/organisations/dto/update-organisation.dto.ts b/src/modules/organisations/dto/update-organisation.dto.ts new file mode 100644 index 000000000..d9c438ee5 --- /dev/null +++ b/src/modules/organisations/dto/update-organisation.dto.ts @@ -0,0 +1,56 @@ +import { IsBoolean, IsEmail, IsOptional, IsString, MinLength } from 'class-validator'; +import { User } from '../../user/entities/user.entity'; +import { ApiProperty, PartialType } from '@nestjs/swagger'; + +export class UpdateOrganisationDto { + @ApiProperty({ + example: "CodeGhinux's Organisation", + description: 'Name of organisation', + }) + @IsString() + @MinLength(2, { message: 'Organization name must be at least 2 characters long' }) + @IsOptional() + name?: string; + + @ApiProperty({ + example: "CodeGhinux's Organisation Description", + description: 'description of organisation', + }) + @IsString() + @IsOptional() + description?: string; + + @IsEmail() + @IsOptional() + email?: string; + + @IsString() + @IsOptional() + industry?: string; + + @IsString() + @IsOptional() + type?: string; + + @IsString() + @IsOptional() + country?: string; + + @IsString() + @IsOptional() + address?: string; + + @IsOptional() + owner?: User; + + @IsString() + @IsOptional() + state?: string; + + @IsOptional() + creator?: User; + + @IsBoolean() + @IsOptional() + isDeleted?: boolean; +} diff --git a/src/modules/organisations/organisations.controller.ts b/src/modules/organisations/organisations.controller.ts index 190de51d7..a52af1aca 100644 --- a/src/modules/organisations/organisations.controller.ts +++ b/src/modules/organisations/organisations.controller.ts @@ -1,8 +1,13 @@ -import { Body, Controller, Post, Request, UseGuards } from '@nestjs/common'; +import { Body, Controller, HttpException, Param, Patch, Post, Request, UseGuards } from '@nestjs/common'; import { OrganisationsService } from './organisations.service'; import { OrganisationRequestDto } from './dto/organisation.dto'; import { AuthGuard } from '@nestjs/passport'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { UpdateOrganisationDto } from './dto/update-organisation.dto'; +import { skipAuth } from 'src/helpers/skipAuth'; +@ApiBearerAuth() +@ApiTags('Organisation') @Controller('organisations') export class OrganisationsController { constructor(private readonly organisationsService: OrganisationsService) {} @@ -12,4 +17,16 @@ export class OrganisationsController { const user = req['user']; return this.organisationsService.create(createOrganisationDto, user.sub); } + + @ApiOperation({ summary: 'Update Organisation' }) + @ApiResponse({ + status: 200, + description: 'The found record', + type: UpdateOrganisationDto, + }) + @Patch(':id') + async update(@Param('id') id: string, @Body() updateOrganisationDto: UpdateOrganisationDto) { + const updatedOrg = await this.organisationsService.updateOrganisation(id, updateOrganisationDto); + return { message: 'Organisation successfully updated', org: updatedOrg }; + } } diff --git a/src/modules/organisations/organisations.service.ts b/src/modules/organisations/organisations.service.ts index dff686e93..f2c8e30d7 100644 --- a/src/modules/organisations/organisations.service.ts +++ b/src/modules/organisations/organisations.service.ts @@ -1,4 +1,10 @@ -import { Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + InternalServerErrorException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; import { Repository } from 'typeorm'; import { Organisation } from './entities/organisations.entity'; import { OrganisationRequestDto } from './dto/organisation.dto'; @@ -6,6 +12,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { User } from '../user/entities/user.entity'; import { OrganisationMapper } from './mapper/organisation.mapper'; import { CreateOrganisationMapper } from './mapper/create-organisation.mapper'; +import { UpdateOrganisationDto } from './dto/update-organisation.dto'; @Injectable() export class OrganisationsService { @@ -43,4 +50,25 @@ export class OrganisationsService { const emailFound = await this.organisationRepository.findBy({ email }); return emailFound?.length ? true : false; } + + async updateOrganisation( + id: string, + updateOrganisationDto: UpdateOrganisationDto + ): Promise<{ message: string; org: Organisation }> { + try { + const org = await this.organisationRepository.findOneBy({ id }); + if (!org) { + throw new NotFoundException('Organization not found'); + } + await this.organisationRepository.update(id, updateOrganisationDto); + const updatedOrg = await this.organisationRepository.findOneBy({ id }); + + return { message: 'Organisation successfully updated', org: updatedOrg }; + } catch (error) { + if (error instanceof NotFoundException || error instanceof BadRequestException) { + throw error; + } + throw new InternalServerErrorException(`An internal server error occurred: ${error.message}`); + } + } } diff --git a/src/modules/organisations/tests/organisations.service.spec.ts b/src/modules/organisations/tests/organisations.service.spec.ts index 52170dfb8..93cc8a77c 100644 --- a/src/modules/organisations/tests/organisations.service.spec.ts +++ b/src/modules/organisations/tests/organisations.service.spec.ts @@ -8,7 +8,12 @@ import { validate } from 'class-validator'; import { orgMock } from '../tests/mocks/organisation.mock'; import { createMockOrganisationRequestDto } from '../tests/mocks/organisation-dto.mock'; import UserService from '../../user/user.service'; -import { UnprocessableEntityException } from '@nestjs/common'; +import { + BadRequestException, + InternalServerErrorException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; describe('OrganisationsService', () => { let service: OrganisationsService; @@ -26,6 +31,8 @@ describe('OrganisationsService', () => { findOne: jest.fn(), create: jest.fn(), save: jest.fn(), + findOneBy: jest.fn(), + update: jest.fn(), }, }, UserService, @@ -39,6 +46,7 @@ describe('OrganisationsService', () => { }, ], }).compile(); + service = module.get(OrganisationsService); userRepository = module.get>(getRepositoryToken(User)); organisationRepository = module.get>(getRepositoryToken(Organisation)); @@ -48,7 +56,7 @@ describe('OrganisationsService', () => { expect(service).toBeDefined(); }); - describe('it should create an organisation', () => { + describe('create organisation', () => { beforeEach(async () => { const errors = await validate(createMockOrganisationRequestDto()); expect(errors).toHaveLength(0); @@ -68,13 +76,6 @@ describe('OrganisationsService', () => { expect(result.status).toEqual('success'); expect(result.message).toEqual('organisation created successfully'); }); - }); - - describe('error for an exsiting email', () => { - beforeEach(async () => { - const errors = await validate(createMockOrganisationRequestDto()); - expect(errors).toHaveLength(0); - }); it('should throw an error if the email already exists', async () => { jest.spyOn(organisationRepository, 'findBy').mockResolvedValue([orgMock]); @@ -87,4 +88,43 @@ describe('OrganisationsService', () => { ); }); }); + + describe('update organisation', () => { + it('should update an organisation successfully', async () => { + const id = '1'; + const updateOrganisationDto = { name: 'New Name', description: 'Updated Description' }; + const organisation = new Organisation(); + + jest.spyOn(organisationRepository, 'findOneBy').mockResolvedValueOnce(organisation); + jest.spyOn(organisationRepository, 'update').mockResolvedValueOnce({ affected: 1 } as any); + jest + .spyOn(organisationRepository, 'findOneBy') + .mockResolvedValueOnce({ ...organisation, ...updateOrganisationDto }); + + const result = await service.updateOrganisation(id, updateOrganisationDto); + + expect(result).toEqual({ + message: 'Organisation successfully updated', + org: { ...organisation, ...updateOrganisationDto }, + }); + }); + + it('should throw NotFoundException if organisation not found', async () => { + const id = '1'; + const updateOrganisationDto = { name: 'New Name', description: 'Updated Description' }; + + jest.spyOn(organisationRepository, 'findOneBy').mockResolvedValueOnce(null); + + await expect(service.updateOrganisation(id, updateOrganisationDto)).rejects.toThrow(NotFoundException); + }); + + it('should throw InternalServerErrorException if an unexpected error occurs', async () => { + const id = '1'; + const updateOrganisationDto = { name: 'New Name', description: 'Updated Description' }; + + jest.spyOn(organisationRepository, 'findOneBy').mockRejectedValueOnce(new Error('Unexpected error')); + + await expect(service.updateOrganisation(id, updateOrganisationDto)).rejects.toThrow(InternalServerErrorException); + }); + }); }); diff --git a/src/shared/inteceptors/response.interceptor.ts b/src/shared/inteceptors/response.interceptor.ts new file mode 100644 index 000000000..61f964796 --- /dev/null +++ b/src/shared/inteceptors/response.interceptor.ts @@ -0,0 +1,52 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler, HttpException, HttpStatus } from '@nestjs/common'; +import { Observable, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + +@Injectable() +export class ResponseInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + map((res: any) => this.responseHandler(res, context)), + catchError((err: HttpException) => throwError(() => this.errorHandler(err, context))) + ); + } + + errorHandler(exception: HttpException, context: ExecutionContext) { + const ctx = context.switchToHttp(); + const response = ctx.getResponse(); + const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + const exceptionResponse: any = exception.getResponse(); + + let errorMessage = 'An error occurred'; + + if (typeof exceptionResponse === 'object' && 'message' in exceptionResponse) { + if (Array.isArray(exceptionResponse.message)) { + errorMessage = exceptionResponse.message.join(', '); + } else { + errorMessage = exceptionResponse.message; + } + } + + response.status(status).json({ + status: false, + status_code: status, + error: exceptionResponse.error || exceptionResponse, + message: errorMessage, + }); + } + + responseHandler(res: any, context: ExecutionContext) { + const ctx = context.switchToHttp(); + const response = ctx.getResponse(); + const status_code = response.statusCode; + + const { message, ...data } = res; + + return { + status: true, + status_code, + message, + ...data, + }; + } +}