diff --git a/src/app.ts b/src/app.ts index a144bf43..1fa5cbc1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -17,6 +17,7 @@ import menteeRouter from './routes/mentee/mentee.route' import mentorRouter from './routes/mentor/mentor.route' import profileRouter from './routes/profile/profile.route' import path from 'path' +import countryRouter from './routes/country/country.route' const app = express() const staticFolder = 'uploads' @@ -45,6 +46,7 @@ app.use('/api/mentors', mentorRouter) app.use('/api/mentees', menteeRouter) app.use('/api/categories', categoryRouter) app.use('/api/emails', emailRouter) +app.use('/api/countries', countryRouter) if (!fs.existsSync(staticFolder)) { fs.mkdirSync(staticFolder, { recursive: true }) diff --git a/src/controllers/country.controller.ts b/src/controllers/country.controller.ts new file mode 100644 index 00000000..8b1f7706 --- /dev/null +++ b/src/controllers/country.controller.ts @@ -0,0 +1,25 @@ +import type { Request, Response } from 'express' +import type { ApiResponse } from '../types' +import type Country from '../entities/country.entity' +import { getAllCountries } from '../services/country.service' + +export const getCountries = async ( + req: Request, + res: Response +): Promise> => { + try { + const data = await getAllCountries() + if (!data) { + return res.status(404).json({ message: 'Countries Not Found' }) + } + return res.status(200).json({ data, message: 'Countries Found' }) + } catch (err) { + if (err instanceof Error) { + console.error('Error executing query', err) + return res + .status(500) + .json({ error: 'Internal server error', message: err.message }) + } + throw err + } +} diff --git a/src/entities/country.entity.ts b/src/entities/country.entity.ts new file mode 100644 index 00000000..48628c8a --- /dev/null +++ b/src/entities/country.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity } from 'typeorm' +import BaseEntity from './baseEntity' + +@Entity() +export class Country extends BaseEntity { + @Column() + code: string + + @Column() + name: string + + constructor(code: string, name: string) { + super() + this.code = code + this.name = name + } +} + +export default Country diff --git a/src/migrations/1726849469636-CreateCountryTable.ts b/src/migrations/1726849469636-CreateCountryTable.ts new file mode 100644 index 00000000..060875c3 --- /dev/null +++ b/src/migrations/1726849469636-CreateCountryTable.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class CreateCountryTable1726849469636 implements MigrationInterface { + name = 'CreateCountryTable1726849469636' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "country" ("uuid" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "code" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_4e06beff3ecfb1a974312fe536d" PRIMARY KEY ("uuid"))` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "country"`) + } +} diff --git a/src/routes/country/country.route.ts b/src/routes/country/country.route.ts new file mode 100644 index 00000000..e6efa33d --- /dev/null +++ b/src/routes/country/country.route.ts @@ -0,0 +1,8 @@ +import express from 'express' +import { getCountries } from '../../controllers/country.controller' + +const countryRouter = express.Router() + +countryRouter.get('/', getCountries) + +export default countryRouter diff --git a/src/scripts/countries.json b/src/scripts/countries.json new file mode 100644 index 00000000..87c71213 --- /dev/null +++ b/src/scripts/countries.json @@ -0,0 +1,254 @@ +{ + "ad": "Andorra", + "ae": "United Arab Emirates", + "af": "Afghanistan", + "ag": "Antigua and Barbuda", + "ai": "Anguilla", + "al": "Albania", + "am": "Armenia", + "ao": "Angola", + "aq": "Antarctica", + "ar": "Argentina", + "as": "American Samoa", + "at": "Austria", + "au": "Australia", + "aw": "Aruba", + "ax": "Åland Islands", + "az": "Azerbaijan", + "ba": "Bosnia and Herzegovina", + "bb": "Barbados", + "bd": "Bangladesh", + "be": "Belgium", + "bf": "Burkina Faso", + "bg": "Bulgaria", + "bh": "Bahrain", + "bi": "Burundi", + "bj": "Benin", + "bl": "Saint Barthélemy", + "bm": "Bermuda", + "bn": "Brunei", + "bo": "Bolivia", + "bq": "Caribbean Netherlands", + "br": "Brazil", + "bs": "Bahamas", + "bt": "Bhutan", + "bv": "Bouvet Island", + "bw": "Botswana", + "by": "Belarus", + "bz": "Belize", + "ca": "Canada", + "cc": "Cocos (Keeling) Islands", + "cd": "DR Congo", + "cf": "Central African Republic", + "cg": "Republic of the Congo", + "ch": "Switzerland", + "ci": "Côte d'Ivoire (Ivory Coast)", + "ck": "Cook Islands", + "cl": "Chile", + "cm": "Cameroon", + "cn": "China", + "co": "Colombia", + "cr": "Costa Rica", + "cu": "Cuba", + "cv": "Cape Verde", + "cw": "Curaçao", + "cx": "Christmas Island", + "cy": "Cyprus", + "cz": "Czechia", + "de": "Germany", + "dj": "Djibouti", + "dk": "Denmark", + "dm": "Dominica", + "do": "Dominican Republic", + "dz": "Algeria", + "ec": "Ecuador", + "ee": "Estonia", + "eg": "Egypt", + "eh": "Western Sahara", + "er": "Eritrea", + "es": "Spain", + "et": "Ethiopia", + "eu": "European Union", + "fi": "Finland", + "fj": "Fiji", + "fk": "Falkland Islands", + "fm": "Micronesia", + "fo": "Faroe Islands", + "fr": "France", + "ga": "Gabon", + "gb": "United Kingdom", + "gd": "Grenada", + "ge": "Georgia", + "gf": "French Guiana", + "gg": "Guernsey", + "gh": "Ghana", + "gi": "Gibraltar", + "gl": "Greenland", + "gm": "Gambia", + "gn": "Guinea", + "gp": "Guadeloupe", + "gq": "Equatorial Guinea", + "gr": "Greece", + "gs": "South Georgia", + "gt": "Guatemala", + "gu": "Guam", + "gw": "Guinea-Bissau", + "gy": "Guyana", + "hk": "Hong Kong", + "hm": "Heard Island and McDonald Islands", + "hn": "Honduras", + "hr": "Croatia", + "ht": "Haiti", + "hu": "Hungary", + "id": "Indonesia", + "ie": "Ireland", + "il": "Israel", + "im": "Isle of Man", + "in": "India", + "io": "British Indian Ocean Territory", + "iq": "Iraq", + "ir": "Iran", + "is": "Iceland", + "it": "Italy", + "je": "Jersey", + "jm": "Jamaica", + "jo": "Jordan", + "jp": "Japan", + "ke": "Kenya", + "kg": "Kyrgyzstan", + "kh": "Cambodia", + "ki": "Kiribati", + "km": "Comoros", + "kn": "Saint Kitts and Nevis", + "kp": "North Korea", + "kr": "South Korea", + "kw": "Kuwait", + "ky": "Cayman Islands", + "kz": "Kazakhstan", + "la": "Laos", + "lb": "Lebanon", + "lc": "Saint Lucia", + "li": "Liechtenstein", + "lk": "Sri Lanka", + "lr": "Liberia", + "ls": "Lesotho", + "lt": "Lithuania", + "lu": "Luxembourg", + "lv": "Latvia", + "ly": "Libya", + "ma": "Morocco", + "mc": "Monaco", + "md": "Moldova", + "me": "Montenegro", + "mf": "Saint Martin", + "mg": "Madagascar", + "mh": "Marshall Islands", + "mk": "North Macedonia", + "ml": "Mali", + "mm": "Myanmar", + "mn": "Mongolia", + "mo": "Macau", + "mp": "Northern Mariana Islands", + "mq": "Martinique", + "mr": "Mauritania", + "ms": "Montserrat", + "mt": "Malta", + "mu": "Mauritius", + "mv": "Maldives", + "mw": "Malawi", + "mx": "Mexico", + "my": "Malaysia", + "mz": "Mozambique", + "na": "Namibia", + "nc": "New Caledonia", + "ne": "Niger", + "nf": "Norfolk Island", + "ng": "Nigeria", + "ni": "Nicaragua", + "nl": "Netherlands", + "no": "Norway", + "np": "Nepal", + "nr": "Nauru", + "nu": "Niue", + "nz": "New Zealand", + "om": "Oman", + "pa": "Panama", + "pe": "Peru", + "pf": "French Polynesia", + "pg": "Papua New Guinea", + "ph": "Philippines", + "pk": "Pakistan", + "pl": "Poland", + "pm": "Saint Pierre and Miquelon", + "pn": "Pitcairn Islands", + "pr": "Puerto Rico", + "ps": "Palestine", + "pt": "Portugal", + "pw": "Palau", + "py": "Paraguay", + "qa": "Qatar", + "re": "Réunion", + "ro": "Romania", + "rs": "Serbia", + "ru": "Russia", + "rw": "Rwanda", + "sa": "Saudi Arabia", + "sb": "Solomon Islands", + "sc": "Seychelles", + "sd": "Sudan", + "se": "Sweden", + "sg": "Singapore", + "sh": "Saint Helena, Ascension and Tristan da Cunha", + "si": "Slovenia", + "sj": "Svalbard and Jan Mayen", + "sk": "Slovakia", + "sl": "Sierra Leone", + "sm": "San Marino", + "sn": "Senegal", + "so": "Somalia", + "sr": "Suriname", + "ss": "South Sudan", + "st": "São Tomé and Príncipe", + "sv": "El Salvador", + "sx": "Sint Maarten", + "sy": "Syria", + "sz": "Eswatini (Swaziland)", + "tc": "Turks and Caicos Islands", + "td": "Chad", + "tf": "French Southern and Antarctic Lands", + "tg": "Togo", + "th": "Thailand", + "tj": "Tajikistan", + "tk": "Tokelau", + "tl": "Timor-Leste", + "tm": "Turkmenistan", + "tn": "Tunisia", + "to": "Tonga", + "tr": "Turkey", + "tt": "Trinidad and Tobago", + "tv": "Tuvalu", + "tw": "Taiwan", + "tz": "Tanzania", + "ua": "Ukraine", + "ug": "Uganda", + "um": "United States Minor Outlying Islands", + "un": "United Nations", + "us": "United States", + "uy": "Uruguay", + "uz": "Uzbekistan", + "va": "Vatican City (Holy See)", + "vc": "Saint Vincent and the Grenadines", + "ve": "Venezuela", + "vg": "British Virgin Islands", + "vi": "United States Virgin Islands", + "vn": "Vietnam", + "vu": "Vanuatu", + "wf": "Wallis and Futuna", + "ws": "Samoa", + "xk": "Kosovo", + "ye": "Yemen", + "yt": "Mayotte", + "za": "South Africa", + "zm": "Zambia", + "zw": "Zimbabwe" +} \ No newline at end of file diff --git a/src/scripts/seed-db.ts b/src/scripts/seed-db.ts index aa149014..8787d24a 100644 --- a/src/scripts/seed-db.ts +++ b/src/scripts/seed-db.ts @@ -1,10 +1,14 @@ import { faker } from '@faker-js/faker' +import path from 'path' +import fs from 'fs' import { dataSource } from '../configs/dbConfig' import Category from '../entities/category.entity' import Email from '../entities/email.entity' import Mentee from '../entities/mentee.entity' import Mentor from '../entities/mentor.entity' import Profile from '../entities/profile.entity' +import Country from '../entities/country.entity' + import { EmailStatusTypes, MenteeApplicationStatus, @@ -20,12 +24,14 @@ export const seedDatabaseService = async (): Promise => { const emailRepository = dataSource.getRepository(Email) const menteeRepository = dataSource.getRepository(Mentee) const mentorRepository = dataSource.getRepository(Mentor) + const countryRepository = dataSource.getRepository(Country) await menteeRepository.remove(await menteeRepository.find()) await mentorRepository.remove(await mentorRepository.find()) await profileRepository.remove(await profileRepository.find()) await categoryRepository.remove(await categoryRepository.find()) await emailRepository.remove(await emailRepository.find()) + await countryRepository.remove(await countryRepository.find()) const genProfiles = faker.helpers.multiple(createRandomProfile, { count: 100 @@ -61,6 +67,22 @@ export const seedDatabaseService = async (): Promise => { ) const categories = await categoryRepository.save(genCategories) + // Countries data file must be in the same directory as the seeding file in build directory. + const countriesDataFilePath = path.join(__dirname, 'countries.json') + const countriesData: Record = JSON.parse( + fs.readFileSync(countriesDataFilePath, 'utf-8') + ) + + for (const [code, name] of Object.entries(countriesData)) { + const existingCountry = await countryRepository.findOne({ + where: { code } + }) + + if (!existingCountry) { + await countryRepository.save(new Country(code, name)) + } + } + const genMentors = ( categories: Category[], profiles: Profile[] @@ -108,6 +130,7 @@ export const seedDatabaseService = async (): Promise => { await menteeRepository.save(menteesEntities) + await dataSource.destroy() return 'Database seeded successfully' } catch (err) { console.error(err) diff --git a/src/services/country.service.test.ts b/src/services/country.service.test.ts new file mode 100644 index 00000000..c518f37a --- /dev/null +++ b/src/services/country.service.test.ts @@ -0,0 +1,50 @@ +import { dataSource } from '../configs/dbConfig' +import type Country from '../entities/country.entity' +import { getAllCountries } from './country.service' + +jest.mock('../configs/dbConfig', () => ({ + dataSource: { + getRepository: jest.fn() + } +})) + +describe('Country Service - getAllCountries', () => { + it('should get all countries successfully', async () => { + const mockCountries = [ + { + uuid: 'mock-uuid-1', + code: 'C1', + name: 'Country 1' + }, + { + uuid: 'mock-uuid-2', + code: 'C2', + name: 'Country 2' + } + ] as Country[] + + const mockCountryRepository = { + find: jest.fn().mockResolvedValue(mockCountries) + } + + ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( + mockCountryRepository + ) + + const result = await getAllCountries() + expect(result?.length).toBe(2) + }) + + it('should handle when countries not found', async () => { + const mockCountryRepository = { + find: jest.fn().mockResolvedValue([]) + } + + ;(dataSource.getRepository as jest.Mock).mockReturnValueOnce( + mockCountryRepository + ) + + const result = await getAllCountries() + expect(result).toBe(null) + }) +}) diff --git a/src/services/country.service.ts b/src/services/country.service.ts new file mode 100644 index 00000000..8b754ae5 --- /dev/null +++ b/src/services/country.service.ts @@ -0,0 +1,13 @@ +import { dataSource } from '../configs/dbConfig' +import Country from '../entities/country.entity' + +export const getAllCountries = async (): Promise => { + const countryRepository = dataSource.getRepository(Country) + const countries: Country[] = await countryRepository.find({ + select: ['uuid', 'code', 'name'] + }) + if (countries && countries.length > 0) { + return countries + } + return null +}