Skip to content

Develop #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
17 changes: 17 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
root: true,
env: {
node: true,
}
};
43 changes: 41 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
node_modules
# Keep environment variables out of version control
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
*.swp

pids
logs
results
tmp

# Build
public/css/main.css

# Coverage reports
coverage

# API keys and secrets
.env

# Dependency directory
node_modules
bower_components

# Editors
.idea
.husky
*.iml

# OS metadata
.DS_Store
Thumbs.db

# Ignore built ts files
dist/**/*

# ignore yarn.lock
yarn.lock
5 changes: 5 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
trailingComma: "all",
tabWidth: 2,
singleQuote: true,
};
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: npm run start
65 changes: 63 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,66 @@
# rest-api-with-hapi-typescript-tsoa-prisma-and-postgresql

This is a church office management REST api built with Hapi js, TypeScript, Prisma ORM and Postgresql. With Unit tests and End-toEnd tests.
## 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 an effective setup for complex projects.

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.

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.

Prisma ORM is a typescript ORM that helps with database migration, etc.

## Documentation link for reference and demo

[LINK TO DOCUMENTATION](https://church-management-api.herokuapp.com/documentation)

## SIDE NOTE

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.

## TODO

Handle all errors instead of simply logging to the console. For now, you will see the logs when you run the tests.

## Installation

```bash
$ npm install
```

## Run Database Migration with Prisma ORM

This will automatically create the relevant tables in the database

```bash
$ npm run migrate-db
```

## Build the app to compile typscript

This will also create the prisma client required for performing database operations in the app

```bash
$ npm run build
```

## Start application / server

```bash
$ npm run start
```

## Start development server with auto reload

```bash
$ npm run dev
```

## Run automated tests

Test without watching

```bash
$ npm run test
```

It is an example of how to modularize / structure hapi API implementation into controllers and services. Good separation of concerns for ease of maintenance for large projects, and type safety, courtesy of typescript.
92 changes: 68 additions & 24 deletions dist/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,96 @@ var __importStar = (this && this.__importStar) || function (mod) {
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.appInstance = void 0;
const Hapi = __importStar(require("@hapi/hapi"));
const attendanceSumRoutes_1 = __importDefault(require("./attendance-sum/attendanceSumRoutes"));
const attendanceRoutes_1 = __importDefault(require("./attendance/attendanceRoutes"));
const baseRoute_1 = __importDefault(require("./baseRoute"));
const inventoryRoutes_1 = __importDefault(require("./inventory/inventoryRoutes"));
const offeringRecordsRoutes_1 = __importDefault(require("./offering-records/offeringRecordsRoutes"));
const teachingsRoutes_1 = __importDefault(require("./teachings/teachingsRoutes"));
const titheRecordsRoutes_1 = __importDefault(require("./tithe-records/titheRecordsRoutes"));
const usersRoutes_1 = __importDefault(require("./users/usersRoutes"));
const HapiSwagger = __importStar(require("hapi-swagger"));
const Inert = __importStar(require("@hapi/inert"));
const Vision = __importStar(require("@hapi/vision"));
class App {
constructor() {
}
// function to initialize the server after routes have been registered
init() {
// set up server
this.theApp = Hapi.server({
port: process.env.PORT || 3000,
host: process.env.HOST || '0.0.0.0',
});
// register routes
this.theApp.register([
attendanceRoutes_1.default,
attendanceSumRoutes_1.default,
inventoryRoutes_1.default,
offeringRecordsRoutes_1.default,
teachingsRoutes_1.default,
titheRecordsRoutes_1.default,
usersRoutes_1.default
]).then(() => {
console.log("Route(s) have been registered");
});
// initialize app with routes
this.theApp.initialize().then(() => {
console.log("The app has been initialized");
return __awaiter(this, void 0, void 0, function* () {
// set up server
this.theApp = Hapi.server({
port: process.env.PORT || 3000,
host: process.env.HOST || '0.0.0.0',
});
// Configure swagger documentation
const swaggerOptions = {
info: {
title: "Church Office Management REST API Documentation",
version: "1.0.0",
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.",
contact: {
name: "Lucky Okoedion",
url: "https://www.linkedin.com/in/lucky-okoedion-28b7286a/"
}
}
};
const swaggerPlugins = [
{
plugin: Inert
},
{
plugin: Vision
},
{
plugin: HapiSwagger,
options: swaggerOptions
}
];
// register swagger plugins
yield this.theApp.register(swaggerPlugins, { once: true });
// register routes
yield this.theApp.register([
baseRoute_1.default,
attendanceRoutes_1.default,
attendanceSumRoutes_1.default,
inventoryRoutes_1.default,
offeringRecordsRoutes_1.default,
teachingsRoutes_1.default,
titheRecordsRoutes_1.default,
usersRoutes_1.default
], { once: true }).then(() => __awaiter(this, void 0, void 0, function* () {
var _a;
console.log("Route(s) have been registered");
// initialize app with routes
yield ((_a = this.theApp) === null || _a === void 0 ? void 0 : _a.initialize().then(() => {
console.log("The app has been initialized");
}));
}));
});
}
// Function to start the server for the main application or for tests
start() {
this.theApp.start().then(() => {
console.log(`Server running at: ${this.theApp.info.uri}`);
var _a;
return __awaiter(this, void 0, void 0, function* () {
yield ((_a = this.theApp) === null || _a === void 0 ? void 0 : _a.start());
});
}
}
// create singleton for use in main app or for tests.
exports.appInstance = new App();
const appInstance = new App();
exports.default = appInstance;
25 changes: 20 additions & 5 deletions dist/attendance-sum/attendanceSumRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,42 @@ const attendanceSumRoutes = {
{
method: 'POST',
path: '/attendance-sum',
handler: controller.create
handler: controller.create,
options: {
tags: ['api']
}
},
{
method: 'GET',
path: '/attendance-sum',
handler: controller.getAll
handler: controller.getAll,
options: {
tags: ['api']
}
},
{
method: 'GET',
path: '/attendance-sum/{id}',
handler: controller.getById
handler: controller.getById,
options: {
tags: ['api']
}
},
{
method: 'PUT',
path: '/attendance-sum/{id}',
handler: controller.update
handler: controller.update,
options: {
tags: ['api']
}
},
{
method: 'DELETE',
path: '/attendance-sum/{id}',
handler: controller.delete
handler: controller.delete,
options: {
tags: ['api']
}
}
]);
})
Expand Down
2 changes: 1 addition & 1 deletion dist/attendance/attendanceController.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.AttendanceController = void 0;
const attendanceService_1 = require("./attendanceService");
const boom_1 = __importDefault(require("boom"));
const boom_1 = __importDefault(require("@hapi/boom"));
class AttendanceController {
create(request, h) {
return __awaiter(this, void 0, void 0, function* () {
Expand Down
27 changes: 21 additions & 6 deletions dist/attendance/attendanceRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,48 @@ const attendanceController_1 = require("./attendanceController");
const controller = new attendanceController_1.AttendanceController();
// configure the routes
const attendanceRoutes = {
name: "attendance",
name: "theAttendance",
register: (server) => __awaiter(void 0, void 0, void 0, function* () {
server.route([
{
method: 'POST',
path: '/attendance',
handler: controller.create
handler: controller.create,
options: {
tags: ['api']
}
},
{
method: 'GET',
path: '/attendance',
handler: controller.getAll
handler: controller.getAll,
options: {
tags: ['api']
}
},
{
method: 'GET',
path: '/attendance/{id}',
handler: controller.getById
handler: controller.getById,
options: {
tags: ['api']
}
},
{
method: 'PUT',
path: '/attendance/{id}',
handler: controller.update
handler: controller.update,
options: {
tags: ['api']
}
},
{
method: 'DELETE',
path: '/attendance/{id}',
handler: controller.delete
handler: controller.delete,
options: {
tags: ['api']
}
}
]);
})
Expand Down
Loading