├── babel.config.json ├── .env.example ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20241026124158_add_user_model │ │ └── migration.sql │ ├── 20241026124317_add_contact_model │ │ └── migration.sql │ └── 20241026124653_add_address_model │ │ └── migration.sql └── schema.prisma ├── src ├── errors │ └── response-error.ts ├── main.ts ├── types │ └── user-request.ts ├── validations │ ├── validation.ts │ ├── user-validation.ts │ ├── contact-validation.ts │ └── address-validation.ts ├── models │ ├── page.ts │ ├── user-model.ts │ ├── contact-model.ts │ └── address-model.ts ├── apps │ ├── logging.ts │ ├── web.ts │ └── database.ts ├── routes │ ├── public-api.ts │ └── api.ts ├── middlewares │ ├── error-middleware.ts │ └── auth-middleware.ts ├── controllers │ ├── user-controller.ts │ ├── contact-controller.ts │ └── address-controller.ts └── services │ ├── user-service.ts │ ├── address-service.ts │ └── contact-service.ts ├── .gitignore ├── .github └── FUNDING.yml ├── LICENSE ├── package.json ├── docs ├── user.md ├── address.md └── contact.md ├── tests ├── test-util.ts ├── user.test.ts ├── contact.test.ts └── address.test.ts ├── README.md └── tsconfig.json /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Database Configuration 2 | DATABASE_URL="mysql://USER:PASSWORD@HOST:PORT/typescript_express_prisma_starter" -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /src/errors/response-error.ts: -------------------------------------------------------------------------------- 1 | export class ResponseError extends Error { 2 | constructor(public status: number, public message: string) { 3 | super(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "./apps/logging"; 2 | import { web } from "./apps/web"; 3 | 4 | web.listen(3000, () => { 5 | logger.info("Listening on port 3000"); 6 | }); 7 | -------------------------------------------------------------------------------- /src/types/user-request.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | import { Request } from "express"; 3 | 4 | export interface UserRequest extends Request { 5 | user?: User; 6 | } 7 | -------------------------------------------------------------------------------- /src/validations/validation.ts: -------------------------------------------------------------------------------- 1 | import { ZodType } from "zod"; 2 | 3 | export class Validation { 4 | static validate(schema: ZodType, data: T): T { 5 | return schema.parse(data); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/models/page.ts: -------------------------------------------------------------------------------- 1 | export type Paging = { 2 | size: number; 3 | total_page: number; 4 | current_page: number; 5 | }; 6 | 7 | export type Pageable = { 8 | data: Array; 9 | paging: Paging; 10 | }; 11 | -------------------------------------------------------------------------------- /src/apps/logging.ts: -------------------------------------------------------------------------------- 1 | import winston from "winston"; 2 | 3 | export const logger = winston.createLogger({ 4 | level: "debug", 5 | format: winston.format.json(), 6 | transports: [new winston.transports.Console({})], 7 | }); 8 | -------------------------------------------------------------------------------- /src/routes/public-api.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { UserController } from "../controllers/user-controller"; 3 | 4 | export const publicRouter = express.Router(); 5 | publicRouter.post("/api/users", UserController.register); 6 | publicRouter.post("/api/users/login", UserController.login); 7 | -------------------------------------------------------------------------------- /prisma/migrations/20241026124158_add_user_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `users` ( 3 | `username` VARCHAR(100) NOT NULL, 4 | `password` VARCHAR(100) NOT NULL, 5 | `name` VARCHAR(100) NOT NULL, 6 | `token` VARCHAR(100) NULL, 7 | 8 | PRIMARY KEY (`username`) 9 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 10 | -------------------------------------------------------------------------------- /src/apps/web.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { publicRouter } from "../routes/public-api"; 3 | import { errorMiddleware } from "../middlewares/error-middleware"; 4 | import { apiRouter } from "../routes/api"; 5 | 6 | export const web = express(); 7 | web.use(express.json()); 8 | web.use(publicRouter); 9 | web.use(apiRouter); 10 | web.use(errorMiddleware); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | package-lock.json 10 | 11 | .env 12 | node_modules 13 | dist 14 | dist-ssr 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? -------------------------------------------------------------------------------- /prisma/migrations/20241026124317_add_contact_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `contacts` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `first_name` VARCHAR(100) NOT NULL, 5 | `last_name` VARCHAR(100) NULL, 6 | `email` VARCHAR(100) NULL, 7 | `phone` VARCHAR(20) NULL, 8 | `username` VARCHAR(100) NOT NULL, 9 | 10 | PRIMARY KEY (`id`) 11 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 12 | 13 | -- AddForeignKey 14 | ALTER TABLE `contacts` ADD CONSTRAINT `contacts_username_fkey` FOREIGN KEY (`username`) REFERENCES `users`(`username`) ON DELETE RESTRICT ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /src/validations/user-validation.ts: -------------------------------------------------------------------------------- 1 | import { z, ZodType } from "zod"; 2 | 3 | export class UserValidation { 4 | static readonly REGISTER: ZodType = z.object({ 5 | username: z.string().min(1).max(100), 6 | password: z.string().min(1).max(100), 7 | name: z.string().min(1).max(100), 8 | }); 9 | 10 | static readonly LOGIN: ZodType = z.object({ 11 | username: z.string().min(1).max(100), 12 | password: z.string().min(1).max(100), 13 | }); 14 | 15 | static readonly UPDATE: ZodType = z.object({ 16 | password: z.string().min(1).max(100).optional(), 17 | name: z.string().min(1).max(100).optional(), 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/models/user-model.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | 3 | export type UserResponse = { 4 | username: string; 5 | name: string; 6 | token?: string; 7 | }; 8 | 9 | export type CreateUserRequest = { 10 | username: string; 11 | name: string; 12 | password: string; 13 | }; 14 | 15 | export type LoginUserRequest = { 16 | username: string; 17 | password: string; 18 | }; 19 | 20 | export type UpdateUserRequest = { 21 | name?: string; 22 | password?: string; 23 | }; 24 | 25 | export function toUserResponse(user: User): UserResponse { 26 | return { 27 | name: user.name, 28 | username: user.username, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /prisma/migrations/20241026124653_add_address_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `addresses` ( 3 | `id` INTEGER NOT NULL AUTO_INCREMENT, 4 | `street` VARCHAR(255) NULL, 5 | `city` VARCHAR(100) NULL, 6 | `province` VARCHAR(100) NULL, 7 | `country` VARCHAR(100) NOT NULL, 8 | `postal_code` VARCHAR(10) NOT NULL, 9 | `contact_id` INTEGER NOT NULL, 10 | 11 | PRIMARY KEY (`id`) 12 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 13 | 14 | -- AddForeignKey 15 | ALTER TABLE `addresses` ADD CONSTRAINT `addresses_contact_id_fkey` FOREIGN KEY (`contact_id`) REFERENCES `contacts`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /src/middlewares/error-middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, Request, NextFunction } from "express"; 2 | import { ZodError } from "zod"; 3 | import { ResponseError } from "../errors/response-error"; 4 | 5 | export const errorMiddleware = async ( 6 | error: Error, 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ) => { 11 | if (error instanceof ZodError) { 12 | res.status(400).json({ 13 | errors: `Validation Error : ${JSON.stringify(error)}`, 14 | }); 15 | } else if (error instanceof ResponseError) { 16 | res.status(error.status).json({ 17 | errors: error.message, 18 | }); 19 | } else { 20 | res.status(500).json({ 21 | errors: error.message, 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: berthutapea 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://saweria.co/berthutapea'] 14 | -------------------------------------------------------------------------------- /src/middlewares/auth-middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { prismaClient } from "../apps/database"; 3 | import { UserRequest } from "../types/user-request"; 4 | 5 | export const authMiddleware = async ( 6 | req: UserRequest, 7 | res: Response, 8 | next: NextFunction 9 | ) => { 10 | const token = req.get("X-API-TOKEN"); 11 | 12 | if (token) { 13 | const user = await prismaClient.user.findFirst({ 14 | where: { 15 | token: token, 16 | }, 17 | }); 18 | 19 | if (user) { 20 | req.user = user; 21 | next(); 22 | return; 23 | } 24 | } 25 | 26 | res 27 | .status(401) 28 | .json({ 29 | errors: "Unauthorized", 30 | }) 31 | .end(); 32 | }; 33 | -------------------------------------------------------------------------------- /src/apps/database.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { logger } from "./logging"; 3 | 4 | export const prismaClient = new PrismaClient({ 5 | log: [ 6 | { 7 | emit: "event", 8 | level: "query", 9 | }, 10 | { 11 | emit: "event", 12 | level: "error", 13 | }, 14 | { 15 | emit: "event", 16 | level: "info", 17 | }, 18 | { 19 | emit: "event", 20 | level: "warn", 21 | }, 22 | ], 23 | }); 24 | 25 | prismaClient.$on("error", (e) => { 26 | logger.error(e); 27 | }); 28 | 29 | prismaClient.$on("warn", (e) => { 30 | logger.warn(e); 31 | }); 32 | 33 | prismaClient.$on("info", (e) => { 34 | logger.info(e); 35 | }); 36 | 37 | prismaClient.$on("query", (e) => { 38 | logger.info(e); 39 | }); 40 | -------------------------------------------------------------------------------- /src/models/contact-model.ts: -------------------------------------------------------------------------------- 1 | import { Contact } from "@prisma/client"; 2 | 3 | export type ContactResponse = { 4 | id: number; 5 | first_name: string; 6 | last_name?: string | null; 7 | email?: string | null; 8 | phone?: string | null; 9 | }; 10 | 11 | export type CreateContactRequest = { 12 | first_name: string; 13 | last_name?: string; 14 | email?: string; 15 | phone?: string; 16 | }; 17 | 18 | export type UpdateContactRequest = { 19 | id: number; 20 | first_name: string; 21 | last_name?: string; 22 | email?: string; 23 | phone?: string; 24 | }; 25 | 26 | export type SearchContactRequest = { 27 | name?: string; 28 | phone?: string; 29 | email?: string; 30 | page: number; 31 | size: number; 32 | }; 33 | 34 | export function toContactResponse(contact: Contact): ContactResponse { 35 | return { 36 | id: contact.id, 37 | first_name: contact.first_name, 38 | last_name: contact.last_name, 39 | email: contact.email, 40 | phone: contact.phone, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/validations/contact-validation.ts: -------------------------------------------------------------------------------- 1 | import { z, ZodType } from "zod"; 2 | 3 | export class ContactValidation { 4 | static readonly CREATE: ZodType = z.object({ 5 | first_name: z.string().min(1).max(100), 6 | last_name: z.string().min(1).max(100).optional(), 7 | email: z.string().min(1).max(100).email().optional(), 8 | phone: z.string().min(1).max(20).optional(), 9 | }); 10 | 11 | static readonly UPDATE: ZodType = z.object({ 12 | id: z.number().positive(), 13 | first_name: z.string().min(1).max(100), 14 | last_name: z.string().min(1).max(100).optional(), 15 | email: z.string().min(1).max(100).email().optional(), 16 | phone: z.string().min(1).max(20).optional(), 17 | }); 18 | 19 | static readonly SEARCH: ZodType = z.object({ 20 | name: z.string().min(1).optional(), 21 | phone: z.string().min(1).optional(), 22 | email: z.string().min(1).optional(), 23 | page: z.number().min(1).positive(), 24 | size: z.number().min(1).max(100).positive(), 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Gilbert Hutapea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/models/address-model.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "@prisma/client"; 2 | 3 | export type AddressResponse = { 4 | id: number; 5 | street?: string | null; 6 | city?: string | null; 7 | province?: string | null; 8 | country: string; 9 | postal_code: string; 10 | }; 11 | 12 | export type CreateAddressRequest = { 13 | contact_id: number; 14 | street?: string; 15 | city?: string; 16 | province?: string; 17 | country: string; 18 | postal_code: string; 19 | }; 20 | 21 | export type GetAddressRequest = { 22 | contact_id: number; 23 | id: number; 24 | }; 25 | 26 | export type UpdateAddressRequest = { 27 | id: number; 28 | contact_id: number; 29 | street?: string; 30 | city?: string; 31 | province?: string; 32 | country: string; 33 | postal_code: string; 34 | }; 35 | 36 | export type RemoveAddressRequest = GetAddressRequest; 37 | 38 | export function toAddressResponse(address: Address): AddressResponse { 39 | return { 40 | id: address.id, 41 | street: address.street, 42 | city: address.city, 43 | province: address.province, 44 | country: address.country, 45 | postal_code: address.postal_code, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/validations/address-validation.ts: -------------------------------------------------------------------------------- 1 | import { z, ZodType } from "zod"; 2 | 3 | export class AddressValidation { 4 | static readonly CREATE: ZodType = z.object({ 5 | contact_id: z.number().positive(), 6 | street: z.string().min(1).max(255).optional(), 7 | city: z.string().min(1).max(100).optional(), 8 | province: z.string().min(1).max(100).optional(), 9 | country: z.string().min(1).max(100), 10 | postal_code: z.string().min(1).max(10), 11 | }); 12 | 13 | static readonly GET: ZodType = z.object({ 14 | contact_id: z.number().positive(), 15 | id: z.number().positive(), 16 | }); 17 | 18 | static readonly REMOVE: ZodType = z.object({ 19 | contact_id: z.number().positive(), 20 | id: z.number().positive(), 21 | }); 22 | 23 | static readonly UPDATE: ZodType = z.object({ 24 | id: z.number().positive(), 25 | contact_id: z.number().positive(), 26 | street: z.string().min(1).max(255).optional(), 27 | city: z.string().min(1).max(100).optional(), 28 | province: z.string().min(1).max(100).optional(), 29 | country: z.string().min(1).max(100), 30 | postal_code: z.string().min(1).max(10), 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-express-prisma-starter", 3 | "version": "1.0.0", 4 | "description": "🤖 Express.js + Prisma + TypeScript starter and boilerplate packed with useful development features", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --runInBand", 8 | "build": "tsc", 9 | "start": "node dist/main.js" 10 | }, 11 | "jest": { 12 | "transform": { 13 | "^.+\\.[t|j]sx?$": "babel-jest" 14 | } 15 | }, 16 | "author": "Gilbert Hutapea", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@prisma/client": "^5.21.1", 20 | "bcrypt": "^5.1.1", 21 | "express": "^4.21.1", 22 | "uuid": "^10.0.0", 23 | "winston": "^3.15.0", 24 | "zod": "^3.23.8" 25 | }, 26 | "devDependencies": { 27 | "@babel/preset-env": "^7.25.8", 28 | "@babel/preset-typescript": "^7.25.7", 29 | "@jest/globals": "^29.7.0", 30 | "@types/bcrypt": "^5.0.2", 31 | "@types/express": "^5.0.0", 32 | "@types/jest": "^29.5.14", 33 | "@types/supertest": "^6.0.2", 34 | "@types/uuid": "^10.0.0", 35 | "babel-jest": "^29.7.0", 36 | "jest": "^29.7.0", 37 | "prisma": "^5.21.1", 38 | "supertest": "^7.0.0", 39 | "typescript": "^5.6.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /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 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "mysql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model User { 17 | username String @id @db.VarChar(100) 18 | password String @db.VarChar(100) 19 | name String @db.VarChar(100) 20 | token String? @db.VarChar(100) 21 | 22 | contacts Contact[] 23 | 24 | @@map("users") 25 | } 26 | 27 | model Contact { 28 | id Int @id @default(autoincrement()) 29 | first_name String @db.VarChar(100) 30 | last_name String? @db.VarChar(100) 31 | email String? @db.VarChar(100) 32 | phone String? @db.VarChar(20) 33 | username String @db.VarChar(100) 34 | 35 | user User @relation(fields: [username], references: [username]) 36 | addresses Address[] 37 | 38 | @@map("contacts") 39 | } 40 | 41 | model Address { 42 | id Int @id @default(autoincrement()) 43 | street String? @db.VarChar(255) 44 | city String? @db.VarChar(100) 45 | province String? @db.VarChar(100) 46 | country String @db.VarChar(100) 47 | postal_code String @db.VarChar(10) 48 | contact_id Int 49 | 50 | contact Contact @relation(fields: [contact_id], references: [id]) 51 | 52 | @@map("addresses") 53 | } -------------------------------------------------------------------------------- /src/routes/api.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { authMiddleware } from "../middlewares/auth-middleware"; 3 | import { UserController } from "../controllers/user-controller"; 4 | import { ContactController } from "../controllers/contact-controller"; 5 | import { AddressController } from "../controllers/address-controller"; 6 | 7 | export const apiRouter = express.Router(); 8 | apiRouter.use(authMiddleware); 9 | 10 | // User API 11 | apiRouter.get("/api/users/current", UserController.get); 12 | apiRouter.patch("/api/users/current", UserController.update); 13 | apiRouter.delete("/api/users/current", UserController.logout); 14 | 15 | // Contact API 16 | apiRouter.post("/api/contacts", ContactController.create); 17 | apiRouter.get("/api/contacts/:contactId(\\d+)", ContactController.get); 18 | apiRouter.put("/api/contacts/:contactId(\\d+)", ContactController.update); 19 | apiRouter.delete("/api/contacts/:contactId(\\d+)", ContactController.remove); 20 | apiRouter.get("/api/contacts", ContactController.search); 21 | 22 | // Address API 23 | apiRouter.post( 24 | "/api/contacts/:contactId(\\d+)/addresses", 25 | AddressController.create 26 | ); 27 | apiRouter.get( 28 | "/api/contacts/:contactId(\\d+)/addresses/:addressId(\\d+)", 29 | AddressController.get 30 | ); 31 | 32 | apiRouter.put( 33 | "/api/contacts/:contactId(\\d+)/addresses/:addressId(\\d+)", 34 | AddressController.update 35 | ); 36 | 37 | apiRouter.delete( 38 | "/api/contacts/:contactId(\\d+)/addresses/:addressId(\\d+)", 39 | AddressController.remove 40 | ); 41 | 42 | apiRouter.get( 43 | "/api/contacts/:contactId(\\d+)/addresses", 44 | AddressController.list 45 | ); 46 | -------------------------------------------------------------------------------- /src/controllers/user-controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { 3 | CreateUserRequest, 4 | LoginUserRequest, 5 | UpdateUserRequest, 6 | } from "../models/user-model"; 7 | import { UserService } from "../services/user-service"; 8 | import { UserRequest } from "../types/user-request"; 9 | 10 | export class UserController { 11 | static async register(req: Request, res: Response, next: NextFunction) { 12 | try { 13 | const request: CreateUserRequest = req.body as CreateUserRequest; 14 | const response = await UserService.register(request); 15 | res.status(200).json({ 16 | data: response, 17 | }); 18 | } catch (e) { 19 | next(e); 20 | } 21 | } 22 | 23 | static async login(req: Request, res: Response, next: NextFunction) { 24 | try { 25 | const request: LoginUserRequest = req.body as LoginUserRequest; 26 | const response = await UserService.login(request); 27 | res.status(200).json({ 28 | data: response, 29 | }); 30 | } catch (e) { 31 | next(e); 32 | } 33 | } 34 | 35 | static async get(req: UserRequest, res: Response, next: NextFunction) { 36 | try { 37 | const response = await UserService.get(req.user!); 38 | res.status(200).json({ 39 | data: response, 40 | }); 41 | } catch (e) { 42 | next(e); 43 | } 44 | } 45 | 46 | static async update(req: UserRequest, res: Response, next: NextFunction) { 47 | try { 48 | const request: UpdateUserRequest = req.body as UpdateUserRequest; 49 | const response = await UserService.update(req.user!, request); 50 | res.status(200).json({ 51 | data: response, 52 | }); 53 | } catch (e) { 54 | next(e); 55 | } 56 | } 57 | 58 | static async logout(req: UserRequest, res: Response, next: NextFunction) { 59 | try { 60 | await UserService.logout(req.user!); 61 | res.status(200).json({ 62 | data: "OK", 63 | }); 64 | } catch (e) { 65 | next(e); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/user.md: -------------------------------------------------------------------------------- 1 | # User API Spec 2 | 3 | ## Register User 4 | 5 | Endpoint : POST /api/users 6 | 7 | Request Body : 8 | 9 | ```json 10 | { 11 | "username": "gilbert", 12 | "password": "confidential", 13 | "name": "Gilbert Hutapea" 14 | } 15 | ``` 16 | 17 | Response Body (Success) : 18 | 19 | ```json 20 | { 21 | "data": { 22 | "username": "gilbert", 23 | "name": "Gilbert Hutapea" 24 | } 25 | } 26 | ``` 27 | 28 | Request Body (Failed) : 29 | 30 | ```json 31 | { 32 | "errors": "username must not blank, ..." 33 | } 34 | ``` 35 | 36 | ## Login User 37 | 38 | Endpoint : POST /api/login 39 | 40 | Request Body : 41 | 42 | ```json 43 | { 44 | "username": "gilbert", 45 | "password": "confidential" 46 | } 47 | ``` 48 | 49 | Response Body (Success) : 50 | 51 | ```json 52 | { 53 | "data": { 54 | "username": "gilbert", 55 | "name": "Gilbert Hutapea", 56 | "token": "uuid" 57 | } 58 | } 59 | ``` 60 | 61 | Request Body (Failed) : 62 | 63 | ```json 64 | { 65 | "errors": "username or password wrong, ..." 66 | } 67 | ``` 68 | 69 | ## Get User 70 | 71 | Endpoint : GET /api/users/current 72 | 73 | Request Header : 74 | 75 | - X-API-TOKEN : token 76 | 77 | Response Body (Success) : 78 | 79 | ```json 80 | { 81 | "data": { 82 | "username": "gilbert", 83 | "name": "Gilbert Hutapea" 84 | } 85 | } 86 | ``` 87 | 88 | Request Body (Failed) : 89 | 90 | ```json 91 | { 92 | "errors": "Unauthorized, ..." 93 | } 94 | ``` 95 | 96 | ## Update User 97 | 98 | Endpoint : PATCH /api/users/current 99 | 100 | Request Header : 101 | 102 | - X-API-TOKEN : token 103 | 104 | Request Body : 105 | 106 | ```json 107 | { 108 | "password": "confidential", // not mandatory 109 | "name": "Gilbert Hutapea" // not mandatory 110 | } 111 | ``` 112 | 113 | Response Body (Success) : 114 | 115 | ```json 116 | { 117 | "data": { 118 | "username": "gilbert", 119 | "name": "Gilbert Hutapea" 120 | } 121 | } 122 | ``` 123 | 124 | Request Body (Failed) : 125 | 126 | ```json 127 | { 128 | "errors": "Unauthorized, ..." 129 | } 130 | ``` 131 | 132 | ## Logout User 133 | 134 | Endpoint : DELETE /api/users/current 135 | 136 | Request Header : 137 | 138 | - X-API-TOKEN : token 139 | 140 | Response Body (Success) : 141 | 142 | ```json 143 | { 144 | "data": "OK" 145 | } 146 | ``` 147 | 148 | Request Body (Failed) : 149 | 150 | ```json 151 | { 152 | "errors": "Unauthorized, ..." 153 | } 154 | ``` 155 | -------------------------------------------------------------------------------- /src/controllers/contact-controller.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response } from "express"; 2 | import { UserRequest } from "../types/user-request"; 3 | import { 4 | CreateContactRequest, 5 | SearchContactRequest, 6 | UpdateContactRequest, 7 | } from "../models/contact-model"; 8 | import { ContactService } from "../services/contact-service"; 9 | 10 | export class ContactController { 11 | static async create(req: UserRequest, res: Response, next: NextFunction) { 12 | try { 13 | const request: CreateContactRequest = req.body as CreateContactRequest; 14 | const response = await ContactService.create(req.user!, request); 15 | res.status(200).json({ 16 | data: response, 17 | }); 18 | } catch (e) { 19 | next(e); 20 | } 21 | } 22 | 23 | static async get(req: UserRequest, res: Response, next: NextFunction) { 24 | try { 25 | const contactId = Number(req.params.contactId); 26 | const response = await ContactService.get(req.user!, contactId); 27 | res.status(200).json({ 28 | data: response, 29 | }); 30 | } catch (e) { 31 | next(e); 32 | } 33 | } 34 | 35 | static async update(req: UserRequest, res: Response, next: NextFunction) { 36 | try { 37 | const request: UpdateContactRequest = req.body as UpdateContactRequest; 38 | request.id = Number(req.params.contactId); 39 | 40 | const response = await ContactService.update(req.user!, request); 41 | res.status(200).json({ 42 | data: response, 43 | }); 44 | } catch (e) { 45 | next(e); 46 | } 47 | } 48 | 49 | static async remove(req: UserRequest, res: Response, next: NextFunction) { 50 | try { 51 | const contactId = Number(req.params.contactId); 52 | const response = await ContactService.remove(req.user!, contactId); 53 | res.status(200).json({ 54 | data: "OK", 55 | }); 56 | } catch (e) { 57 | next(e); 58 | } 59 | } 60 | 61 | static async search(req: UserRequest, res: Response, next: NextFunction) { 62 | try { 63 | const request: SearchContactRequest = { 64 | name: req.query.name as string, 65 | email: req.query.email as string, 66 | phone: req.query.phone as string, 67 | page: req.query.page ? Number(req.query.page) : 1, 68 | size: req.query.size ? Number(req.query.size) : 10, 69 | }; 70 | const response = await ContactService.search(req.user!, request); 71 | res.status(200).json(response); 72 | } catch (e) { 73 | next(e); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/controllers/address-controller.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from "express"; 2 | import { UserRequest } from "../types/user-request"; 3 | import { 4 | CreateAddressRequest, 5 | GetAddressRequest, 6 | RemoveAddressRequest, 7 | UpdateAddressRequest, 8 | } from "../models/address-model"; 9 | import { AddressService } from "../services/address-service"; 10 | 11 | export class AddressController { 12 | static async create(req: UserRequest, res: Response, next: NextFunction) { 13 | try { 14 | const request: CreateAddressRequest = req.body as CreateAddressRequest; 15 | request.contact_id = Number(req.params.contactId); 16 | 17 | const response = await AddressService.create(req.user!, request); 18 | res.status(200).json({ 19 | data: response, 20 | }); 21 | } catch (e) { 22 | next(e); 23 | } 24 | } 25 | 26 | static async get(req: UserRequest, res: Response, next: NextFunction) { 27 | try { 28 | const request: GetAddressRequest = { 29 | id: Number(req.params.addressId), 30 | contact_id: Number(req.params.contactId), 31 | }; 32 | 33 | const response = await AddressService.get(req.user!, request); 34 | res.status(200).json({ 35 | data: response, 36 | }); 37 | } catch (e) { 38 | next(e); 39 | } 40 | } 41 | 42 | static async update(req: UserRequest, res: Response, next: NextFunction) { 43 | try { 44 | const request: UpdateAddressRequest = req.body as UpdateAddressRequest; 45 | request.contact_id = Number(req.params.contactId); 46 | request.id = Number(req.params.addressId); 47 | 48 | const response = await AddressService.update(req.user!, request); 49 | res.status(200).json({ 50 | data: response, 51 | }); 52 | } catch (e) { 53 | next(e); 54 | } 55 | } 56 | 57 | static async remove(req: UserRequest, res: Response, next: NextFunction) { 58 | try { 59 | const request: RemoveAddressRequest = { 60 | id: Number(req.params.addressId), 61 | contact_id: Number(req.params.contactId), 62 | }; 63 | 64 | await AddressService.remove(req.user!, request); 65 | res.status(200).json({ 66 | data: "OK", 67 | }); 68 | } catch (e) { 69 | next(e); 70 | } 71 | } 72 | 73 | static async list(req: UserRequest, res: Response, next: NextFunction) { 74 | try { 75 | const contactId = Number(req.params.contactId); 76 | const response = await AddressService.list(req.user!, contactId); 77 | res.status(200).json({ 78 | data: response, 79 | }); 80 | } catch (e) { 81 | next(e); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/test-util.ts: -------------------------------------------------------------------------------- 1 | import { prismaClient } from "../src/apps/database"; 2 | import bcrypt from "bcrypt"; 3 | import { Address, Contact, User } from "@prisma/client"; 4 | 5 | export class UserTest { 6 | static async delete() { 7 | await prismaClient.user.deleteMany({ 8 | where: { 9 | username: "test", 10 | }, 11 | }); 12 | } 13 | 14 | static async create() { 15 | await prismaClient.user.create({ 16 | data: { 17 | username: "test", 18 | name: "test", 19 | password: await bcrypt.hash("test", 10), 20 | token: "test", 21 | }, 22 | }); 23 | } 24 | 25 | static async get(): Promise { 26 | const user = await prismaClient.user.findFirst({ 27 | where: { 28 | username: "test", 29 | }, 30 | }); 31 | 32 | if (!user) { 33 | throw new Error("User is not found"); 34 | } 35 | 36 | return user; 37 | } 38 | } 39 | 40 | export class ContactTest { 41 | static async deleteAll() { 42 | await prismaClient.contact.deleteMany({ 43 | where: { 44 | username: "test", 45 | }, 46 | }); 47 | } 48 | 49 | static async create() { 50 | await prismaClient.contact.create({ 51 | data: { 52 | first_name: "test", 53 | last_name: "test", 54 | email: "test@example.com", 55 | phone: "08999999", 56 | username: "test", 57 | }, 58 | }); 59 | } 60 | 61 | static async get(): Promise { 62 | const contact = await prismaClient.contact.findFirst({ 63 | where: { 64 | username: "test", 65 | }, 66 | }); 67 | 68 | if (!contact) { 69 | throw new Error("Contact is not found"); 70 | } 71 | 72 | return contact; 73 | } 74 | } 75 | 76 | export class AddressTest { 77 | static async deleteAll() { 78 | await prismaClient.address.deleteMany({ 79 | where: { 80 | contact: { 81 | username: "test", 82 | }, 83 | }, 84 | }); 85 | } 86 | 87 | static async create() { 88 | const contact = await ContactTest.get(); 89 | await prismaClient.address.create({ 90 | data: { 91 | contact_id: contact.id, 92 | street: "Jl. Central Jakarta test", 93 | city: "Jakarta test", 94 | province: "DKI Jakarta test", 95 | country: "Indonesia", 96 | postal_code: "11111", 97 | }, 98 | }); 99 | } 100 | 101 | static async get(): Promise
{ 102 | const address = await prismaClient.address.findFirst({ 103 | where: { 104 | contact: { 105 | username: "test", 106 | }, 107 | }, 108 | }); 109 | 110 | if (!address) { 111 | throw new Error("Address is not found"); 112 | } 113 | return address; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /docs/address.md: -------------------------------------------------------------------------------- 1 | # Address API Spec 2 | 3 | ## Create Address 4 | 5 | Endpoint : POST api/contacts/:idContact/addresses 6 | 7 | Request Header : 8 | 9 | - X-API-TOKEN : token 10 | 11 | Request Body: 12 | 13 | ```json 14 | { 15 | "street": "Jl. Central Jakarta", 16 | "city": "Jakarta", 17 | "province": "DKI Jakarta", 18 | "country": "Indonesia", 19 | "postal_code": "4123" 20 | } 21 | ``` 22 | 23 | Response Body (Success) : 24 | 25 | ```json 26 | { 27 | "data": { 28 | "id": 1, 29 | "street": "Jl. Central Jakarta", 30 | "city": "Jakarta", 31 | "province": "DKI Jakarta", 32 | "country": "Indonesia", 33 | "postal_code": "4123" 34 | } 35 | } 36 | ``` 37 | 38 | Response Body (Failed) : 39 | 40 | ```json 41 | { 42 | "errors": "postal_code is required" 43 | } 44 | ``` 45 | 46 | ## Get Address 47 | 48 | Endpoint : GET api/contacts/:idContact/addresses/:idAddress 49 | 50 | Request Header : 51 | 52 | - X-API-TOKEN : token 53 | 54 | Response Body (Success) : 55 | 56 | ```json 57 | { 58 | "data": { 59 | "id": 1, 60 | "street": "Jl. Central Jakarta", 61 | "city": "Jakarta", 62 | "province": "DKI Jakarta", 63 | "country": "Indonesia", 64 | "postal_code": "4123" 65 | } 66 | } 67 | ``` 68 | 69 | Response Body (Failed) : 70 | 71 | ```json 72 | { 73 | "errors": "Address is not found" 74 | } 75 | ``` 76 | 77 | ## Update Address 78 | 79 | Endpoint : PUT api/contacts/:idContact/addresses/:idAddress 80 | 81 | Request Header : 82 | 83 | - X-API-TOKEN : token 84 | 85 | Request Body: 86 | 87 | ```json 88 | { 89 | "street": "Jl. Central Jakarta", 90 | "city": "Jakarta", 91 | "province": "DKI Jakarta", 92 | "country": "Indonesia", 93 | "postal_code": "4123" 94 | } 95 | ``` 96 | 97 | Response Body (Success) : 98 | 99 | ```json 100 | { 101 | "data": { 102 | "id": 1, 103 | "street": "Jl. Central Jakarta", 104 | "city": "Jakarta", 105 | "province": "DKI Jakarta", 106 | "country": "Indonesia", 107 | "postal_code": "4123" 108 | } 109 | } 110 | ``` 111 | 112 | Response Body (Failed) : 113 | 114 | ```json 115 | { 116 | "errors": "postal_code is required" 117 | } 118 | ``` 119 | 120 | ## Remove Address 121 | 122 | Endpoint : DELETE api/contacts/:idContact/addresses/:idAddress 123 | 124 | Request Header : 125 | 126 | - X-API-TOKEN : token 127 | 128 | Response Body (Success) : 129 | 130 | ```json 131 | { 132 | "data": "OK" 133 | } 134 | ``` 135 | 136 | Response Body (Failed) : 137 | 138 | ```json 139 | { 140 | "errors": "Address is not found" 141 | } 142 | ``` 143 | 144 | ## List Address 145 | 146 | Endpoint : GET api/contacts/:idContact/addresses 147 | 148 | Request Header : 149 | 150 | - X-API-TOKEN : token 151 | 152 | Response Body (Success) : 153 | 154 | ```json 155 | { 156 | "data": [ 157 | { 158 | "id": 1, 159 | "street": "Jl. Central Jakarta", 160 | "city": "Jakarta", 161 | "province": "DKI Jakarta", 162 | "country": "Indonesia", 163 | "postal_code": "4123" 164 | }, 165 | { 166 | "id": 2, 167 | "street": "Jl. Central Jakarta", 168 | "city": "Jakarta", 169 | "province": "DKI Jakarta", 170 | "country": "Indonesia", 171 | "postal_code": "4123" 172 | } 173 | ] 174 | } 175 | ``` 176 | 177 | Response Body (Failed) : 178 | 179 | ```json 180 | { 181 | "errors": "Contact is not found" 182 | } 183 | ``` 184 | -------------------------------------------------------------------------------- /src/services/user-service.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | import { prismaClient } from "../apps/database"; 3 | import { ResponseError } from "../errors/response-error"; 4 | import { 5 | CreateUserRequest, 6 | LoginUserRequest, 7 | toUserResponse, 8 | UpdateUserRequest, 9 | UserResponse, 10 | } from "../models/user-model"; 11 | import { UserValidation } from "../validations/user-validation"; 12 | import { Validation } from "../validations/validation"; 13 | import bcrypt from "bcrypt"; 14 | import { v4 as uuid } from "uuid"; 15 | 16 | export class UserService { 17 | static async register(request: CreateUserRequest): Promise { 18 | const registerRequest = Validation.validate( 19 | UserValidation.REGISTER, 20 | request 21 | ); 22 | 23 | const totalUserWithSameUsername = await prismaClient.user.count({ 24 | where: { 25 | username: registerRequest.username, 26 | }, 27 | }); 28 | 29 | if (totalUserWithSameUsername != 0) { 30 | throw new ResponseError(400, "Username already exists"); 31 | } 32 | 33 | registerRequest.password = await bcrypt.hash(registerRequest.password, 10); 34 | 35 | const user = await prismaClient.user.create({ 36 | data: registerRequest, 37 | }); 38 | 39 | return toUserResponse(user); 40 | } 41 | 42 | static async login(request: LoginUserRequest): Promise { 43 | const loginRequest = Validation.validate(UserValidation.LOGIN, request); 44 | 45 | let user = await prismaClient.user.findUnique({ 46 | where: { 47 | username: loginRequest.username, 48 | }, 49 | }); 50 | 51 | if (!user) { 52 | throw new ResponseError(401, "Username or password is wrong"); 53 | } 54 | 55 | const isPasswordValid = await bcrypt.compare( 56 | loginRequest.password, 57 | user.password 58 | ); 59 | if (!isPasswordValid) { 60 | throw new ResponseError(401, "Username or password is wrong"); 61 | } 62 | 63 | user = await prismaClient.user.update({ 64 | where: { 65 | username: loginRequest.username, 66 | }, 67 | data: { 68 | token: uuid(), 69 | }, 70 | }); 71 | 72 | const response = toUserResponse(user); 73 | response.token = user.token!; 74 | return response; 75 | } 76 | 77 | static async get(user: User): Promise { 78 | return toUserResponse(user); 79 | } 80 | 81 | static async update( 82 | user: User, 83 | request: UpdateUserRequest 84 | ): Promise { 85 | const updateRequest = Validation.validate(UserValidation.UPDATE, request); 86 | 87 | if (updateRequest.name) { 88 | user.name = updateRequest.name; 89 | } 90 | 91 | if (updateRequest.password) { 92 | user.password = await bcrypt.hash(updateRequest.password, 10); 93 | } 94 | 95 | const result = await prismaClient.user.update({ 96 | where: { 97 | username: user.username, 98 | }, 99 | data: user, 100 | }); 101 | 102 | return toUserResponse(result); 103 | } 104 | 105 | static async logout(user: User): Promise { 106 | const result = await prismaClient.user.update({ 107 | where: { 108 | username: user.username, 109 | }, 110 | data: { 111 | token: null, 112 | }, 113 | }); 114 | return toUserResponse(result); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /docs/contact.md: -------------------------------------------------------------------------------- 1 | # Contact API Spec 2 | 3 | ## Create Contact 4 | 5 | Endpoint : POST /api/contacts 6 | 7 | Request Header : 8 | 9 | - X-API-TOKEN : token 10 | 11 | Request Body : 12 | 13 | ```json 14 | { 15 | "first_name": "Gilbert", 16 | "last_name": "Hutapea", 17 | "email": "gilbert@example.com", 18 | "phone": "08999999999" 19 | } 20 | ``` 21 | 22 | Response Body (Success) : 23 | 24 | ```json 25 | { 26 | "data": { 27 | "id": 1, 28 | "first_name": "Gilbert", 29 | "last_name": "Hutapea", 30 | "email": "gilbert@example.com", 31 | "phone": "08999999999" 32 | } 33 | } 34 | ``` 35 | 36 | Response Body (Failed) : 37 | 38 | ```json 39 | { 40 | "errors": "first_name must not blank, ..." 41 | } 42 | ``` 43 | 44 | ## Get Contact 45 | 46 | Endpoint : GET /api/contacts/:id 47 | 48 | Request Header : 49 | 50 | - X-API-TOKEN : token 51 | 52 | Response Body (Success) : 53 | 54 | ```json 55 | { 56 | "data": { 57 | "id": 1, 58 | "first_name": "Gilbert", 59 | "last_name": "Hutapea", 60 | "email": "gilbert@example.com", 61 | "phone": "08999999999" 62 | } 63 | } 64 | ``` 65 | 66 | Response Body (Failed) : 67 | 68 | ```json 69 | { 70 | "errors": "Contact is not found" 71 | } 72 | ``` 73 | 74 | ## Update Contact 75 | 76 | Endpoint : PUT /api/contacts/:id 77 | 78 | Request Header : 79 | 80 | - X-API-TOKEN : token 81 | 82 | Request Body : 83 | 84 | ```json 85 | { 86 | "first_name": "Gilbert", 87 | "last_name": "Hutapea", 88 | "email": "gilbert@example.com", 89 | "phone": "08999999999" 90 | } 91 | ``` 92 | 93 | Response Body (Success) : 94 | 95 | ```json 96 | { 97 | "data": { 98 | "id": 1, 99 | "first_name": "Gilbert", 100 | "last_name": "Hutapea", 101 | "email": "gilbert@example.com", 102 | "phone": "08999999999" 103 | } 104 | } 105 | ``` 106 | 107 | Response Body (Failed) : 108 | 109 | ```json 110 | { 111 | "errors": "first_name must not blank, ..." 112 | } 113 | ``` 114 | 115 | ## Remove Contact 116 | 117 | Endpoint : DELETE /api/contacts/:id 118 | 119 | Request Header : 120 | 121 | - X-API-TOKEN : token 122 | 123 | Response Body (Success) : 124 | 125 | ```json 126 | { 127 | "data": "OK" 128 | } 129 | ``` 130 | 131 | Response Body (Failed) : 132 | 133 | ```json 134 | { 135 | "errors": "Contact is not found" 136 | } 137 | ``` 138 | 139 | ## Search Contact 140 | 141 | Endpoint : GET /api/contacts 142 | 143 | Query Parameter: 144 | 145 | - name : string contact first name or contact last name, optional 146 | - phone : string, contact phone, optional 147 | - email : string, contact email, optional 148 | - page : number, default 1, 149 | - size : number, default 10, 150 | 151 | Request Header : 152 | 153 | - X-API-TOKEN : token 154 | 155 | Response Body (Success) : 156 | 157 | ```json 158 | { 159 | "data": [ 160 | { 161 | "id": 1, 162 | "first_name": "Gilbert", 163 | "last_name": "Hutapea", 164 | "email": "gilbert@example.com", 165 | "phone": "08999999999" 166 | }, 167 | { 168 | "id": 2, 169 | "first_name": "Gilbert", 170 | "last_name": "Hutapea", 171 | "email": "gilbert@example.com", 172 | "phone": "08999999999" 173 | } 174 | ], 175 | "paging": { 176 | "current_page": 1, 177 | "total_page": 10, 178 | "size": 10 179 | } 180 | } 181 | ``` 182 | 183 | Response Body (Failed) : 184 | 185 | ```json 186 | { 187 | "errors": "Unauthorized" 188 | } 189 | ``` 190 | -------------------------------------------------------------------------------- /src/services/address-service.ts: -------------------------------------------------------------------------------- 1 | import { Address, User } from "@prisma/client"; 2 | import { 3 | AddressResponse, 4 | CreateAddressRequest, 5 | GetAddressRequest, 6 | RemoveAddressRequest, 7 | toAddressResponse, 8 | UpdateAddressRequest, 9 | } from "../models/address-model"; 10 | import { Validation } from "../validations/validation"; 11 | import { AddressValidation } from "../validations/address-validation"; 12 | import { ContactService } from "./contact-service"; 13 | import { prismaClient } from "../apps/database"; 14 | import { ResponseError } from "../errors/response-error"; 15 | 16 | export class AddressService { 17 | static async create( 18 | user: User, 19 | request: CreateAddressRequest 20 | ): Promise { 21 | const createRequest = Validation.validate( 22 | AddressValidation.CREATE, 23 | request 24 | ); 25 | await ContactService.checkContactMustExists( 26 | user.username, 27 | request.contact_id 28 | ); 29 | 30 | const address = await prismaClient.address.create({ 31 | data: createRequest, 32 | }); 33 | 34 | return toAddressResponse(address); 35 | } 36 | 37 | static async checkAddressMustExists( 38 | contactId: number, 39 | addressId: number 40 | ): Promise
{ 41 | const address = await prismaClient.address.findFirst({ 42 | where: { 43 | id: addressId, 44 | contact_id: contactId, 45 | }, 46 | }); 47 | 48 | if (!address) { 49 | throw new ResponseError(404, "Address is not found"); 50 | } 51 | 52 | return address; 53 | } 54 | 55 | static async get( 56 | user: User, 57 | request: GetAddressRequest 58 | ): Promise { 59 | const getRequest = Validation.validate(AddressValidation.GET, request); 60 | await ContactService.checkContactMustExists( 61 | user.username, 62 | request.contact_id 63 | ); 64 | const address = await this.checkAddressMustExists( 65 | getRequest.contact_id, 66 | getRequest.id 67 | ); 68 | return toAddressResponse(address); 69 | } 70 | 71 | static async update( 72 | user: User, 73 | request: UpdateAddressRequest 74 | ): Promise { 75 | const updateRequest = Validation.validate( 76 | AddressValidation.UPDATE, 77 | request 78 | ); 79 | await ContactService.checkContactMustExists( 80 | user.username, 81 | request.contact_id 82 | ); 83 | await this.checkAddressMustExists( 84 | updateRequest.contact_id, 85 | updateRequest.id 86 | ); 87 | 88 | const address = await prismaClient.address.update({ 89 | where: { 90 | id: updateRequest.id, 91 | contact_id: updateRequest.contact_id, 92 | }, 93 | data: updateRequest, 94 | }); 95 | 96 | return toAddressResponse(address); 97 | } 98 | 99 | static async remove( 100 | user: User, 101 | request: RemoveAddressRequest 102 | ): Promise { 103 | const removeRequest = Validation.validate( 104 | AddressValidation.REMOVE, 105 | request 106 | ); 107 | await ContactService.checkContactMustExists( 108 | user.username, 109 | request.contact_id 110 | ); 111 | await this.checkAddressMustExists( 112 | removeRequest.contact_id, 113 | removeRequest.id 114 | ); 115 | 116 | const address = await prismaClient.address.delete({ 117 | where: { 118 | id: removeRequest.id, 119 | }, 120 | }); 121 | 122 | return toAddressResponse(address); 123 | } 124 | 125 | static async list( 126 | user: User, 127 | contactId: number 128 | ): Promise> { 129 | await ContactService.checkContactMustExists(user.username, contactId); 130 | 131 | const addresses = await prismaClient.address.findMany({ 132 | where: { 133 | contact_id: contactId, 134 | }, 135 | }); 136 | 137 | return addresses.map((address) => toAddressResponse(address)); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/services/contact-service.ts: -------------------------------------------------------------------------------- 1 | import { Contact, User } from "@prisma/client"; 2 | import { 3 | ContactResponse, 4 | CreateContactRequest, 5 | SearchContactRequest, 6 | toContactResponse, 7 | UpdateContactRequest, 8 | } from "../models/contact-model"; 9 | import { ContactValidation } from "../validations/contact-validation"; 10 | import { Validation } from "../validations/validation"; 11 | import { prismaClient } from "../apps/database"; 12 | import { ResponseError } from "../errors/response-error"; 13 | import { Pageable } from "../models/page"; 14 | 15 | export class ContactService { 16 | static async create( 17 | user: User, 18 | request: CreateContactRequest 19 | ): Promise { 20 | const createRequest = Validation.validate( 21 | ContactValidation.CREATE, 22 | request 23 | ); 24 | 25 | const record = { 26 | ...createRequest, 27 | ...{ username: user.username }, 28 | }; 29 | 30 | const contact = await prismaClient.contact.create({ 31 | data: record, 32 | }); 33 | return toContactResponse(contact); 34 | } 35 | 36 | static async checkContactMustExists( 37 | username: string, 38 | contactId: number 39 | ): Promise { 40 | const contact = await prismaClient.contact.findFirst({ 41 | where: { 42 | id: contactId, 43 | username: username, 44 | }, 45 | }); 46 | 47 | if (!contact) { 48 | throw new ResponseError(404, "Contact not found"); 49 | } 50 | 51 | return contact; 52 | } 53 | 54 | static async get(user: User, id: number): Promise { 55 | const contact = await this.checkContactMustExists(user.username, id); 56 | return toContactResponse(contact); 57 | } 58 | 59 | static async update( 60 | user: User, 61 | request: UpdateContactRequest 62 | ): Promise { 63 | const updateRequest = Validation.validate( 64 | ContactValidation.UPDATE, 65 | request 66 | ); 67 | await this.checkContactMustExists(user.username, updateRequest.id); 68 | 69 | const contact = await prismaClient.contact.update({ 70 | where: { 71 | id: updateRequest.id, 72 | username: user.username, 73 | }, 74 | data: updateRequest, 75 | }); 76 | 77 | return toContactResponse(contact); 78 | } 79 | 80 | static async remove(user: User, id: number): Promise { 81 | await this.checkContactMustExists(user.username, id); 82 | 83 | const contact = await prismaClient.contact.delete({ 84 | where: { 85 | id: id, 86 | username: user.username, 87 | }, 88 | }); 89 | 90 | return toContactResponse(contact); 91 | } 92 | 93 | static async search( 94 | user: User, 95 | request: SearchContactRequest 96 | ): Promise> { 97 | const searchRequest = Validation.validate( 98 | ContactValidation.SEARCH, 99 | request 100 | ); 101 | const skip = (searchRequest.page - 1) * searchRequest.size; 102 | 103 | const filters = []; 104 | // check if name exists 105 | if (searchRequest.name) { 106 | filters.push({ 107 | OR: [ 108 | { 109 | first_name: { 110 | contains: searchRequest.name, 111 | }, 112 | }, 113 | { 114 | last_name: { 115 | contains: searchRequest.name, 116 | }, 117 | }, 118 | ], 119 | }); 120 | } 121 | // check if email exists 122 | if (searchRequest.email) { 123 | filters.push({ 124 | email: { 125 | contains: searchRequest.email, 126 | }, 127 | }); 128 | } 129 | // check if phone exists 130 | if (searchRequest.phone) { 131 | filters.push({ 132 | phone: { 133 | contains: searchRequest.phone, 134 | }, 135 | }); 136 | } 137 | 138 | const contacts = await prismaClient.contact.findMany({ 139 | where: { 140 | username: user.username, 141 | AND: filters, 142 | }, 143 | take: searchRequest.size, 144 | skip: skip, 145 | }); 146 | 147 | const total = await prismaClient.contact.count({ 148 | where: { 149 | username: user.username, 150 | AND: filters, 151 | }, 152 | }); 153 | 154 | return { 155 | data: contacts.map((contact) => toContactResponse(contact)), 156 | paging: { 157 | current_page: searchRequest.page, 158 | total_page: Math.ceil(total / searchRequest.size), 159 | size: searchRequest.size, 160 | }, 161 | }; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /tests/user.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { web } from "../src/apps/web"; 3 | import { logger } from "../src/apps/logging"; 4 | import { UserTest } from "./test-util"; 5 | import bcrypt from "bcrypt"; 6 | 7 | describe("POST /api/users", () => { 8 | afterEach(async () => { 9 | await UserTest.delete(); 10 | }); 11 | 12 | it("should reject register new user if request is invalid", async () => { 13 | const response = await supertest(web).post("/api/users").send({ 14 | username: "", 15 | password: "", 16 | name: "", 17 | }); 18 | 19 | logger.debug(response.body); 20 | expect(response.status).toBe(400); 21 | expect(response.body.errors).toBeDefined(); 22 | }); 23 | 24 | it("should register new user", async () => { 25 | const response = await supertest(web).post("/api/users").send({ 26 | username: "test", 27 | password: "test", 28 | name: "test", 29 | }); 30 | 31 | logger.debug(response.body); 32 | expect(response.status).toBe(200); 33 | expect(response.body.data.username).toBe("test"); 34 | expect(response.body.data.name).toBe("test"); 35 | }); 36 | }); 37 | 38 | describe("POST /api/users/login", () => { 39 | beforeEach(async () => { 40 | await UserTest.create(); 41 | }); 42 | 43 | afterEach(async () => { 44 | await UserTest.delete(); 45 | }); 46 | 47 | it("should be able to login", async () => { 48 | const response = await supertest(web).post("/api/users/login").send({ 49 | username: "test", 50 | password: "test", 51 | }); 52 | 53 | logger.debug(response.body); 54 | expect(response.status).toBe(200); 55 | expect(response.body.data.username).toBe("test"); 56 | expect(response.body.data.name).toBe("test"); 57 | expect(response.body.data.token).toBeDefined(); 58 | }); 59 | 60 | it("should reject login user if username is wrong", async () => { 61 | const response = await supertest(web).post("/api/users/login").send({ 62 | username: "false", 63 | password: "test", 64 | }); 65 | 66 | logger.debug(response.body); 67 | expect(response.status).toBe(401); 68 | expect(response.body.errors).toBeDefined(); 69 | }); 70 | 71 | it("should reject login user if password is wrong", async () => { 72 | const response = await supertest(web).post("/api/users/login").send({ 73 | username: "test", 74 | password: "false", 75 | }); 76 | 77 | logger.debug(response.body); 78 | expect(response.status).toBe(401); 79 | expect(response.body.errors).toBeDefined(); 80 | }); 81 | }); 82 | 83 | describe("GET /api/users/current", () => { 84 | beforeEach(async () => { 85 | await UserTest.create(); 86 | }); 87 | 88 | afterEach(async () => { 89 | await UserTest.delete(); 90 | }); 91 | 92 | it("should be able to get user", async () => { 93 | const response = await supertest(web) 94 | .get("/api/users/current") 95 | .set("X-API-TOKEN", "test"); 96 | 97 | logger.debug(response.body); 98 | expect(response.status).toBe(200); 99 | expect(response.body.data.username).toBe("test"); 100 | expect(response.body.data.name).toBe("test"); 101 | }); 102 | 103 | it("should reject get user if token is invalid", async () => { 104 | const response = await supertest(web) 105 | .get("/api/users/current") 106 | .set("X-API-TOKEN", "false"); 107 | 108 | logger.debug(response.body); 109 | expect(response.status).toBe(401); 110 | expect(response.body.errors).toBeDefined(); 111 | }); 112 | }); 113 | 114 | describe("PATCH /api/users/current", () => { 115 | beforeEach(async () => { 116 | await UserTest.create(); 117 | }); 118 | 119 | afterEach(async () => { 120 | await UserTest.delete(); 121 | }); 122 | 123 | it("should reject update user if request is invalid", async () => { 124 | const response = await supertest(web) 125 | .patch("/api/users/current") 126 | .set("X-API-TOKEN", "test") 127 | .send({ 128 | paswword: "", 129 | name: "", 130 | }); 131 | 132 | logger.debug(response.body); 133 | expect(response.status).toBe(400); 134 | expect(response.body.errors).toBeDefined; 135 | }); 136 | 137 | it("should reject update user if token is wrong", async () => { 138 | const response = await supertest(web) 139 | .patch("/api/users/current") 140 | .set("X-API-TOKEN", "false") 141 | .send({ 142 | paswword: "true", 143 | name: "true", 144 | }); 145 | 146 | logger.debug(response.body); 147 | expect(response.status).toBe(401); 148 | expect(response.body.errors).toBeDefined; 149 | }); 150 | 151 | it("should be able to update user name", async () => { 152 | const response = await supertest(web) 153 | .patch("/api/users/current") 154 | .set("X-API-TOKEN", "test") 155 | .send({ 156 | name: "true", 157 | }); 158 | 159 | logger.debug(response.body); 160 | expect(response.status).toBe(200); 161 | expect(response.body.data.name).toBe("true"); 162 | }); 163 | 164 | it("should be able to update user password", async () => { 165 | const response = await supertest(web) 166 | .patch("/api/users/current") 167 | .set("X-API-TOKEN", "test") 168 | .send({ 169 | password: "true", 170 | }); 171 | 172 | logger.debug(response.body); 173 | expect(response.status).toBe(200); 174 | 175 | const user = await UserTest.get(); 176 | expect(await bcrypt.compare("true", user.password)).toBe(true); 177 | }); 178 | }); 179 | 180 | describe("DELETE /api/users/current", () => { 181 | beforeEach(async () => { 182 | await UserTest.create(); 183 | }); 184 | 185 | afterEach(async () => { 186 | await UserTest.delete(); 187 | }); 188 | 189 | it("should be able to logout", async () => { 190 | const response = await supertest(web) 191 | .delete("/api/users/current") 192 | .set("X-API-TOKEN", "test"); 193 | 194 | logger.debug(response.body); 195 | expect(response.status).toBe(200); 196 | expect(response.body.data).toBe("OK"); 197 | 198 | const user = await UserTest.get(); 199 | expect(user.token).toBeNull(); 200 | }); 201 | 202 | it("should reject logout user if token is wrong", async () => { 203 | const response = await supertest(web) 204 | .delete("/api/users/current") 205 | .set("X-API-TOKEN", "false"); 206 | 207 | logger.debug(response.body); 208 | expect(response.status).toBe(401); 209 | expect(response.body.errors).toBeDefined(); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Express.js + Prisma + TypeScript Starter & boilerplate

2 |
3 | 🤖 Express.js + Prisma + TypeScript starter and boilerplate packed with useful development features.
4 |
5 | 6 | ## Description 7 | 8 | Starter template and boilerplate combining Express.js, Prisma, and TypeScript. This project setup offers a well-organized and type-safe environment for building scalable backend applications. With Express.js as the lightweight server framework, Prisma as the powerful ORM for seamless database interactions, and TypeScript ensuring type safety and reducing errors, this boilerplate provides a solid foundation for rapid development. Ideal for developers looking to quickly start projects with a structured codebase, best practices, and improved maintainability. 9 | 10 | - [Configuration and Setup](#configuration-and-setup) 11 | - [Technologies used](#technologies-used) 12 | - [Project Structure](#project-structure) 13 | - [Postman](#postman) 14 | - [Author](#author) 15 | - [License](#license) 16 | 17 | ## Configuration and Setup 18 | 19 | In order to run this project locally, simply fork and clone the repository or download as zip and unzip on your machine. 20 | 21 | - Open the project in your prefered code editor. 22 | - Go to terminal -> New terminal (If you are using VS code) 23 | 24 | In the first terminal 25 | 26 | ``` 27 | $ cd typescript-express-prisma-starter 28 | $ npm install 29 | $ npm run start 30 | ``` 31 | 32 | In the second terminal 33 | 34 | - Create your MySQL database, which you will use as your database 35 | - Supply the following credentials 36 | 37 | ``` 38 | # --- .env --- 39 | 40 | # Database Configuration 41 | DATABASE_URL="mysql://USER:PASSWORD@HOST:PORT/typescript_express_prisma_starter" 42 | 43 | ``` 44 | 45 | ## Technologies used 46 | 47 | This project was created using the following technologies. 48 | 49 | - [Node](https://nodejs.org/en/) — A runtime environment to help build fast server applications using TypeScript 50 | - [Express](https://www.npmjs.com/package/express) — The server for handling and routing HTTP requests 51 | - [TypeScript](https://www.npmjs.com/package/typescript) — A strongly-typed programming language that builds on JavaScript by adding static types, helping developers catch errors early and enabling better tooling support, making code more robust and maintainable. 52 | - [Prisma](https://www.prisma.io) — An Object-Relational Mapping (ORM) tool for Node.js that simplifies database access in TypeScript applications, supporting relational databases like MySQL, PostgreSQL, and SQLite. 53 | - [Jest](https://www.npmjs.com/package/jest) — A popular testing framework for TypeScript, designed to provide a fast, reliable, and easy-to-use solution for unit testing, integration testing, and mocking, making it ideal for testing Node.js and React applications. 54 | - [Supertest](https://www.npmjs.com/package/supertest) — A testing library for HTTP assertions in Node.js, commonly used to test API endpoints by simulating HTTP requests and verifying responses, making it ideal for integration testing in web applications. 55 | - [Zod](https://www.npmjs.com/package/zod) — A TypeScript first schema validation library for data parsing and validation, allowing developers to define schemas that ensure data integrity, handle errors gracefully, and provide type safety in applications. 56 | - [Bcrypt](https://www.npmjs.com/package/bcryptjs) — For data encryption 57 | - [Winston](https://www.npmjs.com/package/winston) — A versatile logging library for Node.js, allowing developers to log information with different levels, formats, and transports, making it easy to track application events, errors, and system performance. 58 | - [Babel](https://babeljs.io/setup#installation) — A JavaScript compiler that enables developers to use the latest JavaScript features by transforming modern JavaScript code into a backward-compatible version, ensuring compatibility across various environments and browsers. 59 | - [Uuid](https://www.npmjs.com/package/uuid) — A library for generating unique identifiers (UUIDs) in JavaScript, commonly used to create unique IDs for database entries, session tokens, or other cases where distinct identification is required. 60 | 61 | ## Project Structure 62 | 63 | ```bash 64 | ├── docs 65 | │ ├── address.md 66 | │ ├── contact.md 67 | │ ├── user.md 68 | ├── prisma 69 | │ ├── migrations 70 | │ ├── schema.prisma 71 | ├── src 72 | │ ├── apps 73 | │ │ ├── database.ts 74 | │ │ ├── logging.ts 75 | │ │ ├── web.ts 76 | │ ├── controllers 77 | │ │ ├── address-controller.ts 78 | │ │ ├── contact-controller.ts 79 | │ │ ├── user-controller.ts 80 | │ ├── errors 81 | │ │ ├── response-error.ts 82 | │ ├── middlewares 83 | │ │ ├── auth-middleware.ts 84 | │ │ ├── error-middleware.ts 85 | │ ├── models 86 | │ │ ├── address-model.ts 87 | │ │ ├── contact-model.ts 88 | │ │ ├── page.ts 89 | │ │ ├── user-model.ts 90 | │ ├── routes 91 | │ │ ├── api.ts 92 | │ │ ├── public-api.ts 93 | │ ├── services 94 | │ │ ├── address-service.ts 95 | │ │ ├── contact-service.ts 96 | │ │ ├── user-service.ts 97 | │ ├── types 98 | │ │ ├── user-request.ts 99 | │ ├── validations 100 | │ │ ├── address-validation.ts 101 | │ │ ├── contact-validation.ts 102 | │ │ ├── user-validation.ts 103 | │ │ ├── validation.ts 104 | │ ├── main.ts 105 | ├── tests 106 | │ ├── address.test.ts 107 | │ ├── contact.test.ts 108 | │ ├── test-util.ts 109 | │ ├── user.test.ts 110 | ├── .env.example 111 | ├── .gitignore 112 | ├── LICENSE 113 | ├── README.md 114 | ├── babel.config.json 115 | ├── package.json 116 | ├── tsconfig.json 117 | ``` 118 | 119 | ## Postman 120 | 121 | - [View API Documentation](https://documenter.getpostman.com/view/26058900/2sAY4yegUt) : https://documenter.getpostman.com/view/26058900/2sAY4yegUt 122 | 123 | ## Author 124 | 125 | - [Gilbert Hutapea](https://www.linkedin.com/in/gilberthutapea/) 126 | 127 | ## License 128 | 129 | MIT License 130 | 131 | Copyright (c) 2024 Gilbert Hutapea 132 | 133 | Permission is hereby granted, free of charge, to any person obtaining a copy 134 | of this software and associated documentation files (the "Software"), to deal 135 | in the Software without restriction, including without limitation the rights 136 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 137 | copies of the Software, and to permit persons to whom the Software is 138 | furnished to do so, subject to the following conditions: 139 | 140 | The above copyright notice and this permission notice shall be included in all 141 | copies or substantial portions of the Software. 142 | 143 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 144 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 145 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 146 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 147 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 148 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 149 | SOFTWARE. 150 | -------------------------------------------------------------------------------- /tests/contact.test.ts: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import { ContactTest, UserTest } from "./test-util"; 3 | import { web } from "../src/apps/web"; 4 | import { logger } from "../src/apps/logging"; 5 | 6 | describe("POST /api/contacts", () => { 7 | beforeEach(async () => { 8 | await UserTest.create(); 9 | }); 10 | 11 | afterEach(async () => { 12 | await ContactTest.deleteAll(); 13 | await UserTest.delete(); 14 | }); 15 | 16 | it("should create new contact", async () => { 17 | const response = await supertest(web) 18 | .post("/api/contacts") 19 | .set("X-API-TOKEN", "test") 20 | .send({ 21 | first_name: "gilbert", 22 | last_name: "hutapea", 23 | email: "gilbert@example.com", 24 | phone: "0812345", 25 | }); 26 | 27 | logger.debug(response.body); 28 | expect(response.status).toBe(200); 29 | expect(response.body.data.id).toBeDefined(); 30 | expect(response.body.data.first_name).toBe("gilbert"); 31 | expect(response.body.data.last_name).toBe("hutapea"); 32 | expect(response.body.data.email).toBe("gilbert@example.com"); 33 | expect(response.body.data.phone).toBe("0812345"); 34 | }); 35 | 36 | it("should reject create new contact if data is invalid", async () => { 37 | const response = await supertest(web) 38 | .post("/api/contacts") 39 | .set("X-API-TOKEN", "test") 40 | .send({ 41 | first_name: "", 42 | last_name: "", 43 | email: "gilbert", 44 | phone: "0812345081234508123450812345081234508123450812345081234", 45 | }); 46 | 47 | logger.debug(response.body); 48 | expect(response.status).toBe(400); 49 | expect(response.body.errors).toBeDefined(); 50 | }); 51 | }); 52 | 53 | describe("GET /api/contacts/:contactId", () => { 54 | beforeEach(async () => { 55 | await UserTest.create(); 56 | await ContactTest.create(); 57 | }); 58 | 59 | afterEach(async () => { 60 | await ContactTest.deleteAll(); 61 | await UserTest.delete(); 62 | }); 63 | 64 | it("should be able get contact", async () => { 65 | const contact = await ContactTest.get(); 66 | const response = await supertest(web) 67 | .get(`/api/contacts/${contact.id}`) 68 | .set("X-API-TOKEN", "test"); 69 | 70 | logger.debug(response.body); 71 | expect(response.status).toBe(200); 72 | expect(response.body.data.id).toBeDefined(); 73 | expect(response.body.data.first_name).toBe(contact.first_name); 74 | expect(response.body.data.last_name).toBe(contact.last_name); 75 | expect(response.body.data.email).toBe(contact.email); 76 | expect(response.body.data.phone).toBe(contact.phone); 77 | }); 78 | 79 | it("should reject get contact if contact is not found", async () => { 80 | const contact = await ContactTest.get(); 81 | const response = await supertest(web) 82 | .get(`/api/contacts/${contact.id + 1}`) 83 | .set("X-API-TOKEN", "test"); 84 | 85 | logger.debug(response.body); 86 | expect(response.status).toBe(404); 87 | expect(response.body.errors).toBeDefined(); 88 | }); 89 | }); 90 | 91 | describe("PUT /api/contacts/:contactId", () => { 92 | beforeEach(async () => { 93 | await UserTest.create(); 94 | await ContactTest.create(); 95 | }); 96 | 97 | afterEach(async () => { 98 | await ContactTest.deleteAll(); 99 | await UserTest.delete(); 100 | }); 101 | 102 | it("should be able to update contact", async () => { 103 | const contact = await ContactTest.get(); 104 | const response = await supertest(web) 105 | .put(`/api/contacts/${contact.id}`) 106 | .set("X-API-TOKEN", "test") 107 | .send({ 108 | first_name: "gilbert", 109 | last_name: "hutapea", 110 | email: "gilbert@example.com", 111 | phone: "9999", 112 | }); 113 | 114 | logger.debug(response.body); 115 | expect(response.status).toBe(200); 116 | expect(response.body.data.id).toBe(contact.id); 117 | expect(response.body.data.first_name).toBe("gilbert"); 118 | expect(response.body.data.last_name).toBe("hutapea"); 119 | expect(response.body.data.email).toBe("gilbert@example.com"); 120 | expect(response.body.data.phone).toBe("9999"); 121 | }); 122 | 123 | it("should reject update contact if request is invalid", async () => { 124 | const contact = await ContactTest.get(); 125 | const response = await supertest(web) 126 | .put(`/api/contacts/${contact.id}`) 127 | .set("X-API-TOKEN", "test") 128 | .send({ 129 | first_name: "", 130 | last_name: "", 131 | email: "gilbert", 132 | phone: "", 133 | }); 134 | 135 | logger.debug(response.body); 136 | expect(response.status).toBe(400); 137 | expect(response.body.errors).toBeDefined; 138 | }); 139 | }); 140 | 141 | describe("DELETE /api/contacts/:contactId", () => { 142 | beforeEach(async () => { 143 | await UserTest.create(); 144 | await ContactTest.create(); 145 | }); 146 | 147 | afterEach(async () => { 148 | await ContactTest.deleteAll(); 149 | await UserTest.delete(); 150 | }); 151 | 152 | it("should be able to remove contact", async () => { 153 | const contact = await ContactTest.get(); 154 | const response = await supertest(web) 155 | .delete(`/api/contacts/${contact.id}`) 156 | .set("X-API-TOKEN", "test"); 157 | 158 | logger.debug(response.body); 159 | expect(response.status).toBe(200); 160 | expect(response.body.data).toBe("OK"); 161 | }); 162 | 163 | it("should reject remove contact if contact is not found", async () => { 164 | const contact = await ContactTest.get(); 165 | const response = await supertest(web) 166 | .delete(`/api/contacts/${contact.id + 1}`) 167 | .set("X-API-TOKEN", "test"); 168 | 169 | logger.debug(response.body); 170 | expect(response.status).toBe(404); 171 | expect(response.body.errors).toBeDefined(); 172 | }); 173 | }); 174 | 175 | describe("GET /api/contacts", () => { 176 | beforeEach(async () => { 177 | await UserTest.create(); 178 | await ContactTest.create(); 179 | }); 180 | 181 | afterEach(async () => { 182 | await ContactTest.deleteAll(); 183 | await UserTest.delete(); 184 | }); 185 | 186 | it("should be able to search contact", async () => { 187 | const response = await supertest(web) 188 | .get("/api/contacts") 189 | .set("X-API-TOKEN", "test"); 190 | 191 | logger.debug(response.body); 192 | expect(response.status).toBe(200); 193 | expect(response.body.data.length).toBe(1); 194 | expect(response.body.paging.current_page).toBe(1); 195 | expect(response.body.paging.total_page).toBe(1); 196 | expect(response.body.paging.size).toBe(10); 197 | }); 198 | 199 | it("should be able to search contact using name", async () => { 200 | const response = await supertest(web) 201 | .get("/api/contacts") 202 | .query({ 203 | name: "es", 204 | }) 205 | .set("X-API-TOKEN", "test"); 206 | 207 | logger.debug(response.body); 208 | expect(response.status).toBe(200); 209 | expect(response.body.data.length).toBe(1); 210 | expect(response.body.paging.current_page).toBe(1); 211 | expect(response.body.paging.total_page).toBe(1); 212 | expect(response.body.paging.size).toBe(10); 213 | }); 214 | 215 | it("should be able to search contact using email", async () => { 216 | const response = await supertest(web) 217 | .get("/api/contacts") 218 | .query({ 219 | email: ".com", 220 | }) 221 | .set("X-API-TOKEN", "test"); 222 | 223 | logger.debug(response.body); 224 | expect(response.status).toBe(200); 225 | expect(response.body.data.length).toBe(1); 226 | expect(response.body.paging.current_page).toBe(1); 227 | expect(response.body.paging.total_page).toBe(1); 228 | expect(response.body.paging.size).toBe(10); 229 | }); 230 | 231 | it("should be able to search contact using phone", async () => { 232 | const response = await supertest(web) 233 | .get("/api/contacts") 234 | .query({ 235 | phone: "99", 236 | }) 237 | .set("X-API-TOKEN", "test"); 238 | 239 | logger.debug(response.body); 240 | expect(response.status).toBe(200); 241 | expect(response.body.data.length).toBe(1); 242 | expect(response.body.paging.current_page).toBe(1); 243 | expect(response.body.paging.total_page).toBe(1); 244 | expect(response.body.paging.size).toBe(10); 245 | }); 246 | 247 | it("should be able to search contact no result", async () => { 248 | const response = await supertest(web) 249 | .get("/api/contacts") 250 | .query({ 251 | name: "salah", 252 | }) 253 | .set("X-API-TOKEN", "test"); 254 | 255 | logger.debug(response.body); 256 | expect(response.status).toBe(200); 257 | expect(response.body.data.length).toBe(0); 258 | expect(response.body.paging.current_page).toBe(1); 259 | expect(response.body.paging.total_page).toBe(0); 260 | expect(response.body.paging.size).toBe(10); 261 | }); 262 | 263 | it("should be able to search contact with paging", async () => { 264 | const response = await supertest(web) 265 | .get("/api/contacts") 266 | .query({ 267 | page: 2, 268 | size: 1, 269 | }) 270 | .set("X-API-TOKEN", "test"); 271 | 272 | logger.debug(response.body); 273 | expect(response.status).toBe(200); 274 | expect(response.body.data.length).toBe(0); 275 | expect(response.body.paging.current_page).toBe(2); 276 | expect(response.body.paging.total_page).toBe(1); 277 | expect(response.body.paging.size).toBe(1); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /tests/address.test.ts: -------------------------------------------------------------------------------- 1 | import { AddressTest, ContactTest, UserTest } from "./test-util"; 2 | import supertest from "supertest"; 3 | import { web } from "../src/apps/web"; 4 | import { logger } from "../src/apps/logging"; 5 | 6 | describe("POST /api/contacts/:contactId/addresses", () => { 7 | beforeEach(async () => { 8 | await UserTest.create(); 9 | await ContactTest.create(); 10 | }); 11 | afterEach(async () => { 12 | await AddressTest.deleteAll(); 13 | await ContactTest.deleteAll(); 14 | await UserTest.delete(); 15 | }); 16 | 17 | it("should be able to create address", async () => { 18 | const contact = await ContactTest.get(); 19 | const response = await supertest(web) 20 | .post(`/api/contacts/${contact.id}/addresses`) 21 | .set("X-API-TOKEN", "test") 22 | .send({ 23 | street: "Jl. Central Jakarta", 24 | city: "Jakarta", 25 | province: "DKI Jakarta", 26 | country: "Indonesia", 27 | postal_code: "11111", 28 | }); 29 | 30 | logger.debug(response.body); 31 | expect(response.status).toBe(200); 32 | expect(response.body.data.id).toBeDefined(); 33 | expect(response.body.data.street).toBe("Jl. Central Jakarta"); 34 | expect(response.body.data.city).toBe("Jakarta"); 35 | expect(response.body.data.province).toBe("DKI Jakarta"); 36 | expect(response.body.data.country).toBe("Indonesia"); 37 | expect(response.body.data.postal_code).toBe("11111"); 38 | }); 39 | 40 | it("should reject create new address if request is invalid", async () => { 41 | const contact = await ContactTest.get(); 42 | const response = await supertest(web) 43 | .post(`/api/contacts/${contact.id}/addresses`) 44 | .set("X-API-TOKEN", "test") 45 | .send({ 46 | street: "Jl. Central Jakarta", 47 | city: "Jakarta", 48 | province: "DKI Jakarta", 49 | country: "", 50 | postal_code: "", 51 | }); 52 | 53 | logger.debug(response.body); 54 | expect(response.status).toBe(400); 55 | expect(response.body.errors).toBeDefined(); 56 | }); 57 | 58 | it("should reject create new address if contact is not found", async () => { 59 | const contact = await ContactTest.get(); 60 | const response = await supertest(web) 61 | .post(`/api/contacts/${contact.id + 1}/addresses`) 62 | .set("X-API-TOKEN", "test") 63 | .send({ 64 | street: "Jl. Central Jakarta", 65 | city: "Jakarta", 66 | province: "DKI Jakarta", 67 | country: "Indonesia", 68 | postal_code: "11111", 69 | }); 70 | 71 | logger.debug(response.body); 72 | expect(response.status).toBe(404); 73 | expect(response.body.errors).toBeDefined(); 74 | }); 75 | }); 76 | 77 | describe("GET /api/contacts/:contactId/addresses/:addressId", () => { 78 | beforeEach(async () => { 79 | await UserTest.create(); 80 | await ContactTest.create(); 81 | await AddressTest.create(); 82 | }); 83 | afterEach(async () => { 84 | await AddressTest.deleteAll(); 85 | await ContactTest.deleteAll(); 86 | await UserTest.delete(); 87 | }); 88 | 89 | it("should be able to get address", async () => { 90 | const contact = await ContactTest.get(); 91 | const address = await AddressTest.get(); 92 | 93 | const response = await supertest(web) 94 | .get(`/api/contacts/${contact.id}/addresses/${address.id}`) 95 | .set("X-API-TOKEN", "test"); 96 | 97 | logger.debug(response.body); 98 | expect(response.status).toBe(200); 99 | expect(response.body.data.id).toBeDefined(); 100 | expect(response.body.data.street).toBe(address.street); 101 | expect(response.body.data.city).toBe(address.city); 102 | expect(response.body.data.province).toBe(address.province); 103 | expect(response.body.data.country).toBe(address.country); 104 | expect(response.body.data.postal_code).toBe(address.postal_code); 105 | }); 106 | 107 | it("should reject get address if address is not found", async () => { 108 | const contact = await ContactTest.get(); 109 | const address = await AddressTest.get(); 110 | 111 | const response = await supertest(web) 112 | .get(`/api/contacts/${contact.id}/addresses/${address.id + 1}`) 113 | .set("X-API-TOKEN", "test"); 114 | 115 | logger.debug(response.body); 116 | expect(response.status).toBe(404); 117 | expect(response.body.errors).toBeDefined(); 118 | }); 119 | 120 | it("should reject get address if contact is not found", async () => { 121 | const contact = await ContactTest.get(); 122 | const address = await AddressTest.get(); 123 | 124 | const response = await supertest(web) 125 | .get(`/api/contacts/${contact.id + 1}/addresses/${address.id}`) 126 | .set("X-API-TOKEN", "test"); 127 | 128 | logger.debug(response.body); 129 | expect(response.status).toBe(404); 130 | expect(response.body.errors).toBeDefined(); 131 | }); 132 | }); 133 | 134 | describe("PUT /api/contacts/:contactId/addresses/:addressId", () => { 135 | beforeEach(async () => { 136 | await UserTest.create(); 137 | await ContactTest.create(); 138 | await AddressTest.create(); 139 | }); 140 | afterEach(async () => { 141 | await AddressTest.deleteAll(); 142 | await ContactTest.deleteAll(); 143 | await UserTest.delete(); 144 | }); 145 | 146 | it("should be able to update address", async () => { 147 | const contact = await ContactTest.get(); 148 | const address = await AddressTest.get(); 149 | 150 | const response = await supertest(web) 151 | .put(`/api/contacts/${contact.id}/addresses/${address.id}`) 152 | .set("X-API-TOKEN", "test") 153 | .send({ 154 | street: "Jl. Central Jakarta", 155 | city: "Jakarta", 156 | province: "DKI Jakarta", 157 | country: "Indonesia", 158 | postal_code: "11111", 159 | }); 160 | 161 | logger.debug(response.body); 162 | expect(response.status).toBe(200); 163 | expect(response.body.data.id).toBe(address.id); 164 | expect(response.body.data.street).toBe("Jl. Central Jakarta"); 165 | expect(response.body.data.city).toBe("Jakarta"); 166 | expect(response.body.data.province).toBe("DKI Jakarta"); 167 | expect(response.body.data.country).toBe("Indonesia"); 168 | expect(response.body.data.postal_code).toBe("11111"); 169 | }); 170 | 171 | it("should reject update address if data is invalid", async () => { 172 | const contact = await ContactTest.get(); 173 | const address = await AddressTest.get(); 174 | 175 | const response = await supertest(web) 176 | .put(`/api/contacts/${contact.id}/addresses/${address.id}`) 177 | .set("X-API-TOKEN", "test") 178 | .send({ 179 | street: "Jl. Central Jakarta", 180 | city: "Jakarta", 181 | province: "DKI Jakarta", 182 | country: "", 183 | postal_code: "", 184 | }); 185 | 186 | logger.debug(response.body); 187 | expect(response.status).toBe(400); 188 | expect(response.body.errors).toBeDefined(); 189 | }); 190 | 191 | it("should reject update address if address is not found", async () => { 192 | const contact = await ContactTest.get(); 193 | const address = await AddressTest.get(); 194 | 195 | const response = await supertest(web) 196 | .put(`/api/contacts/${contact.id}/addresses/${address.id + 1}`) 197 | .set("X-API-TOKEN", "test") 198 | .send({ 199 | street: "Jl. Central Jakarta", 200 | city: "Jakarta", 201 | province: "DKI Jakarta", 202 | country: "Indonesia", 203 | postal_code: "1111", 204 | }); 205 | 206 | logger.debug(response.body); 207 | expect(response.status).toBe(404); 208 | expect(response.body.errors).toBeDefined(); 209 | }); 210 | 211 | it("should reject update address if contact is not found", async () => { 212 | const contact = await ContactTest.get(); 213 | const address = await AddressTest.get(); 214 | 215 | const response = await supertest(web) 216 | .put(`/api/contacts/${contact.id + 1}/addresses/${address.id}`) 217 | .set("X-API-TOKEN", "test") 218 | .send({ 219 | street: "Jl. Central Jakarta", 220 | city: "Jakarta", 221 | province: "DKI Jakarta", 222 | country: "Indonesia", 223 | postal_code: "1111", 224 | }); 225 | 226 | logger.debug(response.body); 227 | expect(response.status).toBe(404); 228 | expect(response.body.errors).toBeDefined(); 229 | }); 230 | }); 231 | 232 | describe("DELETE /api/contacts/:contactId/addresses/:addressId", () => { 233 | beforeEach(async () => { 234 | await UserTest.create(); 235 | await ContactTest.create(); 236 | await AddressTest.create(); 237 | }); 238 | afterEach(async () => { 239 | await AddressTest.deleteAll(); 240 | await ContactTest.deleteAll(); 241 | await UserTest.delete(); 242 | }); 243 | 244 | it("should be able to remove address", async () => { 245 | const contact = await ContactTest.get(); 246 | const address = await AddressTest.get(); 247 | 248 | const response = await supertest(web) 249 | .delete(`/api/contacts/${contact.id}/addresses/${address.id}`) 250 | .set("X-API-TOKEN", "test"); 251 | 252 | logger.debug(response.body); 253 | expect(response.status).toBe(200); 254 | expect(response.body.data).toBe("OK"); 255 | }); 256 | 257 | it("should reject remove address if address is not found", async () => { 258 | const contact = await ContactTest.get(); 259 | const address = await AddressTest.get(); 260 | 261 | const response = await supertest(web) 262 | .delete(`/api/contacts/${contact.id}/addresses/${address.id + 1}`) 263 | .set("X-API-TOKEN", "test"); 264 | 265 | logger.debug(response.body); 266 | expect(response.status).toBe(404); 267 | expect(response.body.errors).toBeDefined(); 268 | }); 269 | 270 | it("should reject remove address if contact is not found", async () => { 271 | const contact = await ContactTest.get(); 272 | const address = await AddressTest.get(); 273 | 274 | const response = await supertest(web) 275 | .delete(`/api/contacts/${contact.id + 1}/addresses/${address.id}`) 276 | .set("X-API-TOKEN", "test"); 277 | 278 | logger.debug(response.body); 279 | expect(response.status).toBe(404); 280 | expect(response.body.errors).toBeDefined(); 281 | }); 282 | }); 283 | 284 | describe("GET /api/contacts/:contactId/addresses", () => { 285 | beforeEach(async () => { 286 | await UserTest.create(); 287 | await ContactTest.create(); 288 | await AddressTest.create(); 289 | }); 290 | afterEach(async () => { 291 | await AddressTest.deleteAll(); 292 | await ContactTest.deleteAll(); 293 | await UserTest.delete(); 294 | }); 295 | 296 | it("should be able to list addresses", async () => { 297 | const contact = await ContactTest.get(); 298 | 299 | const response = await supertest(web) 300 | .get(`/api/contacts/${contact.id}/addresses`) 301 | .set("X-API-TOKEN", "test"); 302 | 303 | logger.debug(response.body); 304 | expect(response.status).toBe(200); 305 | expect(response.body.data.length).toBe(1); 306 | }); 307 | 308 | it("should reject list address if contact is not found", async () => { 309 | const contact = await ContactTest.get(); 310 | 311 | const response = await supertest(web) 312 | .get(`/api/contacts/${contact.id + 1}/addresses`) 313 | .set("X-API-TOKEN", "test"); 314 | 315 | logger.debug(response.body); 316 | expect(response.status).toBe(404); 317 | expect(response.body.errors).toBeDefined(); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*", "tests/**/*"], 3 | "compilerOptions": { 4 | /* Visit https://aka.ms/tsconfig to read more about this file */ 5 | 6 | /* Projects */ 7 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 8 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 9 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 10 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 11 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 12 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 13 | 14 | /* Language and Environment */ 15 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 16 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 27 | 28 | /* Modules */ 29 | "module": "commonjs" /* Specify what module code is generated. */, 30 | // "rootDir": "./", /* Specify the root folder within your source files. */ 31 | "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, 32 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 33 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 35 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 36 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 39 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 40 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 41 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 42 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 43 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "noEmit": true, /* Disable emitting files from a compilation. */ 60 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 61 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 62 | // "removeComments": true, /* Disable emitting comments. */ 63 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 64 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 65 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 66 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 67 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 68 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 69 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 70 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 71 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 72 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 73 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 74 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 80 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 81 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 82 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 83 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 84 | 85 | /* Type Checking */ 86 | "strict": true /* Enable all strict type-checking options. */, 87 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 88 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 89 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 90 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 91 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 92 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 93 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 94 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 95 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 96 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 97 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 98 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 99 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 100 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 101 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 102 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 103 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 104 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 105 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 106 | 107 | /* Completeness */ 108 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 109 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 110 | } 111 | } 112 | --------------------------------------------------------------------------------