├── .nvmrc ├── .husky ├── .gitignore ├── pre-commit ├── pre-push └── commit-msg ├── .env.example ├── .eslintignore ├── src ├── load-env-vars.ts ├── users │ ├── domain │ │ ├── user.ts │ │ ├── user-not-found.ts │ │ └── user-repository.ts │ ├── infrastructure │ │ ├── user-repository │ │ │ ├── user-database.json │ │ │ ├── mongo-user-repository.ts │ │ │ ├── mysql-user-repository.ts │ │ │ └── elastic-user-repository.ts │ │ ├── http │ │ │ ├── user-router.ts │ │ │ └── user-get-controller.ts │ │ └── dependencies.ts │ └── application │ │ └── user-by-id-finder.ts ├── config.ts ├── health │ ├── health-controller.ts │ └── health-router.ts └── main.ts ├── .gitignore ├── commitlint.config.js ├── jest.config.js ├── tests └── dummy-test.test.ts ├── tsconfig.prod.json ├── .github ├── workflows │ ├── conventional-label.yml │ ├── nodejs.yml │ └── check-pr-title.yml └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.json ├── .eslintrc.js ├── README.md ├── LICENSE └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.0.0 2 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | DATABASE=mongo 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /src/load-env-vars.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | config(); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .tmp 4 | .idea 5 | .env 6 | coverage/ 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run test 5 | npm run build -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /src/users/domain/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | constructor(public readonly id: string, public readonly name: string) {} 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | cacheDirectory: '.tmp/jestCache' 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy-test.test.ts: -------------------------------------------------------------------------------- 1 | describe("Dummy test", () => { 2 | it("One should be One", () => { 3 | expect(1).toBe(1); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | server: { 3 | port: process.env.PORT || 3000, 4 | }, 5 | database: process.env.DATABASE || "mongo", 6 | }; 7 | -------------------------------------------------------------------------------- /src/users/domain/user-not-found.ts: -------------------------------------------------------------------------------- 1 | export class UserNotFound extends Error { 2 | constructor(id: string) { 3 | super(`User not found "${id}"`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/users/domain/user-repository.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./user"; 2 | 3 | export interface UserRepository { 4 | getById(id: string): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | }, 6 | "exclude": ["node_modules", "tests", "playground"] 7 | } 8 | -------------------------------------------------------------------------------- /src/users/infrastructure/user-repository/user-database.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "name": "juan" 5 | }, 6 | { 7 | "id": "2", 8 | "name": "pepe" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /src/health/health-controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | export class HealthController { 4 | async run(req: Request, res: Response) { 5 | res.status(200).send(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/conventional-label.yml: -------------------------------------------------------------------------------- 1 | name: conventional-release-labels 2 | 3 | on: 4 | pull_request_target: 5 | types: [ opened, edited ] 6 | 7 | jobs: 8 | label: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: bcoe/conventional-release-labels@v1 -------------------------------------------------------------------------------- /src/users/infrastructure/http/user-router.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | import { userGetController } from "../dependencies"; 4 | 5 | const userRouter = express.Router(); 6 | 7 | userRouter.get("/:id", userGetController.run.bind(userGetController)); 8 | 9 | export { userRouter }; 10 | -------------------------------------------------------------------------------- /src/health/health-router.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | import { HealthController } from "./health-controller"; 4 | 5 | const healthRouter = express.Router(); 6 | 7 | const healthController = new HealthController(); 8 | 9 | healthRouter.get("/", healthController.run.bind(healthController)); 10 | 11 | export { healthRouter }; 12 | -------------------------------------------------------------------------------- /src/users/application/user-by-id-finder.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../domain/user"; 2 | import { UserNotFound } from "../domain/user-not-found"; 3 | import { UserRepository } from "../domain/user-repository"; 4 | 5 | export class UserByIdFinder { 6 | constructor(private readonly userRepository: UserRepository) {} 7 | 8 | async run(id: string): Promise { 9 | const user = await this.userRepository.getById(id); 10 | 11 | if (!user) { 12 | throw new UserNotFound(id); 13 | } 14 | 15 | return user; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/users/infrastructure/user-repository/mongo-user-repository.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../../domain/user"; 2 | import { UserRepository } from "../../domain/user-repository"; 3 | import userDatabase from "./user-database.json"; 4 | 5 | export class MongoUserRepository implements UserRepository { 6 | async getById(id: string): Promise { 7 | console.log("Using Mongo!"); 8 | 9 | const rawUser = userDatabase.find((user) => user.id === id); 10 | 11 | return rawUser ? new User(rawUser.id, rawUser.name) : null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/users/infrastructure/user-repository/mysql-user-repository.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../../domain/user"; 2 | import { UserRepository } from "../../domain/user-repository"; 3 | import userDatabase from "./user-database.json"; 4 | 5 | export class MySQLUserRepository implements UserRepository { 6 | async getById(id: string): Promise { 7 | console.log("Using MySQL!"); 8 | 9 | const rawUser = userDatabase.find((user) => user.id === id); 10 | 11 | return rawUser ? new User(rawUser.id, rawUser.name) : null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/users/infrastructure/user-repository/elastic-user-repository.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../../domain/user"; 2 | import { UserRepository } from "../../domain/user-repository"; 3 | import userDatabase from "./user-database.json"; 4 | 5 | export class ElasticUserRepository implements UserRepository { 6 | async getById(id: string): Promise { 7 | console.log("Using Elastic!"); 8 | 9 | const rawUser = userDatabase.find((user) => user.id === id); 10 | 11 | return rawUser ? new User(rawUser.id, rawUser.name) : null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": false, 9 | "rootDir": ".", 10 | "strict": true, 11 | "noEmit": false, 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "outDir": "./dist" 15 | }, 16 | "include": [ 17 | "src/**/**.ts", 18 | "tests/**/**.ts", 19 | "playground/**/**.ts", 20 | ], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Use Node.js 18 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: 18 14 | - name: npm install 15 | run: | 16 | npm install 17 | - name: npm run build 18 | run: | 19 | npm run build --if-present 20 | npm run lint 21 | - name: npm test 22 | run: | 23 | npm test 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin', 'simple-import-sort'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | rules: { 13 | "simple-import-sort/imports": "error", 14 | "simple-import-sort/exports": "error", 15 | }, 16 | root: true, 17 | env: { 18 | node: true, 19 | jest: true, 20 | }, 21 | ignorePatterns: ['.eslintrc.js', "commitlint.config.js", "jest.config.js"], 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/check-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Check PR title 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: aslafy-z/conventional-pr-title-action@v2.2.5 15 | with: 16 | success-state: Title follows the specification. 17 | failure-state: Title does not follow the specification. 18 | context-name: conventional-pr-title 19 | preset: conventional-changelog-angular@latest 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { config as dotEnvConfig } from "dotenv"; 2 | dotEnvConfig(); 3 | 4 | import bodyParser from "body-parser"; 5 | import express from "express"; 6 | 7 | import { config } from "./config"; 8 | import { healthRouter } from "./health/health-router"; 9 | import { userRouter } from "./users/infrastructure/http/user-router"; 10 | 11 | function boostrap() { 12 | const app = express(); 13 | 14 | app.use(bodyParser.json()); 15 | app.use("/health", healthRouter); 16 | app.use("/users", userRouter); 17 | 18 | const { port } = config.server; 19 | 20 | app.listen(port, () => { 21 | console.log(`[APP] - Starting application on port ${port}`); 22 | }); 23 | } 24 | 25 | boostrap(); 26 | -------------------------------------------------------------------------------- /src/users/infrastructure/http/user-get-controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | import { UserByIdFinder } from "../../application/user-by-id-finder"; 4 | import { UserNotFound } from "../../domain/user-not-found"; 5 | 6 | export class UserGetController { 7 | constructor(private readonly userByIdFinder: UserByIdFinder) {} 8 | 9 | async run(req: Request, res: Response) { 10 | const { id } = req.params; 11 | 12 | try { 13 | const user = await this.userByIdFinder.run(id); 14 | return res.status(200).send(user); 15 | } catch (error) { 16 | if (error instanceof UserNotFound) { 17 | return res.status(404).send(); 18 | } 19 | 20 | return res.status(500).send(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/users/infrastructure/dependencies.ts: -------------------------------------------------------------------------------- 1 | import { config } from "../../config"; 2 | import { UserByIdFinder } from "../application/user-by-id-finder"; 3 | import { UserRepository } from "../domain/user-repository"; 4 | import { UserGetController } from "./http/user-get-controller"; 5 | import { ElasticUserRepository } from "./user-repository/elastic-user-repository"; 6 | import { MongoUserRepository } from "./user-repository/mongo-user-repository"; 7 | import { MySQLUserRepository } from "./user-repository/mysql-user-repository"; 8 | 9 | const getUserRepository = (): UserRepository => { 10 | switch (config.database) { 11 | case "mongo": 12 | return new MongoUserRepository(); 13 | case "elastic": 14 | return new ElasticUserRepository(); 15 | case "mySQL": 16 | return new MySQLUserRepository(); 17 | default: 18 | throw new Error("Invalid Database type"); 19 | } 20 | }; 21 | 22 | const userByIdFinder = new UserByIdFinder(getUserRepository()); 23 | 24 | export const userGetController = new UserGetController(userByIdFinder); 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Repository Pattern Typescript Example

2 | 3 |

4 | Example of how to implement the repository pattern using Typescript 5 |

6 | 7 |

8 | nodejs 9 |

10 | 11 | ## Table of Contents 12 | 13 | - [Installing](#installing) 14 | - [Building](#building) 15 | - [Testing](#testing) 16 | - [Linting](#linting) 17 | - [Contributing](#contributing) 18 | 19 | ## Installing 20 | 21 | ```bash 22 | nvm install 18.0.0 23 | nvm use 24 | npm install npm@8.3.0 -g 25 | npm install 26 | ``` 27 | 28 | ## Building 29 | 30 | ```bash 31 | npm run build 32 | ``` 33 | 34 | ## Testing 35 | 36 | ### Jest with Testing Library 37 | 38 | ```bash 39 | npm run test 40 | ``` 41 | 42 | ## Linting 43 | 44 | Run the linter 45 | 46 | ```bash 47 | npm run lint 48 | ``` 49 | 50 | Fix lint issues automatically 51 | 52 | ```bash 53 | npm run lint:fix 54 | ``` 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 Codely Enseña y Entretiene SL. https://codely.tv 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to Appium? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | - [ ] Documentation Update (if none of the other choices apply) 14 | 15 | ## Checklist 16 | 17 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 18 | 19 | - [ ] I have added tests that prove my fix is effective or that my feature works 20 | - [ ] I have added necessary documentation (if appropriate) 21 | - [ ] Any dependent changes have been merged and published in downstream modules 22 | 23 | ## Further comments 24 | 25 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repository-pattern-typescript-example", 3 | "version": "1.0.0", 4 | "description": "Example of how to implement the repository pattern using Typescript", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "node dist/src/main.js", 8 | "start:dev": "tsnd --respawn src/main.ts", 9 | "test": "npm run test:unit", 10 | "test:unit": "NODE_ENV=test jest --coverage", 11 | "prepare": "husky install", 12 | "lint": "eslint --ignore-path .gitignore . --ext .js,.ts", 13 | "lint:fix": "npm run lint -- --fix", 14 | "build": "npm run build:clean && npm run build:tsc", 15 | "build:clean": "rimraf dist; exit 0", 16 | "build:tsc": "tsc -p tsconfig.prod.json" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/AlbertHernandez/repository-pattern-typescript-example.git" 21 | }, 22 | "keywords": [ 23 | "typescript", 24 | "express", 25 | "skeleton" 26 | ], 27 | "author": "alberthernandezdev@gmail.com", 28 | "license": "MIT", 29 | "engines": { 30 | "node": ">=18.0.0", 31 | "npm": ">=9.3.0" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/AlbertHernandez/repository-pattern-typescript-example/issues" 35 | }, 36 | "homepage": "https://github.com/AlbertHernandez/repository-pattern-typescript-example#readme", 37 | "devDependencies": { 38 | "@commitlint/cli": "^17.4.2", 39 | "@commitlint/config-conventional": "^17.4.2", 40 | "@types/express": "^4.17.15", 41 | "@types/jest": "^29.2.5", 42 | "@typescript-eslint/eslint-plugin": "^5.48.1", 43 | "@typescript-eslint/parser": "^5.48.1", 44 | "eslint": "^8.31.0", 45 | "eslint-config-prettier": "^8.6.0", 46 | "eslint-plugin-prettier": "^4.2.1", 47 | "eslint-plugin-simple-import-sort": "^8.0.0", 48 | "husky": "^8.0.3", 49 | "jest": "^29.3.1", 50 | "lint-staged": "^13.1.0", 51 | "prettier": "^2.8.3", 52 | "rimraf": "^4.0.4", 53 | "ts-jest": "^29.0.5", 54 | "ts-node-dev": "^2.0.0", 55 | "typescript": "^4.9.4" 56 | }, 57 | "lint-staged": { 58 | "*.(js|ts)": [ 59 | "npm run lint:fix" 60 | ] 61 | }, 62 | "dependencies": { 63 | "body-parser": "^1.20.1", 64 | "dotenv": "^16.0.3", 65 | "express": "^4.18.2" 66 | } 67 | } 68 | --------------------------------------------------------------------------------