├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── README.md ├── docker-compose.yml ├── nest-cli.json ├── package-lock.json ├── package.json ├── pm2.yaml ├── src ├── app.module.ts ├── components │ └── product │ │ ├── dto │ │ └── create-product.dto.ts │ │ ├── entity │ │ └── product.entity.ts │ │ ├── interface │ │ ├── product.repository.interface.ts │ │ └── product.service.interface.ts │ │ ├── model │ │ └── product.search.object.ts │ │ ├── product.controller.ts │ │ ├── product.module.ts │ │ └── product.service.ts ├── constant │ └── common.ts ├── database │ └── config │ │ └── ormconfig.ts ├── main.ts ├── observers │ ├── observer.module.ts │ └── subscribers │ │ └── product.subscriber.ts ├── repositories │ ├── base │ │ ├── base.abstract.repository.ts │ │ └── base.interface.repository.ts │ └── product.repository.ts └── services │ └── search │ ├── config │ └── config.search.ts │ ├── constant │ └── product.elastic.ts │ ├── interface │ └── search.service.interface.ts │ ├── search-index │ └── product.elastic.index.ts │ ├── search.module.ts │ └── search.service.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | SERVER_TIMEOUT=1080000 3 | SERVER_PORT=3002 4 | 5 | DATABASE_HOST=localhost 6 | DATABASE_PORT=27017 7 | DATABASE_USERNAME=root 8 | DATABASE_PASSWORD='' 9 | DATABASE_NAME=admin 10 | DATABASE_TYPE=mongodb 11 | DATABASE_CONNECTION_TIME_OUT=150000 12 | DATABASE_ACQUIRE_TIME_OUT=150000 13 | DATABASE_CONNECTION_LIMIT=20 14 | 15 | ELASTIC_SEARCH_URL=http://elasticsearch:9200/ 16 | ELASTIC_SEARCH_HOST=elasticsearch 17 | ELASTIC_SEARCH_PORT=9200 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | .env 20 | .docker 21 | data 22 | 23 | # IDEs and editors 24 | /.idea 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | # Install PM2 4 | RUN npm install -g pm2 5 | 6 | # Set working directory 7 | RUN mkdir -p /var/www/nest-demo 8 | WORKDIR /var/www/nest-demo 9 | 10 | # add `/usr/src/app/node_modules/.bin` to $PATH 11 | ENV PATH /var/www/nest-demo/node_modules/.bin:$PATH 12 | # create user with no password 13 | RUN adduser --disabled-password demo 14 | 15 | # Copy existing application directory contents 16 | COPY . /var/www/nest-demo 17 | # install and cache app dependencies 18 | COPY package.json /var/www/nest-demo/package.json 19 | COPY package-lock.json /var/www/nest-demo/package-lock.json 20 | 21 | # grant a permission to the application 22 | RUN chown -R demo:demo /var/www/nest-demo 23 | USER demo 24 | 25 | # clear application caching 26 | RUN npm cache clean --force 27 | # install all dependencies 28 | RUN npm install 29 | 30 | EXPOSE 3002 31 | # start run in production environment 32 | #CMD [ "npm", "run", "pm2:delete" ] 33 | #CMD [ "npm", "run", "build-docker:dev" ] 34 | 35 | # start run in development environment 36 | CMD [ "npm", "run", "start:dev" ] 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | * Implement Elasticsearch in NestJS in a real-world project and run on Docker 2 | 3 | * Follow these steps below to make sure the application running in the correct way 4 | 5 | - Run the command `npm i` to install all the application dependencies 6 | - Modify `.env.example` file to `.env` and config the corresponding information into `.env` file 7 | - Run the command `npm run migration` to migrate all tables into the database 8 | - Run the command `npm run format` to format all coding styles 9 | - Run the command `npm run lint` to checking the coding convention are strictly 10 | - To start the application rely on the NestJS Framework CLI, let following to those commands shows below: 11 | + Run the command `npm run start:dev` to starting development the application 12 | + Run the command `npm run start:prod` to starting production the application 13 | + Run the command `npm run build` to build the application 14 | - To start the application and run in the clusters, let following to those commands shows below: 15 | + Run the command `npm run build:dev` to starting development the application 16 | + Run the command `npm run build:prod` to starting production the application 17 | - Run the application in Docker Environment (Currently, this point to development environment) 18 | + Make sure your local machine has been installed the Docker 19 | + Run the command `docker-compose up` to starting development the application 20 | + Note*: There are some others docker command need to hand on your self 21 | 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker compose version 2 | version: '3.7' 3 | # all the containers have to declare inside services 4 | services: 5 | # App service 6 | demoapp: 7 | # application rely on database running 8 | depends_on: 9 | - db 10 | # this build context will take the commands from Dockerfile 11 | build: 12 | context: . 13 | dockerfile: Dockerfile 14 | # image name 15 | image: nest-demo-docker 16 | # container name 17 | container_name: demoapp 18 | # always restart the container if it stops. 19 | restart: always 20 | # docker run -t is allow 21 | tty: true 22 | # application port, this is take value from env file 23 | ports: 24 | - "${SERVER_PORT}:${SERVER_PORT}" 25 | # working directory 26 | working_dir: /var/www/nest-demo 27 | # application environment 28 | environment: 29 | SERVICE_NAME: demoapp 30 | SERVICE_TAGS: dev 31 | SERVICE_DB_HOST: ${DATABASE_HOST}:${DATABASE_PORT} 32 | SERVICE_DB_USER: ${DATABASE_USERNAME} 33 | SERVICE_DB_PASSWORD: ${DATABASE_PASSWORD} 34 | SERVICE_ES_HOST: ${ELASTIC_SEARCH_HOST}:${ELASTIC_SEARCH_PORT} 35 | ELASTICSEARCH_URL: ${ELASTIC_SEARCH_URL} 36 | # save (persist) data and also to share data between containers 37 | volumes: 38 | - ./:/var/www/nest-demo 39 | - /var/www/nest-demo/node_modules 40 | # application network, each container for a service joins this network 41 | networks: 42 | - nest-demo-network 43 | # Database service 44 | db: 45 | # pull image from docker hub 46 | image: mongo 47 | # container name 48 | container_name: nestmongo 49 | # always restart the container if it stops. 50 | restart: always 51 | # database credentials, this is take value from env file 52 | environment: 53 | MONGO_INITDB_ROOT_DATABASE: ${DATABASE_NAME} 54 | MONGO_INITDB_ROOT_USERNAME: ${DATABASE_USERNAME} 55 | MONGO_INITDB_ROOT_PASSWORD: ${DATABASE_PASSWORD} 56 | # save (persist) data and also to share data between containers 57 | volumes: 58 | - db_data:/data/db 59 | # database port 60 | ports: 61 | - "${DATABASE_PORT}:${DATABASE_PORT}" 62 | # application network, each container for a service joins this network 63 | networks: 64 | - nest-demo-network 65 | 66 | elasticsearch: 67 | image: docker.elastic.co/elasticsearch/elasticsearch:7.9.0 68 | container_name: elasticsearch 69 | environment: 70 | - node.name=elasticsearch 71 | - http.port=9200 72 | - http.host=0.0.0.0 73 | - transport.host=127.0.0.1 74 | - cluster.name=es-docker-cluster 75 | - discovery.seed_hosts=elasticsearch 76 | - cluster.initial_master_nodes=elasticsearch 77 | - bootstrap.memory_lock=true 78 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 79 | ulimits: 80 | memlock: 81 | soft: -1 82 | hard: -1 83 | volumes: 84 | - es_data:/var/lib/elasticsearch 85 | ports: 86 | - "${ELASTIC_SEARCH_PORT}:${ELASTIC_SEARCH_PORT}" 87 | networks: 88 | - nest-demo-network 89 | 90 | #Docker Networks 91 | networks: 92 | # All container connect in a network 93 | nest-demo-network: 94 | driver: bridge 95 | # save (persist) data 96 | volumes: 97 | db_data: {} 98 | es_data: {} 99 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-demo", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json", 22 | "migration": "nest start --watch && ts-node ./node_modules/typeorm/cli.js migration:run", 23 | "build:dev": "nest build && pm2 start pm2.yaml", 24 | "build:prod": "nest build && NODE_PORT=3002 pm2 start pm2.yaml -n nest-demo --env production", 25 | "pm2:delete": "pm2 delete nest-demo-app", 26 | "build-docker:prod": "nest build && NODE_PORT=3002 pm2 start pm2.yaml -n nest-demo --no-daemon --env production", 27 | "build-docker:dev": "nest build && NODE_PORT=3002 pm2 start pm2.yaml -n nest-demo --no-daemon --env development" 28 | }, 29 | "dependencies": { 30 | "@elastic/elasticsearch": "^7.9.0", 31 | "@nestjs/common": "^7.0.0", 32 | "@nestjs/config": "^0.5.0", 33 | "@nestjs/core": "^7.0.0", 34 | "@nestjs/elasticsearch": "^7.1.0", 35 | "@nestjs/platform-express": "^7.0.0", 36 | "@nestjs/typeorm": "^7.1.0", 37 | "class-transformer": "^0.3.1", 38 | "class-validator": "^0.12.2", 39 | "dotenv": "^8.2.0", 40 | "elasticsearch": "^16.7.1", 41 | "mongodb": "^3.6.0", 42 | "pm2": "^4.4.1", 43 | "reflect-metadata": "^0.1.13", 44 | "rimraf": "^3.0.2", 45 | "rxjs": "^6.5.4", 46 | "typeorm": "^0.2.25", 47 | "typescript-transform-paths": "^2.0.3" 48 | }, 49 | "devDependencies": { 50 | "@nestjs/cli": "^7.0.0", 51 | "@nestjs/schematics": "^7.0.0", 52 | "@nestjs/testing": "^7.0.0", 53 | "@types/express": "^4.17.3", 54 | "@types/jest": "25.2.3", 55 | "@types/node": "^13.9.1", 56 | "@types/supertest": "^2.0.8", 57 | "@typescript-eslint/eslint-plugin": "3.0.2", 58 | "@typescript-eslint/parser": "3.0.2", 59 | "eslint": "7.1.0", 60 | "eslint-config-prettier": "^6.10.0", 61 | "eslint-plugin-import": "^2.20.1", 62 | "jest": "26.0.1", 63 | "prettier": "^1.19.1", 64 | "supertest": "^4.0.2", 65 | "ts-jest": "26.1.0", 66 | "ts-loader": "^6.2.1", 67 | "ts-node": "^8.6.2", 68 | "tsconfig-paths": "^3.9.0", 69 | "typescript": "^3.7.4" 70 | }, 71 | "jest": { 72 | "moduleFileExtensions": [ 73 | "js", 74 | "json", 75 | "ts" 76 | ], 77 | "rootDir": "src", 78 | "testRegex": ".spec.ts$", 79 | "transform": { 80 | "^.+\\.(t|j)s$": "ts-jest" 81 | }, 82 | "coverageDirectory": "../coverage", 83 | "testEnvironment": "node" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pm2.yaml: -------------------------------------------------------------------------------- 1 | apps: 2 | - script: ./dist/main.js 3 | name: nest-demo-app 4 | watch: true 5 | instances: max 6 | exec_mode: cluster 7 | env: 8 | PORT: ${SERVER_PORT} 9 | NODE_ENV: development 10 | env_production: 11 | NODE_PORT: ${SERVER_PORT} 12 | NODE_ENV: production 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { ConfigModule } from "@nestjs/config"; 3 | import { TypeOrmModule } from "@nestjs/typeorm"; 4 | import { ormConfig } from "@database/config/ormconfig"; 5 | import { ProductModule } from "@components/product/product.module"; 6 | import { SearchModule } from "@services/search/search.module"; 7 | import { ObserverModule } from "@observers/observer.module"; 8 | 9 | @Module({ 10 | imports: [ 11 | ConfigModule.forRoot({ 12 | isGlobal: true 13 | }), 14 | TypeOrmModule.forRoot(ormConfig()), 15 | ProductModule, 16 | SearchModule, 17 | ObserverModule 18 | ], 19 | controllers: [], 20 | providers: [] 21 | }) 22 | export class AppModule {} 23 | -------------------------------------------------------------------------------- /src/components/product/dto/create-product.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString} from "class-validator"; 2 | 3 | export class CreateProductDto { 4 | @IsNotEmpty() 5 | @IsString() 6 | name: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | description: string; 11 | 12 | @IsNotEmpty() 13 | @IsString() 14 | price: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/product/entity/product.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, ObjectIdColumn 4 | } from "typeorm"; 5 | 6 | @Entity({ name: "products" }) 7 | export class Product { 8 | @ObjectIdColumn() 9 | id: number; 10 | 11 | @Column({ 12 | type: "string" 13 | }) 14 | name: string; 15 | 16 | @Column({ 17 | type: "string" 18 | }) 19 | description: string; 20 | 21 | @Column({ 22 | type: "string" 23 | }) 24 | price: string; 25 | 26 | @Column({ 27 | type: "date" 28 | }) 29 | createdAt: any; 30 | 31 | @Column({ 32 | type: "date" 33 | }) 34 | updatedAt: any; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/product/interface/product.repository.interface.ts: -------------------------------------------------------------------------------- 1 | import { BaseInterfaceRepository } from '@repositories/base/base.interface.repository'; 2 | import { Product } from "@components/product/entity/product.entity"; 3 | 4 | export interface ProductRepositoryInterface extends BaseInterfaceRepository { 5 | } 6 | -------------------------------------------------------------------------------- /src/components/product/interface/product.service.interface.ts: -------------------------------------------------------------------------------- 1 | import { CreateProductDto } from "@components/product/dto/create-product.dto"; 2 | import { Product } from "@components/product/entity/product.entity"; 3 | 4 | export interface ProductServiceInterface { 5 | create(productDto: CreateProductDto): Promise; 6 | 7 | update(productId: any, updateProduct: any): Promise; 8 | 9 | search(q: any): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/product/model/product.search.object.ts: -------------------------------------------------------------------------------- 1 | import { productIndex } from "../../../services/search/constant/product.elastic"; 2 | 3 | export class ElasticSearchBody { 4 | size: number; 5 | from: number; 6 | query: any; 7 | 8 | constructor( 9 | size: number, 10 | from: number, 11 | query: any 12 | ) { 13 | this.size = size; 14 | this.from = from; 15 | this.query = query; 16 | } 17 | } 18 | 19 | 20 | export class ProductSearchObject { 21 | public static searchObject(q: any) { 22 | const body = this.elasticSearchBody(q); 23 | return { index: productIndex._index, body, q }; 24 | } 25 | 26 | public static elasticSearchBody(q: any): ElasticSearchBody { 27 | const query = { 28 | match: { 29 | url: q 30 | } 31 | }; 32 | return new ElasticSearchBody( 33 | 10, 34 | 0, 35 | query 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/product/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | Inject, 6 | Param, 7 | Patch, 8 | Post, 9 | Query 10 | } from "@nestjs/common"; 11 | import { ProductServiceInterface } from "@components/product/interface/product.service.interface"; 12 | import { CreateProductDto } from "@components/product/dto/create-product.dto"; 13 | import { Product } from "@components/product/entity/product.entity"; 14 | 15 | @Controller("products") 16 | export class ProductController { 17 | constructor( 18 | @Inject("ProductServiceInterface") 19 | private readonly productService: ProductServiceInterface 20 | ) {} 21 | 22 | @Post() 23 | public async create(@Body() productDto: CreateProductDto): Promise { 24 | return this.productService.create(productDto); 25 | } 26 | 27 | @Patch("/:id") 28 | public async update( 29 | @Param("id") id: string, 30 | @Body() updateProduct: any 31 | ): Promise { 32 | return this.productService.update(id, updateProduct); 33 | } 34 | 35 | @Get("/search") 36 | public async search(@Query() query: any): Promise { 37 | return this.productService.search(query.q); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/product/product.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { TypeOrmModule } from "@nestjs/typeorm"; 3 | import { Product } from "./entity/product.entity"; 4 | import { ProductRepositoryInterface } from "@components/product/interface/product.repository.interface"; 5 | import { ProductRepository } from "@repositories/product.repository"; 6 | import { ProductServiceInterface } from "@components/product/interface/product.service.interface"; 7 | import { ProductController } from "@components/product/product.controller"; 8 | import { ProductService } from "@components/product/product.service"; 9 | import { SearchService } from "@services/search/search.service"; 10 | import { SearchServiceInterface } from "@services/search/interface/search.service.interface"; 11 | 12 | @Module({ 13 | imports: [TypeOrmModule.forFeature([Product])], 14 | providers: [ 15 | { 16 | provide: "ProductRepositoryInterface", 17 | useClass: ProductRepository 18 | }, 19 | { 20 | provide: "ProductServiceInterface", 21 | useClass: ProductService 22 | }, 23 | { 24 | provide: "SearchServiceInterface", 25 | useClass: SearchService 26 | } 27 | ], 28 | controllers: [ProductController] 29 | }) 30 | export class ProductModule {} 31 | -------------------------------------------------------------------------------- /src/components/product/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { ProductRepositoryInterface } from "@components/product/interface/product.repository.interface"; 3 | import { ProductServiceInterface } from "@components/product/interface/product.service.interface"; 4 | import { CreateProductDto } from "@components/product/dto/create-product.dto"; 5 | import { Product } from "@components/product/entity/product.entity"; 6 | import { ProductSearchObject } from "@components/product/model/product.search.object"; 7 | import { SearchServiceInterface } from "@services/search/interface/search.service.interface"; 8 | 9 | @Injectable() 10 | export class ProductService implements ProductServiceInterface { 11 | constructor( 12 | @Inject("ProductRepositoryInterface") 13 | private readonly productRepository: ProductRepositoryInterface, 14 | @Inject("SearchServiceInterface") 15 | private readonly searchService: SearchServiceInterface 16 | ) {} 17 | 18 | public async create(productDto: CreateProductDto): Promise { 19 | const product = new Product(); 20 | product.name = productDto.name; 21 | product.description = productDto.description; 22 | product.price = productDto.price; 23 | return this.productRepository.create(product); 24 | } 25 | 26 | public async update(productId: any, updateProduct: any): Promise { 27 | const product = await this.productRepository.findOneById(productId); 28 | product.name = updateProduct.name; 29 | product.description = updateProduct.description; 30 | product.price = updateProduct.price; 31 | return this.productRepository.create(product); 32 | } 33 | 34 | public async search(q: any): Promise { 35 | const data = ProductSearchObject.searchObject(q); 36 | return this.searchService.searchIndex(data); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/constant/common.ts: -------------------------------------------------------------------------------- 1 | export enum APIPrefix { 2 | Version = "api/v1" 3 | } 4 | -------------------------------------------------------------------------------- /src/database/config/ormconfig.ts: -------------------------------------------------------------------------------- 1 | export function ormConfig(): any { 2 | return { 3 | type: process.env.DATABASE_TYPE, 4 | host: process.env.DATABASE_HOST, 5 | port: parseInt(process.env.DATABASE_PORT), 6 | username: process.env.DATABASE_USERNAME, 7 | password: process.env.DATABASE_PASSWORD, 8 | database: process.env.DATABASE_NAME, 9 | synchronize: true, 10 | logging: false, 11 | autoLoadEntities: true, 12 | useUnifiedTopology: true, 13 | useNewUrlParser: true, 14 | connectTimeout: parseInt(process.env.DATABASE_CONNECTION_TIME_OUT), 15 | acquireTimeout: parseInt(process.env.DATABASE_ACQUIRE_TIME_OUT), 16 | extra: { 17 | connectionLimit: parseInt(process.env.DATABASE_CONNECTION_LIMIT) 18 | }, 19 | entities: ["dist/**/entity/*.entity.js"], 20 | migrations: ["dist/database/migrations/*.js"], 21 | subscribers: ["dist/observers/subscribers/*.subscriber.js"], 22 | cli: { 23 | entitiesDir: "src/components/**/entity", 24 | migrationsDir: "src/database/migrations", 25 | subscribersDir: "src/observers/subscribers" 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { AppModule } from "./app.module"; 3 | import { ValidationPipe } from "@nestjs/common"; 4 | import { APIPrefix } from "@constant/common"; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | app.useGlobalPipes(new ValidationPipe()); 9 | app.setGlobalPrefix(APIPrefix.Version); 10 | const port = parseInt(process.env.SERVER_PORT); 11 | await app.listen(port); 12 | } 13 | 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /src/observers/observer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { TypeOrmModule } from "@nestjs/typeorm"; 3 | import { Product } from "@components/product/entity/product.entity"; 4 | import { SearchService } from "@services/search/search.service"; 5 | import { ProductElasticIndex } from "@services/search/search-index/product.elastic.index"; 6 | import { SearchServiceInterface } from "@services/search/interface/search.service.interface"; 7 | import { PostSubscriber } from "@observers/subscribers/product.subscriber"; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([Product])], 11 | providers: [ 12 | { 13 | provide: "SearchServiceInterface", 14 | useClass: SearchService 15 | }, 16 | ProductElasticIndex, 17 | PostSubscriber 18 | ], 19 | controllers: [], 20 | exports: [] 21 | }) 22 | export class ObserverModule {} 23 | -------------------------------------------------------------------------------- /src/observers/subscribers/product.subscriber.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | EntitySubscriberInterface, 4 | InsertEvent, 5 | UpdateEvent 6 | } from "typeorm"; 7 | import { Product } from "@components/product/entity/product.entity"; 8 | import { ProductElasticIndex } from "@services/search/search-index/product.elastic.index"; 9 | import { InjectConnection } from "@nestjs/typeorm"; 10 | import { Injectable } from "@nestjs/common"; 11 | 12 | @Injectable() 13 | export class PostSubscriber implements EntitySubscriberInterface { 14 | constructor( 15 | @InjectConnection() readonly connection: Connection, 16 | private readonly productEsIndex: ProductElasticIndex 17 | ) { 18 | connection.subscribers.push(this); 19 | } 20 | 21 | public listenTo(): any { 22 | return Product; 23 | } 24 | 25 | public async afterInsert(event: InsertEvent): Promise { 26 | return this.productEsIndex.insertProductDocument(event.entity); 27 | } 28 | 29 | public async afterUpdate(event: UpdateEvent): Promise { 30 | return this.productEsIndex.updateProductDocument(event.entity); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/repositories/base/base.abstract.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseInterfaceRepository } from "@repositories/base/base.interface.repository"; 2 | import { DeleteResult, Repository } from "typeorm"; 3 | 4 | export abstract class BaseAbstractRepository 5 | implements BaseInterfaceRepository { 6 | private entity: Repository; 7 | 8 | protected constructor(entity: Repository) { 9 | this.entity = entity; 10 | } 11 | 12 | public async create(data: T | any): Promise { 13 | return this.entity.save(data); 14 | } 15 | 16 | public async findOneById(id: number): Promise { 17 | return this.entity.findOne(id); 18 | } 19 | 20 | public async findByCondition(filterCondition: any): Promise { 21 | return this.entity.findOne({ where: filterCondition }); 22 | } 23 | 24 | public async findWithRelations(relations: any): Promise { 25 | return this.entity.find(relations); 26 | } 27 | 28 | public async findAll(): Promise { 29 | return this.entity.find(); 30 | } 31 | 32 | public async remove(id: string): Promise { 33 | return this.entity.delete(id); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/repositories/base/base.interface.repository.ts: -------------------------------------------------------------------------------- 1 | import { DeleteResult } from "typeorm"; 2 | 3 | export interface BaseInterfaceRepository { 4 | create(data: T | any): Promise; 5 | 6 | findOneById(id: number): Promise; 7 | 8 | findByCondition(filterCondition: any): Promise; 9 | 10 | findAll(): Promise; 11 | 12 | remove(id: string): Promise; 13 | 14 | findWithRelations(relations: any): Promise; 15 | } 16 | -------------------------------------------------------------------------------- /src/repositories/product.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseAbstractRepository } from "./base/base.abstract.repository"; 2 | import { Injectable } from "@nestjs/common"; 3 | import { Repository } from "typeorm"; 4 | import { InjectRepository } from "@nestjs/typeorm"; 5 | import { ProductRepositoryInterface } from "@components/product/interface/product.repository.interface"; 6 | import { Product } from "@components/product/entity/product.entity"; 7 | 8 | @Injectable() 9 | export class ProductRepository extends BaseAbstractRepository 10 | implements ProductRepositoryInterface { 11 | constructor( 12 | @InjectRepository(Product) 13 | private readonly productRepository: Repository 14 | ) { 15 | super(productRepository); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/services/search/config/config.search.ts: -------------------------------------------------------------------------------- 1 | export class ConfigSearch { 2 | public static searchConfig(url: string): any { 3 | return { 4 | node: url, 5 | maxRetries: 5, 6 | requestTimeout: 60000, 7 | sniffOnStart: true 8 | }; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/services/search/constant/product.elastic.ts: -------------------------------------------------------------------------------- 1 | export const productIndex = { 2 | _index: 'product', 3 | _type: 'products' 4 | }; 5 | -------------------------------------------------------------------------------- /src/services/search/interface/search.service.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SearchServiceInterface { 2 | insertIndex(bulkData: T): Promise; 3 | 4 | updateIndex(updateData: T): Promise; 5 | 6 | searchIndex(searchData: T): Promise; 7 | 8 | deleteIndex(indexData: T): Promise; 9 | 10 | deleteDocument(indexData: T): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /src/services/search/search-index/product.elastic.index.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from "@nestjs/common"; 2 | import { SearchServiceInterface } from "@services/search/interface/search.service.interface"; 3 | import { productIndex } from "@services/search/constant/product.elastic"; 4 | import { Product } from "@components/product/entity/product.entity"; 5 | 6 | @Injectable() 7 | export class ProductElasticIndex { 8 | constructor( 9 | @Inject("SearchServiceInterface") 10 | private readonly searchService: SearchServiceInterface 11 | ) {} 12 | 13 | public async insertProductDocument(product: Product): Promise { 14 | const data = this.productDocument(product); 15 | return this.searchService.insertIndex(data); 16 | } 17 | 18 | public async updateProductDocument(product: Product): Promise { 19 | const data = this.productDocument(product); 20 | await this.deleteProductDocument(product.id); 21 | return this.searchService.insertIndex(data); 22 | } 23 | 24 | private async deleteProductDocument(prodId: number): Promise { 25 | const data = { 26 | index: productIndex._index, 27 | type: productIndex._type, 28 | id: prodId.toString() 29 | }; 30 | return this.searchService.deleteDocument(data); 31 | } 32 | 33 | private bulkIndex(productId: number): any { 34 | return { 35 | _index: productIndex._index, 36 | _type: productIndex._type, 37 | _id: productId 38 | }; 39 | } 40 | 41 | private productDocument(product: Product): any { 42 | const bulk = []; 43 | bulk.push({ 44 | index: this.bulkIndex(product.id) 45 | }); 46 | bulk.push(product); 47 | return { 48 | body: bulk, 49 | index: productIndex._index, 50 | type: productIndex._type 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/services/search/search.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { SearchService } from "@services/search/search.service"; 3 | import { SearchServiceInterface } from "@services/search/interface/search.service.interface"; 4 | 5 | @Module({ 6 | imports: [], 7 | providers: [ 8 | { 9 | provide: "SearchServiceInterface", 10 | useClass: SearchService 11 | } 12 | ], 13 | controllers: [], 14 | exports: [SearchModule] 15 | }) 16 | export class SearchModule {} 17 | -------------------------------------------------------------------------------- /src/services/search/search.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus, Injectable } from "@nestjs/common"; 2 | import { ElasticsearchService } from "@nestjs/elasticsearch"; 3 | import { SearchServiceInterface } from "@services/search/interface/search.service.interface"; 4 | import { ConfigSearch } from "@services/search/config/config.search"; 5 | 6 | @Injectable() 7 | export class SearchService extends ElasticsearchService 8 | implements SearchServiceInterface { 9 | constructor() { 10 | super(ConfigSearch.searchConfig(process.env.ELASTIC_SEARCH_URL)); 11 | } 12 | 13 | public async insertIndex(bulkData: any): Promise { 14 | return this.bulk(bulkData) 15 | .then(res => res) 16 | .catch(err => { 17 | console.log(err); 18 | throw new HttpException(err, HttpStatus.INTERNAL_SERVER_ERROR); 19 | }); 20 | } 21 | 22 | public async updateIndex(updateData: any): Promise { 23 | return this.update(updateData) 24 | .then(res => res) 25 | .catch(err => { 26 | throw new HttpException(err, HttpStatus.INTERNAL_SERVER_ERROR); 27 | }); 28 | } 29 | 30 | public async searchIndex(searchData: any): Promise { 31 | return this.search(searchData) 32 | .then(res => { 33 | return res.body.hits.hits; 34 | }) 35 | .catch(err => { 36 | throw new HttpException(err, HttpStatus.INTERNAL_SERVER_ERROR); 37 | }); 38 | } 39 | 40 | public async deleteIndex(indexData: any): Promise { 41 | return this.indices 42 | .delete(indexData) 43 | .then(res => res) 44 | .catch(err => { 45 | throw new HttpException(err, HttpStatus.INTERNAL_SERVER_ERROR); 46 | }); 47 | } 48 | 49 | public async deleteDocument(indexData: any): Promise { 50 | return this.delete(indexData) 51 | .then(res => res) 52 | .catch(err => { 53 | throw new HttpException(err, HttpStatus.INTERNAL_SERVER_ERROR); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "paths": { 15 | "@components/*": ["src/components/*"], 16 | "@services/*": ["src/services/*"], 17 | "@constant/*": ["src/constant/*"], 18 | "@database/*": ["src/database/*"], 19 | "@observers/*": ["src/observers/*"], 20 | "@repositories/*": ["src/repositories/*"] 21 | }, 22 | "plugins": [{ "transform": "typescript-transform-paths" }] 23 | } 24 | } 25 | --------------------------------------------------------------------------------