├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── Procfile ├── README.md ├── e2e.spec.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── prisma └── schema.prisma ├── src ├── app.ts ├── attendance-sum │ ├── attendance-sum.ts │ ├── attendanceSumController.spec.ts │ ├── attendanceSumController.ts │ ├── attendanceSumRoutes.ts │ ├── attendanceSumService.spec.ts │ └── attendanceSumService.ts ├── attendance │ ├── attendance.ts │ ├── attendanceController.spec.ts │ ├── attendanceController.ts │ ├── attendanceRoutes.ts │ ├── attendanceService.spec.ts │ └── attendanceService.ts ├── baseRoute.ts ├── inventory │ ├── inventory.ts │ ├── inventoryController.spec.ts │ ├── inventoryController.ts │ ├── inventoryRoutes.ts │ ├── inventoryService.spec.ts │ └── inventoryService.ts ├── offering-records │ ├── offering-records.ts │ ├── offeringRecordsController.spec.ts │ ├── offeringRecordsController.ts │ ├── offeringRecordsRoutes.ts │ ├── offeringRecordsService.spec.ts │ └── offeringRecordsService.ts ├── server.ts ├── teachings │ ├── teachings.ts │ ├── teachingsController.spec.ts │ ├── teachingsController.ts │ ├── teachingsRoutes.ts │ ├── teachingsService.spec.ts │ └── teachingsService.ts ├── tithe-records │ ├── tithe-record.ts │ ├── titheRecordsController.spec.ts │ ├── titheRecordsController.ts │ ├── titheRecordsRoutes.ts │ ├── titheRecordsService.spec.ts │ └── titheRecordsService.ts └── users │ ├── user.ts │ ├── userController.spec.ts │ ├── userController.ts │ ├── userService.spec.ts │ ├── userService.ts │ └── usersRoutes.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | } 17 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .husky 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | 37 | .env -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "all", 3 | tabWidth: 2, 4 | singleQuote: true, 5 | }; -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REST API With Hapi.js, Typescript, Prisma ORM and Postgresql 2 | 3 | ## Description 4 | 5 | There are not many working examples out there on how to use one of the alternatives to express js called 'Hapi Js' with typescript, and do an effective setup for complex projects. 6 | 7 | This example is a church office management REST api built with Hapi js, Typescript , Prisma ORM and Postgresql. It is an example of how to structure a hapi js REST Api project into models, routes, controllers and services for effective separation of concerns and unit testing. 8 | 9 | Unlike its unfortunately or fortunately more popular rival - express js, Hapi provides many tools out of the box. Enabling you to do session management, security, connectivity, and testing without installing many extra packages or middlewares. And yet these built in tools are extensible. 10 | 11 | Prisma ORM is a typescript ORM that helps with database migration, etc. 12 | 13 | ## Documentation link for reference and demo 14 | 15 | [LINK TO DEMO AND DOCUMENTATION](https://church-management-api.herokuapp.com/documentation) 16 | 17 | ## SIDE NOTE 18 | 19 | Jest module mock seem not to be working AS EXPECTED when using hapi js built in inject method for http tests. Though it uses the mock implementation provided, it still validates inputs against dependencies in the mocked classes / services / functions. Alternative is to use chaihttp as used in the e2e tests till I figure out how to mock dependencies with Hapi inject without such behaviour. Though that behaviour is an advantage in some other scenarios, but not for what I needed in the tests written. 20 | 21 | ## TODO 22 | 23 | Handle all errors instead of simply logging to the console. For now, you will see the logs when you run the tests. 24 | 25 | ## Installation 26 | 27 | ```bash 28 | $ npm install 29 | ``` 30 | 31 | ## Run Database Migration with Prisma ORM 32 | 33 | Before this, make sure you set up a postgresql database on your machine and create environment variable named 'DATABASE_URL' with the connection url. Then the following command will automatically create the relevant tables in the database 34 | 35 | ```bash 36 | $ npm run migrate-db 37 | ``` 38 | 39 | ## Build the app to compile typscript 40 | 41 | This will also create the prisma client required for performing database operations in the app 42 | 43 | ```bash 44 | $ npm run build 45 | ``` 46 | 47 | ## Start application / server 48 | 49 | ```bash 50 | $ npm run start 51 | ``` 52 | 53 | ## Start development server with auto reload 54 | 55 | ```bash 56 | $ npm run dev 57 | ``` 58 | 59 | ## Run automated tests 60 | 61 | Note that by design, the first time you run the tests, a couple of the tests will fail because the database will only be populated on this first run. But on subsequent run, all tests will pass. 62 | 63 | ```bash 64 | $ npm run test 65 | ``` 66 | -------------------------------------------------------------------------------- /e2e.spec.ts: -------------------------------------------------------------------------------- 1 | // Here are the end to end tests for the routes. 2 | // But the unit tests are in the same folder with the file containing the functions they test 3 | // We will use chai-http to test the api over http 4 | // and to test external services such as the postgresq database working with the prisma ORM client 5 | 6 | import chai, { expect } from "chai" 7 | import chaiHttp from "chai-http"; 8 | import * as Hapi from "@hapi/hapi"; 9 | import appInstance from "./src/app"; 10 | 11 | // Bring in chai-http 12 | 13 | chai.use(chaiHttp); 14 | 15 | 16 | 17 | describe("end-to-end / integration tests", () => { 18 | // bring in the server 19 | 20 | let server: Hapi.Server; 21 | 22 | // Pass 'API' below to request instead of 'server' above since hapi js server instance does not have 23 | // address() function like express which chai-http expects 24 | 25 | const API = 'http://localhost:3000' 26 | 27 | // Set hooks 28 | 29 | beforeAll(async () => { 30 | await appInstance.init(); 31 | server = await appInstance.theApp; 32 | await server.start() 33 | }); 34 | 35 | beforeEach(async () => { 36 | 37 | }); 38 | 39 | afterAll(async () => { 40 | await server.stop(); 41 | }); 42 | 43 | // Let us test only all the getAll() enpoints for each Model for example 44 | 45 | describe("Base route endpoint", () => { 46 | it("Live server should return success from base route", done => { 47 | chai.request(API) 48 | .get("/") 49 | .end((err, res) => { 50 | expect(res).to.have.status(200); 51 | done(); 52 | }); 53 | }); 54 | }); 55 | 56 | describe("Attendance model endpoint", () => { 57 | it("Live server should return all attendance in the database", done => { 58 | chai.request(API) 59 | .get("/attendance") 60 | .end((err, res) => { 61 | expect(res).to.have.status(200); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | describe("Attendance Sum model endpoint", () => { 68 | it("Live server should return all attendance sum in the database", done => { 69 | chai.request(API) 70 | .get("/attendance-sum") 71 | .end((err, res) => { 72 | expect(res).to.have.status(200); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | 78 | describe("Inventory model endpoint", () => { 79 | it("Live server should return all inventories in the database", done => { 80 | chai.request(API) 81 | .get("/inventory") 82 | .end((err, res) => { 83 | expect(res).to.have.status(200); 84 | done(); 85 | }); 86 | }); 87 | }); 88 | 89 | describe("Offering Records model endpoint", () => { 90 | it("Live server should return all offering records in the database", done => { 91 | chai.request(API) 92 | .get("/offering-records") 93 | .end((err, res) => { 94 | expect(res).to.have.status(200); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | 100 | describe("Teachings model endpoint", () => { 101 | it("Live server should return all teaching records in the database", done => { 102 | chai.request(API) 103 | .get("/teachings") 104 | .end((err, res) => { 105 | expect(res).to.have.status(200); 106 | done(); 107 | }); 108 | }); 109 | }); 110 | 111 | describe("Tithe Records model endpoint", () => { 112 | it("Live server should return all tithe records in the database", done => { 113 | chai.request(API) 114 | .get("/tithe-records") 115 | .end((err, res) => { 116 | expect(res).to.have.status(200); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | 122 | describe("User model endpoint", () => { 123 | it("Live server should return all users in the database", done => { 124 | chai.request(API) 125 | .get("/users") 126 | .end((err, res) => { 127 | expect(res).to.have.status(200); 128 | done(); 129 | }); 130 | }); 131 | }); 132 | 133 | 134 | 135 | }) 136 | 137 | 138 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testTimeout: 20000 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "church-office-management-rest-api", 3 | "version": "1.0.0", 4 | "description": "There are not many working examples out there on how to use one of the alternatives to express js called 'Hapi Js' with typescript and effective setup for complex projects. This is a church office management REST api built with Hapi js, Typescript , Prisma ORM and Postgresql. It is an example of how to structure a hapi js REST Api project into models, routes, controllers and services for effective separation of concerns and unit testing.", 5 | "scripts": { 6 | "migrate-db": "npx prisma db push", 7 | "prebuild": "rimraf dist", 8 | "build": "prisma generate && tsc", 9 | "start": "node dist/server.js", 10 | "dev": "ts-node-dev --respawn server.ts", 11 | "test": "TEST=true jest", 12 | "lint": "eslint src --ext .js,.jsx,.ts,.tsx", 13 | "prepare": "husky install" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/LuckyOkoedion/rest-api-with-hapi-typescript-prisma-and-postgresql.git" 18 | }, 19 | "author": "Lucky Okoedion", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/LuckyOkoedion/rest-api-with-hapi-typescript-prisma-and-postgresql/issues" 23 | }, 24 | "homepage": "https://github.com/LuckyOkoedion/rest-api-with-hapi-typescript-prisma-and-postgresql#readme", 25 | "dependencies": { 26 | "@hapi/boom": "^9.1.4", 27 | "@hapi/hapi": "^20.2.0", 28 | "@hapi/inert": "^6.0.4", 29 | "@hapi/vision": "^6.1.0", 30 | "@prisma/client": "^3.1.1", 31 | "@types/chai": "^4.2.22", 32 | "@types/hapi": "^18.0.6", 33 | "@types/jest": "^27.0.2", 34 | "chai": "^4.3.4", 35 | "chai-http": "^4.3.0", 36 | "jest": "^27.2.4", 37 | "ts-jest": "^27.0.5", 38 | "ts-node": "^10.2.1", 39 | "ts-node-dev": "^1.1.8", 40 | "typescript": "^4.4.3", 41 | "hapi-swagger": "^14.2.4" 42 | }, 43 | "devDependencies": { 44 | "@commitlint/cli": "^13.2.0", 45 | "@commitlint/config-conventional": "^13.2.0", 46 | "@types/hapi__hapi": "^20.0.9", 47 | "@types/hapi__inert": "^5.2.3", 48 | "@types/hapi__joi": "^17.1.7", 49 | "@types/hapi__vision": "^5.5.3", 50 | "@types/node": "^16.10.2", 51 | "@typescript-eslint/eslint-plugin": "^4.32.0", 52 | "@typescript-eslint/parser": "^4.32.0", 53 | "eslint": "^7.32.0", 54 | "eslint-config-prettier": "^8.3.0", 55 | "eslint-plugin-import": "^2.24.2", 56 | "husky": "^7.0.2", 57 | "prettier": "^2.4.1", 58 | "pretty-quick": "^3.1.1", 59 | "prisma": "^3.1.1" 60 | }, 61 | "engines": { 62 | "node": "16.x" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = env("DATABASE_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model Attendance { 14 | id Int @id @default(autoincrement()) 15 | eventName String @db.VarChar(255) 16 | date String @db.VarChar(255) 17 | user Int 18 | } 19 | 20 | model AttendanceSum { 21 | id Int @id @default(autoincrement()) 22 | eventName String @db.VarChar(255) 23 | date String @db.VarChar(255) 24 | totalMale Int 25 | totalFemale Int 26 | totalChildren Int 27 | } 28 | 29 | model Inventory { 30 | id Int @id @default(autoincrement()) 31 | name String @db.VarChar(255) 32 | description String 33 | dateAcquired String @db.VarChar(255) 34 | initialValue Decimal @db.Decimal(9, 2) 35 | calculatedDepreciation Decimal @db.Decimal(9, 2) 36 | currentLocation String 37 | inWhoseCustody Int 38 | custodyApprovedBy Int 39 | } 40 | 41 | model OfferingRecords { 42 | id Int @id @default(autoincrement()) 43 | eventName String 44 | date String @db.VarChar(255) 45 | amount Decimal @db.Decimal(9, 2) 46 | 47 | } 48 | 49 | model Teachings { 50 | id Int @id @default(autoincrement()) 51 | eventName String @db.VarChar(255) 52 | date String @db.VarChar(255) 53 | preacher String @db.VarChar(255) 54 | topic String @db.VarChar(255) 55 | summary String 56 | audioLink String @db.VarChar(255) 57 | videoLink String @db.VarChar(255) 58 | slidesLink String @db.VarChar(255) 59 | 60 | } 61 | 62 | model TitheRecords { 63 | id Int @id @default(autoincrement()) 64 | date String @db.VarChar(255) 65 | user Int 66 | amount Decimal @db.Decimal(9, 2) 67 | } 68 | 69 | model User { 70 | id Int @id @default(autoincrement()) 71 | email String @db.VarChar(255) 72 | name String @db.VarChar(255) 73 | role String @default("member") @db.VarChar(255) 74 | phoneNumber String @db.VarChar(255) 75 | permissions String 76 | } 77 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import attendanceSumRoutes from "./attendance-sum/attendanceSumRoutes"; 3 | import attendanceRoutes from "./attendance/attendanceRoutes"; 4 | import baseRoute from "./baseRoute"; 5 | import inventoryRoutes from "./inventory/inventoryRoutes"; 6 | import offeringRecordsRoutes from "./offering-records/offeringRecordsRoutes"; 7 | import teachingsRoutes from "./teachings/teachingsRoutes"; 8 | import titheRecordsRoutes from "./tithe-records/titheRecordsRoutes"; 9 | import userRoutes from "./users/usersRoutes"; 10 | import * as HapiSwagger from "../node_modules/hapi-swagger"; 11 | import * as Inert from "@hapi/inert"; 12 | import * as Vision from "@hapi/vision"; 13 | 14 | 15 | 16 | 17 | 18 | class App { 19 | 20 | theApp: Hapi.Server | undefined; 21 | 22 | constructor() { 23 | } 24 | 25 | // function to initialize the server after routes have been registered 26 | 27 | public async init() { 28 | 29 | // set up server 30 | this.theApp = Hapi.server({ 31 | port: process.env.PORT || 3000, 32 | host: process.env.HOST || '0.0.0.0', 33 | }); 34 | 35 | // Configure swagger documentation 36 | 37 | const swaggerOptions: HapiSwagger.RegisterOptions = { 38 | info: { 39 | title: "Church Office Management REST API Documentation", 40 | version: "1.0.0", 41 | description: "There are not many working examples out there on how to use one of the alternatives to express js called 'Hapi Js' with typescript and do effective setup for complex projects. This is a church office management REST api built with Hapi js, Typescript , Prisma ORM and Postgresql. It is an example of how to structure a hapi js REST Api project into models, routes, controllers and services for effective separation of concerns and unit testing.", 42 | contact: { 43 | name: "Lucky Okoedion", 44 | url: "https://www.linkedin.com/in/lucky-okoedion-28b7286a/" 45 | } 46 | } 47 | 48 | }; 49 | 50 | const swaggerPlugins: Array> = [ 51 | { 52 | plugin: Inert 53 | }, 54 | { 55 | plugin: Vision 56 | }, 57 | { 58 | plugin: HapiSwagger, 59 | options: swaggerOptions 60 | } 61 | ] 62 | 63 | // register swagger plugins 64 | await this.theApp.register(swaggerPlugins, { once: true }); 65 | 66 | // register routes 67 | await this.theApp.register([ 68 | baseRoute, 69 | attendanceRoutes, 70 | attendanceSumRoutes, 71 | inventoryRoutes, 72 | offeringRecordsRoutes, 73 | teachingsRoutes, 74 | titheRecordsRoutes, 75 | userRoutes 76 | 77 | ], { once: true }).then(async () => { 78 | console.log("Route(s) have been registered"); 79 | 80 | // initialize app with routes 81 | await this.theApp?.initialize().then(() => { 82 | console.log("The app has been initialized"); 83 | }); 84 | 85 | }); 86 | } 87 | 88 | // Function to start the server for the main application or for tests 89 | 90 | public async start() { 91 | await this.theApp?.start(); 92 | } 93 | 94 | 95 | } 96 | 97 | // create singleton for use in main app or for tests. 98 | const appInstance = new App(); 99 | 100 | export default appInstance; 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /src/attendance-sum/attendance-sum.ts: -------------------------------------------------------------------------------- 1 | export interface AttendanceSum { 2 | id: number; 3 | eventName: string; 4 | date: string; 5 | totalMale: number; 6 | totalFemale: number; 7 | totalChildren: number; 8 | 9 | } 10 | 11 | export interface CreateAttendanceSum { 12 | eventName: string; 13 | date: string; 14 | totalMale: number; 15 | totalFemale: number; 16 | totalChildren: number; 17 | 18 | } 19 | 20 | export interface UpdateAttendanceSum { 21 | eventName?: string; 22 | date?: string; 23 | totalMale?: number; 24 | totalFemale?: number; 25 | totalChildren?: number; 26 | 27 | } -------------------------------------------------------------------------------- /src/attendance-sum/attendanceSumController.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import appInstance from "../app"; 3 | import { mocked } from 'ts-jest/utils'; 4 | import { AttendanceSum } from "./attendance-sum"; 5 | import { AttendanceSumService } from "./attendanceSumService"; 6 | 7 | describe("tests for AttendanceSum controller", () => { 8 | // set dependencies 9 | let server: Hapi.Server; 10 | 11 | // Construct dummy test data 12 | let testData: AttendanceSum[] = [ 13 | { 14 | id: 1, 15 | eventName: "Meeting", 16 | date: "12345678", 17 | totalChildren: 200, 18 | totalFemale: 100, 19 | totalMale: 50 20 | }, 21 | { 22 | id: 2, 23 | eventName: "Coding", 24 | date: "12345678", 25 | totalChildren: 200, 26 | totalFemale: 100, 27 | totalMale: 50 28 | }, 29 | { 30 | id: 3, 31 | eventName: "Meeting", 32 | date: "12345678", 33 | totalChildren: 200, 34 | totalFemale: 100, 35 | totalMale: 50 36 | } 37 | ] 38 | 39 | // Mock service class 40 | 41 | // jest.mock("./attendanceSumService", () => { 42 | // return { 43 | // create: (theDto) => { 44 | // let keys = ["eventName", "date", "totalChildren", "totalFemale", "totalMale"]; 45 | // let isObject = typeof theDto === 'object'; 46 | // let isRightType = keys.every(value => theDto.hasOwnProperty(value)); 47 | // let isRightLength = Object.keys(theDto).length === keys.length; 48 | 49 | // if (isObject && isRightType && isRightLength) { 50 | // return theDto; 51 | // } else { 52 | // return { error: "Wrong implementation" } 53 | // } 54 | // }, 55 | // getById: (id) => { 56 | // // controller should convert request param from string to number 57 | // if (typeof id !== 'number') { 58 | // return { error: "Wrong implementation" } 59 | // } else { 60 | // let theItem = testData.find(value => value.id === id); 61 | // return theItem; 62 | // } 63 | // }, 64 | // getAll: () => { 65 | // return testData; 66 | // }, 67 | // update: (theDto, id) => { 68 | 69 | // if (typeof theDto === 'object') { 70 | // let theItem = testData.find(value => value.id === id); 71 | // return theItem; 72 | // } else { 73 | // return { error: "Wrong implementation" } 74 | // } 75 | 76 | // }, 77 | // delete: (id) => { 78 | // // controller should convert request param from string to number 79 | // if (typeof id !== 'number') { 80 | // return { error: "Wrong implementation" } 81 | // } else { 82 | // let theItem = testData.find(value => value.id === id); 83 | // return theItem; 84 | // } 85 | // } 86 | // } 87 | // }); 88 | 89 | // effect the mock 90 | // TODO - FIGURE OUT WHY JEST MOCKS DON'T WORK WITH HAPI'S INJECT TEST METHOD OR USE CHAI-HTTP AS ALTERNATIVE 91 | 92 | // mocked(AttendanceSumService, true); 93 | 94 | // Set hooks 95 | 96 | beforeAll(async () => { 97 | await appInstance.init(); 98 | server = await appInstance.theApp; 99 | }); 100 | 101 | beforeEach(async () => { 102 | 103 | }); 104 | 105 | afterAll(async () => { 106 | await server.stop(); 107 | }); 108 | 109 | 110 | // Write tests 111 | test("#create() should create the entity when passed the right input", async () => { 112 | const input = { 113 | eventName: "Coding", 114 | date: "12345678", 115 | totalChildren: 200, 116 | totalFemale: 100, 117 | totalMale: 50 118 | } 119 | const response = await server.inject({ 120 | method: 'POST', 121 | url: '/attendance-sum', 122 | payload: { 123 | ...input 124 | } 125 | }); 126 | await expect(response.statusCode).toBe(201); 127 | 128 | 129 | }); 130 | 131 | 132 | test("#getAll() should function as expected", async () => { 133 | const response = await server.inject({ 134 | method: 'GET', 135 | url: '/attendance-sum' 136 | }); 137 | 138 | await expect(response.statusCode).toBe(200); 139 | 140 | 141 | }); 142 | 143 | 144 | }); -------------------------------------------------------------------------------- /src/attendance-sum/attendanceSumController.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CreateAttendanceSum, UpdateAttendanceSum } from "./attendance-sum"; 3 | import { AttendanceSumService } from "./attendanceSumService"; 4 | import * as Hapi from "@hapi/hapi"; 5 | import Boom from "@hapi/boom"; 6 | 7 | 8 | export class AttendanceSumController { 9 | 10 | public async create(request: Hapi.Request, h: Hapi.ResponseToolkit) { 11 | try { 12 | const requestBody: CreateAttendanceSum = request.payload as CreateAttendanceSum 13 | const result = await new AttendanceSumService().create(requestBody); 14 | return h.response(result).code(201); 15 | } catch (error) { 16 | request.log("error", error); 17 | return Boom.badImplementation(JSON.stringify(error)) 18 | } 19 | 20 | } 21 | 22 | 23 | public async getAll(request: Hapi.Request, h: Hapi.ResponseToolkit) { 24 | try { 25 | const result = await new AttendanceSumService().getAll(); 26 | return h.response(result).code(200); 27 | } catch (error) { 28 | request.log("error", error); 29 | return Boom.badImplementation(JSON.stringify(error)) 30 | } 31 | } 32 | 33 | 34 | public async getById(request: Hapi.Request, h: Hapi.ResponseToolkit) { 35 | try { 36 | const id: number = +request.params.id; 37 | const result = await new AttendanceSumService().getById(id); 38 | return h.response(result).code(200); 39 | } catch (error) { 40 | request.log("error", error); 41 | return Boom.badImplementation(JSON.stringify(error)) 42 | } 43 | 44 | } 45 | 46 | 47 | public async update(request: Hapi.Request, h: Hapi.ResponseToolkit) { 48 | try { 49 | const id: number = +request.params.id; 50 | const requestBody: UpdateAttendanceSum = request.payload as UpdateAttendanceSum; 51 | const result = await new AttendanceSumService().update(requestBody, id); 52 | return h.response(result).code(200); 53 | } catch (error) { 54 | request.log("error", error); 55 | return Boom.badImplementation(JSON.stringify(error)) 56 | } 57 | 58 | 59 | } 60 | 61 | 62 | public async delete(request: Hapi.Request, h: Hapi.ResponseToolkit) { 63 | try { 64 | const id: number = +request.params.id; 65 | const result = await new AttendanceSumService().delete(id); 66 | return h.response(result).code(200); 67 | } catch (error) { 68 | request.log("error", error); 69 | return Boom.badImplementation(JSON.stringify(error)) 70 | } 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/attendance-sum/attendanceSumRoutes.ts: -------------------------------------------------------------------------------- 1 | import { AttendanceSumController } from "./attendanceSumController"; 2 | import Hapi from "@hapi/hapi" 3 | 4 | // create instance of controller 5 | const controller = new AttendanceSumController(); 6 | 7 | // configure the routes 8 | const attendanceSumRoutes = { 9 | name: "attendance-sum", 10 | register: async (server: Hapi.Server) => { 11 | server.route([ 12 | { 13 | method: 'POST', 14 | path: '/attendance-sum', 15 | handler: controller.create, 16 | options: { 17 | tags: ['api'] 18 | } 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/attendance-sum', 23 | handler: controller.getAll, 24 | options: { 25 | tags: ['api'] 26 | } 27 | }, 28 | { 29 | method: 'GET', 30 | path: '/attendance-sum/{id}', 31 | handler: controller.getById, 32 | options: { 33 | tags: ['api'] 34 | } 35 | }, 36 | { 37 | method: 'PUT', 38 | path: '/attendance-sum/{id}', 39 | handler: controller.update, 40 | options: { 41 | tags: ['api'] 42 | } 43 | }, 44 | { 45 | method: 'DELETE', 46 | path: '/attendance-sum/{id}', 47 | handler: controller.delete, 48 | options: { 49 | tags: ['api'] 50 | } 51 | } 52 | ]); 53 | } 54 | } 55 | 56 | export default attendanceSumRoutes; -------------------------------------------------------------------------------- /src/attendance-sum/attendanceSumService.spec.ts: -------------------------------------------------------------------------------- 1 | import { AttendanceSumService } from "./attendanceSumService"; 2 | 3 | //Simple tests to ensure that all functions are defined. 4 | // Unlike the controller tests and the end to end tests, These tests do not make use 5 | // of Hapi server's inject method 6 | 7 | describe("attendanceSumService tests", () => { 8 | // Get an instance of the service 9 | const theService: AttendanceSumService = new AttendanceSumService(); 10 | 11 | // Only these simple tests here just as example, since no much business logic except CRUD 12 | test("#create() function should be defined", () => { 13 | expect(theService.create).toBeDefined(); 14 | }); 15 | 16 | test("#getAll() function should be defined", () => { 17 | expect(theService.getAll).toBeDefined(); 18 | }); 19 | 20 | test("#getById() function should be defined", () => { 21 | expect(theService.getById).toBeDefined(); 22 | }); 23 | 24 | test("#update() function should be defined", () => { 25 | expect(theService.update).toBeDefined(); 26 | }); 27 | 28 | test("#delete() function should be defined", () => { 29 | expect(theService.delete).toBeDefined(); 30 | }); 31 | 32 | }) -------------------------------------------------------------------------------- /src/attendance-sum/attendanceSumService.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { AttendanceSum, CreateAttendanceSum, UpdateAttendanceSum } from "./attendance-sum"; 3 | 4 | export class AttendanceSumService { 5 | 6 | private prisma: PrismaClient; 7 | 8 | constructor() { 9 | 10 | this.prisma = new PrismaClient(); 11 | 12 | 13 | } 14 | 15 | public async create(theDto: CreateAttendanceSum): Promise { 16 | 17 | try { 18 | 19 | return await this.prisma.attendanceSum.create({ 20 | data: { 21 | eventName: theDto.eventName, 22 | date: theDto.date, 23 | totalChildren: theDto.totalChildren, 24 | totalFemale: theDto.totalFemale, 25 | totalMale: theDto.totalMale 26 | } 27 | }); 28 | 29 | } catch (error) { 30 | console.log(error); 31 | 32 | } finally { 33 | 34 | await this.prisma.$disconnect(); 35 | 36 | } 37 | 38 | } 39 | 40 | public async getById(id: number): Promise { 41 | 42 | try { 43 | 44 | return await this.prisma.attendanceSum.findUnique({ 45 | where: { 46 | id 47 | } 48 | }); 49 | 50 | } catch (error) { 51 | console.log(error); 52 | 53 | } finally { 54 | await this.prisma.$disconnect(); 55 | 56 | 57 | } 58 | 59 | } 60 | 61 | public async getAll(): Promise { 62 | 63 | try { 64 | 65 | return await this.prisma.attendanceSum.findMany(); 66 | 67 | } catch (error) { 68 | console.log(error); 69 | 70 | } finally { 71 | await this.prisma.$disconnect(); 72 | 73 | 74 | } 75 | 76 | } 77 | 78 | public async update(theDto: UpdateAttendanceSum, id: number): Promise { 79 | 80 | try { 81 | 82 | return await this.prisma.attendanceSum.update({ 83 | where: { 84 | id: id 85 | }, 86 | data: { 87 | ...theDto 88 | } 89 | }) 90 | 91 | } catch (error) { 92 | console.log(error); 93 | 94 | } finally { 95 | await this.prisma.$disconnect(); 96 | 97 | 98 | } 99 | 100 | } 101 | 102 | public async delete(id: number): Promise { 103 | 104 | try { 105 | 106 | return await this.prisma.attendanceSum.delete({ 107 | where: { 108 | id 109 | } 110 | }); 111 | 112 | } catch (error) { 113 | console.log(error); 114 | 115 | } finally { 116 | await this.prisma.$disconnect(); 117 | 118 | 119 | } 120 | 121 | } 122 | 123 | 124 | } -------------------------------------------------------------------------------- /src/attendance/attendance.ts: -------------------------------------------------------------------------------- 1 | export interface Attendance { 2 | id: number; 3 | eventName: string; 4 | date: string; 5 | user: number; 6 | } 7 | 8 | export interface UpdateAttendance { 9 | eventName?: string; 10 | date?: string; 11 | user?: number; 12 | } -------------------------------------------------------------------------------- /src/attendance/attendanceController.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AttendanceService } from "./attendanceService"; 3 | import * as Hapi from "@hapi/hapi"; 4 | import appInstance from "../app"; 5 | import { mocked } from 'ts-jest/utils'; 6 | import { Attendance } from "./attendance"; 7 | 8 | describe("tests for attendance controller", () => { 9 | // set dependencies 10 | let server: Hapi.Server; 11 | 12 | // Construct dummy test data 13 | let testData: Attendance[] = [ 14 | { 15 | id: 1, 16 | eventName: "Meeting", 17 | date: "12345678", 18 | user: 1 19 | }, 20 | { 21 | id: 2, 22 | eventName: "Coding", 23 | date: "12345678", 24 | user: 3 25 | }, 26 | { 27 | id: 3, 28 | eventName: "Meeting", 29 | date: "12345678", 30 | user: 5 31 | } 32 | ] 33 | 34 | // Mock service class 35 | 36 | // jest.mock("./attendanceService", () => { 37 | // return { 38 | // create: (theDto) => { 39 | // let keys = ["eventName", "date", "user"]; 40 | // let isObject = typeof theDto === 'object'; 41 | // let isRightType = keys.every(value => theDto.hasOwnProperty(value)); 42 | // let isRightLength = Object.keys(theDto).length === keys.length; 43 | 44 | // if (isObject && isRightType && isRightLength) { 45 | // return theDto; 46 | // } else { 47 | // return { error: "Wrong implementation" } 48 | // } 49 | // }, 50 | // getById: (id) => { 51 | // // controller should convert request param from string to number 52 | // if (typeof id !== 'number') { 53 | // return { error: "Wrong implementation" } 54 | // } else { 55 | // let theItem = testData.find(value => value.id === id); 56 | // return theItem; 57 | // } 58 | // }, 59 | // getAll: () => { 60 | // return testData; 61 | // }, 62 | // update: (theDto, id) => { 63 | 64 | // if (typeof theDto === 'object') { 65 | // let theItem = testData.find(value => value.id === id); 66 | // return theItem; 67 | // } else { 68 | // return { error: "Wrong implementation" } 69 | // } 70 | 71 | // }, 72 | // delete: (id) => { 73 | // // controller should convert request param from string to number 74 | // if (typeof id !== 'number') { 75 | // return { error: "Wrong implementation" } 76 | // } else { 77 | // let theItem = testData.find(value => value.id === id); 78 | // return theItem; 79 | // } 80 | // } 81 | // } 82 | // }); 83 | 84 | // effect the mock 85 | // TODO - FIGURE OUT WHY JEST MOCKS DON'T WORK WITH HAPI'S INJECT TEST METHOD OR USE CHAI-HTTP AS ALTERNATIVE 86 | 87 | // mocked(AttendanceService, true); 88 | 89 | // Set hooks 90 | 91 | beforeAll(async () => { 92 | await appInstance.init(); 93 | server = await appInstance.theApp; 94 | }); 95 | 96 | beforeEach(async () => { 97 | 98 | }); 99 | 100 | afterAll(async () => { 101 | await server.stop(); 102 | }); 103 | 104 | 105 | // Write tests 106 | test("#create() should create the entity when passed the right input", async () => { 107 | const input = { 108 | eventName: "Coding", 109 | date: "12345678", 110 | user: 3 111 | } 112 | const response = await server.inject({ 113 | method: 'POST', 114 | url: '/attendance', 115 | payload: { 116 | ...input 117 | } 118 | }); 119 | await expect(response.statusCode).toBe(201); 120 | 121 | 122 | }); 123 | 124 | 125 | 126 | test("#getAll() should function as expected", async () => { 127 | const response = await server.inject({ 128 | method: 'GET', 129 | url: '/attendance' 130 | }); 131 | 132 | await expect(response.statusCode).toBe(200); 133 | 134 | 135 | }); 136 | 137 | 138 | 139 | }); -------------------------------------------------------------------------------- /src/attendance/attendanceController.ts: -------------------------------------------------------------------------------- 1 | 2 | import { UpdateAttendance } from "./attendance"; 3 | import { AttendanceCreationParams, AttendanceService } from "./attendanceService"; 4 | import * as Hapi from "@hapi/hapi"; 5 | import Boom from "@hapi/boom"; 6 | 7 | 8 | 9 | export class AttendanceController { 10 | 11 | public async create(request: Hapi.Request, h: Hapi.ResponseToolkit) { 12 | try { 13 | const requestBody: AttendanceCreationParams = request.payload as AttendanceCreationParams 14 | const result = await new AttendanceService().create(requestBody); 15 | return h.response(result).code(201); 16 | } catch (error) { 17 | request.log("error", error); 18 | return Boom.badImplementation(JSON.stringify(error)) 19 | } 20 | 21 | } 22 | 23 | 24 | public async getAll(request: Hapi.Request, h: Hapi.ResponseToolkit) { 25 | try { 26 | const result = await new AttendanceService().getAll(); 27 | return h.response(result).code(200); 28 | } catch (error) { 29 | request.log("error", error); 30 | return Boom.badImplementation(JSON.stringify(error)) 31 | } 32 | } 33 | 34 | 35 | public async getById(request: Hapi.Request, h: Hapi.ResponseToolkit) { 36 | try { 37 | const id: number = +request.params.id; 38 | const result = await new AttendanceService().getById(id); 39 | return h.response(result).code(200); 40 | } catch (error) { 41 | request.log("error", error); 42 | return Boom.badImplementation(JSON.stringify(error)) 43 | } 44 | 45 | } 46 | 47 | 48 | public async update(request: Hapi.Request, h: Hapi.ResponseToolkit) { 49 | try { 50 | const id: number = +request.params.id; 51 | const requestBody: UpdateAttendance = request.payload as UpdateAttendance; 52 | const result = await new AttendanceService().update(requestBody, id); 53 | return h.response(result).code(200); 54 | } catch (error) { 55 | request.log("error", error); 56 | return Boom.badImplementation(JSON.stringify(error)) 57 | } 58 | 59 | 60 | } 61 | 62 | 63 | public async delete(request: Hapi.Request, h: Hapi.ResponseToolkit) { 64 | try { 65 | const id: number = +request.params.id; 66 | const result = await new AttendanceService().delete(id); 67 | return h.response(result).code(200); 68 | } catch (error) { 69 | request.log("error", error); 70 | return Boom.badImplementation(JSON.stringify(error)) 71 | } 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /src/attendance/attendanceRoutes.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import { AttendanceController } from "./attendanceController"; 3 | 4 | // create instance of controller 5 | const controller = new AttendanceController(); 6 | 7 | // configure the routes 8 | const attendanceRoutes = { 9 | name: "theAttendance", 10 | register: async (server: Hapi.Server) => { 11 | server.route([ 12 | { 13 | method: 'POST', 14 | path: '/attendance', 15 | handler: controller.create, 16 | options: { 17 | tags: ['api'] 18 | } 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/attendance', 23 | handler: controller.getAll, 24 | options: { 25 | tags: ['api'] 26 | } 27 | }, 28 | { 29 | method: 'GET', 30 | path: '/attendance/{id}', 31 | handler: controller.getById, 32 | options: { 33 | tags: ['api'] 34 | } 35 | }, 36 | { 37 | method: 'PUT', 38 | path: '/attendance/{id}', 39 | handler: controller.update, 40 | options: { 41 | tags: ['api'] 42 | } 43 | }, 44 | { 45 | method: 'DELETE', 46 | path: '/attendance/{id}', 47 | handler: controller.delete, 48 | options: { 49 | tags: ['api'] 50 | } 51 | } 52 | ]); 53 | } 54 | } 55 | 56 | export default attendanceRoutes; -------------------------------------------------------------------------------- /src/attendance/attendanceService.spec.ts: -------------------------------------------------------------------------------- 1 | import { AttendanceService } from "./attendanceService" 2 | 3 | //Simple tests to ensure that all functions are defined. 4 | // Unlike the controller tests and the end to end tests, These tests do not make use 5 | // of Hapi server's inject method 6 | 7 | describe("attendanceService tests", () => { 8 | // Get an instance of the service 9 | const theService: AttendanceService = new AttendanceService(); 10 | 11 | // Only these simple tests here just as example, since no much business logic except CRUD 12 | test("#create() function should be defined", () => { 13 | expect(theService.create).toBeDefined(); 14 | }); 15 | 16 | test("#getAll() function should be defined", () => { 17 | expect(theService.getAll).toBeDefined(); 18 | }); 19 | 20 | test("#getById() function should be defined", () => { 21 | expect(theService.getById).toBeDefined(); 22 | }); 23 | 24 | test("#update() function should be defined", () => { 25 | expect(theService.update).toBeDefined(); 26 | }); 27 | 28 | test("#delete() function should be defined", () => { 29 | expect(theService.delete).toBeDefined(); 30 | }); 31 | 32 | }) -------------------------------------------------------------------------------- /src/attendance/attendanceService.ts: -------------------------------------------------------------------------------- 1 | import { Attendance, UpdateAttendance } from "./attendance"; 2 | import { PrismaClient } from ".prisma/client"; 3 | 4 | export type AttendanceCreationParams = Pick< 5 | Attendance, 6 | "id" | 7 | "eventName" | 8 | "date" | 9 | "user" 10 | > 11 | 12 | export class AttendanceService { 13 | 14 | private prisma: PrismaClient; 15 | 16 | constructor() { 17 | 18 | this.prisma = new PrismaClient(); 19 | 20 | 21 | } 22 | 23 | public async create(theDto: AttendanceCreationParams): Promise { 24 | 25 | try { 26 | 27 | return await this.prisma.attendance.create({ 28 | data: { 29 | eventName: theDto.eventName, 30 | date: theDto.date, 31 | user: theDto.user 32 | } 33 | }); 34 | 35 | } catch (error) { 36 | console.log(error); 37 | 38 | } finally { 39 | 40 | await this.prisma.$disconnect(); 41 | 42 | } 43 | 44 | } 45 | 46 | public async getById(id: number): Promise { 47 | 48 | try { 49 | 50 | return await this.prisma.attendance.findUnique({ 51 | where: { 52 | id 53 | } 54 | }); 55 | 56 | } catch (error) { 57 | console.log(error); 58 | 59 | } finally { 60 | await this.prisma.$disconnect(); 61 | 62 | 63 | } 64 | 65 | } 66 | 67 | public async getAll(): Promise { 68 | 69 | try { 70 | 71 | return await this.prisma.attendance.findMany(); 72 | 73 | } catch (error) { 74 | console.log(error); 75 | 76 | } finally { 77 | await this.prisma.$disconnect(); 78 | 79 | 80 | } 81 | 82 | } 83 | 84 | public async update(theDto: UpdateAttendance, id: number): Promise { 85 | 86 | try { 87 | 88 | return await this.prisma.attendance.update({ 89 | where: { 90 | id: id 91 | }, 92 | data: { 93 | ...theDto 94 | } 95 | }) 96 | 97 | } catch (error) { 98 | console.log(error); 99 | 100 | } finally { 101 | await this.prisma.$disconnect(); 102 | 103 | 104 | } 105 | 106 | } 107 | 108 | public async delete(id: number): Promise { 109 | 110 | try { 111 | 112 | return await this.prisma.attendance.delete({ 113 | where: { 114 | id 115 | } 116 | }); 117 | 118 | } catch (error) { 119 | console.log(error); 120 | 121 | } finally { 122 | await this.prisma.$disconnect(); 123 | 124 | 125 | } 126 | 127 | } 128 | 129 | 130 | } -------------------------------------------------------------------------------- /src/baseRoute.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | 3 | 4 | // configure the base route 5 | const baseRoute = { 6 | name: "attendance", 7 | register: async (server: Hapi.Server) => { 8 | server.route( 9 | { 10 | method: 'GET', 11 | path: '/', 12 | handler: (request: Hapi.Request, 13 | // h: Hapi.ResponseToolkit 14 | ) => { 15 | 16 | return `Welcome ! please find the api documentation at ${request.info.host}/documentation` 17 | 18 | } 19 | } 20 | ); 21 | } 22 | } 23 | 24 | export default baseRoute; -------------------------------------------------------------------------------- /src/inventory/inventory.ts: -------------------------------------------------------------------------------- 1 | export interface Inventory { 2 | id: number; 3 | name: string; 4 | description: string; 5 | dateAcquired: string; 6 | initialValue: any; 7 | calculatedDepreciation: any; 8 | currentLocation: string; 9 | inWhoseCustody: number; 10 | custodyApprovedBy: number; 11 | 12 | } 13 | 14 | export interface CreateInventory { 15 | name: string; 16 | description: string; 17 | dateAcquired: string; 18 | initialValue: any; 19 | calculatedDepreciation: any; 20 | currentLocation: string; 21 | inWhoseCustody: number; 22 | custodyApprovedBy: number; 23 | 24 | } 25 | 26 | export interface UpdateInventory { 27 | name?: string; 28 | description?: string; 29 | dateAcquired?: string; 30 | initialValue?: any; 31 | calculatedDepreciation?: any; 32 | currentLocation?: string; 33 | inWhoseCustody?: number; 34 | custodyApprovedBy?: number; 35 | 36 | } -------------------------------------------------------------------------------- /src/inventory/inventoryController.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import appInstance from "../app"; 3 | import { mocked } from 'ts-jest/utils'; 4 | import { Inventory } from "./inventory"; 5 | import { InventoryService } from "./inventoryService"; 6 | 7 | describe("tests for inventory controller", () => { 8 | // set dependencies 9 | let server: Hapi.Server; 10 | 11 | // Construct dummy test data 12 | let testData: Inventory[] = [ 13 | { 14 | id: 1, 15 | name: "Andrew Tables", 16 | description: "The ljaljahahlakljkajkjakl right", 17 | dateAcquired: "123456799", 18 | initialValue: 2345.09, 19 | calculatedDepreciation: 2345.09, 20 | currentLocation: "Lagos Office", 21 | inWhoseCustody: 3, 22 | custodyApprovedBy: 1 23 | }, 24 | { 25 | id: 2, 26 | name: "TVs", 27 | description: "The ljaljahahlakljkajkjakl right", 28 | dateAcquired: "123456799", 29 | initialValue: 2345.09, 30 | calculatedDepreciation: 2345.09, 31 | currentLocation: "Lagos Office", 32 | inWhoseCustody: 2, 33 | custodyApprovedBy: 1 34 | }, 35 | { 36 | id: 3, 37 | name: "Chairs", 38 | description: "The ljaljahahlakljkajkjakl right", 39 | dateAcquired: "123456799", 40 | initialValue: 2345.09, 41 | calculatedDepreciation: 2345.09, 42 | currentLocation: "Abuja Office", 43 | inWhoseCustody: 9, 44 | custodyApprovedBy: 5 45 | } 46 | ] 47 | 48 | // Mock service class 49 | 50 | // jest.mock("./inventoryService", () => { 51 | // return { 52 | // create: (theDto) => { 53 | // let keys = ["name", "description", "dateAcquired", "initialValue", "calculatedDepreciation", "currentLocation", "inWhoseCustody", "custodyApprovedBy"]; 54 | // let isObject = typeof theDto === 'object'; 55 | // let isRightType = keys.every(value => theDto.hasOwnProperty(value)); 56 | // let isRightLength = Object.keys(theDto).length === keys.length; 57 | 58 | // if (isObject && isRightType && isRightLength) { 59 | // return theDto; 60 | // } else { 61 | // return { error: "Wrong implementation" } 62 | // } 63 | // }, 64 | // getById: (id) => { 65 | // // controller should convert request param from string to number 66 | // if (typeof id !== 'number') { 67 | // return { error: "Wrong implementation" } 68 | // } else { 69 | // let theItem = testData.find(value => value.id === id); 70 | // return theItem; 71 | // } 72 | // }, 73 | // getAll: () => { 74 | // return testData; 75 | // }, 76 | // update: (theDto, id) => { 77 | 78 | // if (typeof theDto === 'object') { 79 | // let theItem = testData.find(value => value.id === id); 80 | // return theItem; 81 | // } else { 82 | // return { error: "Wrong implementation" } 83 | // } 84 | 85 | // }, 86 | // delete: (id) => { 87 | // // controller should convert request param from string to number 88 | // if (typeof id !== 'number') { 89 | // return { error: "Wrong implementation" } 90 | // } else { 91 | // let theItem = testData.find(value => value.id === id); 92 | // return theItem; 93 | // } 94 | // } 95 | // } 96 | // }); 97 | 98 | // effect the mock 99 | // TODO - FIGURE OUT WHY JEST MOCKS DON'T WORK WITH HAPI'S INJECT TEST METHOD OR USE CHAI-HTTP AS ALTERNATIVE 100 | 101 | // mocked(InventoryService, true); 102 | 103 | // Set hooks 104 | 105 | beforeAll(async () => { 106 | await appInstance.init(); 107 | server = await appInstance.theApp; 108 | }); 109 | 110 | beforeEach(async () => { 111 | 112 | }); 113 | 114 | afterAll(async () => { 115 | await server.stop(); 116 | }); 117 | 118 | 119 | // Write tests 120 | test("#create() should create the entity when passed the right input", async () => { 121 | const input = { 122 | name: "Andrew Tables", 123 | description: "The ljaljahahlakljkajkjakl right", 124 | dateAcquired: "123456799", 125 | initialValue: 2345.09, 126 | calculatedDepreciation: 2345.09, 127 | currentLocation: "Lagos Office", 128 | inWhoseCustody: 3, 129 | custodyApprovedBy: 1 130 | } 131 | const response = await server.inject({ 132 | method: 'POST', 133 | url: '/inventory', 134 | payload: { 135 | ...input 136 | } 137 | }); 138 | await expect(response.statusCode).toBe(201); 139 | 140 | 141 | }); 142 | 143 | 144 | 145 | test("#getAll() should function as expected", async () => { 146 | const response = await server.inject({ 147 | method: 'GET', 148 | url: '/inventory' 149 | }); 150 | 151 | await expect(response.statusCode).toBe(200); 152 | 153 | 154 | }); 155 | 156 | 157 | }); -------------------------------------------------------------------------------- /src/inventory/inventoryController.ts: -------------------------------------------------------------------------------- 1 | import Boom from "@hapi/boom"; 2 | import * as Hapi from "@hapi/hapi"; 3 | import { CreateInventory, UpdateInventory } from "./inventory"; 4 | import { InventoryService } from "./inventoryService"; 5 | 6 | 7 | export class InventoryController { 8 | 9 | public async create(request: Hapi.Request, h: Hapi.ResponseToolkit) { 10 | try { 11 | const requestBody: CreateInventory = request.payload as CreateInventory 12 | const result = await new InventoryService().create(requestBody); 13 | return h.response(result).code(201); 14 | } catch (error) { 15 | request.log("error", error); 16 | return Boom.badImplementation(JSON.stringify(error)) 17 | } 18 | 19 | } 20 | 21 | 22 | public async getAll(request: Hapi.Request, h: Hapi.ResponseToolkit) { 23 | try { 24 | const result = await new InventoryService().getAll(); 25 | return h.response(result).code(200); 26 | } catch (error) { 27 | request.log("error", error); 28 | return Boom.badImplementation(JSON.stringify(error)) 29 | } 30 | } 31 | 32 | 33 | public async getById(request: Hapi.Request, h: Hapi.ResponseToolkit) { 34 | try { 35 | const id: number = +request.params.id; 36 | const result = await new InventoryService().getById(id); 37 | return h.response(result).code(200); 38 | } catch (error) { 39 | request.log("error", error); 40 | return Boom.badImplementation(JSON.stringify(error)) 41 | } 42 | 43 | } 44 | 45 | 46 | public async update(request: Hapi.Request, h: Hapi.ResponseToolkit) { 47 | try { 48 | const id: number = +request.params.id; 49 | const requestBody: UpdateInventory = request.payload as UpdateInventory; 50 | const result = await new InventoryService().update(requestBody, id); 51 | return h.response(result).code(200); 52 | } catch (error) { 53 | request.log("error", error); 54 | return Boom.badImplementation(JSON.stringify(error)) 55 | } 56 | 57 | 58 | } 59 | 60 | 61 | public async delete(request: Hapi.Request, h: Hapi.ResponseToolkit) { 62 | try { 63 | const id: number = +request.params.id; 64 | const result = await new InventoryService().delete(id); 65 | return h.response(result).code(200); 66 | } catch (error) { 67 | request.log("error", error); 68 | return Boom.badImplementation(JSON.stringify(error)) 69 | } 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/inventory/inventoryRoutes.ts: -------------------------------------------------------------------------------- 1 | import { InventoryController } from "./inventoryController"; 2 | import * as Hapi from "@hapi/hapi"; 3 | 4 | // create instance of controller 5 | const controller = new InventoryController(); 6 | 7 | // configure the routes 8 | const inventoryRoutes = { 9 | name: "inventory", 10 | register: async (server: Hapi.Server) => { 11 | server.route([ 12 | { 13 | method: 'POST', 14 | path: '/inventory', 15 | handler: controller.create, 16 | options: { 17 | tags: ['api'] 18 | } 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/inventory', 23 | handler: controller.getAll, 24 | options: { 25 | tags: ['api'] 26 | } 27 | }, 28 | { 29 | method: 'GET', 30 | path: '/inventory/{id}', 31 | handler: controller.getById, 32 | options: { 33 | tags: ['api'] 34 | } 35 | }, 36 | { 37 | method: 'PUT', 38 | path: '/inventory/{id}', 39 | handler: controller.update, 40 | options: { 41 | tags: ['api'] 42 | } 43 | }, 44 | { 45 | method: 'DELETE', 46 | path: '/inventory/{id}', 47 | handler: controller.delete, 48 | options: { 49 | tags: ['api'] 50 | } 51 | } 52 | ]); 53 | } 54 | } 55 | 56 | export default inventoryRoutes; -------------------------------------------------------------------------------- /src/inventory/inventoryService.spec.ts: -------------------------------------------------------------------------------- 1 | //Simple tests to ensure that all functions are defined. 2 | // Unlike the controller tests and the end to end tests, These tests do not make use 3 | // of Hapi server's inject method 4 | 5 | import { InventoryService } from "./inventoryService"; 6 | 7 | describe("inventoryService tests", () => { 8 | // Get an instance of the service 9 | const theService: InventoryService = new InventoryService(); 10 | 11 | // Only these simple tests here just as example, since no much business logic except CRUD 12 | test("#create() function should be defined", () => { 13 | expect(theService.create).toBeDefined(); 14 | }); 15 | 16 | test("#getAll() function should be defined", () => { 17 | expect(theService.getAll).toBeDefined(); 18 | }); 19 | 20 | test("#getById() function should be defined", () => { 21 | expect(theService.getById).toBeDefined(); 22 | }); 23 | 24 | test("#update() function should be defined", () => { 25 | expect(theService.update).toBeDefined(); 26 | }); 27 | 28 | test("#delete() function should be defined", () => { 29 | expect(theService.delete).toBeDefined(); 30 | }); 31 | 32 | }) -------------------------------------------------------------------------------- /src/inventory/inventoryService.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Inventory } from "@prisma/client"; 2 | import { CreateInventory, UpdateInventory } from "./inventory"; 3 | 4 | export class InventoryService { 5 | 6 | private prisma: PrismaClient; 7 | 8 | constructor() { 9 | 10 | this.prisma = new PrismaClient(); 11 | 12 | 13 | } 14 | 15 | public async create(theDto: CreateInventory): Promise { 16 | 17 | try { 18 | 19 | return await this.prisma.inventory.create({ 20 | data: { 21 | ...theDto 22 | } 23 | }); 24 | 25 | } catch (error) { 26 | console.log(error); 27 | 28 | } finally { 29 | 30 | await this.prisma.$disconnect(); 31 | 32 | } 33 | 34 | } 35 | 36 | public async getById(id: number): Promise { 37 | 38 | try { 39 | 40 | return await this.prisma.inventory.findUnique({ 41 | where: { 42 | id 43 | } 44 | }); 45 | 46 | } catch (error) { 47 | console.log(error); 48 | 49 | } finally { 50 | await this.prisma.$disconnect(); 51 | 52 | 53 | } 54 | 55 | } 56 | 57 | public async getAll(): Promise { 58 | 59 | try { 60 | 61 | return await this.prisma.inventory.findMany(); 62 | 63 | } catch (error) { 64 | console.log(error); 65 | 66 | } finally { 67 | await this.prisma.$disconnect(); 68 | 69 | 70 | } 71 | 72 | } 73 | 74 | public async update(theDto: UpdateInventory, id: number): Promise { 75 | 76 | try { 77 | 78 | return await this.prisma.inventory.update({ 79 | where: { 80 | id: id 81 | }, 82 | data: { 83 | ...theDto 84 | } 85 | }) 86 | 87 | } catch (error) { 88 | console.log(error); 89 | 90 | } finally { 91 | await this.prisma.$disconnect(); 92 | 93 | 94 | } 95 | 96 | } 97 | 98 | public async delete(id: number): Promise { 99 | 100 | try { 101 | 102 | return await this.prisma.inventory.delete({ 103 | where: { 104 | id 105 | } 106 | }); 107 | 108 | } catch (error) { 109 | console.log(error); 110 | 111 | } finally { 112 | await this.prisma.$disconnect(); 113 | 114 | 115 | } 116 | 117 | } 118 | 119 | 120 | } -------------------------------------------------------------------------------- /src/offering-records/offering-records.ts: -------------------------------------------------------------------------------- 1 | export interface OfferingRecords { 2 | id: number; 3 | eventName: string; 4 | date: string; 5 | amount: any; 6 | } 7 | 8 | 9 | export interface CreateOfferingRecords { 10 | eventName: string; 11 | date: string; 12 | amount: any; 13 | } 14 | 15 | 16 | export interface UpdateOfferingRecords { 17 | eventName?: string; 18 | date?: string; 19 | amount?: any; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/offering-records/offeringRecordsController.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import appInstance from "../app"; 3 | import { mocked } from 'ts-jest/utils'; 4 | import { OfferingRecords } from "./offering-records"; 5 | import { OfferingRecordsService } from "./offeringRecordsService"; 6 | 7 | describe("tests for OfferingRecords controller", () => { 8 | // set dependencies 9 | let server: Hapi.Server; 10 | 11 | // Construct dummy test data 12 | let testData: OfferingRecords[] = [ 13 | { 14 | id: 1, 15 | eventName: "Meeting", 16 | date: "12345678", 17 | amount: 234.70 18 | }, 19 | { 20 | id: 2, 21 | eventName: "Coding", 22 | date: "12345678", 23 | amount: 234.70 24 | }, 25 | { 26 | id: 3, 27 | eventName: "Meeting", 28 | date: "12345678", 29 | amount: 234.70 30 | } 31 | ] 32 | 33 | // Mock service class 34 | 35 | // jest.mock("./offeringRecordsService", () => { 36 | // return { 37 | // create: (theDto) => { 38 | // let keys = ["eventName", "date", "amount"]; 39 | // let isObject = typeof theDto === 'object'; 40 | // let isRightType = keys.every(value => theDto.hasOwnProperty(value)); 41 | // let isRightLength = Object.keys(theDto).length === keys.length; 42 | 43 | // if (isObject && isRightType && isRightLength) { 44 | // return theDto; 45 | // } else { 46 | // return { error: "Wrong implementation" } 47 | // } 48 | // }, 49 | // getById: (id) => { 50 | // // controller should convert request param from string to number 51 | // if (typeof id !== 'number') { 52 | // return { error: "Wrong implementation" } 53 | // } else { 54 | // let theItem = testData.find(value => value.id === id); 55 | // return theItem; 56 | // } 57 | // }, 58 | // getAll: () => { 59 | // return testData; 60 | // }, 61 | // update: (theDto, id) => { 62 | 63 | // if (typeof theDto === 'object') { 64 | // let theItem = testData.find(value => value.id === id); 65 | // return theItem; 66 | // } else { 67 | // return { error: "Wrong implementation" } 68 | // } 69 | 70 | // }, 71 | // delete: (id) => { 72 | // // controller should convert request param from string to number 73 | // if (typeof id !== 'number') { 74 | // return { error: "Wrong implementation" } 75 | // } else { 76 | // let theItem = testData.find(value => value.id === id); 77 | // return theItem; 78 | // } 79 | // } 80 | // } 81 | // }); 82 | 83 | // effect the mock 84 | // TODO - FIGURE OUT WHY JEST MOCKS DON'T WORK WITH HAPI'S INJECT TEST METHOD OR USE CHAI-HTTP AS ALTERNATIVE 85 | 86 | // mocked(OfferingRecordsService, true); 87 | 88 | // Set hooks 89 | 90 | beforeAll(async () => { 91 | await appInstance.init(); 92 | server = await appInstance.theApp; 93 | }); 94 | 95 | beforeEach(async () => { 96 | 97 | }); 98 | 99 | afterAll(async () => { 100 | await server.stop(); 101 | }); 102 | 103 | 104 | // Write tests 105 | test("#create() should create the entity when passed the right input", async () => { 106 | const input = { 107 | eventName: "Coding", 108 | date: "12345678", 109 | amount: 234.70 110 | } 111 | const response = await server.inject({ 112 | method: 'POST', 113 | url: '/offering-records', 114 | payload: { 115 | ...input 116 | } 117 | }); 118 | await expect(response.statusCode).toBe(201); 119 | 120 | 121 | }); 122 | 123 | 124 | test("#getAll() should function as expected", async () => { 125 | const response = await server.inject({ 126 | method: 'GET', 127 | url: '/offering-records' 128 | }); 129 | 130 | await expect(response.statusCode).toBe(200); 131 | 132 | 133 | }); 134 | 135 | 136 | }); -------------------------------------------------------------------------------- /src/offering-records/offeringRecordsController.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import { CreateOfferingRecords, UpdateOfferingRecords } from "./offering-records"; 3 | import { OfferingRecordsService } from "./offeringRecordsService"; 4 | import Boom from "@hapi/boom"; 5 | 6 | export class OfferingRecordsController { 7 | 8 | public async create(request: Hapi.Request, h: Hapi.ResponseToolkit) { 9 | try { 10 | const requestBody: CreateOfferingRecords = request.payload as CreateOfferingRecords 11 | const result = await new OfferingRecordsService().create(requestBody); 12 | return h.response(result).code(201); 13 | } catch (error) { 14 | request.log("error", error); 15 | return Boom.badImplementation(JSON.stringify(error)) 16 | } 17 | 18 | } 19 | 20 | 21 | public async getAll(request: Hapi.Request, h: Hapi.ResponseToolkit) { 22 | try { 23 | const result = await new OfferingRecordsService().getAll(); 24 | return h.response(result).code(200); 25 | } catch (error) { 26 | request.log("error", error); 27 | return Boom.badImplementation(JSON.stringify(error)) 28 | } 29 | } 30 | 31 | 32 | public async getById(request: Hapi.Request, h: Hapi.ResponseToolkit) { 33 | try { 34 | const id: number = +request.params.id; 35 | const result = await new OfferingRecordsService().getById(id); 36 | return h.response(result).code(200); 37 | } catch (error) { 38 | request.log("error", error); 39 | return Boom.badImplementation(JSON.stringify(error)) 40 | } 41 | 42 | } 43 | 44 | 45 | public async update(request: Hapi.Request, h: Hapi.ResponseToolkit) { 46 | try { 47 | const id: number = +request.params.id; 48 | const requestBody: UpdateOfferingRecords = request.payload as UpdateOfferingRecords; 49 | const result = await new OfferingRecordsService().update(requestBody, id); 50 | return h.response(result).code(200); 51 | } catch (error) { 52 | request.log("error", error); 53 | return Boom.badImplementation(JSON.stringify(error)) 54 | } 55 | 56 | 57 | } 58 | 59 | 60 | public async delete(request: Hapi.Request, h: Hapi.ResponseToolkit) { 61 | try { 62 | const id: number = +request.params.id; 63 | const result = await new OfferingRecordsService().delete(id); 64 | return h.response(result).code(200); 65 | } catch (error) { 66 | request.log("error", error); 67 | return Boom.badImplementation(JSON.stringify(error)) 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/offering-records/offeringRecordsRoutes.ts: -------------------------------------------------------------------------------- 1 | import { OfferingRecordsController } from "./offeringRecordsController"; 2 | import * as Hapi from "@hapi/hapi"; 3 | 4 | // create instance of controller 5 | const controller = new OfferingRecordsController(); 6 | 7 | // configure the routes 8 | const offeringRecordsRoutes = { 9 | name: "offering-records", 10 | register: async (server: Hapi.Server) => { 11 | server.route([ 12 | { 13 | method: 'POST', 14 | path: '/offering-records', 15 | handler: controller.create, 16 | options: { 17 | tags: ['api'] 18 | } 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/offering-records', 23 | handler: controller.getAll, 24 | options: { 25 | tags: ['api'] 26 | } 27 | }, 28 | { 29 | method: 'GET', 30 | path: '/offering-records/{id}', 31 | handler: controller.getById, 32 | options: { 33 | tags: ['api'] 34 | } 35 | }, 36 | { 37 | method: 'PUT', 38 | path: '/offering-records/{id}', 39 | handler: controller.update, 40 | options: { 41 | tags: ['api'] 42 | } 43 | }, 44 | { 45 | method: 'DELETE', 46 | path: '/offering-records/{id}', 47 | handler: controller.delete, 48 | options: { 49 | tags: ['api'] 50 | } 51 | } 52 | ]); 53 | } 54 | } 55 | 56 | export default offeringRecordsRoutes; -------------------------------------------------------------------------------- /src/offering-records/offeringRecordsService.spec.ts: -------------------------------------------------------------------------------- 1 | //Simple tests to ensure that all functions are defined. 2 | // Unlike the controller tests and the end to end tests, These tests do not make use 3 | // of Hapi server's inject method 4 | 5 | import { OfferingRecordsService } from "./offeringRecordsService"; 6 | 7 | describe("OfferingRecordsService tests", () => { 8 | // Get an instance of the service 9 | const theService: OfferingRecordsService = new OfferingRecordsService(); 10 | 11 | // Only these simple tests here just as example, since no much business logic except CRUD 12 | test("#create() function should be defined", () => { 13 | expect(theService.create).toBeDefined(); 14 | }); 15 | 16 | test("#getAll() function should be defined", () => { 17 | expect(theService.getAll).toBeDefined(); 18 | }); 19 | 20 | test("#getById() function should be defined", () => { 21 | expect(theService.getById).toBeDefined(); 22 | }); 23 | 24 | test("#update() function should be defined", () => { 25 | expect(theService.update).toBeDefined(); 26 | }); 27 | 28 | test("#delete() function should be defined", () => { 29 | expect(theService.delete).toBeDefined(); 30 | }); 31 | 32 | }) -------------------------------------------------------------------------------- /src/offering-records/offeringRecordsService.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, OfferingRecords } from "@prisma/client"; 2 | import { CreateOfferingRecords, UpdateOfferingRecords } from "./offering-records"; 3 | 4 | export class OfferingRecordsService { 5 | 6 | private prisma: PrismaClient; 7 | 8 | constructor() { 9 | 10 | this.prisma = new PrismaClient(); 11 | 12 | 13 | } 14 | 15 | public async create(theDto: CreateOfferingRecords): Promise { 16 | 17 | try { 18 | 19 | return await this.prisma.offeringRecords.create({ 20 | data: { 21 | ...theDto 22 | } 23 | }); 24 | 25 | } catch (error) { 26 | console.log(error); 27 | 28 | } finally { 29 | 30 | await this.prisma.$disconnect(); 31 | 32 | } 33 | 34 | } 35 | 36 | public async getById(id: number): Promise { 37 | 38 | try { 39 | 40 | return await this.prisma.offeringRecords.findUnique({ 41 | where: { 42 | id 43 | } 44 | }); 45 | 46 | } catch (error) { 47 | console.log(error); 48 | 49 | } finally { 50 | await this.prisma.$disconnect(); 51 | 52 | 53 | } 54 | 55 | } 56 | 57 | public async getAll(): Promise { 58 | 59 | try { 60 | 61 | return await this.prisma.offeringRecords.findMany(); 62 | 63 | } catch (error) { 64 | console.log(error); 65 | 66 | } finally { 67 | await this.prisma.$disconnect(); 68 | 69 | 70 | } 71 | 72 | } 73 | 74 | public async update(theDto: UpdateOfferingRecords, id: number): Promise { 75 | 76 | try { 77 | 78 | return await this.prisma.offeringRecords.update({ 79 | where: { 80 | id: id 81 | }, 82 | data: { 83 | ...theDto 84 | } 85 | }) 86 | 87 | } catch (error) { 88 | console.log(error); 89 | 90 | } finally { 91 | await this.prisma.$disconnect(); 92 | 93 | 94 | } 95 | 96 | } 97 | 98 | public async delete(id: number): Promise { 99 | 100 | try { 101 | 102 | return await this.prisma.offeringRecords.delete({ 103 | where: { 104 | id 105 | } 106 | }); 107 | 108 | } catch (error) { 109 | console.log(error); 110 | 111 | } finally { 112 | await this.prisma.$disconnect(); 113 | 114 | 115 | } 116 | 117 | } 118 | 119 | 120 | } -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import appInstance from "./app"; 2 | 3 | // Initialize app 4 | 5 | 6 | 7 | appInstance.init().then(async () => { 8 | // Start the server 9 | await appInstance.start().then(() => { 10 | console.log(`Server is running at: ${appInstance.theApp.info.uri}`); 11 | }) 12 | 13 | }); 14 | 15 | process.on('unhandledRejection', (err) => { 16 | 17 | console.log(err); 18 | process.exit(1); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /src/teachings/teachings.ts: -------------------------------------------------------------------------------- 1 | export interface Teachings { 2 | id: number; 3 | eventName: string; 4 | date: string; 5 | preacher: string; 6 | topic: string; 7 | summary: string; 8 | audioLink: string; 9 | videoLink: string; 10 | slidesLink: string; 11 | } 12 | 13 | export interface CreateTeachings { 14 | eventName: string; 15 | date: string; 16 | preacher: string; 17 | topic: string; 18 | summary: string; 19 | audioLink: string; 20 | videoLink: string; 21 | slidesLink: string; 22 | } 23 | 24 | export interface UpdateTeachings { 25 | eventName?: string; 26 | date?: string; 27 | preacher?: string; 28 | topic?: string; 29 | summary?: string; 30 | audioLink?: string; 31 | videoLink?: string; 32 | slidesLink?: string; 33 | } -------------------------------------------------------------------------------- /src/teachings/teachingsController.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import appInstance from "../app"; 3 | import { mocked } from 'ts-jest/utils'; 4 | import { Teachings } from "./teachings"; 5 | import { TeachingsService } from "./teachingsService"; 6 | 7 | describe("tests for teachings controller", () => { 8 | // set dependencies 9 | let server: Hapi.Server; 10 | 11 | // Construct dummy test data 12 | let testData: Teachings[] = [ 13 | { 14 | id: 1, 15 | eventName: "Meeting", 16 | date: "12345678", 17 | preacher: "WF Kumuyi", 18 | topic: "The beatitudes", 19 | summary: "Be true to yourself", 20 | audioLink: "jkljalkjakljkaj", 21 | videoLink: "ljkjaljkljakjaj", 22 | slidesLink: "oouiepioiep" 23 | }, 24 | { 25 | id: 2, 26 | eventName: "Coding", 27 | date: "12345678", 28 | preacher: "WF Kumuyi", 29 | topic: "The beatitudes", 30 | summary: "Be true to yourself", 31 | audioLink: "jkljalkjakljkaj", 32 | videoLink: "ljkjaljkljakjaj", 33 | slidesLink: "oouiepioiep" 34 | }, 35 | { 36 | id: 3, 37 | eventName: "Meeting", 38 | date: "12345678", 39 | preacher: "WF Kumuyi", 40 | topic: "The beatitudes", 41 | summary: "Be true to yourself", 42 | audioLink: "jkljalkjakljkaj", 43 | videoLink: "ljkjaljkljakjaj", 44 | slidesLink: "oouiepioiep" 45 | } 46 | ] 47 | 48 | // Mock service class 49 | 50 | // jest.mock("./teachingsService", () => { 51 | // return { 52 | // create: (theDto) => { 53 | // let keys = ["eventName", "date", "preacher", "topic", "summary", "audioLink", "videoLink", "slidesLink"]; 54 | // let isObject = typeof theDto === 'object'; 55 | // let isRightType = keys.every(value => theDto.hasOwnProperty(value)); 56 | // let isRightLength = Object.keys(theDto).length === keys.length; 57 | 58 | // if (isObject && isRightType && isRightLength) { 59 | // return theDto; 60 | // } else { 61 | // return { error: "Wrong implementation" } 62 | // } 63 | // }, 64 | // getById: (id) => { 65 | // // controller should convert request param from string to number 66 | // if (typeof id !== 'number') { 67 | // return { error: "Wrong implementation" } 68 | // } else { 69 | // let theItem = testData.find(value => value.id === id); 70 | // return theItem; 71 | // } 72 | // }, 73 | // getAll: () => { 74 | // return testData; 75 | // }, 76 | // update: (theDto, id) => { 77 | 78 | // if (typeof theDto === 'object') { 79 | // let theItem = testData.find(value => value.id === id); 80 | // return theItem; 81 | // } else { 82 | // return { error: "Wrong implementation" } 83 | // } 84 | 85 | // }, 86 | // delete: (id) => { 87 | // // controller should convert request param from string to number 88 | // if (typeof id !== 'number') { 89 | // return { error: "Wrong implementation" } 90 | // } else { 91 | // let theItem = testData.find(value => value.id === id); 92 | // return theItem; 93 | // } 94 | // } 95 | // } 96 | // }); 97 | 98 | // effect the mock 99 | // TODO - FIGURE OUT WHY JEST MOCKS DON'T WORK WITH HAPI'S INJECT TEST METHOD OR USE CHAI-HTTP AS ALTERNATIVE 100 | 101 | // mocked(TeachingsService, true); 102 | 103 | // Set hooks 104 | 105 | beforeAll(async () => { 106 | await appInstance.init(); 107 | server = await appInstance.theApp; 108 | }); 109 | 110 | beforeEach(async () => { 111 | 112 | }); 113 | 114 | afterAll(async () => { 115 | await server.stop(); 116 | }); 117 | 118 | 119 | // Write tests 120 | test("#create() should create the entity when passed the right input", async () => { 121 | const input = { 122 | eventName: "Coding", 123 | date: "12345678", 124 | preacher: "WF Kumuyi", 125 | topic: "The beatitudes", 126 | summary: "Be true to yourself", 127 | audioLink: "jkljalkjakljkaj", 128 | videoLink: "ljkjaljkljakjaj", 129 | slidesLink: "oouiepio" 130 | } 131 | const response = await server.inject({ 132 | method: 'POST', 133 | url: '/teachings', 134 | payload: { 135 | ...input 136 | } 137 | }); 138 | await expect(response.statusCode).toBe(201); 139 | 140 | 141 | }); 142 | 143 | 144 | test("#getAll() should function as expected", async () => { 145 | const response = await server.inject({ 146 | method: 'GET', 147 | url: '/teachings' 148 | }); 149 | 150 | await expect(response.statusCode).toBe(200); 151 | 152 | 153 | }); 154 | 155 | 156 | }); -------------------------------------------------------------------------------- /src/teachings/teachingsController.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import { CreateTeachings, UpdateTeachings } from "./teachings"; 3 | import { TeachingsService } from "./teachingsService"; 4 | import Boom from "@hapi/boom"; 5 | 6 | export class TeachingsController { 7 | 8 | 9 | public async create(request: Hapi.Request, h: Hapi.ResponseToolkit) { 10 | try { 11 | const requestBody: CreateTeachings = request.payload as CreateTeachings 12 | const result = await new TeachingsService().create(requestBody); 13 | return h.response(result).code(201); 14 | } catch (error) { 15 | request.log("error", error); 16 | return Boom.badImplementation(JSON.stringify(error)) 17 | } 18 | 19 | } 20 | 21 | 22 | public async getAll(request: Hapi.Request, h: Hapi.ResponseToolkit) { 23 | try { 24 | const result = await new TeachingsService().getAll(); 25 | return h.response(result).code(200); 26 | } catch (error) { 27 | request.log("error", error); 28 | return Boom.badImplementation(JSON.stringify(error)) 29 | } 30 | } 31 | 32 | 33 | public async getById(request: Hapi.Request, h: Hapi.ResponseToolkit) { 34 | try { 35 | const id: number = +request.params.id; 36 | const result = await new TeachingsService().getById(id); 37 | return h.response(result).code(200); 38 | } catch (error) { 39 | request.log("error", error); 40 | return Boom.badImplementation(JSON.stringify(error)) 41 | } 42 | 43 | } 44 | 45 | 46 | public async update(request: Hapi.Request, h: Hapi.ResponseToolkit) { 47 | try { 48 | const id: number = +request.params.id; 49 | const requestBody: UpdateTeachings = request.payload as UpdateTeachings; 50 | const result = await new TeachingsService().update(requestBody, id); 51 | return h.response(result).code(200); 52 | } catch (error) { 53 | request.log("error", error); 54 | return Boom.badImplementation(JSON.stringify(error)) 55 | } 56 | 57 | 58 | } 59 | 60 | 61 | public async delete(request: Hapi.Request, h: Hapi.ResponseToolkit) { 62 | try { 63 | const id: number = +request.params.id; 64 | const result = await new TeachingsService().delete(id); 65 | return h.response(result).code(200); 66 | } catch (error) { 67 | request.log("error", error); 68 | return Boom.badImplementation(JSON.stringify(error)) 69 | } 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/teachings/teachingsRoutes.ts: -------------------------------------------------------------------------------- 1 | import { TeachingsController } from "./teachingsController"; 2 | import * as Hapi from "@hapi/hapi"; 3 | 4 | // create instance of controller 5 | const controller = new TeachingsController(); 6 | 7 | // configure the routes 8 | const teachingsRoutes = { 9 | name: "teachings", 10 | register: async (server: Hapi.Server) => { 11 | server.route([ 12 | { 13 | method: 'POST', 14 | path: '/teachings', 15 | handler: controller.create, 16 | options: { 17 | tags: ['api'] 18 | } 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/teachings', 23 | handler: controller.getAll, 24 | options: { 25 | tags: ['api'] 26 | } 27 | }, 28 | { 29 | method: 'GET', 30 | path: '/teachings/{id}', 31 | handler: controller.getById, 32 | options: { 33 | tags: ['api'] 34 | } 35 | }, 36 | { 37 | method: 'PUT', 38 | path: '/teachings/{id}', 39 | handler: controller.update, 40 | options: { 41 | tags: ['api'] 42 | } 43 | }, 44 | { 45 | method: 'DELETE', 46 | path: '/teachings/{id}', 47 | handler: controller.delete, 48 | options: { 49 | tags: ['api'] 50 | } 51 | } 52 | ]); 53 | } 54 | } 55 | 56 | export default teachingsRoutes; -------------------------------------------------------------------------------- /src/teachings/teachingsService.spec.ts: -------------------------------------------------------------------------------- 1 | //Simple tests to ensure that all functions are defined. 2 | // Unlike the controller tests and the end to end tests, These tests do not make use 3 | // of Hapi server's inject method 4 | 5 | import { TeachingsService } from "./teachingsService"; 6 | 7 | describe("TeachingsService tests", () => { 8 | // Get an instance of the service 9 | const theService: TeachingsService = new TeachingsService(); 10 | 11 | // Only these simple tests here just as example, since no much business logic except CRUD 12 | test("#create() function should be defined", () => { 13 | expect(theService.create).toBeDefined(); 14 | }); 15 | 16 | test("#getAll() function should be defined", () => { 17 | expect(theService.getAll).toBeDefined(); 18 | }); 19 | 20 | test("#getById() function should be defined", () => { 21 | expect(theService.getById).toBeDefined(); 22 | }); 23 | 24 | test("#update() function should be defined", () => { 25 | expect(theService.update).toBeDefined(); 26 | }); 27 | 28 | test("#delete() function should be defined", () => { 29 | expect(theService.delete).toBeDefined(); 30 | }); 31 | 32 | }) -------------------------------------------------------------------------------- /src/teachings/teachingsService.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, Teachings } from "@prisma/client"; 2 | import { CreateTeachings, UpdateTeachings } from "./teachings"; 3 | 4 | export class TeachingsService { 5 | 6 | private prisma: PrismaClient; 7 | 8 | constructor() { 9 | 10 | this.prisma = new PrismaClient(); 11 | 12 | 13 | } 14 | 15 | public async create(theDto: CreateTeachings): Promise { 16 | 17 | try { 18 | 19 | return await this.prisma.teachings.create({ 20 | data: { 21 | ...theDto 22 | } 23 | }); 24 | 25 | } catch (error) { 26 | console.log(error); 27 | 28 | } finally { 29 | 30 | await this.prisma.$disconnect(); 31 | 32 | } 33 | 34 | } 35 | 36 | public async getById(id: number): Promise { 37 | 38 | try { 39 | 40 | return await this.prisma.teachings.findUnique({ 41 | where: { 42 | id 43 | } 44 | }); 45 | 46 | } catch (error) { 47 | console.log(error); 48 | 49 | } finally { 50 | await this.prisma.$disconnect(); 51 | 52 | 53 | } 54 | 55 | } 56 | 57 | public async getAll(): Promise { 58 | 59 | try { 60 | 61 | return await this.prisma.teachings.findMany(); 62 | 63 | } catch (error) { 64 | console.log(error); 65 | 66 | } finally { 67 | await this.prisma.$disconnect(); 68 | 69 | 70 | } 71 | 72 | } 73 | 74 | public async update(theDto: UpdateTeachings, id: number): Promise { 75 | 76 | try { 77 | 78 | return await this.prisma.teachings.update({ 79 | where: { 80 | id: id 81 | }, 82 | data: { 83 | ...theDto 84 | } 85 | }) 86 | 87 | } catch (error) { 88 | console.log(error); 89 | 90 | } finally { 91 | await this.prisma.$disconnect(); 92 | 93 | 94 | } 95 | 96 | } 97 | 98 | public async delete(id: number): Promise { 99 | 100 | try { 101 | 102 | return await this.prisma.teachings.delete({ 103 | where: { 104 | id 105 | } 106 | }); 107 | 108 | } catch (error) { 109 | console.log(error); 110 | 111 | } finally { 112 | await this.prisma.$disconnect(); 113 | 114 | 115 | } 116 | 117 | } 118 | 119 | 120 | } -------------------------------------------------------------------------------- /src/tithe-records/tithe-record.ts: -------------------------------------------------------------------------------- 1 | export interface TitheRecords { 2 | id: number; 3 | date: string; 4 | user: number; 5 | amount: number; 6 | } 7 | 8 | export interface CreateTitheRecords { 9 | date: string; 10 | user: number; 11 | amount: number; 12 | } 13 | 14 | export interface UpdateTitheRecords { 15 | date?: string; 16 | user?: number; 17 | amount?: number; 18 | } -------------------------------------------------------------------------------- /src/tithe-records/titheRecordsController.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import appInstance from "../app"; 3 | import { mocked } from 'ts-jest/utils'; 4 | import { TitheRecords } from "./tithe-record"; 5 | import { TitheRecordsService } from "./titheRecordsService"; 6 | 7 | describe("tests for TitheRecords controller", () => { 8 | // set dependencies 9 | let server: Hapi.Server; 10 | 11 | // Construct dummy test data 12 | let testData: TitheRecords[] = [ 13 | { 14 | id: 1, 15 | amount: 465.76, 16 | date: "12345678", 17 | user: 1 18 | }, 19 | { 20 | id: 2, 21 | amount: 465.76, 22 | date: "12345678", 23 | user: 3 24 | }, 25 | { 26 | id: 3, 27 | amount: 465.76, 28 | date: "12345678", 29 | user: 5 30 | } 31 | ] 32 | 33 | // Mock service class 34 | 35 | // jest.mock("./titheRecordsService", () => { 36 | // return { 37 | // create: (theDto) => { 38 | // let keys = ["amount", "date", "user"]; 39 | // let isObject = typeof theDto === 'object'; 40 | // let isRightType = keys.every(value => theDto.hasOwnProperty(value)); 41 | // let isRightLength = Object.keys(theDto).length === keys.length; 42 | 43 | // if (isObject && isRightType && isRightLength) { 44 | // return theDto; 45 | // } else { 46 | // return { error: "Wrong implementation" } 47 | // } 48 | // }, 49 | // getById: (id) => { 50 | // // controller should convert request param from string to number 51 | // if (typeof id !== 'number') { 52 | // return { error: "Wrong implementation" } 53 | // } else { 54 | // let theItem = testData.find(value => value.id === id); 55 | // return theItem; 56 | // } 57 | // }, 58 | // getAll: () => { 59 | // return testData; 60 | // }, 61 | // update: (theDto, id) => { 62 | 63 | // if (typeof theDto === 'object') { 64 | // let theItem = testData.find(value => value.id === id); 65 | // return theItem; 66 | // } else { 67 | // return { error: "Wrong implementation" } 68 | // } 69 | 70 | // }, 71 | // delete: (id) => { 72 | // // controller should convert request param from string to number 73 | // if (typeof id !== 'number') { 74 | // return { error: "Wrong implementation" } 75 | // } else { 76 | // let theItem = testData.find(value => value.id === id); 77 | // return theItem; 78 | // } 79 | // } 80 | // } 81 | // }); 82 | 83 | // effect the mock 84 | // TODO - FIGURE OUT WHY JEST MOCKS DON'T WORK WITH HAPI'S INJECT TEST METHOD OR USE CHAI-HTTP AS ALTERNATIVE 85 | 86 | // mocked(TitheRecordsService, true); 87 | 88 | // Set hooks 89 | 90 | beforeAll(async () => { 91 | await appInstance.init(); 92 | server = await appInstance.theApp; 93 | }); 94 | 95 | beforeEach(async () => { 96 | 97 | }); 98 | 99 | afterAll(async () => { 100 | await server.stop(); 101 | }); 102 | 103 | 104 | // Write tests 105 | test("#create() should create the entity when passed the right input", async () => { 106 | const input = { 107 | amount: 465.76, 108 | date: "12345678", 109 | user: 3 110 | } 111 | const response = await server.inject({ 112 | method: 'POST', 113 | url: '/tithe-records', 114 | payload: { 115 | ...input 116 | } 117 | }); 118 | await expect(response.statusCode).toBe(201); 119 | 120 | 121 | }); 122 | 123 | 124 | test("#getAll() should function as expected", async () => { 125 | const response = await server.inject({ 126 | method: 'GET', 127 | url: '/tithe-records' 128 | }); 129 | 130 | await expect(response.statusCode).toBe(200); 131 | 132 | 133 | }); 134 | 135 | }); -------------------------------------------------------------------------------- /src/tithe-records/titheRecordsController.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import { CreateTitheRecords, UpdateTitheRecords } from "./tithe-record"; 3 | import { TitheRecordsService } from "./titheRecordsService"; 4 | import Boom from "@hapi/boom"; 5 | 6 | export class TitheRecordsController { 7 | 8 | 9 | public async create(request: Hapi.Request, h: Hapi.ResponseToolkit) { 10 | try { 11 | const requestBody: CreateTitheRecords = request.payload as CreateTitheRecords 12 | const result = await new TitheRecordsService().create(requestBody); 13 | return h.response(result).code(201); 14 | } catch (error) { 15 | request.log("error", error); 16 | return Boom.badImplementation(JSON.stringify(error)) 17 | } 18 | 19 | } 20 | 21 | 22 | public async getAll(request: Hapi.Request, h: Hapi.ResponseToolkit) { 23 | try { 24 | const result = await new TitheRecordsService().getAll(); 25 | return h.response(result).code(200); 26 | } catch (error) { 27 | request.log("error", error); 28 | return Boom.badImplementation(JSON.stringify(error)) 29 | } 30 | } 31 | 32 | 33 | public async getById(request: Hapi.Request, h: Hapi.ResponseToolkit) { 34 | try { 35 | const id: number = +request.params.id; 36 | const result = await new TitheRecordsService().getById(id); 37 | return h.response(result).code(200); 38 | } catch (error) { 39 | request.log("error", error); 40 | return Boom.badImplementation(JSON.stringify(error)) 41 | } 42 | 43 | } 44 | 45 | 46 | public async update(request: Hapi.Request, h: Hapi.ResponseToolkit) { 47 | try { 48 | const id: number = +request.params.id; 49 | const requestBody: UpdateTitheRecords = request.payload as UpdateTitheRecords; 50 | const result = await new TitheRecordsService().update(requestBody, id); 51 | return h.response(result).code(200); 52 | } catch (error) { 53 | request.log("error", error); 54 | return Boom.badImplementation(JSON.stringify(error)) 55 | } 56 | 57 | 58 | } 59 | 60 | 61 | public async delete(request: Hapi.Request, h: Hapi.ResponseToolkit) { 62 | try { 63 | const id: number = +request.params.id; 64 | const result = await new TitheRecordsService().delete(id); 65 | return h.response(result).code(200); 66 | } catch (error) { 67 | request.log("error", error); 68 | return Boom.badImplementation(JSON.stringify(error)) 69 | } 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/tithe-records/titheRecordsRoutes.ts: -------------------------------------------------------------------------------- 1 | import { TitheRecordsController } from "./titheRecordsController"; 2 | import * as Hapi from "@hapi/hapi"; 3 | 4 | // create instance of controller 5 | const controller = new TitheRecordsController(); 6 | 7 | // configure the routes 8 | const titheRecordsRoutes = { 9 | name: "tithe-records", 10 | register: async (server: Hapi.Server) => { 11 | server.route([ 12 | { 13 | method: 'POST', 14 | path: '/tithe-records', 15 | handler: controller.create, 16 | options: { 17 | tags: ['api'] 18 | } 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/tithe-records', 23 | handler: controller.getAll, 24 | options: { 25 | tags: ['api'] 26 | } 27 | }, 28 | { 29 | method: 'GET', 30 | path: '/tithe-records/{id}', 31 | handler: controller.getById, 32 | options: { 33 | tags: ['api'] 34 | } 35 | }, 36 | { 37 | method: 'PUT', 38 | path: '/tithe-records/{id}', 39 | handler: controller.update, 40 | options: { 41 | tags: ['api'] 42 | } 43 | }, 44 | { 45 | method: 'DELETE', 46 | path: '/tithe-records/{id}', 47 | handler: controller.delete, 48 | options: { 49 | tags: ['api'] 50 | } 51 | } 52 | ]); 53 | } 54 | } 55 | 56 | export default titheRecordsRoutes; -------------------------------------------------------------------------------- /src/tithe-records/titheRecordsService.spec.ts: -------------------------------------------------------------------------------- 1 | //Simple tests to ensure that all functions are defined. 2 | // Unlike the controller tests and the end to end tests, These tests do not make use 3 | // of Hapi server's inject method 4 | 5 | import { TitheRecordsService } from "./titheRecordsService"; 6 | 7 | describe("TitheRecordsService tests", () => { 8 | // Get an instance of the service 9 | const theService: TitheRecordsService = new TitheRecordsService(); 10 | 11 | // Only these simple tests here just as example, since no much business logic except CRUD 12 | test("#create() function should be defined", () => { 13 | expect(theService.create).toBeDefined(); 14 | }); 15 | 16 | test("#getAll() function should be defined", () => { 17 | expect(theService.getAll).toBeDefined(); 18 | }); 19 | 20 | test("#getById() function should be defined", () => { 21 | expect(theService.getById).toBeDefined(); 22 | }); 23 | 24 | test("#update() function should be defined", () => { 25 | expect(theService.update).toBeDefined(); 26 | }); 27 | 28 | test("#delete() function should be defined", () => { 29 | expect(theService.delete).toBeDefined(); 30 | }); 31 | 32 | }) -------------------------------------------------------------------------------- /src/tithe-records/titheRecordsService.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, TitheRecords } from "@prisma/client"; 2 | import { CreateTitheRecords, UpdateTitheRecords } from "./tithe-record"; 3 | 4 | export class TitheRecordsService { 5 | 6 | private prisma: PrismaClient; 7 | 8 | constructor() { 9 | 10 | this.prisma = new PrismaClient(); 11 | 12 | 13 | } 14 | 15 | public async create(theDto: CreateTitheRecords): Promise { 16 | 17 | try { 18 | 19 | return await this.prisma.titheRecords.create({ 20 | data: { 21 | ...theDto 22 | } 23 | }); 24 | 25 | } catch (error) { 26 | console.log(error); 27 | 28 | } finally { 29 | 30 | await this.prisma.$disconnect(); 31 | 32 | } 33 | 34 | } 35 | 36 | public async getById(id: number): Promise { 37 | 38 | try { 39 | 40 | return await this.prisma.titheRecords.findUnique({ 41 | where: { 42 | id 43 | } 44 | }); 45 | 46 | } catch (error) { 47 | console.log(error); 48 | 49 | } finally { 50 | await this.prisma.$disconnect(); 51 | 52 | 53 | } 54 | 55 | } 56 | 57 | public async getAll(): Promise { 58 | 59 | try { 60 | 61 | return await this.prisma.titheRecords.findMany(); 62 | 63 | } catch (error) { 64 | console.log(error); 65 | 66 | } finally { 67 | await this.prisma.$disconnect(); 68 | 69 | 70 | } 71 | 72 | } 73 | 74 | public async update(theDto: UpdateTitheRecords, id: number): Promise { 75 | 76 | try { 77 | 78 | return await this.prisma.titheRecords.update({ 79 | where: { 80 | id: id 81 | }, 82 | data: { 83 | ...theDto 84 | } 85 | }) 86 | 87 | } catch (error) { 88 | console.log(error); 89 | 90 | } finally { 91 | await this.prisma.$disconnect(); 92 | 93 | 94 | } 95 | 96 | } 97 | 98 | public async delete(id: number): Promise { 99 | 100 | try { 101 | 102 | return await this.prisma.titheRecords.delete({ 103 | where: { 104 | id 105 | } 106 | }); 107 | 108 | } catch (error) { 109 | console.log(error); 110 | 111 | } finally { 112 | await this.prisma.$disconnect(); 113 | 114 | 115 | } 116 | 117 | } 118 | 119 | 120 | } -------------------------------------------------------------------------------- /src/users/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | email: string; 4 | name: string; 5 | role: "staff" | "member" | "pastor"; 6 | phoneNumber: string; 7 | permissions: string; 8 | } 9 | 10 | 11 | export interface CreateUser { 12 | email: string; 13 | name: string; 14 | role: "staff" | "member" | "pastor"; 15 | phoneNumber: string; 16 | permissions: string; 17 | } 18 | 19 | export interface UpdateUser { 20 | email?: string; 21 | name?: string; 22 | role?: "staff" | "member" | "pastor"; 23 | phoneNumber?: string; 24 | permissions?: string; 25 | } -------------------------------------------------------------------------------- /src/users/userController.spec.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import appInstance from "../app"; 3 | import { mocked } from 'ts-jest/utils'; 4 | import { User } from "./user"; 5 | import { UserService } from "./userService"; 6 | 7 | describe("tests for User controller", () => { 8 | // set dependencies 9 | let server: Hapi.Server; 10 | 11 | // Construct dummy test data 12 | let testData: User[] = [ 13 | { 14 | id: 1, 15 | email: "lucky@okoedion.com", 16 | name: "Lucky Okoedion", 17 | role: "staff", 18 | phoneNumber: "ajljkahakajjlak", 19 | permissions: "ljakjkajljakjaklj" 20 | }, 21 | { 22 | id: 2, 23 | email: "camilla@emmanuel.com", 24 | name: "Camilla Emmanuel", 25 | role: "pastor", 26 | phoneNumber: "ajljkahakajjlak", 27 | permissions: "ljakjkajljakjaklj" 28 | }, 29 | { 30 | id: 3, 31 | email: "mitchel@saturday.com", 32 | name: "Mitchel Saturday", 33 | role: "staff", 34 | phoneNumber: "ajljkahakajjlak", 35 | permissions: "ljakjkajljakjaklj" 36 | } 37 | ] 38 | 39 | // Mock service class 40 | 41 | // jest.mock("./userService", () => { 42 | // return { 43 | // create: (theDto) => { 44 | // let keys = ["email", "name", "role", "phoneNumber", "permissions"]; 45 | // let isObject = typeof theDto === 'object'; 46 | // let isRightType = keys.every(value => theDto.hasOwnProperty(value)); 47 | // let isRightLength = Object.keys(theDto).length === keys.length; 48 | 49 | // if (isObject && isRightType && isRightLength) { 50 | // return theDto; 51 | // } else { 52 | // return { error: "Wrong implementation" } 53 | // } 54 | // }, 55 | // getById: (id) => { 56 | // // controller should convert request param from string to number 57 | // if (typeof id !== 'number') { 58 | // return { error: "Wrong implementation" } 59 | // } else { 60 | // let theItem = testData.find(value => value.id === id); 61 | // return theItem; 62 | // } 63 | // }, 64 | // getAll: () => { 65 | // return testData; 66 | // }, 67 | // update: (theDto, id) => { 68 | 69 | // if (typeof theDto === 'object') { 70 | // let theItem = testData.find(value => value.id === id); 71 | // return theItem; 72 | // } else { 73 | // return { error: "Wrong implementation" } 74 | // } 75 | 76 | // }, 77 | // delete: (id) => { 78 | // // controller should convert request param from string to number 79 | // if (typeof id !== 'number') { 80 | // return { error: "Wrong implementation" } 81 | // } else { 82 | // let theItem = testData.find(value => value.id === id); 83 | // return theItem; 84 | // } 85 | // } 86 | // } 87 | // }); 88 | 89 | // effect the mock 90 | // TODO - FIGURE OUT WHY JEST MOCKS DON'T WORK WITH HAPI'S INJECT TEST METHOD OR USE CHAI-HTTP AS ALTERNATIVE 91 | 92 | 93 | // mocked(UserService, true); 94 | 95 | // Set hooks 96 | 97 | beforeAll(async () => { 98 | await appInstance.init(); 99 | server = await appInstance.theApp; 100 | }); 101 | 102 | beforeEach(async () => { 103 | 104 | }); 105 | 106 | afterAll(async () => { 107 | await server.stop(); 108 | }); 109 | 110 | 111 | // Write tests 112 | test("#create() should create the entity when passed the right input", async () => { 113 | const input = { 114 | email: "lucky@okoedion.com", 115 | name: "Lucky Okoedion", 116 | role: "staff", 117 | phoneNumber: "ajljkahakajjlak", 118 | permissions: "ljakjkajljakjaklj" 119 | } 120 | const response = await server.inject({ 121 | method: 'POST', 122 | url: '/users', 123 | payload: { 124 | ...input 125 | } 126 | }); 127 | await expect(response.statusCode).toBe(201); 128 | 129 | 130 | 131 | }); 132 | 133 | 134 | test("#getAll() should function as expected", async () => { 135 | const response = await server.inject({ 136 | method: 'GET', 137 | url: '/users' 138 | }); 139 | 140 | await expect(response.statusCode).toBe(200); 141 | 142 | 143 | }); 144 | 145 | 146 | }); -------------------------------------------------------------------------------- /src/users/userController.ts: -------------------------------------------------------------------------------- 1 | import * as Hapi from "@hapi/hapi"; 2 | import { CreateUser, UpdateUser } from "./user"; 3 | import { UserService } from "./userService"; 4 | import Boom from "@hapi/boom"; 5 | 6 | export class UserController { 7 | 8 | 9 | public async create(request: Hapi.Request, h: Hapi.ResponseToolkit) { 10 | try { 11 | const requestBody: CreateUser = request.payload as CreateUser 12 | const result = await new UserService().create(requestBody); 13 | return h.response(result).code(201); 14 | } catch (error) { 15 | request.log("error", error); 16 | return Boom.badImplementation(JSON.stringify(error)) 17 | } 18 | 19 | } 20 | 21 | 22 | public async getAll(request: Hapi.Request, h: Hapi.ResponseToolkit) { 23 | try { 24 | const result = await new UserService().getAll(); 25 | return h.response(result).code(200); 26 | } catch (error) { 27 | request.log("error", error); 28 | return Boom.badImplementation(JSON.stringify(error)) 29 | } 30 | } 31 | 32 | 33 | public async getById(request: Hapi.Request, h: Hapi.ResponseToolkit) { 34 | try { 35 | const id: number = +request.params.id; 36 | const result = await new UserService().getById(id); 37 | return h.response(result).code(200); 38 | } catch (error) { 39 | request.log("error", error); 40 | return Boom.badImplementation(JSON.stringify(error)) 41 | } 42 | 43 | } 44 | 45 | 46 | public async update(request: Hapi.Request, h: Hapi.ResponseToolkit) { 47 | try { 48 | const id: number = +request.params.id; 49 | const requestBody: UpdateUser = request.payload as UpdateUser; 50 | const result = await new UserService().update(requestBody, id); 51 | return h.response(result).code(200); 52 | } catch (error) { 53 | request.log("error", error); 54 | return Boom.badImplementation(JSON.stringify(error)) 55 | } 56 | 57 | 58 | } 59 | 60 | 61 | public async delete(request: Hapi.Request, h: Hapi.ResponseToolkit) { 62 | try { 63 | const id: number = +request.params.id; 64 | const result = await new UserService().delete(id); 65 | return h.response(result).code(200); 66 | } catch (error) { 67 | request.log("error", error); 68 | return Boom.badImplementation(JSON.stringify(error)) 69 | } 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/users/userService.spec.ts: -------------------------------------------------------------------------------- 1 | //Simple tests to ensure that all functions are defined. 2 | // Unlike the controller tests and the end to end tests, These tests do not make use 3 | // of Hapi server's inject method 4 | 5 | import { UserService } from "./userService"; 6 | 7 | describe("UserService tests", () => { 8 | // Get an instance of the service 9 | const theService: UserService = new UserService(); 10 | 11 | // Only these simple tests here just as example, since no much business logic except CRUD 12 | test("#create() function should be defined", () => { 13 | expect(theService.create).toBeDefined(); 14 | }); 15 | 16 | test("#getAll() function should be defined", () => { 17 | expect(theService.getAll).toBeDefined(); 18 | }); 19 | 20 | test("#getById() function should be defined", () => { 21 | expect(theService.getById).toBeDefined(); 22 | }); 23 | 24 | test("#update() function should be defined", () => { 25 | expect(theService.update).toBeDefined(); 26 | }); 27 | 28 | test("#delete() function should be defined", () => { 29 | expect(theService.delete).toBeDefined(); 30 | }); 31 | 32 | }) -------------------------------------------------------------------------------- /src/users/userService.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient, User } from "@prisma/client"; 2 | import { CreateUser, UpdateUser } from "./user"; 3 | 4 | export class UserService { 5 | 6 | private prisma: PrismaClient; 7 | 8 | constructor() { 9 | 10 | this.prisma = new PrismaClient(); 11 | 12 | 13 | } 14 | 15 | public async create(theDto: CreateUser): Promise { 16 | 17 | try { 18 | 19 | return await this.prisma.user.create({ 20 | data: { 21 | ...theDto 22 | } 23 | }); 24 | 25 | } catch (error) { 26 | console.log(error); 27 | 28 | } finally { 29 | 30 | await this.prisma.$disconnect(); 31 | 32 | } 33 | 34 | } 35 | 36 | public async getById(id: number): Promise { 37 | 38 | try { 39 | 40 | return await this.prisma.user.findUnique({ 41 | where: { 42 | id 43 | } 44 | }); 45 | 46 | } catch (error) { 47 | console.log(error); 48 | 49 | } finally { 50 | await this.prisma.$disconnect(); 51 | 52 | 53 | } 54 | 55 | } 56 | 57 | public async getAll(): Promise { 58 | 59 | try { 60 | 61 | return await this.prisma.user.findMany(); 62 | 63 | } catch (error) { 64 | console.log(error); 65 | 66 | } finally { 67 | await this.prisma.$disconnect(); 68 | 69 | 70 | } 71 | 72 | } 73 | 74 | public async update(theDto: UpdateUser, id: number): Promise { 75 | 76 | try { 77 | 78 | return await this.prisma.user.update({ 79 | where: { 80 | id: id 81 | }, 82 | data: { 83 | ...theDto 84 | } 85 | }) 86 | 87 | } catch (error) { 88 | console.log(error); 89 | 90 | } finally { 91 | await this.prisma.$disconnect(); 92 | 93 | 94 | } 95 | 96 | } 97 | 98 | public async delete(id: number): Promise { 99 | 100 | try { 101 | 102 | return await this.prisma.user.delete({ 103 | where: { 104 | id 105 | } 106 | }); 107 | 108 | } catch (error) { 109 | console.log(error); 110 | 111 | } finally { 112 | await this.prisma.$disconnect(); 113 | 114 | 115 | } 116 | 117 | } 118 | 119 | 120 | } -------------------------------------------------------------------------------- /src/users/usersRoutes.ts: -------------------------------------------------------------------------------- 1 | import { UserController } from "./userController"; 2 | import * as Hapi from "@hapi/hapi"; 3 | 4 | // create instance of controller 5 | const controller = new UserController(); 6 | 7 | // configure the routes 8 | const userRoutes = { 9 | name: "users", 10 | register: async (server: Hapi.Server) => { 11 | server.route([ 12 | { 13 | method: 'POST', 14 | path: '/users', 15 | handler: controller.create, 16 | options: { 17 | tags: ['api'] 18 | } 19 | }, 20 | { 21 | method: 'GET', 22 | path: '/users', 23 | handler: controller.getAll, 24 | options: { 25 | tags: ['api'] 26 | } 27 | }, 28 | { 29 | method: 'GET', 30 | path: '/users/{id}', 31 | handler: controller.getById, 32 | options: { 33 | tags: ['api'] 34 | } 35 | }, 36 | { 37 | method: 'PUT', 38 | path: '/users/{id}', 39 | handler: controller.update, 40 | options: { 41 | tags: ['api'] 42 | } 43 | }, 44 | { 45 | method: 'DELETE', 46 | path: '/users/{id}', 47 | handler: controller.delete, 48 | options: { 49 | tags: ['api'] 50 | } 51 | } 52 | ]); 53 | } 54 | } 55 | 56 | export default userRoutes; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "lib": ["es2021"], 6 | "module": "commonjs", 7 | "target": "es2021", 8 | "strict": false, 9 | "noImplicitAny": false, 10 | "strictNullChecks": false, 11 | "strictPropertyInitialization": false, 12 | "alwaysStrict": false, 13 | "skipLibCheck": true, 14 | "esModuleInterop": true 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules", "dist"] 18 | } --------------------------------------------------------------------------------