├── .circleci
└── config.yml
├── .dockerignore
├── .env
├── .eslintrc.js
├── .github
└── workflows
│ └── publish-github-docker.yml
├── .gitignore
├── .prettierrc
├── .vscode
└── launch.json
├── Dockerfile
├── Procfile
├── README.md
├── database
└── datamodel.graphql
├── docker-compose.yml
├── generated
└── prisma-client
│ ├── index.ts
│ └── prisma-schema.ts
├── mock.ts
├── nest-cli.json
├── package.json
├── prisma.yml
├── redis.conf
├── src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── auth
│ ├── auth.module.ts
│ ├── auth.resolver.spec.ts
│ ├── auth.resolver.ts
│ ├── auth.service.spec.ts
│ ├── auth.service.ts
│ ├── facebook.strategy.ts
│ ├── github
│ │ ├── github.controller.spec.ts
│ │ └── github.controller.ts
│ ├── google.strategy.ts
│ ├── graphql-auth.guard.ts
│ ├── jwt.strategy.ts
│ └── sign-up-input.dto.ts
├── aws
│ ├── aws.service.spec.ts
│ └── aws.service.ts
├── cron
│ ├── cron.module.ts
│ ├── cron.service.spec.ts
│ └── cron.service.ts
├── graphql.options.ts
├── graphql.schema.generated.ts
├── hotel
│ ├── hotel.module.ts
│ ├── hotel.resolver.spec.ts
│ ├── hotel.resolver.ts
│ ├── hotel.service.spec.ts
│ └── hotel.service.ts
├── main.ts
├── prisma
│ ├── prisma.module.ts
│ ├── prisma.service.spec.ts
│ └── prisma.service.ts
├── schema
│ └── gql-api.graphql
├── services
│ ├── sendEmail.ts
│ └── template
│ │ └── reset.pug
├── shared
│ └── decorators
│ │ └── decorator.ts
├── transaction
│ ├── transaction.module.ts
│ ├── transaction.resolver.spec.ts
│ └── transaction.resolver.ts
├── user
│ ├── user.module.ts
│ ├── user.resolver.spec.ts
│ └── user.resolver.ts
└── utils
│ ├── FilesAndMockUpload.ts
│ ├── data.ts
│ ├── seed.ts
│ ├── stripe.ts
│ ├── upload.input.ts
│ ├── upload.scalar.ts
│ └── utils.module.ts
├── test
├── app.e2e-spec.ts
└── jest-e2e.json
├── tsconfig.build.json
├── tsconfig.json
├── vercel.json
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | orbs:
3 | heroku: circleci/heroku@0.0.10
4 | workflows:
5 | heroku_deploy:
6 | jobs:
7 | - heroku/deploy-via-git
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /node_modules
3 | .env
4 | redis.conf
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | JWT_SECRET=myscret_JWT
2 | EMAIL_SEND=redias2048@gmail.com
3 | LOCAL_EMAIL=redias2048@gmail.com
4 | LOCAL_PASSWORD=pinodien
5 |
6 | STAGING_SECRET_KEY=12345678
7 | STAGING_EMAIL=andrewhavard1807@gmail.com
8 | STAGING_PASSWORD=pinodien
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/eslint-recommended',
10 | 'plugin:@typescript-eslint/recommended',
11 | 'prettier',
12 | 'prettier/@typescript-eslint',
13 | ],
14 | root: true,
15 | env: {
16 | node: true,
17 | jest: true,
18 | },
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish-github-docker.yml:
--------------------------------------------------------------------------------
1 | name: Push Docker Image To DockerHub and Github Package
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout code
15 | uses: actions/checkout@v2
16 | - name: Push to GitHub Packages
17 | uses: docker/build-push-action@v1
18 | with:
19 | username: ${{ github.actor }}
20 | password: ${{ secrets.GITHUB_TOKEN }}
21 | registry: docker.pkg.github.com
22 | repository: php1301/doanfullstack-be-admin/nest-prisma-mongo-redis-v1
23 | tags: github-actions-docker-v1
24 | - name: Build and push Docker images To DockerHub
25 | uses: docker/build-push-action@v1
26 | with:
27 | username: ${{ secrets.DOCKER_USERNAME }}
28 | password: ${{ secrets.DOCKER_PASSWORD }}
29 | repository: phucpham1301/hotelprisma-v1.0
30 | tags: github-actions-docker-v1
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 | /.env
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
35 | .vercel
36 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Debug Nest Framework",
11 | "args": ["${workspaceFolder}/src/main.ts"],
12 | "runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
13 | "sourceMaps": true,
14 | "cwd": "${workspaceRoot}",
15 | "protocol": "inspector"
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12.13-alpine As development
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm install
8 |
9 | COPY . .
10 |
11 | RUN npm run build
12 |
13 | FROM node:12.13-alpine as production
14 |
15 |
16 | WORKDIR /usr/src/app
17 |
18 | COPY package*.json ./
19 |
20 | RUN npm install
21 |
22 | COPY . .
23 |
24 | COPY --from=development /usr/src/app/dist ./dist
25 |
26 | CMD ["node", "dist/src/main"]
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm run start:prod
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Currently on offline mode for saving GCP fund
2 |
3 |
4 |
5 |
6 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
7 | [travis-url]: https://travis-ci.org/nestjs/nest
8 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
9 | [linux-url]: https://travis-ci.org/nestjs/nest
10 |
11 | A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
27 |
28 | ## Description
29 |
30 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
31 |
32 | ## Installation
33 |
34 | ```bash
35 | $ npm install
36 | ```
37 |
38 | ## Running the app
39 |
40 | ```bash
41 | # development
42 | $ npm run start
43 |
44 | # watch mode
45 | $ npm run start:dev
46 |
47 | # production mode
48 | $ npm run start:prod
49 | ```
50 |
51 | ## Test
52 |
53 | ```bash
54 | # unit tests
55 | $ npm run test
56 |
57 | # e2e tests
58 | $ npm run test:e2e
59 |
60 | # test coverage
61 | $ npm run test:cov
62 | ```
63 |
64 | ## Support
65 |
66 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
67 |
68 | ## Stay in touch
69 |
70 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
71 | - Website - [https://nestjs.com](https://nestjs.com/)
72 | - Twitter - [@nestframework](https://twitter.com/nestframework)
73 |
74 | ## License
75 |
76 | Nest is [MIT licensed](LICENSE).
77 |
--------------------------------------------------------------------------------
/database/datamodel.graphql:
--------------------------------------------------------------------------------
1 | type User {
2 | id: ID! @id
3 | first_name: String!
4 | last_name: String!
5 | username: String!
6 | password: String!
7 | email: String! @unique
8 | role: String
9 | cellNumber: String @defailt(value: "0000")
10 | profile_pic_main: String
11 | cover_pic_main: String
12 | profile_pic: [Gallery] @relation(link: INLINE, name: "Profile")
13 | cover_pic: [Gallery] @relation(link: INLINE, name: "Cover")
14 | date_of_birth: String
15 | gender: String
16 | content: String
17 | notification: [Notification] @relation(link: INLINE, name: "Notification")
18 | unreadNotification: Int @default(value: 0)
19 | agent_location: Location @relation(link: INLINE)
20 | gallery: [Gallery] @relation(link: INLINE, name: "User_gallery")
21 | social_profile: Social @relation(link: INLINE)
22 | # 1 field sử dụng cùng 1 model thì phải khai báo relation name để specify các loại relations
23 | reviews_maked: [Reviews] @relation(name: "Reviews_maked")
24 | listed_posts: [Hotel] @relation(link: INLINE, name: "Hotel_created")
25 | favourite_post: [Hotel] @relation(link: INLINE, name: "Hotel_liked")
26 | reviewed_post: [Hotel] @relation(link: INLINE, name: "Hotel_reviewed")
27 | review_liked: [Reviews] @relation(name: "Review_liked")
28 | review_disliked: [Reviews] @relation(name: "Review_disliked")
29 | stripeId: String
30 | transaction_had: [Transaction]
31 | @relation(link: INLINE, name: "Transaction_had")
32 | transaction_maked: [Transaction]
33 | @relation(link: INLINE, name: "Transaction_maked")
34 | coupons_maked: [Coupon] @relation(link: INLINE, name: "Coupon_maked")
35 | uncheckTransactions: UncheckTransactions
36 | @relation(link: INLINE, name: "Uncheck_transactions")
37 | createdAt: DateTime! @createdAt
38 | updatedAt: DateTime! @updatedAt
39 | }
40 | type Transaction {
41 | TXID: ID! @id
42 | transactionSecretKey: String
43 | transactionHotelName: String
44 | transactionHotelId: String
45 | transactionHotelManager: User @relation(name: "Transaction_had")
46 | transactionHotelManagerId: String
47 | transactionHotelType: String
48 | transactionPrice: Int
49 | transactionAuthor: User @relation(name: "Transaction_maked")
50 | transactionAuthorId: String
51 | transactionAuthorName: String
52 | transactionAuthorEmail: String
53 | transactionAuthorContactNumber: String
54 | transactionAuthorSpecial: String ##Cho đủ field
55 | transactionAuthorNote: String
56 | transactionLocationLat: Float
57 | transactionLocationLng: Float
58 | transactionRoom: Int
59 | transactionGuest: Int
60 | transactionLocationFormattedAddress: String
61 | transactionRange: Int
62 | transactionStatus: String
63 | transactionCoupon: String
64 | transactionCouponType: Int
65 | transactionCouponValue: Int
66 | transactionStartDate: String
67 | transactionEndDate: String
68 | transactionStripeId: String
69 | createdAt: DateTime! @createdAt
70 | updatedAt: DateTime! @updatedAt
71 | }
72 |
73 | type Coupon {
74 | couponId: ID! @id
75 | couponName: String! @unique
76 | couponDescription: String
77 | couponAuthor: User @relation(name: "Coupon_maked")
78 | couponAuthorId: String
79 | couponType: Int #1 là percent 2 là number
80 | couponValue: Int
81 | couponQuantity: Int
82 | couponStartDate: String
83 | couponEndDate: String
84 | couponRange: String
85 | couponTarget: [Hotel] @relation(link: INLINE, name: "Hotel_coupons")
86 | createdAt: DateTime! @createdAt
87 | updatedAt: DateTime! @updatedAt
88 | }
89 | type Hotel {
90 | id: ID! @id
91 | peopleLiked: [User] @relation(name: "Hotel_liked")
92 | peopleReviewed: [User] @relation(name: "Hotel_reviewed")
93 | couponsAvailable: [Coupon] @relation(name: "Hotel_coupons")
94 | connectId: User @relation(name: "Hotel_created")
95 | agentId: String
96 | agentEmail: String
97 | agentName: String
98 | title: String!
99 | slug: String
100 | content: String
101 | status: String @defailt(value: "Public")
102 | price: Int
103 | isNegotiable: Boolean
104 | propertyType: String
105 | condition: String
106 | rating: Float
107 | ratingCount: Int
108 | contactNumber: String
109 | termsAndCondition: String
110 | amenities: [Amenities] @relation(link: INLINE)
111 | image: Image @relation(link: INLINE)
112 | location: [Location] @relation(link: INLINE)
113 | gallery: [Gallery] @relation(link: INLINE)
114 | categories: [Categories] @relation(link: INLINE)
115 | reviews: [Reviews] @relation(link: INLINE, name: "Hotel_reivews")
116 | createdAt: DateTime! @createdAt
117 | updatedAt: DateTime! @updatedAt
118 | }
119 | type Notification {
120 | id: ID! @id
121 | reviewAuthorName: String
122 | reviewedHotelName: String
123 | reviewTitle: String
124 | reviewText: String
125 | read: Boolean @default(value: false)
126 | old: Boolean @default(value: false)
127 | userNotificationId: String ## vì ko cần lấy thêm thông tin user làm gì (đã có trong payload nên parse thẳng id)
128 | peopleReviewedQuantity: Int
129 | query: String
130 | reviewAuthorProfilePic: String
131 | createdAt: DateTime! @createdAt
132 | updatedAt: DateTime! @updatedAt
133 | }
134 | type UncheckTransactions {
135 | id: ID! @id
136 | userUncheckTransactionsId: String
137 | userUncheckTransactions: User @relation(name: "Uncheck_transactions")
138 | totalPrice: Int @default(value: 0)
139 | totalTransactions: Int @default(value: 0)
140 | }
141 | type Reviews {
142 | reviewID: ID! @id
143 | reviewTitle: String
144 | reviewText: String
145 | sortOfTrip: String
146 | reviewAuthorId: User @relation(link: INLINE, name: "Reviews_maked")
147 | peopleLiked: [User] @relation(link: INLINE, name: "Review_liked")
148 | peopleDisliked: [User] @relation(link: INLINE, name: "Review_disliked")
149 | reviewAuthorFirstName: String
150 | reviewTips: String
151 | reviewAuthorLastName: String
152 | reviewAuthorEmail: String
153 | reviewOverall: Float
154 | reviewAuthorPic: String
155 | reviewedHotel: Hotel @relation(name: "Hotel_reivews")
156 | reviewedHotelId: ID
157 | reviewPics: [ReviewImages] @relation(link: INLINE)
158 | reviewDate: DateTime! @createdAt
159 | reviewOptional: [ReviewOptionals] @relation(link: INLINE)
160 | reviewFields: [ReviewFields] @relation(link: INLINE)
161 | }
162 |
163 | type ReviewOptionals {
164 | id: ID! @id
165 | option: String
166 | optionField: String
167 | }
168 | type ReviewFields {
169 | id: ID! @id
170 | rating: Int
171 | ratingFieldName: String
172 | }
173 | type Social {
174 | id: ID! @id
175 | facebook: String
176 | twitter: String
177 | linkedIn: String
178 | instagram: String
179 | }
180 |
181 | enum Gender {
182 | Male
183 | Female
184 | Other
185 | }
186 |
187 | type Amenities {
188 | id: ID! @id
189 | guestRoom: Int
190 | bedRoom: Int
191 | wifiAvailability: Boolean
192 | parkingAvailability: Boolean
193 | poolAvailability: Boolean
194 | airCondition: Boolean
195 | extraBedFacility: Boolean
196 | }
197 |
198 | type Image {
199 | id: ID! @id
200 | url: String
201 | thumb_url: String
202 | }
203 | type CategoryImages {
204 | id: ID! @id
205 | url: String
206 | }
207 | type ReviewImages {
208 | id: ID! @id
209 | url: String
210 | }
211 | type Gallery {
212 | id: ID! @id
213 | uid: String
214 | url: String
215 | signedRequest: String
216 | }
217 |
218 | type Location {
219 | id: ID! @id
220 | lat: Float
221 | lng: Float
222 | formattedAddress: String
223 | zipcode: String
224 | city: String
225 | state_long: String
226 | state_short: String
227 | country_long: String
228 | country_short: String
229 | }
230 |
231 | type Categories {
232 | id: ID! @id
233 | slug: String
234 | name: String
235 | image: CategoryImages @relation(link: INLINE)
236 | }
237 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | # API container
5 | api:
6 | container_name: api
7 | # build:
8 | # context: ./
9 | image: phucpham1301/hotelprisma-v1.0:github-actions-docker-v1
10 | restart: on-failure
11 | volumes:
12 | - api:/usr/src/app
13 | ports:
14 | - 3000:3000
15 | env_file:
16 | - ./.env
17 | depends_on:
18 | - redis
19 | - prisma
20 | networks:
21 | - api
22 | redis:
23 | image: redis:latest
24 | container_name: redis
25 | restart: on-failure
26 | ports:
27 | - 6379:6379
28 | volumes:
29 | - redis-data:/var/lib/redis
30 | - ./redis.conf:/usr/local/etc/redis/redis.conf
31 | entrypoint: redis-server /usr/local/etc/redis/redis.conf
32 | networks:
33 | api:
34 | ipv4_address: 172.28.1.4
35 | prisma:
36 | image: prismagraphql/prisma:1.34
37 | restart: always
38 | ports:
39 | - '4466:4466'
40 | environment:
41 | PRISMA_CONFIG: |
42 | port: 4466
43 | databases:
44 | default:
45 | connector: mongo
46 | uri: mongodb://prisma:prisma@mongo:27017/HotelPrisma?authSource=admin
47 | database: HotelPrisma
48 | networks:
49 | - api
50 | depends_on:
51 | - mongo
52 | mongo:
53 | image: mongo:3.6
54 | restart: always
55 | environment:
56 | MONGO_INITDB_ROOT_USERNAME: prisma
57 | MONGO_INITDB_ROOT_PASSWORD: prisma
58 | ports:
59 | - '27017:27017'
60 | volumes:
61 | - mongo:/var/lib/mongo
62 | networks:
63 | - api
64 | volumes:
65 | mongo:
66 | api:
67 | redis-data:
68 | networks:
69 | api:
70 | ipam:
71 | driver: default
72 | config:
73 | - subnet: 172.28.0.0/16
74 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src",
4 | "compilerOptions": {
5 | "assets": ["**/*.pug", "**/*.graphql"],
6 | "watchAssets": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prisma-nest",
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": "node dist/src/main",
13 | "start:dev": "nest start --watch",
14 | "start:debug": "nest start --debug --watch",
15 | "start:prod": "node dist/src/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 | },
23 | "dependencies": {
24 | "@nestjs-modules/mailer": "^1.5.1",
25 | "@nestjs/common": "^7.0.0",
26 | "@nestjs/config": "^0.5.0",
27 | "@nestjs/core": "^7.0.0",
28 | "@nestjs/graphql": "^7.4.1",
29 | "@nestjs/jwt": "^7.0.0",
30 | "@nestjs/passport": "^7.1.0",
31 | "@nestjs/platform-express": "^7.0.0",
32 | "@prisma/cli": "^2.0.1",
33 | "@types/lodash": "^4.14.159",
34 | "@types/node-mailjet": "^3.3.3",
35 | "@types/passport-facebook-token": "^0.4.34",
36 | "@types/passport-google-oauth20": "^2.0.3",
37 | "@types/passport-jwt": "^3.0.3",
38 | "@types/stripe": "^7.13.24",
39 | "@types/superagent": "^4.1.10",
40 | "@vercel/node": "^1.8.1",
41 | "apollo-server-express": "^2.14.5",
42 | "aws-sdk": "^2.709.0",
43 | "bcryptjs": "^2.4.3",
44 | "class-transformer": "^0.2.3",
45 | "class-validator": "^0.12.2",
46 | "cookie-parser": "^1.4.5",
47 | "global": "^4.4.0",
48 | "graphql": "14.6.0",
49 | "graphql-redis-subscriptions": "^2.2.1",
50 | "graphql-subscriptions": "^1.1.0",
51 | "graphql-tools": "^6.0.10",
52 | "ioredis": "^4.17.3",
53 | "lodash": "^4.17.19",
54 | "node-mailjet": "^3.3.1",
55 | "nodemailer": "^6.4.10",
56 | "passport": "^0.4.1",
57 | "passport-facebook-token": "^4.0.0",
58 | "passport-google-oauth20": "^2.0.0",
59 | "passport-jwt": "^4.0.0",
60 | "prisma": "^1.34.10",
61 | "prisma-binding": "^2.3.16",
62 | "prisma-client-lib": "^1.34.10",
63 | "pug": "^3.0.0",
64 | "reflect-metadata": "^0.1.13",
65 | "rimraf": "^3.0.2",
66 | "rxjs": "^6.5.4",
67 | "stripe": "^8.89.0",
68 | "superagent": "^6.0.0",
69 | "uuid": "^8.3.0"
70 | },
71 | "devDependencies": {
72 | "@nestjs/cli": "^7.0.0",
73 | "@nestjs/schedule": "^0.4.0",
74 | "@nestjs/schematics": "^7.0.0",
75 | "@nestjs/testing": "^7.0.0",
76 | "@types/express": "^4.17.3",
77 | "@types/jest": "25.2.3",
78 | "@types/node": "^13.9.1",
79 | "@types/supertest": "^2.0.8",
80 | "@typescript-eslint/eslint-plugin": "3.0.2",
81 | "@typescript-eslint/parser": "3.0.2",
82 | "eslint": "7.1.0",
83 | "eslint-config-prettier": "^6.10.0",
84 | "eslint-plugin-import": "^2.20.1",
85 | "jest": "26.0.1",
86 | "prettier": "^1.19.1",
87 | "supertest": "^4.0.2",
88 | "ts-jest": "26.1.0",
89 | "ts-loader": "^6.2.1",
90 | "ts-node": "^8.6.2",
91 | "tsconfig-paths": "^3.9.0",
92 | "typescript": "^3.7.4"
93 | },
94 | "jest": {
95 | "moduleFileExtensions": [
96 | "js",
97 | "json",
98 | "ts"
99 | ],
100 | "rootDir": "src",
101 | "testRegex": ".spec.ts$",
102 | "transform": {
103 | "^.+\\.(t|j)s$": "ts-jest"
104 | },
105 | "coverageDirectory": "../coverage",
106 | "testEnvironment": "node"
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/prisma.yml:
--------------------------------------------------------------------------------
1 | endpoint: ${env:PRISMA_ENDPOINT}
2 | datamodel: database/datamodel.graphql
3 | seed:
4 | # import: src/schema/seed.graphql
5 | run: ts-node src/utils/seed.ts
6 | generate:
7 | - generator: typescript-client
8 | output: ./generated/prisma-client/
9 |
--------------------------------------------------------------------------------
/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 |
4 | describe('AppController', () => {
5 | let appController: AppController;
6 |
7 | beforeEach(async () => {
8 | const app: TestingModule = await Test.createTestingModule({
9 | controllers: [AppController],
10 | }).compile();
11 |
12 | appController = app.get(AppController);
13 | });
14 |
15 | describe('root', () => {
16 | it('should return alive', () => {
17 | expect(appController.alive()).toBe('alive!');
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 |
3 | @Controller()
4 | export class AppController {
5 | @Get('livecheck')
6 | alive(): string {
7 | return 'alive!';
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, ValidationPipe } from '@nestjs/common';
2 | import { PrismaModule } from './prisma/prisma.module';
3 | import { GraphQLModule } from '@nestjs/graphql';
4 | import { GraphqlOptions } from './graphql.options';
5 | import { AppController } from './app.controller';
6 | import { HotelModule } from './hotel/hotel.module';
7 | import { AuthModule } from './auth/auth.module';
8 | import { APP_PIPE } from '@nestjs/core';
9 | import { ConfigModule } from '@nestjs/config';
10 | import { ScheduleModule } from '@nestjs/schedule';
11 | import { UserModule } from './user/user.module';
12 | // import { PubSub } from 'graphql-subscriptions';
13 | // import { RedisPubSub } from 'graphql-redis-subscriptions';
14 | // import * as Redis from 'ioredis';
15 | // document cho mail https://nest-modules.github.io/mailer/docs/mailer.html
16 | import { MailerModule } from '@nestjs-modules/mailer';
17 | import { PugAdapter } from '@nestjs-modules/mailer/dist/adapters/pug.adapter';
18 | import { AwsService } from './aws/aws.service';
19 | import { UtilsModule } from './utils/utils.module';
20 | import { TransactionModule } from './transaction/transaction.module';
21 | import { StripeService } from './utils/stripe';
22 | import { CronService } from './cron/cron.service';
23 | import { CronModule } from './cron/cron.module';
24 | import { MailService } from './services/sendEmail';
25 |
26 | // Để validation work xuyên suốt app thì phải provide validation pipe
27 | @Module({
28 | imports: [
29 | // Bất đồng bộ, xài forRoot sẽ dính lỗi Plain => do auth ko lấy dc .env
30 | MailerModule.forRootAsync({
31 | useFactory: () => ({
32 | transport: {
33 | host: 'smtp.gmail.com',
34 | port: 587,
35 | secure: false, // upgrade later with STARTTLS
36 | auth: {
37 | user: process.env.LOCAL_EMAIL,
38 | pass: process.env.LOCAL_PASSWORD,
39 | },
40 | },
41 | defaults: {
42 | from: '"nest-modules" ',
43 | },
44 | template: {
45 | // dir: process.cwd() + '/templates/',
46 | adapter: new PugAdapter(),
47 | options: {
48 | strict: true,
49 | },
50 | },
51 | }),
52 | }),
53 | ScheduleModule.forRoot(),
54 | ConfigModule.forRoot({
55 | envFilePath: './.env',
56 | }),
57 | GraphQLModule.forRootAsync({
58 | useClass: GraphqlOptions,
59 | }),
60 | PrismaModule,
61 | HotelModule,
62 | AuthModule,
63 | UserModule,
64 | UtilsModule,
65 | TransactionModule,
66 | CronModule,
67 | ],
68 | controllers: [AppController],
69 | providers: [
70 | {
71 | provide: APP_PIPE,
72 | useClass: ValidationPipe,
73 | },
74 | MailService,
75 | AwsService,
76 | StripeService,
77 | CronService,
78 | ],
79 | })
80 | export class AppModule {}
81 |
--------------------------------------------------------------------------------
/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/auth/auth.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AuthService } from './auth.service';
3 | import { AuthResolver } from './auth.resolver';
4 | import { PrismaModule } from 'src/prisma/prisma.module';
5 | import { PassportModule } from '@nestjs/passport';
6 | import { JwtModule } from '@nestjs/jwt';
7 | import { JwtStrategy } from './jwt.strategy';
8 | import { FacebookStrategy } from './facebook.strategy';
9 | import { GoogleStrategy } from './google.strategy';
10 | import { GithubController } from './github/github.controller';
11 |
12 | @Module({
13 | imports: [
14 | PrismaModule,
15 | PassportModule.register({
16 | defaultStrategy: 'jwt',
17 | }),
18 | JwtModule.register({
19 | privateKey: process.env.JWT_SECRET,
20 | secretOrPrivateKey: process.env.JWT_SECRET,
21 | secret: process.env.JWT_SECRET,
22 | signOptions: {
23 | expiresIn: 3600, //1 giờ
24 | },
25 | }),
26 | ],
27 | providers: [AuthService, AuthResolver, JwtStrategy, FacebookStrategy, GoogleStrategy],
28 | controllers: [GithubController],
29 | })
30 | export class AuthModule {}
31 |
--------------------------------------------------------------------------------
/src/auth/auth.resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AuthResolver } from './auth.resolver';
3 |
4 | describe('AuthResolver', () => {
5 | let resolver: AuthResolver;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [AuthResolver],
10 | }).compile();
11 |
12 | resolver = module.get(AuthResolver);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(resolver).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/auth/auth.resolver.ts:
--------------------------------------------------------------------------------
1 | import * as bcrypt from 'bcryptjs';
2 | import * as jwt2 from 'jsonwebtoken';
3 | import { Resolver, Mutation, Args } from '@nestjs/graphql';
4 | import { JwtService } from '@nestjs/jwt';
5 | import { PrismaService } from 'src/prisma/prisma.service';
6 | import { LoginInput, User, UpdatePassword } from 'src/graphql.schema.generated';
7 | import { Response } from 'express';
8 | import { v4 as uuidv4 } from 'uuid';
9 | import { ResGql, GqlUser } from 'src/shared/decorators/decorator';
10 | import { SignUpInputDto } from './sign-up-input.dto';
11 | import { UseGuards } from '@nestjs/common';
12 | import { GqlAuthGuard } from './graphql-auth.guard';
13 |
14 | @Resolver('Auth')
15 | export class AuthResolver {
16 | constructor(
17 | private readonly jwt: JwtService,
18 | private readonly prisma: PrismaService,
19 | ) {}
20 |
21 | @Mutation()
22 | async login(
23 | @Args('loginInput') { email, password }: LoginInput,
24 | @ResGql() res: Response,
25 | ) {
26 | const user = await this.prisma.client.user({ email });
27 | if (!user) {
28 | throw Error('Email or password is incorrect');
29 | }
30 | const valid = await bcrypt.compare(password, user.password);
31 | if (!valid) {
32 | throw Error('Password is incorrect');
33 | }
34 | console.log('this is new');
35 | console.log(process.env.JWT_SECRET);
36 | const jwt = jwt2.sign(
37 | {
38 | id: user.id,
39 | name: user.first_name + ' ' + user.last_name,
40 | roles: 'user',
41 | },
42 | process.env.JWT_SECRET,
43 | );
44 | console.log(jwt);
45 | console.log('jwt');
46 | res.cookie('token', jwt, {
47 | domain: '.hotel-prisma.ml',
48 | httpOnly: false,
49 | sameSite: 'none',
50 | secure: true,
51 | });
52 | return user;
53 | }
54 | @Mutation()
55 | async facebookLogin(
56 | @Args('email') email,
57 | @Args('accessToken') accessToken,
58 | @Args('socialInfo') socialInfo,
59 | @Args('socialId') socialId,
60 | @Args('socialProfileLink') socialProfileLink,
61 | @ResGql() res: Response,
62 | ) {
63 | // Có thể thêm biến type để tạo nhiều dạng social như google, github,...
64 | const user = await this.prisma.client.user({ email });
65 | const profile_pic_main = `https://graph.facebook.com/${socialId}/picture?width=200&height=200`;
66 | if (!user) {
67 | const socialUser = await this.prisma.client.createUser({
68 | email,
69 | username: socialId,
70 | first_name: socialInfo,
71 | last_name: '',
72 | password: uuidv4(),
73 | profile_pic_main,
74 | cover_pic_main: 'https://i.imgur.com/lXybeGM.png',
75 | social_profile: {
76 | create: {
77 | facebook: socialProfileLink,
78 | },
79 | },
80 | role: 'Normal',
81 | // username: signUpInputDto.username,
82 | });
83 | const jwt = jwt2.sign(
84 | {
85 | id: socialUser.id,
86 | name: socialUser.first_name,
87 | accessToken,
88 | },
89 | process.env.JWT_SECRET,
90 | );
91 | res.cookie('token', jwt, {
92 | domain: '.hotel-prisma.ml',
93 | httpOnly: false,
94 | sameSite: 'none',
95 | secure: true,
96 | });
97 | return socialUser;
98 | }
99 | // sign JWT hợp lệ
100 | // Việc jwt.verify là optional nhờ vào decorator GqlUser()
101 | const jwt = jwt2.sign(
102 | {
103 | id: user.id,
104 | name: user.first_name,
105 | accessToken,
106 | },
107 | process.env.JWT_SECRET,
108 | );
109 | res.cookie('token', jwt, {
110 | domain: '.hotel-prisma.ml',
111 | httpOnly: false,
112 | sameSite: 'none',
113 | secure: true,
114 | });
115 | return user;
116 | }
117 | @Mutation()
118 | async googleLogin(
119 | @Args('email') email,
120 | @Args('accessToken') accessToken,
121 | @Args('socialInfo') socialInfo,
122 | @Args('socialId') socialId,
123 | @Args('profileImage') profileImage,
124 | @ResGql() res: Response,
125 | ) {
126 | // Có thể thêm biến type để tạo nhiều dạng social như google, github,...
127 | const user = await this.prisma.client.user({ email });
128 | if (!user) {
129 | const socialUser = await this.prisma.client.createUser({
130 | email,
131 | username: socialId,
132 | first_name: socialInfo,
133 | last_name: '',
134 | password: uuidv4(),
135 | profile_pic_main: profileImage.replace('s96-c', 's250-c'),
136 | cover_pic_main: 'https://i.imgur.com/lXybeGM.png',
137 | role: 'Normal',
138 | // username: signUpInputDto.username,
139 | });
140 | const jwt = jwt2.sign(
141 | {
142 | id: socialUser.id,
143 | name: socialUser.first_name,
144 | accessToken,
145 | },
146 | process.env.JWT_SECRET,
147 | );
148 | res.cookie('token', jwt, {
149 | domain: '.hotel-prisma.ml',
150 | httpOnly: false,
151 | sameSite: 'none',
152 | secure: true,
153 | });
154 | return socialUser;
155 | }
156 | // sign JWT hợp lệ
157 | // Việc jwt.verify là optional nhờ vào decorator GqlUser()
158 | const jwt = jwt2.sign(
159 | {
160 | id: user.id,
161 | name: user.first_name,
162 | accessToken,
163 | },
164 | process.env.JWT_SECRET,
165 | );
166 | res.cookie('token', jwt, {
167 | domain: '.hotel-prisma.ml',
168 | httpOnly: false,
169 | sameSite: 'none',
170 | secure: true,
171 | });
172 | return user;
173 | }
174 | @Mutation()
175 | async signup(
176 | // DTO file eli5: Extends các field của class và gắn thêm các thuộc tính layer
177 | // Ở đây dùng để validate, rằng buộc type password
178 | @Args('signUpInput') signUpInputDto: SignUpInputDto,
179 | @ResGql() res: Response,
180 | ) {
181 | const emailExists = await this.prisma.client.$exists.user({
182 | email: signUpInputDto.email,
183 | });
184 | if (emailExists) {
185 | throw Error('Email is already in use');
186 | }
187 |
188 | // Validate bằng username thì uncomment dòng dưới
189 |
190 | // const userExists = this.prisma.client.$exists.user({
191 | // username: signUpInputDto.username,
192 | // });
193 |
194 | // if (userExists) {
195 | // console.log(signUpInputDto.username)
196 | // throw Error('Username is already in use');
197 | // }
198 | if (!signUpInputDto.username) {
199 | throw Error('username only contain alphabet letters or numbers');
200 | }
201 | const password = await bcrypt.hash(signUpInputDto.password, 10);
202 | const user = await this.prisma.client.createUser({
203 | ...signUpInputDto,
204 | password,
205 | first_name: signUpInputDto.first_name,
206 | last_name: signUpInputDto.last_name,
207 | role: 'Normal',
208 | // username: signUpInputDto.username,
209 | });
210 | console.log(process.env.JWT_SECRET);
211 | const jwt = jwt2.sign(
212 | {
213 | id: user.id,
214 | name: user.first_name + ' ' + user.last_name,
215 | roles: 'user',
216 | },
217 | process.env.JWT_SECRET,
218 | );
219 | res.cookie('token', jwt, {
220 | domain: '.hotel-prisma.ml',
221 | httpOnly: false,
222 | sameSite: 'none',
223 | secure: true,
224 | });
225 | return user;
226 | }
227 | @Mutation()
228 | @UseGuards(GqlAuthGuard)
229 | async updatePassword(
230 | @GqlUser() user: User,
231 | @Args('password')
232 | { oldPassword, confirmPassword, newPassword }: UpdatePassword, // @ResGql() res: Response,
233 | ) {
234 | const userValid = await this.prisma.client.user({ id: user.id });
235 | if (!userValid) {
236 | throw Error('Must login');
237 | }
238 |
239 | const valid = await bcrypt.compare(oldPassword, user.password);
240 | if (!valid) {
241 | throw Error('Old Password is not right');
242 | }
243 | if (newPassword === oldPassword) {
244 | throw Error('New Password and Old Password field must be different');
245 | }
246 | if (confirmPassword !== newPassword) {
247 | console.log('false');
248 | throw Error('Password Confirm and New Password field must be the same');
249 | }
250 | const password = await bcrypt.hash(newPassword, 10);
251 | console.log('hashed ' + password);
252 | // const jwt = jwt2.sign(
253 | // {
254 | // id: user.id,
255 | // name: user.first_name + ' ' + user.last_name,
256 | // roles: 'user',
257 | // },
258 | // process.env.JWT_SECRET,
259 | // );
260 | // res.cookie('token', jwt, { httpOnly: false });
261 | return this.prisma.client.updateUser({
262 | where: {
263 | id: user.id,
264 | },
265 | data: {
266 | password,
267 | },
268 | });
269 | }
270 | @Mutation()
271 | async changePasswordFromForgetPassword(
272 | @Args('password') password,
273 | @Args('email') email,
274 | @ResGql() res: Response,
275 | ) {
276 | try {
277 | const userValid = await this.prisma.client.user({ email });
278 | if (!userValid) {
279 | throw Error('Invalid user');
280 | }
281 |
282 | const newPassword = await bcrypt.hash(password, 10);
283 | const updatedUser = await this.prisma.client.updateUser({
284 | where: {
285 | email,
286 | },
287 | data: {
288 | password: newPassword,
289 | },
290 | });
291 | const jwt = jwt2.sign(
292 | {
293 | id: updatedUser.id,
294 | name: updatedUser.first_name,
295 | roles: 'user',
296 | },
297 | process.env.JWT_SECRET,
298 | );
299 | res.clearCookie('reset-password', { domain: '.hotel-prisma.ml' }); //Khi làm development thì bỏ domain đi
300 | // res.cookie('token', jwt, { httpOnly: false });
301 | return updatedUser;
302 | } catch (e) {
303 | console.log(e);
304 | }
305 | }
306 | }
307 |
--------------------------------------------------------------------------------
/src/auth/auth.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AuthService } from './auth.service';
3 |
4 | describe('AuthService', () => {
5 | let service: AuthService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [AuthService],
10 | }).compile();
11 |
12 | service = module.get(AuthService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { PrismaService } from 'src/prisma/prisma.service';
3 | import { User } from 'src/graphql.schema.generated';
4 |
5 | @Injectable()
6 | export class AuthService {
7 | constructor(private readonly prisma: PrismaService) {}
8 | async validate({ id }): Promise {
9 | const user = await this.prisma.client.user({ id });
10 | if (!user) throw Error('Authenticate validation error');
11 | return user
12 | }
13 | async socialValidate({ email }): Promise {
14 | const user = await this.prisma.client.user({ email });
15 | console.log(user)
16 | if (!user) throw Error('Authenticate validation error');
17 | return user
18 | }
19 | // Check có user ko bằng cách find Id
20 | }
21 |
--------------------------------------------------------------------------------
/src/auth/facebook.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import * as FacebookTokenStrategy from 'passport-facebook-token';
4 | import { AuthService } from './auth.service';
5 | import { config } from 'dotenv';
6 | config();
7 | @Injectable()
8 | export class FacebookStrategy extends PassportStrategy(
9 | FacebookTokenStrategy,
10 | 'facebook-token',
11 | ) {
12 | constructor(private readonly authService: AuthService) {
13 | super({
14 | clientID: process.env.FACEBOOK_APP_ID,
15 | clientSecret: process.env.FACEBOOK_SECRET_KEY,
16 | });
17 | }
18 |
19 | async validate(
20 | request: any,
21 | accessToken: string,
22 | refreshToken: string,
23 | profile: any,
24 | done: any,
25 | ) {
26 | try {
27 | console.log(accessToken);
28 | console.log(`Got a profile: `, profile);
29 |
30 | const jwt = 'placeholderJWT';
31 | const user = {
32 | jwt,
33 | };
34 | done(null, user);
35 | return this.authService.validate(profile);
36 | } catch (err) {
37 | console.log(`Got an error: `, err);
38 | done(err, false);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/auth/github/github.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { GithubController } from './github.controller';
3 |
4 | describe('Github Controller', () => {
5 | let controller: GithubController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [GithubController],
10 | }).compile();
11 |
12 | controller = module.get(GithubController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/auth/github/github.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Post, Res, Body } from '@nestjs/common';
2 | import { PrismaService } from 'src/prisma/prisma.service';
3 | import { v4 as uuidv4 } from 'uuid';
4 | import { Response } from 'express';
5 | import * as superagent from 'superagent';
6 | import * as jwt2 from 'jsonwebtoken';
7 | // Vì passport cho github strategy còn hạn chế nên làm theo express style
8 | @Controller('github')
9 | export class GithubController {
10 | constructor(private readonly prisma: PrismaService) {}
11 | @Post('callback')
12 | async githubAuth(@Body() code, @Res() res: Response) {
13 | try {
14 | console.log(code);
15 | const accessToken = await superagent
16 | .post('https://github.com/login/oauth/access_token')
17 | .send({
18 | client_id: process.env.GITHUB_CLIENT_ID,
19 | client_secret: process.env.GITHUB_CLIENT_SECRET,
20 | code: code.code,
21 | })
22 | .set('Accept', 'application/json');
23 | const userTokenRequest = accessToken.body.access_token;
24 | const userPayload = await superagent
25 | .get('https://api.github.com/user')
26 | .set('user-agent', 'github') // docs bảo bắt buộc mọi req đén api.github.com phải có header user-agent
27 | .set('Authorization', `Bearer ${userTokenRequest}`); //request bằng header Authorization
28 | const userEmails = await superagent //1 số users hide github email, phải get riêng
29 | .get('https://api.github.com/user/emails')
30 | .set('user-agent', 'github') // docs bảo bắt buộc mọi req đén api.github.com phải có header user-agent
31 | .set('Authorization', `Bearer ${userTokenRequest}`); //request bằng header Authorization
32 | const userInfo = userPayload.body;
33 | const user = await this.prisma.client.user({
34 | email: userEmails.body[0].email,
35 | });
36 | if (user) {
37 | const jwt = jwt2.sign(
38 | {
39 | id: user.id,
40 | name: user.first_name,
41 | accessToken,
42 | },
43 | process.env.JWT_SECRET,
44 | );
45 | res.cookie('token', jwt, {
46 | domain: '.hotel-prisma.ml',
47 | httpOnly: false,
48 | sameSite: 'none',
49 | secure: true,
50 | });
51 | const userSendToClient = {
52 | first_name: user.first_name,
53 | last_name: user.last_name,
54 | cover_pic_main: user.cover_pic_main,
55 | profile_pic_main: user.profile_pic_main,
56 | email: user.email,
57 | id: user.id,
58 | role: user.role,
59 | };
60 | res.status(200).json({ userSendToClient });
61 | return user;
62 | }
63 | // userEmails.body.map(async i => {
64 | // console.log(i.email);
65 | // });
66 | const userCreated = await this.prisma.client.createUser({
67 | first_name: userInfo.login,
68 | last_name: '',
69 | password: uuidv4(),
70 | username: userInfo.node_id,
71 | content: userInfo.bio || 'Nothing',
72 | email: userEmails.body[0].email,
73 | profile_pic_main: userInfo.avatar_url,
74 | cover_pic_main: 'https://i.imgur.com/lXybeGM.png',
75 | role: 'Normal',
76 | });
77 | const jwt = jwt2.sign(
78 | {
79 | id: userCreated.id,
80 | name: userCreated.first_name,
81 | accessToken,
82 | },
83 | process.env.JWT_SECRET,
84 | );
85 | res.cookie('token', jwt, {
86 | domain: '.hotel-prisma.ml',
87 | httpOnly: false,
88 | sameSite: 'none',
89 | secure: true,
90 | });
91 | const userSendToClient = {
92 | first_name: userCreated.first_name,
93 | last_name: userCreated.last_name,
94 | cover_pic_main: userCreated.cover_pic_main,
95 | profile_pic_main: userCreated.profile_pic_main,
96 | email: userCreated.email,
97 | id: userCreated.id,
98 | role: userCreated.role,
99 | };
100 | res.status(200).json({ userSendToClient });
101 | return userCreated;
102 | } catch (e) {
103 | console.log(e);
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/auth/google.strategy.ts:
--------------------------------------------------------------------------------
1 | import { PassportStrategy } from '@nestjs/passport';
2 | import { Strategy, VerifyCallback } from 'passport-google-oauth20';
3 | import { config } from 'dotenv';
4 |
5 | import { Injectable } from '@nestjs/common';
6 |
7 | config();
8 |
9 | @Injectable()
10 | export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
11 |
12 | constructor() {
13 | super({
14 | clientID: process.env.GOOGLE_CLIENT_ID,
15 | clientSecret: process.env.GOOGLE_SECRET,
16 | // callbackURL: 'http://localhost:3000/google/redirect',
17 | // scope: ['email', 'profile'],
18 | });
19 | }
20 |
21 | async validate (accessToken: string, refreshToken: string, profile: any, done: VerifyCallback): Promise {
22 | const { name, emails, photos } = profile
23 | const user = {
24 | email: emails[0].value,
25 | firstName: name.givenName,
26 | lastName: name.familyName,
27 | picture: photos[0].value,
28 | accessToken
29 | }
30 | done(null, user);
31 | }
32 | }
--------------------------------------------------------------------------------
/src/auth/graphql-auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, ExecutionContext } from '@nestjs/common';
2 | import { AuthGuard } from '@nestjs/passport';
3 | import { GqlExecutionContext } from '@nestjs/graphql';
4 | @Injectable()
5 | // Nếu muốn sử dụng trực tiếp ko thông qua decorator GqlUser()
6 | // Hoặc tự config được github strategy thì xài uncomment dòng dưới
7 | // export class GqlAuthGuard extends AuthGuard(['jwt','facebook-token, google']) {
8 | export class GqlAuthGuard extends AuthGuard('jwt') {
9 | getRequest(context: ExecutionContext) {
10 | const ctx = GqlExecutionContext.create(context);
11 | return ctx.getContext().req;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/auth/jwt.strategy.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { PassportStrategy } from '@nestjs/passport';
3 | import { Strategy } from 'passport-jwt';
4 | import { Request } from 'express';
5 | import { AuthService } from './auth.service';
6 |
7 | // Lấy và validate token ở đây
8 | //Lấy từ cookie, và pass .env JWT_SECRET
9 | // Để parse cookies thì defined global cookie-parser
10 | const cookieExtractor = (req: Request): string | null => {
11 | let token = null;
12 | if (req && req.cookies) {
13 | token = req.cookies.token;
14 | }
15 | return token;
16 | };
17 |
18 | @Injectable()
19 | export class JwtStrategy extends PassportStrategy(Strategy) {
20 | constructor(private readonly authService: AuthService) {
21 | super({
22 | jwtFromRequest: cookieExtractor,
23 | secretOrKey: process.env.JWT_SECRET,
24 | });
25 | }
26 | validate(payload) {
27 | return this.authService.validate(payload);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/auth/sign-up-input.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsEmail, MinLength, IsAlphanumeric } from 'class-validator';
2 | import { SignUpInput } from '../graphql.schema.generated';
3 |
4 | export class SignUpInputDto extends SignUpInput {
5 | @IsEmail()
6 | readonly email: string
7 | @MinLength(6)
8 | readonly password: string
9 | @IsAlphanumeric()
10 | readonly username: string
11 | }
12 |
--------------------------------------------------------------------------------
/src/aws/aws.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AwsService } from './aws.service';
3 |
4 | describe('AwsService', () => {
5 | let service: AwsService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [AwsService],
10 | }).compile();
11 |
12 | service = module.get(AwsService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/aws/aws.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import * as AWS from 'aws-sdk';
3 |
4 | @Injectable()
5 | export class AwsService {
6 | public async uploadFile(file: any): Promise {
7 | const urlArray = [];
8 | // console.log( file);
9 | const AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME;
10 | const s3 = new AWS.S3({
11 | accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID,
12 | secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY,
13 | });
14 | await file.map(async (file, i: number) => {
15 | const urlKey = `filePath/${file.originalname}`;
16 | const params = {
17 | Body: file.buffer,
18 | Bucket: AWS_S3_BUCKET_NAME,
19 | Key: urlKey,
20 | ACL: 'public-read', //Để đọc được url
21 | };
22 | // console.log(params);
23 |
24 | // getSignedUrl chỉ có callback, có thể convert thành Promise như cách dưới
25 | // Hoặc xài package bluebird
26 | const url = new Promise((resolve, reject) => {
27 | s3.getSignedUrl('putObject', params, (err, data) => {
28 | if (err) {
29 | console.log(err);
30 | reject(err);
31 | // res.json({success: false, error: err})
32 | }
33 | const returnData = {
34 | // Có thể xài lib uuid()
35 | uid: `${file.originalname}-${file.size - i}`,
36 | signedRequest: data,
37 | url: `https://${AWS_S3_BUCKET_NAME}.s3.amazonaws.com/filePath/${file.originalname}`,
38 | };
39 | resolve(returnData);
40 | });
41 | });
42 | urlArray.push(url);
43 |
44 | // Upload lên aws
45 | const data = await s3
46 | .putObject(params)
47 | .promise()
48 | .then(
49 | data => {
50 | return urlKey;
51 | },
52 | err => {
53 | console.log(err);
54 | return err;
55 | },
56 | );
57 | });
58 | return Promise.all(urlArray).then(r => r);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/cron/cron.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PrismaModule } from 'src/prisma/prisma.module';
3 | import { MailService } from 'src/services/sendEmail';
4 | @Module({
5 | providers: [MailService],
6 | imports: [PrismaModule],
7 | })
8 | export class CronModule {}
9 |
--------------------------------------------------------------------------------
/src/cron/cron.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CronService } from './cron.service';
3 |
4 | describe('CronService', () => {
5 | let service: CronService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [CronService],
10 | }).compile();
11 |
12 | service = module.get(CronService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/cron/cron.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { Cron, CronExpression } from '@nestjs/schedule';
3 | import { PrismaService } from 'src/prisma/prisma.service';
4 | import { MailService } from 'src/services/sendEmail';
5 | @Injectable()
6 | export class CronService {
7 | constructor(
8 | private readonly prisma: PrismaService,
9 | private readonly mailService: MailService,
10 | ) {}
11 | // private readonly mailService: MailService;
12 | private readonly logger = new Logger(CronService.name);
13 |
14 | @Cron(CronExpression.EVERY_DAY_AT_9PM, {
15 | name: 'notiCron',
16 | })
17 | async handleCron() {
18 | // Có thể ứng dụng làm chức năng subscribe blog, newsletter
19 | // Demo mẫu gửi email
20 | // Thường cronn email nên service bên thứ 3
21 | // Tham khảo easycron.com
22 | // Timeout delay lại time
23 | // Interval executes callback đúng sau x asterisk
24 | // Có thể dùng gọi sự kiện delete prisma hay process trong project này là coupon hết hạn,...
25 | // Ở đây mock nhanh bằng cách gửi về process.env.LOCAL_EMAIL (admin)
26 | const pendingTransactions = await this.prisma.client
27 | .transactionsConnection({
28 | where: {
29 | transactionHotelManagerId: '5f2b768aa7b11b00078d09d8',
30 | transactionStatus: 'PENDING',
31 | },
32 | })
33 | .aggregate()
34 | .count();
35 | const message = `You are having ${pendingTransactions} pending transactions`;
36 | this.logger.debug(message);
37 | return this.mailService.sendContact(
38 | 'CRON ABOUT NUMBER OF PENDING TRANSACTIONS',
39 | 'minha1403@gmail.com',
40 | message,
41 | '00000x00000',
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/graphql.options.ts:
--------------------------------------------------------------------------------
1 | import { GqlModuleOptions, GqlOptionsFactory } from '@nestjs/graphql';
2 | import { Injectable } from '@nestjs/common';
3 | import { join } from 'path';
4 |
5 | @Injectable()
6 | export class GraphqlOptions implements GqlOptionsFactory {
7 | createGqlOptions(): Promise | GqlModuleOptions {
8 | return {
9 | context: ({ req, res }) => ({ req, res }),
10 | typePaths: ['./src/*/*.graphql'], // path for gql schema files
11 | installSubscriptionHandlers: true,
12 | resolverValidationOptions: {
13 | requireResolversForResolveType: false,
14 | },
15 | definitions: {
16 | // will generate .ts types from gql schema files
17 | path: join(process.cwd(), 'src/graphql.schema.generated.ts'),
18 | outputAs: 'class',
19 | },
20 | debug: true,
21 | introspection: true,
22 | playground: true,
23 | cors: false,
24 | uploads: {
25 | maxFileSize: 10000000, // 10 MB
26 | maxFiles: 5,
27 | },
28 | };
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/graphql.schema.generated.ts:
--------------------------------------------------------------------------------
1 |
2 | /** ------------------------------------------------------
3 | * THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
4 | * -------------------------------------------------------
5 | */
6 |
7 | /* tslint:disable */
8 | /* eslint-disable */
9 | export class AddHotelInput {
10 | hotelName?: string;
11 | pricePerNight?: number;
12 | hotelDetails?: string;
13 | guest?: number;
14 | rooms?: number;
15 | price?: number;
16 | hotelPhotos?: ImageInput[];
17 | location?: LocationInput[];
18 | locationDescription?: string;
19 | contactNumber?: string;
20 | propertyType?: string;
21 | isNegotiable?: boolean;
22 | wifiAvailability?: boolean;
23 | airCondition?: boolean;
24 | parking?: boolean;
25 | poolAvailability?: boolean;
26 | extraBed?: boolean;
27 | }
28 |
29 | export class AmenitiesSearchInput {
30 | wifiAvailability?: boolean;
31 | poolAvailability?: boolean;
32 | parkingAvailability?: boolean;
33 | airCondition?: boolean;
34 | rooms?: number;
35 | guest?: number;
36 | }
37 |
38 | export class CategoriesInput {
39 | slug?: string;
40 | name?: string;
41 | image?: ImageInput;
42 | }
43 |
44 | export class ContactInput {
45 | subject: string;
46 | message?: string;
47 | email?: string;
48 | cellNumber?: string;
49 | }
50 |
51 | export class CouponCheckedPayload {
52 | couponId?: string;
53 | couponName?: string;
54 | couponValue?: number;
55 | couponType?: number;
56 | }
57 |
58 | export class CouponInput {
59 | couponName: string;
60 | couponDescription?: string;
61 | couponType?: number;
62 | couponValue?: number;
63 | couponQuantity?: number;
64 | couponStartDate?: string;
65 | couponEndDate?: string;
66 | couponRange?: number;
67 | }
68 |
69 | export class DeletePhotosInput {
70 | id: string;
71 | }
72 |
73 | export class ImageInput {
74 | url?: string;
75 | }
76 |
77 | export class LocationInput {
78 | lat?: number;
79 | lng?: number;
80 | formattedAddress?: string;
81 | zipcode?: string;
82 | city?: string;
83 | state_long?: string;
84 | state_short?: string;
85 | country_long?: string;
86 | country_short?: string;
87 | }
88 |
89 | export class LoginInput {
90 | email: string;
91 | password: string;
92 | }
93 |
94 | export class MockInput {
95 | title?: string[];
96 | }
97 |
98 | export class ReviewFieldInput {
99 | rating?: number;
100 | ratingFieldName?: string;
101 | }
102 |
103 | export class ReviewInput {
104 | reviewOverall?: number;
105 | reviewTitle?: string;
106 | reviewTips?: string;
107 | reviewText?: string;
108 | sortOfTrip?: string;
109 | reviewFieldInput?: ReviewFieldInput[];
110 | reviewOptionals?: ReviewOptionalsInput[];
111 | reviewPics?: ImageInput[];
112 | }
113 |
114 | export class ReviewOptionalsInput {
115 | option?: string;
116 | optionField?: string;
117 | }
118 |
119 | export class SearchInput {
120 | minPrice?: number;
121 | maxPrice?: number;
122 | }
123 |
124 | export class SignUpInput {
125 | username?: string;
126 | first_name?: string;
127 | last_name?: string;
128 | email: string;
129 | password: string;
130 | profile_pic_main?: string;
131 | cover_pic_main?: string;
132 | }
133 |
134 | export class SocialInput {
135 | facebook?: string;
136 | instagram?: string;
137 | twitter?: string;
138 | }
139 |
140 | export class TransactionInput {
141 | transactionHotelName?: string;
142 | transactionHotelManagerId?: string;
143 | transactionHotelType?: string;
144 | transactionPrice?: number;
145 | transactionAuthorName?: string;
146 | transactionAuthorEmail?: string;
147 | transactionAuthorContactNumber?: string;
148 | transactionAuthorSpecial?: string;
149 | transactionAuthorNote?: string;
150 | transactionLocationFormattedAddress?: string;
151 | transactionLocationLat?: number;
152 | transactionLocationLng?: number;
153 | transactionRoom?: number;
154 | transactionGuest?: number;
155 | transactionRange?: number;
156 | transactionStartDate?: string;
157 | transactionEndDate?: string;
158 | transactionStripeId?: string;
159 | }
160 |
161 | export class UpdatePassword {
162 | confirmPassword: string;
163 | oldPassword: string;
164 | newPassword: string;
165 | }
166 |
167 | export class UpdatePhotosInput {
168 | url?: string;
169 | uid?: string;
170 | signedRequest?: string;
171 | }
172 |
173 | export class UpdateProfileInput {
174 | first_name?: string;
175 | last_name?: string;
176 | date_of_birth?: string;
177 | gender?: string;
178 | email?: string;
179 | content?: string;
180 | agent_location?: LocationInput;
181 | cellNumber?: string;
182 | }
183 |
184 | export class Amenities {
185 | id: string;
186 | guestRoom?: number;
187 | bedRoom?: number;
188 | wifiAvailability?: boolean;
189 | parkingAvailability?: boolean;
190 | poolAvailability?: boolean;
191 | airCondition?: boolean;
192 | extraBedFacility?: boolean;
193 | }
194 |
195 | export class AuthPayload {
196 | id?: string;
197 | email?: string;
198 | first_name?: string;
199 | last_name?: string;
200 | profile_pic_main?: string;
201 | cover_pic_main?: string;
202 | role?: string;
203 | }
204 |
205 | export class Categories {
206 | id: string;
207 | slug?: string;
208 | name?: string;
209 | image?: Image;
210 | }
211 |
212 | export class Coupon {
213 | couponId: string;
214 | couponName: string;
215 | couponDescription?: string;
216 | couponAuthor?: User;
217 | couponAuthorId?: string;
218 | couponType?: number;
219 | couponValue?: number;
220 | couponQuantity?: number;
221 | couponStartDate?: string;
222 | couponEndDate?: string;
223 | couponRange?: number;
224 | couponTarget?: Hotel[];
225 | createdAt: string;
226 | updatedAt: string;
227 | }
228 |
229 | export class Gallery {
230 | id: string;
231 | uid?: string;
232 | url?: string;
233 | signedRequest?: string;
234 | }
235 |
236 | export class Hotel {
237 | id: string;
238 | peopleLiked?: User[];
239 | peopleReviewed?: User[];
240 | couponsAvailable?: Coupon[];
241 | connectId?: User;
242 | agentId?: string;
243 | agentEmail?: string;
244 | agentName?: string;
245 | title?: string;
246 | slug?: string;
247 | status?: string;
248 | content?: string;
249 | price?: number;
250 | isNegotiable?: boolean;
251 | propertyType?: string;
252 | condition?: string;
253 | rating?: number;
254 | ratingCount?: number;
255 | contactNumber?: string;
256 | termsAndCondition?: string;
257 | amenities?: Amenities[];
258 | image?: Image;
259 | location?: Location[];
260 | gallery?: Gallery[];
261 | categories?: Categories[];
262 | reviews?: Reviews[];
263 | createdAt: string;
264 | updatedAt: string;
265 | }
266 |
267 | export class HotelPhotos {
268 | url: string;
269 | }
270 |
271 | export class Image {
272 | id: string;
273 | url?: string;
274 | thumb_url?: string;
275 | }
276 |
277 | export class Location {
278 | id: string;
279 | lat?: number;
280 | lng?: number;
281 | formattedAddress?: string;
282 | zipcode?: string;
283 | city?: string;
284 | state_long?: string;
285 | state_short?: string;
286 | country_long?: string;
287 | country_short?: string;
288 | }
289 |
290 | export class Message {
291 | reviewAuthorName?: string;
292 | reviewAuthorId?: string;
293 | reviewedHotelId?: Hotel;
294 | reviewedHotelName?: string;
295 | hotelManagerId?: string;
296 | reviewTitle?: string;
297 | reviewText?: string;
298 | peopleReviewedQuanity?: number;
299 | peopleReviewedArr?: User[];
300 | }
301 |
302 | export abstract class IMutation {
303 | abstract signup(signUpInput?: SignUpInput): AuthPayload | Promise;
304 |
305 | abstract login(loginInput?: LoginInput): AuthPayload | Promise;
306 |
307 | abstract facebookLogin(email?: string, accessToken?: string, socialInfo?: string, socialId?: string, socialProfileLink?: string): AuthPayload | Promise;
308 |
309 | abstract googleLogin(email?: string, accessToken?: string, socialInfo?: string, socialId?: string, profileImage?: string): AuthPayload | Promise;
310 |
311 | abstract createHotel(addHotelInput?: AddHotelInput, location?: LocationInput[], image?: ImageInput[], categories?: CategoriesInput[]): Hotel | Promise;
312 |
313 | abstract sortHotel(type?: string): Hotel[] | Promise;
314 |
315 | abstract likeHotel(id: string): Hotel | Promise;
316 |
317 | abstract dislikeHotel(id: string): Hotel | Promise;
318 |
319 | abstract filterHotels(search?: SearchInput, amenities?: AmenitiesSearchInput): Hotel[] | Promise;
320 |
321 | abstract createLocation(location?: LocationInput): Location | Promise;
322 |
323 | abstract updateProfile(profile?: UpdateProfileInput, location?: LocationInput, social?: SocialInput): User | Promise;
324 |
325 | abstract updatePhotos(photos?: UpdatePhotosInput[]): User | Promise;
326 |
327 | abstract deletePhotos(photos?: DeletePhotosInput[]): User | Promise;
328 |
329 | abstract setProfilePic(url?: string): User | Promise;
330 |
331 | abstract setCoverPic(url?: string): User | Promise;
332 |
333 | abstract updatePassword(password?: UpdatePassword): User | Promise;
334 |
335 | abstract forgetPassword(email?: string): User | Promise;
336 |
337 | abstract changePasswordFromForgetPassword(email?: string, password?: string): User | Promise;
338 |
339 | abstract sendContact(contact?: ContactInput): User | Promise;
340 |
341 | abstract makeReviews(reviews?: ReviewInput, hotelId?: string): Hotel | Promise;
342 |
343 | abstract likeOrDislikeReview(id: string, type?: number): Reviews | Promise;
344 |
345 | abstract checkNotification(id: string): User | Promise;
346 |
347 | abstract readNotification(query?: string): User | Promise;
348 |
349 | abstract deleteAllNotifications(id: string): User | Promise;
350 |
351 | abstract createTransaction(transaction?: TransactionInput, hotelId?: string, userId?: string, coupon?: CouponCheckedPayload): Transaction | Promise;
352 |
353 | abstract createCoupon(coupon?: CouponInput, hotelsId?: string[], type?: number): Coupon[] | Promise;
354 |
355 | abstract checkCoupon(hotelId?: string, couponName?: string): Coupon | Promise;
356 |
357 | abstract processTransactions(id?: string[], type?: number): Transaction | Promise;
358 |
359 | abstract updateTotalUnreadTransactions(): User | Promise;
360 |
361 | abstract deleteCoupons(id?: string[]): Coupon[] | Promise;
362 |
363 | abstract updateStripeId(stripeId?: string, type?: string): User | Promise;
364 | }
365 |
366 | export class Notification {
367 | id: string;
368 | reviewAuthorName?: string;
369 | reviewedHotelName?: string;
370 | reviewTitle?: string;
371 | reviewText?: string;
372 | userNotificationId?: string;
373 | peopleReviewedQuantity?: number;
374 | query?: string;
375 | reviewAuthorProfilePic?: string;
376 | read?: boolean;
377 | old?: boolean;
378 | createdAt: string;
379 | updatedAt: string;
380 | }
381 |
382 | export abstract class IQuery {
383 | abstract allAmenities(): Amenities[] | Promise;
384 |
385 | abstract imageId(id: string): Hotel | Promise;
386 |
387 | abstract locationId(id: string): Location | Promise;
388 |
389 | abstract locations(): Location[] | Promise;
390 |
391 | abstract galleryId(id: string): Gallery | Promise;
392 |
393 | abstract galleries(): Gallery[] | Promise;
394 |
395 | abstract categoryId(id: string): Categories | Promise;
396 |
397 | abstract allCategories(): Categories[] | Promise;
398 |
399 | abstract userPosts(id: string): User | Promise;
400 |
401 | abstract favouritePosts(id: string): User | Promise;
402 |
403 | abstract favouritePostsHeart(id: string): User | Promise;
404 |
405 | abstract getUserInfo(id: string): User | Promise;
406 |
407 | abstract getUserGallery(id: string): User | Promise;
408 |
409 | abstract getUserReviews(id: string): User | Promise;
410 |
411 | abstract getUserNotification(id: string): Notification[] | Promise;
412 |
413 | abstract getUserUnreadNotification(id: string): User | Promise;
414 |
415 | abstract getHotelInfo(id: string): Hotel | Promise;
416 |
417 | abstract getHotelReviews(id: string): Reviews[] | Promise;
418 |
419 | abstract getHotelCoupons(id: string): Coupon[] | Promise;
420 |
421 | abstract getHotelManagerCoupons(): Coupon[] | Promise;
422 |
423 | abstract getReviewsLikeDislike(id: string): Reviews | Promise;
424 |
425 | abstract getAllHotels(location?: LocationInput, type?: string, search?: SearchInput, amenities?: AmenitiesSearchInput, property?: string[]): Hotel[] | Promise;
426 |
427 | abstract getFilteredHotels(location?: string, search?: SearchInput, amenities?: AmenitiesSearchInput, property?: string[]): Hotel[] | Promise;
428 |
429 | abstract getTransactionsHaving(orderBy?: string): Transaction[] | Promise;
430 |
431 | abstract getTransactionDetails(transactionSecretKey?: string): Transaction[] | Promise;
432 |
433 | abstract getTotalUnreadTransactions(): User | Promise;
434 |
435 | abstract getVendorStripeId(id?: string): User | Promise;
436 | }
437 |
438 | export class ReviewFields {
439 | id: string;
440 | rating?: number;
441 | ratingFieldName?: string;
442 | }
443 |
444 | export class ReviewOptionals {
445 | id: string;
446 | option?: string;
447 | optionField?: string;
448 | }
449 |
450 | export class Reviews {
451 | reviewID: string;
452 | reviewTitle?: string;
453 | reviewText?: string;
454 | peopleLiked?: User[];
455 | peopleDisliked?: User[];
456 | reviewTips?: string;
457 | sortOfTrip?: string;
458 | reviewAuthorId?: User;
459 | reviewAuthorFirstName?: string;
460 | reviewAuthorLastName?: string;
461 | reviewAuthorEmail?: string;
462 | reviewOverall?: number;
463 | reviewAuthorPic?: string;
464 | reviewedHotel?: Hotel;
465 | reviewedHotelId?: string;
466 | reviewPics?: Image[];
467 | reviewDate?: string;
468 | reviewOptional?: ReviewOptionals[];
469 | reviewFields?: ReviewFields[];
470 | createdAt: string;
471 | updatedAt: string;
472 | }
473 |
474 | export class Social {
475 | facebook?: string;
476 | twitter?: string;
477 | linkedIn?: string;
478 | instagram?: string;
479 | }
480 |
481 | export abstract class ISubscription {
482 | abstract unreadNotification(channelId?: string): Unread | Promise;
483 |
484 | abstract notificationBell(channelId?: string): Notification | Promise;
485 |
486 | abstract realtimeReviews(hotelId?: string): Reviews | Promise;
487 |
488 | abstract realtimeLikeDislike(reviewID?: string): Reviews | Promise;
489 |
490 | abstract realtimeNotificationTransaction(userId?: string): TransactionNotification | Promise;
491 | }
492 |
493 | export class Transaction {
494 | TXID: string;
495 | transactionSecretKey?: string;
496 | transactionHotelName?: string;
497 | transactionHotelManager?: User;
498 | transactionHotelManagerId?: string;
499 | transactionHotelId?: string;
500 | transactionHotelType?: string;
501 | transactionPrice?: number;
502 | transactionAuthor?: User;
503 | transactionAuthorId?: string;
504 | transactionAuthorName?: string;
505 | transactionAuthorEmail?: string;
506 | transactionAuthorContactNumber?: string;
507 | transactionAuthorSpecial?: string;
508 | transactionAuthorNote?: string;
509 | transactionLocationLat?: number;
510 | transactionLocationLng?: number;
511 | transactionLocationFormattedAddress?: string;
512 | transactionRoom?: number;
513 | transactionGuest?: number;
514 | transactionRange?: number;
515 | transactionStatus?: string;
516 | transactionCoupon?: string;
517 | transactionCouponType?: number;
518 | transactionCouponValue?: number;
519 | transactionStartDate?: string;
520 | transactionEndDate?: string;
521 | transactionStripeId?: string;
522 | createdAt: string;
523 | updatedAt: string;
524 | }
525 |
526 | export class TransactionNotification {
527 | TXID: string;
528 | transactionPrice?: number;
529 | }
530 |
531 | export class UncheckTransactions {
532 | id: string;
533 | userUncheckTransactionsId?: string;
534 | userUncheckTransactions?: User;
535 | totalPrice?: number;
536 | totalTransactions?: number;
537 | }
538 |
539 | export class Unread {
540 | unreadNotification?: number;
541 | }
542 |
543 | export class User {
544 | id: string;
545 | first_name: string;
546 | last_name: string;
547 | username: string;
548 | stripeId?: string;
549 | password: string;
550 | email: string;
551 | role?: string;
552 | cellNumber?: string;
553 | profile_pic_main?: string;
554 | cover_pic_main?: string;
555 | profile_pic?: Image[];
556 | cover_pic?: Image[];
557 | date_of_birth?: string;
558 | gender?: string;
559 | content?: string;
560 | agent_location?: Location;
561 | gallery?: Gallery[];
562 | social_profile?: Social;
563 | reviews_maked?: ReviewFields[];
564 | listed_posts?: Hotel[];
565 | notification?: Notification[];
566 | unreadNotification?: number;
567 | favourite_post?: Hotel[];
568 | reviewed_post?: Hotel[];
569 | review_liked?: Reviews[];
570 | review_disliked?: Reviews[];
571 | transaction_had?: Transaction[];
572 | transaction_maked?: Transaction[];
573 | uncheckTransactions?: UncheckTransactions;
574 | coupons_maked?: Coupon[];
575 | createdAt: string;
576 | updatedAt: string;
577 | }
578 |
--------------------------------------------------------------------------------
/src/hotel/hotel.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { HotelService } from './hotel.service';
3 | import { HotelResolver } from './hotel.resolver';
4 | import { PrismaModule } from 'src/prisma/prisma.module';
5 |
6 | @Module({
7 | providers: [HotelService, HotelResolver],
8 | imports: [PrismaModule],
9 | })
10 | export class HotelModule {}
11 |
--------------------------------------------------------------------------------
/src/hotel/hotel.resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { HotelResolver } from './hotel.resolver';
3 |
4 | describe('HotelResolver', () => {
5 | let resolver: HotelResolver;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [HotelResolver],
10 | }).compile();
11 |
12 | resolver = module.get(HotelResolver);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(resolver).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/hotel/hotel.resolver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Resolver,
3 | Mutation,
4 | Args,
5 | Parent,
6 | Query,
7 | ResolveField,
8 | } from '@nestjs/graphql';
9 | import { PrismaService } from 'src/prisma/prisma.service';
10 | import {
11 | AddHotelInput,
12 | LocationInput,
13 | ImageInput,
14 | // CategoriesInput,
15 | User,
16 | Hotel,
17 | // Reviews,
18 | SearchInput,
19 | AmenitiesSearchInput,
20 | } from 'src/graphql.schema.generated';
21 | import { UseGuards } from '@nestjs/common';
22 | import { GqlAuthGuard } from 'src/auth/graphql-auth.guard';
23 | import { GqlUser } from 'src/shared/decorators/decorator';
24 |
25 | @Resolver('Hotel')
26 | export class HotelResolver {
27 | constructor(private readonly prisma: PrismaService) {}
28 | format = s =>
29 | s
30 | .toLowerCase()
31 | .split(/\s|%20/)
32 | .filter(Boolean)
33 | .join('-');
34 |
35 | // Các field được connect nhau thì cần resolve theo đúng id, để mutation và query return được subfield
36 | @ResolveField()
37 | async connectId(@Parent() { id }: Hotel) {
38 | // console.log(id);
39 | return this.prisma.client.hotel({ id }).connectId();
40 | }
41 | @ResolveField()
42 | async peopleLiked(@Parent() { id }: Hotel) {
43 | // console.log(id);
44 | return this.prisma.client.hotel({ id }).peopleLiked();
45 | }
46 | @ResolveField()
47 | async peopleReviewed(@Parent() { id }: Hotel) {
48 | return await this.prisma.client.hotel({ id }).peopleReviewed();
49 | }
50 | // Mẹo để có id resolve trong trường hợp parent - data trả về khác type - khai báo schema file thêm 1 field chứa id đó
51 | // Nếu schema đó có link:INLINE (chứa) thì ko cần
52 | @ResolveField()
53 | async reviews(@Parent() hotel: Hotel) {
54 | // Nếu vừa dùng cho cả query và mutation thì nên ráng bóc ra 1 cái stable từ cái id ta đã gán connect
55 | // const id = reviewedHotelId
56 | // const id = reviewedHotelId ? reviewedHotelId : user[0].reviewedHotelId;
57 | const id = hotel.id;
58 | // console.log(id);
59 | // Có thể ko cần ghi trong fragment field đã resolved
60 | const fragment = `fragment getUserInfoFromReview on User
61 | {
62 | reviewTitle
63 | reviewedHotelId
64 | reviewID
65 | reviewText
66 | peopleLiked {
67 | id
68 | }
69 | peopleDisliked {
70 | id
71 | }
72 | sortOfTrip
73 | reviewAuthorEmail
74 | reviewOverall
75 | reviewTips
76 | reviewAuthorPic
77 | reviewPics{
78 | url
79 | }
80 | reviewDate
81 | reviewOptional{
82 | option
83 | optionField
84 | }
85 | reviewFields{
86 | rating
87 | ratingFieldName
88 | }
89 | reviewAuthorFirstName
90 | reviewAuthorLastName
91 | reviewAuthorId
92 | {
93 | id
94 | first_name
95 | last_name
96 | username
97 | password
98 | email
99 | cellNumber
100 | profile_pic
101 | {
102 | id
103 | url
104 | }
105 | cover_pic
106 | {
107 | id
108 | url
109 | }
110 | date_of_birth
111 | gender
112 | content
113 | agent_location{
114 | id
115 | lat
116 | lng
117 | formattedAddress
118 | zipcode
119 | city
120 | state_long
121 | state_short
122 | country_long
123 | country_short
124 | }
125 | gallery
126 | {
127 | id
128 | url
129 | }
130 | social_profile{
131 | id
132 | facebook
133 | twitter
134 | linkedIn
135 | instagram
136 | }
137 |
138 | createdAt
139 | updatedAt
140 | }
141 | }`;
142 | // console.log(user[0].reviewedHotelId);
143 | return this.prisma.client
144 | .hotel({ id: id })
145 | .reviews({ orderBy: 'reviewDate_DESC' })
146 | .$fragment(fragment);
147 | }
148 | @ResolveField()
149 | async location(@Parent() { id }: Hotel) {
150 | // console.log('id tu con' + id);
151 | return this.prisma.client.hotel({ id }).location();
152 | }
153 | @ResolveField()
154 | async amenities(@Parent() { id }: Hotel) {
155 | return this.prisma.client.hotel({ id }).amenities();
156 | }
157 | @ResolveField()
158 | async image(@Parent() { id }: Hotel) {
159 | return this.prisma.client.hotel({ id }).image();
160 | }
161 | @ResolveField()
162 | async gallery(@Parent() { id }: Hotel) {
163 | return this.prisma.client.hotel({ id }).gallery();
164 | }
165 | @ResolveField()
166 | async categories(@Parent() { id }: Hotel) {
167 | return this.prisma.client.hotel({ id }).categories();
168 | }
169 | @Query()
170 | async locationId(@Args('id') id) {
171 | return this.prisma.client.location({ id });
172 | }
173 | @Query()
174 | async imageId(@Args('id') id) {
175 | return this.prisma.client.image({ id });
176 | }
177 | @Query()
178 | async galleryId(@Args('id') id) {
179 | return this.prisma.client.gallery({ id });
180 | }
181 | @Query()
182 | async categoryId(@Args('id') id) {
183 | return this.prisma.client.categories({ id });
184 | }
185 | @Query()
186 | async allAmenities() {
187 | return this.prisma.client.amenitieses();
188 | }
189 | @Query()
190 | async locations() {
191 | return this.prisma.client.locations();
192 | }
193 | @Query()
194 | async galleries() {
195 | return this.prisma.client.galleries();
196 | }
197 | @Query()
198 | async allCategories() {
199 | return this.prisma.client.categorieses();
200 | }
201 | @Query()
202 | async getHotelInfo(@Args('id') id) {
203 | return this.prisma.client.hotel({ id });
204 | }
205 | @Query()
206 | async getHotelReviews(@Args('id') id) {
207 | const fragment = `fragment getHotelReviews on Reviews
208 | {
209 | reviewTitle
210 | reviewedHotelId
211 | reviewID
212 | reviewText
213 | peopleLiked {
214 | id
215 | }
216 | peopleDisliked {
217 | id
218 | }
219 | sortOfTrip
220 | reviewAuthorEmail
221 | reviewOverall
222 | reviewTips
223 | reviewAuthorPic
224 | reviewPics{
225 | url
226 | }
227 | reviewDate
228 | reviewOptional{
229 | option
230 | optionField
231 | }
232 | reviewFields{
233 | rating
234 | ratingFieldName
235 | }
236 | reviewAuthorFirstName
237 | reviewAuthorLastName
238 | reviewAuthorId
239 | {
240 | id
241 | first_name
242 | last_name
243 | username
244 | password
245 | email
246 | cellNumber
247 | profile_pic
248 | {
249 | id
250 | url
251 | }
252 | cover_pic
253 | {
254 | id
255 | url
256 | }
257 | date_of_birth
258 | gender
259 | content
260 | agent_location{
261 | id
262 | lat
263 | lng
264 | formattedAddress
265 | zipcode
266 | city
267 | state_long
268 | state_short
269 | country_long
270 | country_short
271 | }
272 | gallery
273 | {
274 | id
275 | url
276 | }
277 | social_profile{
278 | id
279 | facebook
280 | twitter
281 | linkedIn
282 | instagram
283 | }
284 |
285 | createdAt
286 | updatedAt
287 | }
288 | }`;
289 | return this.prisma.client
290 | .hotel({ id })
291 | .reviews({ orderBy: 'reviewDate_DESC' })
292 | .$fragment(fragment);
293 | }
294 | @Query()
295 | async getAllHotels(
296 | @Args('type') type,
297 | @Args('search') search: SearchInput,
298 | @Args('amenities') amenities: AmenitiesSearchInput,
299 | @Args('property') property: string[],
300 | @Args('location') location: LocationInput,
301 | ) {
302 | console.log('location');
303 | console.log(location);
304 | console.log('query');
305 | console.log(type);
306 | console.log('search');
307 | console.log(search);
308 | console.log('amenities');
309 | console.log(amenities);
310 | JSON.stringify(amenities) === '{}' ? (amenities = undefined) : amenities;
311 | console.log(amenities);
312 | console.log('property');
313 | console.log(property);
314 | JSON.stringify(property) === '{}' || JSON.stringify(property) === '[]'
315 | ? (property = undefined)
316 | : property;
317 | console.log(property);
318 | return this.prisma.client.hotels({
319 | where: {
320 | amenities_some: {
321 | wifiAvailability: amenities && amenities.wifiAvailability,
322 | parkingAvailability: amenities && amenities.parkingAvailability,
323 | poolAvailability: amenities && amenities.poolAvailability,
324 | airCondition: amenities && amenities.airCondition,
325 | bedRoom: amenities && amenities.rooms,
326 | guestRoom: amenities && amenities.guest,
327 | },
328 | location_some: {
329 | // formattedAddress_contains: location.formattedAddress,
330 | // city_contains: location.city,
331 | // state_short_contains: location.state_short,
332 | country_long_contains: location && location.country_long,
333 | country_short_contains: location && location.country_short,
334 | },
335 | propertyType_in: property,
336 | AND: [
337 | {
338 | price_gte: search && search.minPrice,
339 | },
340 | {
341 | price_lte: search && search.maxPrice,
342 | },
343 | ],
344 | },
345 | orderBy: type,
346 | });
347 | }
348 | @Query()
349 | async getHotelCoupons(@Args('id') id) {
350 | const fragment = `fragment getHotelCoupons on Hotel {
351 | couponId
352 | couponName
353 | couponDescription
354 | couponAuthor{
355 | email
356 | }
357 | couponAuthorId
358 | couponType
359 | couponValue
360 | couponQuantity
361 | couponStartDate
362 | couponEndDate
363 | couponTarget{
364 | id
365 | slug
366 | title
367 | }
368 | createdAt
369 | updatedAt
370 | }`;
371 | return this.prisma.client
372 | .hotel({ id })
373 | .couponsAvailable({ orderBy: 'createdAt_DESC' })
374 | .$fragment(fragment);
375 | }
376 | @Query()
377 | @UseGuards(GqlAuthGuard)
378 | async getHotelManagerCoupons(@GqlUser() user: User) {
379 | const fragment = `fragment getHotelManagerCouponsFrag on User{
380 | couponId
381 | couponName
382 | couponDescription
383 | couponType
384 | couponValue
385 | couponQuantity
386 | couponStartDate
387 | couponEndDate
388 | couponRange
389 | couponTarget{
390 | slug
391 | id
392 | }
393 | createdAt
394 | updatedAt
395 | }`;
396 | return this.prisma.client
397 | .user({ id: user.id })
398 | .coupons_maked({
399 | orderBy: 'createdAt_DESC',
400 | })
401 | .$fragment(fragment);
402 | }
403 | // Test riêng filter
404 | @Query()
405 | async getFilteredHotels(
406 | @Args('search') search: SearchInput,
407 | @Args('amenities') amenities: AmenitiesSearchInput,
408 | @Args('property') property: string[],
409 | ) {
410 | return this.prisma.client.hotels({
411 | where: {
412 | amenities_some: {
413 | wifiAvailability: amenities && amenities.wifiAvailability,
414 | parkingAvailability: amenities && amenities.parkingAvailability,
415 | poolAvailability: amenities && amenities.poolAvailability,
416 | airCondition: amenities && amenities.airCondition,
417 | bedRoom: amenities && amenities.rooms,
418 | guestRoom: amenities && amenities.guest,
419 | },
420 | propertyType_in: property,
421 | AND: [
422 | {
423 | price_gte: search && search.minPrice,
424 | },
425 | {
426 | price_lte: search && search.maxPrice,
427 | },
428 | ],
429 | },
430 | });
431 | }
432 | @Mutation()
433 | // Bóc data từ Input và gọi ORM.createHotel và gán cho các key trong model của prisma
434 | @UseGuards(GqlAuthGuard)
435 | async createHotel(
436 | @GqlUser() user: User,
437 | @Args('addHotelInput')
438 | {
439 | hotelName,
440 | pricePerNight,
441 | hotelDetails,
442 | guest,
443 | rooms,
444 | // price,
445 | // hotelPhotos,
446 | locationDescription,
447 | contactNumber,
448 | wifiAvailability,
449 | airCondition,
450 | isNegotiable,
451 | propertyType,
452 | parking,
453 | poolAvailability,
454 | extraBed,
455 | }: AddHotelInput, // Có thể sử dụng Dto cho bớt dài dòng
456 | @Args('location')
457 | items: LocationInput[],
458 | @Args('image')
459 | imageItems: ImageInput[],
460 | // @Args('categories')
461 | // categoryItems: CategoriesInput[],
462 | ) {
463 | // console.log(user.id);
464 | // console.log(imageItems);
465 | const newHotel = await this.prisma.client.createHotel({
466 | // Của prisma
467 | agentId: user.id,
468 | connectId: {
469 | connect: {
470 | id: user.id,
471 | },
472 | },
473 | // Bind agentId vào user.id bằng connect
474 | agentEmail: user.email,
475 | agentName: user.first_name + ' ' + user.last_name,
476 | title: hotelName,
477 | slug: this.format(hotelName),
478 | content: hotelDetails,
479 | price: pricePerNight,
480 | isNegotiable,
481 | propertyType,
482 | termsAndCondition: locationDescription,
483 | contactNumber,
484 | amenities: {
485 | create: {
486 | guestRoom: guest,
487 | bedRoom: rooms,
488 | wifiAvailability,
489 | airCondition,
490 | parkingAvailability: parking,
491 | poolAvailability,
492 | extraBedFacility: extraBed,
493 | },
494 | },
495 | image: {
496 | create: {
497 | url: imageItems[0].url,
498 | thumb_url: imageItems[1] ? imageItems[1].url : imageItems[0].url,
499 | },
500 | },
501 | location: {
502 | create: items,
503 | },
504 | gallery: {
505 | create: imageItems,
506 | },
507 | // 2 có 1 field con là object (cấp 3 hoặc array)
508 | // categories: {
509 | // create: categoryItems.map(i => ({
510 | // slug: i.slug,
511 | // name: i.name,
512 | // image: {
513 | // create: i.image,
514 | // },
515 | // })),
516 | // },
517 | });
518 | return newHotel;
519 | }
520 | @Mutation()
521 | async sortHotel(@Args('type') type) {
522 | return this.prisma.client.hotels({ orderBy: type });
523 | }
524 | @Mutation()
525 | async filterHotels() // @Args('search') search: SearchInput,
526 | // @Args('amenities') amenities: AmenitiesSearchInput,
527 | {
528 | return this.prisma.client.hotels();
529 | }
530 | }
531 |
--------------------------------------------------------------------------------
/src/hotel/hotel.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { HotelService } from './hotel.service';
3 |
4 | describe('HotelService', () => {
5 | let service: HotelService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [HotelService],
10 | }).compile();
11 |
12 | service = module.get(HotelService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/hotel/hotel.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class HotelService {}
5 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import * as cookierParser from 'cookie-parser';
4 | async function bootstrap() {
5 | const app = await NestFactory.create(AppModule);
6 | app.use(cookierParser());
7 | app.enableCors({
8 | credentials: true,
9 | origin: true,
10 | });
11 | await app.listen(process.env.PORT || 3000);
12 | }
13 | bootstrap();
14 |
--------------------------------------------------------------------------------
/src/prisma/prisma.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PrismaService } from './prisma.service';
3 |
4 | @Module({
5 | providers: [PrismaService],
6 | exports: [PrismaService],
7 | })
8 | export class PrismaModule {}
9 |
--------------------------------------------------------------------------------
/src/prisma/prisma.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { PrismaService } from './prisma.service';
3 |
4 | describe('PrismaService', () => {
5 | let service: PrismaService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [PrismaService],
10 | }).compile();
11 |
12 | service = module.get(PrismaService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/prisma/prisma.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Prisma } from 'generated/prisma-client';
3 | @Injectable()
4 | export class PrismaService {
5 | client: Prisma;
6 |
7 | constructor() {
8 | this.client = new Prisma({
9 | endpoint: process.env.PRISMA_DOCKER || 'http://prisma:4466',
10 | });
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/schema/gql-api.graphql:
--------------------------------------------------------------------------------
1 | ##GraphQL SDL
2 | type User {
3 | id: ID!
4 | first_name: String!
5 | last_name: String!
6 | username: String!
7 | stripeId: String
8 | password: String!
9 | email: String!
10 | role: String
11 | cellNumber: String
12 | profile_pic_main: String
13 | cover_pic_main: String
14 | profile_pic: [Image]
15 | cover_pic: [Image]
16 | date_of_birth: String
17 | gender: String
18 | content: String
19 | agent_location: Location
20 | gallery: [Gallery]
21 | social_profile: Social
22 | reviews_maked: [ReviewFields]
23 | listed_posts: [Hotel]
24 | notification: [Notification]
25 | unreadNotification: Int
26 | favourite_post: [Hotel]
27 | reviewed_post: [Hotel]
28 | review_liked: [Reviews]
29 | review_disliked: [Reviews]
30 | transaction_had: [Transaction]
31 | transaction_maked: [Transaction]
32 | uncheckTransactions: UncheckTransactions
33 | coupons_maked: [Coupon]
34 | createdAt: String!
35 | updatedAt: String!
36 | }
37 |
38 | # enum Gender {
39 | # Male
40 | # Female
41 | # Other
42 | # }
43 | type Hotel {
44 | id: ID!
45 | peopleLiked: [User]
46 | peopleReviewed: [User]
47 | couponsAvailable: [Coupon]
48 | connectId: User
49 | agentId: String
50 | agentEmail: String
51 | agentName: String
52 | title: String
53 | slug: String
54 | status: String
55 | content: String
56 | price: Int
57 | isNegotiable: Boolean
58 | propertyType: String
59 | condition: String
60 | rating: Float
61 | ratingCount: Int
62 | contactNumber: String
63 | termsAndCondition: String
64 | amenities: [Amenities]
65 | image: Image
66 | location: [Location]
67 | gallery: [Gallery]
68 | categories: [Categories]
69 | reviews: [Reviews]
70 | createdAt: String!
71 | updatedAt: String!
72 | }
73 |
74 | type Transaction {
75 | TXID: ID!
76 | transactionSecretKey: String
77 | transactionHotelName: String
78 | transactionHotelManager: User
79 | transactionHotelManagerId: String
80 | transactionHotelId: String
81 | transactionHotelType: String
82 | transactionPrice: Int
83 | transactionAuthor: User
84 | transactionAuthorId: String
85 | transactionAuthorName: String
86 | transactionAuthorEmail: String
87 | transactionAuthorContactNumber: String
88 | transactionAuthorSpecial: String ##Cho đủ field
89 | transactionAuthorNote: String
90 | transactionLocationLat: Float
91 | transactionLocationLng: Float
92 | transactionLocationFormattedAddress: String
93 | transactionRoom: Int
94 | transactionGuest: Int
95 | transactionRange: Int
96 | transactionStatus: String
97 | transactionCoupon: String
98 | transactionCouponType: Int
99 | transactionCouponValue: Int
100 | transactionStartDate: String
101 | transactionEndDate: String
102 | transactionStripeId: String
103 | createdAt: String!
104 | updatedAt: String!
105 | }
106 |
107 | type Coupon {
108 | couponId: ID!
109 | couponName: String!
110 | couponDescription: String
111 | couponAuthor: User
112 | couponAuthorId: String
113 | couponType: Int
114 | couponValue: Int
115 | couponQuantity: Int
116 | couponStartDate: String
117 | couponEndDate: String
118 | couponRange: Int
119 | couponTarget: [Hotel]
120 | createdAt: String!
121 | updatedAt: String!
122 | }
123 | type Reviews {
124 | reviewID: ID!
125 | reviewTitle: String
126 | reviewText: String
127 | peopleLiked: [User]
128 | peopleDisliked: [User]
129 | reviewTips: String
130 | sortOfTrip: String
131 | reviewAuthorId: User
132 | reviewAuthorFirstName: String
133 | reviewAuthorLastName: String
134 | reviewAuthorEmail: String
135 | reviewOverall: Float
136 | reviewAuthorPic: String
137 | reviewedHotel: Hotel
138 | # Có thể ko cần cái này
139 | reviewedHotelId: ID
140 | reviewPics: [Image]
141 | reviewDate: String
142 | reviewOptional: [ReviewOptionals]
143 | reviewFields: [ReviewFields]
144 | createdAt: String!
145 | updatedAt: String!
146 | }
147 |
148 | type ReviewOptionals {
149 | id: ID!
150 | option: String
151 | optionField: String
152 | }
153 | type ReviewFields {
154 | id: ID!
155 | rating: Int
156 | ratingFieldName: String
157 | }
158 | type Amenities {
159 | id: ID!
160 | guestRoom: Int
161 | bedRoom: Int
162 | wifiAvailability: Boolean
163 | parkingAvailability: Boolean
164 | poolAvailability: Boolean
165 | airCondition: Boolean
166 | extraBedFacility: Boolean
167 | }
168 |
169 | type Image {
170 | id: ID!
171 | url: String
172 | thumb_url: String
173 | }
174 | type Gallery {
175 | id: ID!
176 | uid: String
177 | url: String
178 | signedRequest: String
179 | }
180 |
181 | type Location {
182 | id: ID!
183 | lat: Float
184 | lng: Float
185 | formattedAddress: String
186 | zipcode: String
187 | city: String
188 | state_long: String
189 | state_short: String
190 | country_long: String
191 | country_short: String
192 | }
193 |
194 | type Categories {
195 | id: ID!
196 | slug: String
197 | name: String
198 | image: Image
199 | }
200 | type AuthPayload {
201 | id: ID
202 | email: String
203 | first_name: String
204 | last_name: String
205 | profile_pic_main: String
206 | cover_pic_main: String
207 | role: String
208 | }
209 | type HotelPhotos {
210 | url: String!
211 | }
212 | type Social {
213 | facebook: String
214 | twitter: String
215 | linkedIn: String
216 | instagram: String
217 | }
218 | type Query {
219 | allAmenities: [Amenities!]!
220 | imageId(id: ID!): Hotel!
221 | locationId(id: ID!): Location!
222 | locations: [Location]
223 | galleryId(id: ID!): Gallery!
224 | galleries: [Gallery!]!
225 | categoryId(id: ID!): Categories!
226 | allCategories: [Categories!]!
227 | userPosts(id: ID!): User
228 | favouritePosts(id: ID!): User
229 | favouritePostsHeart(id: ID!): User
230 | getUserInfo(id: ID!): User
231 | getUserGallery(id: ID!): User
232 | getUserReviews(id: ID!): User
233 | getUserNotification(id: ID!): [Notification]
234 | getUserUnreadNotification(id: ID!): User
235 | getHotelInfo(id: ID!): Hotel
236 | getHotelReviews(id: ID!): [Reviews] ## Nen tao them 1 folder model Review
237 | getHotelCoupons(id: ID!): [Coupon]
238 | getHotelManagerCoupons: [Coupon]
239 | getReviewsLikeDislike(id: ID!): Reviews
240 | getAllHotels(
241 | location: LocationInput
242 | type: String
243 | search: SearchInput
244 | amenities: AmenitiesSearchInput
245 | property: [String]
246 | ): [Hotel]
247 | getFilteredHotels(
248 | location: String
249 | search: SearchInput
250 | amenities: AmenitiesSearchInput
251 | property: [String]
252 | ): [Hotel]
253 | getTransactionsHaving(orderBy: String): [Transaction]
254 | getTransactionDetails(transactionSecretKey: String): [Transaction] ## Trả về Fragmentable Array
255 | getTotalUnreadTransactions: User
256 | getVendorStripeId(id: String): User
257 | }
258 | type Mutation {
259 | signup(signUpInput: SignUpInput): AuthPayload!
260 | login(loginInput: LoginInput): AuthPayload
261 | facebookLogin(email: String, accessToken: String, socialInfo: String, socialId: String, socialProfileLink: String): AuthPayload
262 | googleLogin(email: String, accessToken: String, socialInfo: String, socialId: String, profileImage: String): AuthPayload
263 | createHotel(
264 | addHotelInput: AddHotelInput
265 | location: [LocationInput]
266 | image: [ImageInput]
267 | categories: [CategoriesInput]
268 | ): Hotel!
269 | sortHotel(type: String): [Hotel]
270 | likeHotel(id: ID!): Hotel
271 | dislikeHotel(id: ID!): Hotel
272 | filterHotels(search: SearchInput, amenities: AmenitiesSearchInput): [Hotel]
273 | createLocation(location: LocationInput): Location!
274 | updateProfile(
275 | profile: UpdateProfileInput
276 | location: LocationInput
277 | social: SocialInput
278 | ): User
279 | updatePhotos(photos: [UpdatePhotosInput]): User
280 | deletePhotos(photos: [DeletePhotosInput]): User
281 | setProfilePic(url: String): User
282 | setCoverPic(url: String): User
283 | updatePassword(password: UpdatePassword): User!
284 | forgetPassword(email: String): User
285 | changePasswordFromForgetPassword(email: String, password: String): User
286 | sendContact(contact: ContactInput): User
287 | makeReviews(reviews: ReviewInput, hotelId: ID): Hotel
288 | likeOrDislikeReview(id: ID!, type: Int): Reviews
289 | checkNotification(id: ID!): User
290 | readNotification(query: String): User
291 | deleteAllNotifications(id: ID!): User
292 | createTransaction(
293 | transaction: TransactionInput
294 | hotelId: String
295 | userId: String
296 | coupon: CouponCheckedPayload
297 | ): Transaction ##userId có thể không có - không bắt login
298 | createCoupon(coupon: CouponInput, hotelsId: [String], type: Int): [Coupon]
299 | checkCoupon(hotelId: String, couponName: String): Coupon
300 | processTransactions(id: [String], type: Int): Transaction
301 | updateTotalUnreadTransactions: User
302 | deleteCoupons(id: [String]): [Coupon]
303 | updateStripeId(stripeId: String, type: String): User
304 | }
305 | input ReviewInput {
306 | reviewOverall: Float
307 | reviewTitle: String
308 | reviewTips: String
309 | reviewText: String
310 | sortOfTrip: String
311 | reviewFieldInput: [ReviewFieldInput]
312 | reviewOptionals: [ReviewOptionalsInput]
313 | reviewPics: [ImageInput]
314 | }
315 | input ReviewFieldInput {
316 | rating: Int
317 | ratingFieldName: String
318 | }
319 | input ReviewOptionalsInput {
320 | option: String
321 | optionField: String
322 | }
323 | input UpdateProfileInput {
324 | first_name: String
325 | last_name: String
326 | date_of_birth: String
327 | gender: String
328 | email: String
329 | content: String
330 | agent_location: LocationInput
331 | cellNumber: String
332 | }
333 |
334 | input UpdatePhotosInput {
335 | url: String
336 | uid: String
337 | signedRequest: String
338 | }
339 | input DeletePhotosInput {
340 | id: ID!
341 | }
342 |
343 | input UpdatePassword {
344 | confirmPassword: String!
345 | oldPassword: String!
346 | newPassword: String!
347 | }
348 | input SocialInput {
349 | facebook: String
350 | instagram: String
351 | twitter: String
352 | }
353 | input LocationInput {
354 | lat: Float
355 | lng: Float
356 | formattedAddress: String
357 | zipcode: String
358 | city: String
359 | state_long: String
360 | state_short: String
361 | country_long: String
362 | country_short: String
363 | }
364 | input ImageInput {
365 | url: String
366 | }
367 | input CategoriesInput {
368 | slug: String
369 | name: String
370 | image: ImageInput
371 | }
372 | input AddHotelInput {
373 | hotelName: String
374 | pricePerNight: Int
375 | hotelDetails: String
376 | guest: Int
377 | rooms: Int
378 | price: Int
379 | # Co the khong can nhung nen ghi vo cho de hieu
380 | hotelPhotos: [ImageInput]
381 | location: [LocationInput]
382 | locationDescription: String
383 | contactNumber: String
384 | propertyType: String
385 | isNegotiable: Boolean
386 | wifiAvailability: Boolean
387 | airCondition: Boolean
388 | parking: Boolean
389 | poolAvailability: Boolean
390 | extraBed: Boolean
391 | }
392 | input SignUpInput {
393 | username: String
394 | first_name: String
395 | last_name: String
396 | email: String!
397 | password: String!
398 | profile_pic_main: String
399 | cover_pic_main: String
400 | }
401 |
402 | input LoginInput {
403 | email: String!
404 | password: String!
405 | }
406 |
407 | input ContactInput {
408 | subject: String!
409 | message: String
410 | email: String
411 | cellNumber: String
412 | }
413 | input MockInput {
414 | title: [String]
415 | }
416 | input AmenitiesSearchInput {
417 | wifiAvailability: Boolean
418 | poolAvailability: Boolean
419 | parkingAvailability: Boolean
420 | airCondition: Boolean
421 | rooms: Int
422 | guest: Int
423 | }
424 | input SearchInput {
425 | minPrice: Int
426 | maxPrice: Int
427 | }
428 | input TransactionInput {
429 | transactionHotelName: String
430 | transactionHotelManagerId: String
431 | transactionHotelType: String
432 | transactionPrice: Int
433 | transactionAuthorName: String
434 | transactionAuthorEmail: String
435 | transactionAuthorContactNumber: String
436 | transactionAuthorSpecial: String ##Cho đủ field
437 | transactionAuthorNote: String
438 | transactionLocationFormattedAddress: String
439 | transactionLocationLat: Float
440 | transactionLocationLng: Float
441 | transactionRoom: Int
442 | transactionGuest: Int
443 | transactionRange: Int
444 | transactionStartDate: String
445 | transactionEndDate: String
446 | transactionStripeId: String
447 | }
448 | input CouponInput {
449 | couponName: String!
450 | couponDescription: String
451 | couponType: Int
452 | couponValue: Int
453 | couponQuantity: Int
454 | couponStartDate: String
455 | couponEndDate: String
456 | couponRange: Int
457 | }
458 | input CouponCheckedPayload {
459 | couponId: ID
460 | couponName: String
461 | couponValue: Int
462 | couponType: Int
463 | }
464 | # Realtime Section
465 | type Subscription {
466 | unreadNotification(channelId: String): Unread
467 | notificationBell(channelId: String): Notification
468 | realtimeReviews(hotelId: String): Reviews
469 | realtimeLikeDislike(reviewID: String): Reviews
470 | realtimeNotificationTransaction(userId: String): TransactionNotification
471 | }
472 | type Message {
473 | # Có thể cho user nếu muốn routing nữa
474 | reviewAuthorName: String
475 | reviewAuthorId: String
476 | reviewedHotelId: Hotel
477 | reviewedHotelName: String
478 | hotelManagerId: String
479 | reviewTitle: String
480 | reviewText: String
481 | peopleReviewedQuanity: Int
482 | peopleReviewedArr: [User]
483 | }
484 | type Notification {
485 | id: ID!
486 | reviewAuthorName: String
487 | reviewedHotelName: String
488 | reviewTitle: String
489 | reviewText: String
490 | userNotificationId: String
491 | peopleReviewedQuantity: Int
492 | query: String
493 | reviewAuthorProfilePic: String
494 | read: Boolean
495 | old: Boolean
496 | createdAt: String!
497 | updatedAt: String!
498 | }
499 | type TransactionNotification {
500 | TXID: ID!
501 | transactionPrice: Int
502 | }
503 | type UncheckTransactions {
504 | id: ID!
505 | userUncheckTransactionsId: String
506 | userUncheckTransactions: User
507 | totalPrice: Int
508 | totalTransactions: Int
509 | }
510 | type Unread {
511 | unreadNotification: Int
512 | }
--------------------------------------------------------------------------------
/src/services/sendEmail.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { MailerService } from '@nestjs-modules/mailer';
3 | import * as mailjet from 'node-mailjet';
4 | @Injectable()
5 | export class MailService {
6 | constructor(private readonly mailerService: MailerService) {}
7 | public sendEmail(email, code): void {
8 | // console.log(email, process.env.LOCAL_EMAIL, process.env.LOCAL_PASSWORD);
9 | this.mailerService
10 | .sendMail({
11 | to: email,
12 | from: process.env.LOCAL_EMAIL,
13 | subject: 'Reset password Link ✔',
14 | template: process.cwd() + '/dist/services/template/reset',
15 | context: {
16 | email,
17 | code,
18 | },
19 | })
20 | .then(success => {
21 | console.log(success + email);
22 | })
23 | .catch(err => {
24 | console.log(err);
25 | });
26 | }
27 | public sendContact(subject, email, messsage, cellNumber): void {
28 | this.mailerService
29 | .sendMail({
30 | from: email,
31 | to: process.env.LOCAL_EMAIL,
32 | subject,
33 | html: `
34 |
${subject}
35 |
From: ${email}
36 |
Message: ${messsage}
37 |
Phone: ${cellNumber}
38 |
`,
39 | })
40 | .then(success => {
41 | console.log(success + email);
42 | })
43 | .catch(err => {
44 | console.log(err);
45 | });
46 | }
47 | public mockMailjet(data): void {
48 | console.log(data.authorEmail);
49 | const request = mailjet
50 | .connect(process.env.MJ_APIKEY_PUBLIC, process.env.MJ_APIKEY_PRIVATE)
51 | .post('send', { version: 'v3.1' })
52 | .request({
53 | Messages: [
54 | {
55 | From: {
56 | Email: process.env.LOCAL_MJ_EMAIL,
57 | Name: 'Palace TripFinder',
58 | },
59 | To: [
60 | {
61 | Email: data.authorEmail,
62 | Name: data.authorName,
63 | },
64 | ],
65 | TemplateID: 1645231,
66 | TemplateLanguage: true,
67 | Subject: `Receipt about ${data.hotelName} Room: ${data.room} Guest: ${data.guest} of ${data.authorName}`,
68 | Variables: {
69 | firstname: data.authorName,
70 | startDate: data.startDate,
71 | endDate: data.endDate,
72 | hotelName: data.hotelName,
73 | guest: data.guest,
74 | room: data.room,
75 | couponName: data.couponName,
76 | total_price: data.total_price,
77 | order_date: data.order_date,
78 | order_id: data.order_id,
79 | },
80 | },
81 | ],
82 | });
83 | request
84 | .then(result => {
85 | console.log(result.body);
86 | })
87 | .catch(err => {
88 | console.log(err.statusCode);
89 | });
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/services/template/reset.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | head
3 | title Reset Password
4 | .container
5 | .row
6 | .col-xs-12.col-sm-6.col-md-4.text-center
7 | h3 Reset Password
8 | h2 #{email}
9 | h2 DO NOT GIVE THIS LINK TO ANYONE
10 | h2 Your password reset link:
11 | https://vercel-v2.hotel-prisma.ml/change-password?code=#{code}
12 | //- .row
13 | //- .col-xs-12.col-sm-6.col-md-4.extra-padding
14 | //- label.idleLabel(for='newPassword') New Password
15 | //- .input-group
16 | //- input#newPassword.form-control.isRequired(type='password' name='newPassword' title='enter your new password' maxlength='50')
17 | //- span.input-group-addon(title='Invalid password')
18 | //- i.fa.fa-lg.fa-fw.fa-times.text-danger
19 | //- .row
20 | //- .col-xs-12.col-sm-6.col-md-4.extra-padding
21 | //- label.idleLabel(for='newPasswordConfirm') Verify password
22 | //- .input-group
23 | //- input#newPasswordConfirm.form-control.isRequired(type='password' name='newPasswordConfirm')
24 | //- span.input-group-addon(title='Passwords match')
25 | //- i.fa.fa-lg.fa-fw.fa-check.text-success
26 | style.
27 | textarea {
28 | resize: none;
29 | }
30 | .idleLabel {
31 | position: absolute;
32 | color: #888888;
33 | margin: 7px 13px;
34 | font-weight: normal;
35 | z-index: 3;
36 | }
37 | .activeLabel {
38 | position: absolute;
39 | color: grey;
40 | font-size: x-small;
41 | margin: -5px 10px;
42 | background-color: #FFFFFF;
43 | border-left: solid #888888 1px;
44 | border-right: solid #888888 1px;
45 | padding: 0 4px;
46 | z-index: 3;
47 | }
48 | .extra-padding {
49 | margin-bottom: 0.5em;
50 | }
51 |
--------------------------------------------------------------------------------
/src/shared/decorators/decorator.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2 | import { Response } from 'express';
3 | import { User } from 'src/graphql.schema.generated';
4 | import { GqlExecutionContext } from '@nestjs/graphql';
5 |
6 | // Giống module.export (req, res, next) bên express
7 | // Để access req và user objects dễ dàng từ graphql ctx thì ta có thể
8 | // Tự tạo custom decorator(thực hiện các logic)
9 | // Thay thế cho jwt.verify
10 | // Chi tiết hơn tại: https://docs.nestjs.com/custom-decorators
11 | export const ResGql = createParamDecorator(
12 | // (data, [root, args, ctx, info]): Response => ctx.res,
13 | (data: unknown, context: ExecutionContext): Response =>
14 | GqlExecutionContext.create(context).getContext().res,
15 | );
16 |
17 | export const GqlUser = createParamDecorator(
18 | // (data, [root, args, ctx, info]): User => ctx.req && ctx.req.user,
19 | (data: unknown, context: ExecutionContext): User => {
20 | const ctx = GqlExecutionContext.create(context).getContext();
21 | return ctx.req && ctx.req.user;
22 | },
23 | );
24 |
--------------------------------------------------------------------------------
/src/transaction/transaction.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { TransactionResolver } from './transaction.resolver';
3 | import { PrismaModule } from 'src/prisma/prisma.module';
4 | import { MailService } from 'src/services/sendEmail';
5 | import { RedisPubSub } from 'graphql-redis-subscriptions';
6 | import * as Redis from 'ioredis';
7 | @Module({
8 | providers: [
9 | TransactionResolver,
10 | MailService,
11 | {
12 | provide: 'PUB_SUB',
13 | useFactory: () => {
14 | const options = {
15 | host: 'redis',
16 | port: 6379,
17 | password: 'no',
18 | };
19 | return new RedisPubSub({
20 | publisher: new Redis(options),
21 | subscriber: new Redis(options),
22 | });
23 | },
24 | // useValue: new PubSub(),
25 | },
26 | ],
27 | imports: [PrismaModule],
28 | })
29 | export class TransactionModule {}
30 |
--------------------------------------------------------------------------------
/src/transaction/transaction.resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { TransactionResolver } from './transaction.resolver';
3 |
4 | describe('TransactionResolver', () => {
5 | let resolver: TransactionResolver;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [TransactionResolver],
10 | }).compile();
11 |
12 | resolver = module.get(TransactionResolver);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(resolver).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/transaction/transaction.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Resolver, Mutation, Args, Query, Subscription } from '@nestjs/graphql';
2 | import { SchedulerRegistry } from '@nestjs/schedule';
3 | import { UseGuards, Inject } from '@nestjs/common';
4 | import { v4 as uuidv4 } from 'uuid';
5 | import { PrismaService } from 'src/prisma/prisma.service';
6 | import { MailService } from 'src/services/sendEmail';
7 | import {
8 | TransactionInput,
9 | User,
10 | CouponCheckedPayload,
11 | TransactionNotification,
12 | } from 'src/graphql.schema.generated';
13 | import { GqlUser } from 'src/shared/decorators/decorator';
14 | import { GqlAuthGuard } from 'src/auth/graphql-auth.guard';
15 | import { PubSubEngine } from 'graphql-subscriptions';
16 |
17 | @Resolver('Transaction')
18 | export class TransactionResolver {
19 | constructor(
20 | private readonly prisma: PrismaService,
21 | private readonly mailService: MailService,
22 | @Inject('PUB_SUB') private pubSub: PubSubEngine,
23 | private schedulerRegistry: SchedulerRegistry,
24 | ) {}
25 | expire = coupon => {
26 | const date = new Date();
27 | const endDate = new Date(coupon);
28 | return date > endDate;
29 | };
30 | @Query()
31 | @UseGuards(GqlAuthGuard)
32 | async getTransactionsHaving(@GqlUser() user: User) {
33 | const fragment = `fragment transactionFragment on User{
34 | TXID
35 | transactionSecretKey
36 | transactionHotelName
37 | transactionHotelId
38 | transactionHotelManager{
39 | first_name
40 | last_name
41 | email
42 | cellNumber
43 | }
44 | transactionHotelManagerId
45 | transactionHotelType
46 | transactionPrice
47 | transactionAuthorId
48 | transactionAuthorName
49 | transactionAuthorEmail
50 | transactionAuthorContactNumber
51 | transactionAuthorSpecial
52 | transactionAuthorNote
53 | transactionLocationLat
54 | transactionLocationLng
55 | transactionLocationFormattedAddress
56 | transactionRoom
57 | transactionGuest
58 | transactionRange
59 | transactionStatus
60 | transactionCoupon
61 | transactionCouponType
62 | transactionCouponValue
63 | transactionStripeId
64 | transactionStartDate
65 | transactionEndDate
66 | }`;
67 | const job = this.schedulerRegistry.getCronJob('notiCron');
68 | job.stop();
69 | console.log(job.lastDate());
70 | return this.prisma.client
71 | .user({ id: user.id })
72 | .transaction_had({
73 | orderBy: 'createdAt_DESC',
74 | })
75 | .$fragment(fragment);
76 | }
77 | @Query()
78 | async getTransactionDetails(
79 | @Args('transactionSecretKey') transactionSecretKey,
80 | ) {
81 | const fragment = `fragment getTransactionDetailsFragment on Transaction{
82 | transactionHotelManager{
83 | first_name
84 | last_name
85 | email
86 | cellNumber
87 | }
88 | transactionAuthorName
89 | transactionHotelName
90 | transactionLocationLat
91 | transactionHotelType
92 | transactionPrice
93 | transactionLocationLng
94 | transactionLocationFormattedAddress
95 | transactionRoom
96 | transactionGuest
97 | transactionRange
98 | transactionStatus
99 | transactionCoupon
100 | transactionCouponType
101 | transactionCouponValue
102 | transactionStartDate
103 | transactionEndDate
104 | }`;
105 | return this.prisma.client
106 | .transactions({
107 | where: {
108 | transactionSecretKey,
109 | },
110 | })
111 | .$fragment(fragment);
112 | }
113 | @Query()
114 | @UseGuards(GqlAuthGuard)
115 | async getTotalUnreadTransactions(@GqlUser() user: User) {
116 | const fragment = `fragment totalUnreadTransactions on User {
117 | uncheckTransactions{
118 | userUncheckTransactionsId
119 | totalPrice
120 | totalTransactions
121 | }
122 | }`;
123 | return await this.prisma.client.user({ id: user.id }).$fragment(fragment);
124 | }
125 | @Mutation()
126 | async checkCoupon(@Args('hotelId') hotelId, @Args('couponName') couponName) {
127 | // Strict thêm chỉ áp dụng cho hotel này
128 | // Field nào unique là đều kiếm được
129 | const fragment = `fragment checkCoupon on Coupon{
130 | couponQuantity
131 | couponName
132 | couponId
133 | couponType
134 | couponValue
135 | }`;
136 | const couponPayload = await this.prisma.client
137 | .hotel({ id: hotelId })
138 | .couponsAvailable({
139 | where: {
140 | couponName,
141 | },
142 | })
143 | .$fragment(fragment);
144 | console.log(couponPayload);
145 |
146 | // Coupon hết số lượng
147 | if (couponPayload && couponPayload[0].couponQuantity === 0) {
148 | throw Error('Coupon is running out of uses');
149 | }
150 | // Coupon hết hạn
151 | if (couponPayload && this.expire(couponPayload[0].couponEndDate)) {
152 | throw Error('Coupon is expired');
153 | }
154 | if (couponPayload && couponPayload[0].couponType) {
155 | await this.prisma.client.updateCoupon({
156 | where: {
157 | couponId: couponPayload[0].couponId,
158 | },
159 | data: {
160 | couponQuantity: couponPayload[0].couponQuantity - 1,
161 | },
162 | });
163 | return couponPayload[0];
164 | }
165 | throw Error('Coupon is invalid for this Hotel');
166 | }
167 | @Mutation()
168 | async createTransaction(
169 | @Args('transaction') transaction: TransactionInput,
170 | @Args('hotelId') hotelId: string,
171 | @Args('coupon') coupon: CouponCheckedPayload,
172 | @Args('userId') userId,
173 | ) {
174 | console.log(userId);
175 | // Không nhập coupon
176 |
177 | if (!coupon) {
178 | console.log('no coupon');
179 | const transactionPayload = await this.prisma.client.createTransaction({
180 | transactionSecretKey: uuidv4(),
181 | transactionHotelId: hotelId,
182 | transactionHotelName: transaction.transactionHotelName,
183 | transactionHotelManagerId: transaction.transactionHotelManagerId,
184 | transactionHotelManager: {
185 | connect: {
186 | id: transaction.transactionHotelManagerId,
187 | },
188 | },
189 | transactionHotelType: transaction.transactionHotelType,
190 | transactionPrice: transaction.transactionPrice,
191 | transactionAuthor: userId && {
192 | connect: {
193 | id: userId,
194 | },
195 | },
196 | transactionAuthorId: userId || 'non-member',
197 | transactionAuthorName: transaction.transactionAuthorName,
198 | transactionAuthorEmail: transaction.transactionAuthorEmail,
199 | transactionAuthorContactNumber:
200 | transaction.transactionAuthorContactNumber,
201 | transactionAuthorSpecial: transaction.transactionAuthorSpecial,
202 | transactionAuthorNote: transaction.transactionAuthorNote,
203 | transactionStartDate: transaction.transactionStartDate,
204 | transactionEndDate: transaction.transactionEndDate,
205 | transactionRange: transaction.transactionRange,
206 | transactionLocationLat: transaction.transactionLocationLat,
207 | transactionLocationLng: transaction.transactionLocationLng,
208 | transactionLocationFormattedAddress:
209 | transaction.transactionLocationFormattedAddress,
210 | transactionRoom: transaction.transactionRoom,
211 | transactionGuest: transaction.transactionGuest,
212 | transactionStatus: 'PENDING',
213 | transactionStripeId: transaction.transactionStripeId,
214 | transactionCoupon: 'None',
215 | transactionCouponType: 3, // Không xài coupon
216 | transactionCouponValue: 0,
217 | });
218 | const date = new Date();
219 | date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
220 | const dataMail = {
221 | authorEmail: transaction.transactionAuthorEmail,
222 | authorName: transaction.transactionAuthorName,
223 | firstName: transaction.transactionAuthorName,
224 | startDate: transaction.transactionStartDate,
225 | endDate: transaction.transactionEndDate,
226 | hotelName: transaction.transactionHotelName,
227 | couponName: 'No Coupon',
228 | room: transactionPayload.transactionRoom,
229 | guest: transactionPayload.transactionGuest,
230 | total_price: transactionPayload.transactionPrice,
231 | order_date:
232 | date.getFullYear() +
233 | '-' +
234 | (date.getMonth() + 1) +
235 | '-' +
236 | date.getDate(),
237 | order_id: transactionPayload.transactionSecretKey,
238 | };
239 | this.mailService.mockMailjet(dataMail);
240 | const transactionNotiPayload = {
241 | TXID: transactionPayload.TXID,
242 | transactionPrice: transaction.transactionPrice,
243 | transactionHotelManagerId: transaction.transactionHotelManagerId,
244 | };
245 | // Đáng lẽ nên refactor lại 1 loại form noti, truyền type là review, like, transaction
246 | this.pubSub.publish('realtimeNotiTransaction', transactionNotiPayload);
247 | const totalTransactions =
248 | (await this.prisma.client
249 | .user({
250 | id: transaction.transactionHotelManagerId,
251 | })
252 | .uncheckTransactions()
253 | .totalTransactions()) || 0;
254 | const totalPrice =
255 | (await this.prisma.client
256 | .user({
257 | id: transaction.transactionHotelManagerId,
258 | })
259 | .uncheckTransactions()
260 | .totalPrice()) || 0;
261 | console.log(totalTransactions);
262 | console.log(totalPrice);
263 | // upsert để tối ưu database storage
264 | await this.prisma.client.updateUser({
265 | where: {
266 | id: transaction.transactionHotelManagerId,
267 | },
268 | data: {
269 | uncheckTransactions: {
270 | upsert: {
271 | update: {
272 | totalTransactions: totalTransactions + 1,
273 | totalPrice: totalPrice + transaction.transactionPrice,
274 | },
275 | create: {
276 | userUncheckTransactionsId:
277 | transaction.transactionHotelManagerId,
278 | totalTransactions: totalTransactions + 1,
279 | totalPrice: totalPrice + transaction.transactionPrice,
280 | },
281 | },
282 | },
283 | },
284 | });
285 | return transactionPayload;
286 | }
287 |
288 | // Có coupon và nhập coupon hợp lệ
289 | if (coupon) {
290 | console.log('coupon');
291 | // Nếu muốn update riêng từng Hotel vs 1 coupon thì create bên Hotel
292 | const transactionPayload = await this.prisma.client.createTransaction({
293 | transactionSecretKey: uuidv4(),
294 | transactionHotelId: hotelId,
295 | transactionHotelName: transaction.transactionHotelName,
296 | transactionHotelManagerId: transaction.transactionHotelManagerId,
297 | transactionHotelManager: {
298 | connect: {
299 | id: transaction.transactionHotelManagerId,
300 | },
301 | },
302 | transactionHotelType: transaction.transactionHotelType,
303 | transactionPrice: transaction.transactionPrice,
304 | transactionAuthor: userId && {
305 | connect: {
306 | id: userId,
307 | },
308 | },
309 | transactionAuthorId: userId || 'non-member',
310 | transactionAuthorName: transaction.transactionAuthorName,
311 | transactionAuthorEmail: transaction.transactionAuthorEmail,
312 | transactionAuthorContactNumber:
313 | transaction.transactionAuthorContactNumber,
314 | transactionAuthorSpecial: transaction.transactionAuthorSpecial,
315 | transactionAuthorNote: transaction.transactionAuthorNote,
316 | transactionStartDate: transaction.transactionStartDate,
317 | transactionEndDate: transaction.transactionEndDate,
318 | transactionRange: transaction.transactionRange,
319 | transactionLocationLat: transaction.transactionLocationLat,
320 | transactionLocationLng: transaction.transactionLocationLng,
321 | transactionLocationFormattedAddress:
322 | transaction.transactionLocationFormattedAddress,
323 | transactionRoom: transaction.transactionRoom,
324 | transactionGuest: transaction.transactionGuest,
325 | transactionStatus: 'PENDING',
326 | transactionStripeId: transaction.transactionStripeId,
327 | transactionCoupon: coupon.couponName,
328 | transactionCouponType: coupon.couponType,
329 | transactionCouponValue: coupon.couponValue,
330 | });
331 | const date = new Date();
332 | date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
333 | const dataMail = {
334 | authorEmail: transaction.transactionAuthorEmail,
335 | authorName: transaction.transactionAuthorName,
336 | firstName: transaction.transactionAuthorName,
337 | startDate: transaction.transactionStartDate,
338 | endDate: transaction.transactionEndDate,
339 | hotelName: transaction.transactionHotelName,
340 | couponName: coupon.couponName,
341 | room: transactionPayload.transactionRoom,
342 | guest: transactionPayload.transactionGuest,
343 | total_price: transactionPayload.transactionPrice,
344 | order_date:
345 | date.getFullYear() +
346 | '-' +
347 | (date.getMonth() + 1) +
348 | '-' +
349 | date.getDate(),
350 | order_id: transactionPayload.transactionSecretKey,
351 | };
352 | this.mailService.mockMailjet(dataMail);
353 | const transactionNotiPayload = {
354 | TXID: transactionPayload.TXID,
355 | transactionPrice: transaction.transactionPrice,
356 | transactionHotelManagerId: transaction.transactionHotelManagerId,
357 | };
358 | // Đáng lẽ nên refactor lại 1 loại form noti, truyền type là review, like, transaction
359 | this.pubSub.publish('realtimeNotiTransaction', transactionNotiPayload);
360 | const totalTransactions =
361 | (await this.prisma.client
362 | .user({
363 | id: transaction.transactionHotelManagerId,
364 | })
365 | .uncheckTransactions()
366 | .totalTransactions()) || 0;
367 | const totalPrice =
368 | (await this.prisma.client
369 | .user({
370 | id: transaction.transactionHotelManagerId,
371 | })
372 | .uncheckTransactions()
373 | .totalPrice()) || 0;
374 | console.log(totalTransactions);
375 | console.log(totalPrice);
376 | // upsert để tối ưu database storage
377 | await this.prisma.client.updateUser({
378 | where: {
379 | id: transaction.transactionHotelManagerId,
380 | },
381 | data: {
382 | uncheckTransactions: {
383 | upsert: {
384 | update: {
385 | totalTransactions: totalTransactions + 1,
386 | totalPrice: totalPrice + transaction.transactionPrice,
387 | },
388 | create: {
389 | userUncheckTransactionsId:
390 | transaction.transactionHotelManagerId,
391 | totalTransactions: totalTransactions + 1,
392 | totalPrice: totalPrice + transaction.transactionPrice,
393 | },
394 | },
395 | },
396 | },
397 | });
398 | return transactionPayload;
399 | }
400 | }
401 | @Mutation()
402 | @UseGuards(GqlAuthGuard)
403 | async updateTotalUnreadTransactions(@GqlUser() user: User) {
404 | return this.prisma.client.updateUser({
405 | where: {
406 | id: user.id,
407 | },
408 | data: {
409 | uncheckTransactions: {
410 | update: {
411 | totalPrice: 0,
412 | totalTransactions: 0,
413 | },
414 | },
415 | },
416 | });
417 | }
418 | @UseGuards(GqlAuthGuard)
419 | @Mutation()
420 | async processTransactions(
421 | @GqlUser() user: User,
422 | @Args('id') id,
423 | @Args('type') type,
424 | ) {
425 | console.log(id);
426 | // Sử dụng cho demo - set tất cả pending
427 | if (type === 3) {
428 | return await this.prisma.client.updateManyTransactions({
429 | where: {
430 | transactionHotelManagerId: user.id,
431 | },
432 | data: {
433 | transactionStatus: 'PENDING',
434 | },
435 | });
436 | }
437 | return await this.prisma.client.updateManyTransactions({
438 | where: {
439 | transactionHotelManagerId: user.id,
440 | TXID_in: id,
441 | },
442 | data: {
443 | transactionStatus: type === 1 ? 'DONE' : 'CANCELLED',
444 | },
445 | });
446 | }
447 | @UseGuards(GqlAuthGuard)
448 | @Mutation()
449 | async deleteCoupons(@GqlUser() user: User, @Args('id') id) {
450 | // Ko show id của coupon ra -> gurantee ko xóa bậy bạ
451 | // Delete many không có in
452 | console.log(id);
453 | return id.map(i => {
454 | return this.prisma.client.deleteCoupon({
455 | couponId: i,
456 | });
457 | });
458 | }
459 | @Subscription(returns => TransactionNotification, {
460 | resolve: payloads => payloads,
461 | filter: (payloads, variables) =>
462 | payloads.transactionHotelManagerId === variables.userId,
463 | })
464 | realtimeNotificationTransaction() {
465 | return this.pubSub.asyncIterator('realtimeNotiTransaction');
466 | }
467 | }
468 |
--------------------------------------------------------------------------------
/src/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UserResolver } from './user.resolver';
3 | import { PrismaModule } from 'src/prisma/prisma.module';
4 | import { MailService } from 'src/services/sendEmail';
5 | import { RedisPubSub } from 'graphql-redis-subscriptions';
6 | import * as Redis from 'ioredis';
7 | @Module({
8 | providers: [
9 | UserResolver,
10 | MailService,
11 | {
12 | provide: 'PUB_SUB',
13 | useFactory: () => {
14 | const options = {
15 | host: 'redis',
16 | port: 6379,
17 | password: 'no',
18 | };
19 | return new RedisPubSub({
20 | publisher: new Redis(options),
21 | subscriber: new Redis(options),
22 | });
23 | },
24 | // useValue: new PubSub(),
25 | },
26 | ],
27 | imports: [PrismaModule],
28 | })
29 | export class UserModule {}
30 |
--------------------------------------------------------------------------------
/src/user/user.resolver.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { UserResolver } from './user.resolver';
3 |
4 | describe('UserResolver', () => {
5 | let resolver: UserResolver;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [UserResolver],
10 | }).compile();
11 |
12 | resolver = module.get(UserResolver);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(resolver).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/user/user.resolver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Resolver,
3 | ResolveField,
4 | Parent,
5 | Args,
6 | Query,
7 | Mutation,
8 | Subscription,
9 | } from '@nestjs/graphql';
10 | import { PrismaService } from 'src/prisma/prisma.service';
11 | import { v4 as uuidv4 } from 'uuid';
12 | import {
13 | User,
14 | Reviews,
15 | UpdatePhotosInput,
16 | ReviewInput,
17 | SocialInput,
18 | UpdateProfileInput,
19 | DeletePhotosInput,
20 | LocationInput,
21 | ContactInput,
22 | Unread,
23 | Notification,
24 | CouponInput,
25 | } from 'src/graphql.schema.generated';
26 | import { GqlUser, ResGql } from 'src/shared/decorators/decorator';
27 | import { Inject, UseGuards } from '@nestjs/common';
28 | import { Response } from 'express';
29 | import { GqlAuthGuard } from 'src/auth/graphql-auth.guard';
30 | import { MailService } from 'src/services/sendEmail';
31 | import { ID_Input } from 'generated/prisma-client';
32 | import { PubSubEngine } from 'graphql-subscriptions';
33 | import _ = require('lodash');
34 | import { isMongoId } from 'class-validator';
35 | @Resolver('User')
36 | export class UserResolver {
37 | constructor(
38 | @Inject('PUB_SUB') private pubSub: PubSubEngine,
39 | private readonly prisma: PrismaService,
40 | private readonly mailService: MailService,
41 | ) {}
42 | @ResolveField()
43 | async agent_location(@Parent() { id }: User) {
44 | return this.prisma.client.user({ id }).agent_location();
45 | }
46 | @ResolveField()
47 | async cover_pic(@Parent() { id }: User) {
48 | return this.prisma.client.user({ id }).cover_pic();
49 | }
50 | @ResolveField()
51 | async profile_pic(@Parent() { id }: User) {
52 | return this.prisma.client.user({ id }).profile_pic();
53 | }
54 | @ResolveField()
55 | async gallery(@Parent() { id }: User) {
56 | return this.prisma.client.user({ id }).gallery();
57 | }
58 | @ResolveField()
59 | async notification(@Parent() { id }: User) {
60 | return this.prisma.client
61 | .user({ id })
62 | .notification({ orderBy: 'createdAt_DESC' });
63 | }
64 |
65 | // Tương tự populate trong express
66 | // Parent chính là data type của object gần nhất nest khi query
67 | // Không ai nest thì parent data type là chính nó
68 | // Trả về parent tương ứng của User.listed_posts
69 | // @UseGuards(GqlAuthGuard
70 | @ResolveField()
71 | async social_profile(@Parent() { id }: User) {
72 | return this.prisma.client.user({ id }).social_profile();
73 | }
74 | // @ResolveField()
75 | // async listed_posts(@Parent() hotel: Hotel[], @Parent() user: User) {
76 | // // Chú ý các quan hệ N-1, 1-N
77 | // // listed_posts bi populated, gán listed_posts vào model nó kế thừa
78 | // // Mẹo cho các field là mảng - nested object - dùng fragment
79 | // // nên log ra data để biết đang loại data gì
80 | // // nên connect dựa vào id hay gì để có thể lợi dụng id đó query đúng
81 | // // Data trả về nhiều cấp nên log ra để biết của cái nào và check null
82 | // console.log(user);
83 | // const userId = (user && user.id) || (hotel[0] && hotel[0].agentId);
84 | // // console.log(user);
85 | // // first id fragment
86 | // // console.log('id từ cha' + userId);
87 | // const fragment = `fragment Somefrag on User
88 | // {
89 | // id
90 | // title
91 | // content
92 | // slug
93 | // price
94 | // status
95 | // isNegotiable
96 | // propertyType
97 | // condition
98 | // contactNumber
99 | // termsAndCondition
100 | // rating
101 | // ratingCount
102 | // amenities{
103 | // id
104 | // guestRoom
105 | // bedRoom
106 | // wifiAvailability
107 | // parkingAvailability
108 | // poolAvailability
109 | // airCondition
110 | // extraBedFacility
111 | // }
112 | // image{
113 | // id
114 | // url
115 | // }
116 | // location{
117 | // id
118 | // lat
119 | // lng
120 | // formattedAddress
121 | // zipcode
122 | // city
123 | // state_long
124 | // state_short
125 | // country_long
126 | // country_short
127 | // }
128 | // gallery{
129 | // id
130 | // url
131 | // }
132 | // categories{
133 | // id
134 | // slug
135 | // name
136 | // image
137 | // {
138 | // id
139 | // url
140 | // }
141 | // }
142 | // createdAt
143 | // updatedAt
144 | // }`;
145 | // return await this.prisma.client
146 | // .user({ id: userId })
147 | // .listed_posts()
148 | // .$fragment(fragment);
149 | // }
150 | // @ResolveField()
151 | // async favourite_post(@Parent() hotel: Hotel[], @Parent() user: User) {
152 | // const userId = (user && user.id) || (hotel[0] && hotel[0].agentId);
153 | // const fragment = `fragment fragFavourite on User
154 | // {
155 | // id
156 | // title
157 | // content
158 | // slug
159 | // price
160 | // status
161 | // isNegotiable
162 | // propertyType
163 | // condition
164 | // contactNumber
165 | // termsAndCondition
166 | // amenities{
167 | // id
168 | // guestRoom
169 | // bedRoom
170 | // wifiAvailability
171 | // parkingAvailability
172 | // poolAvailability
173 | // airCondition
174 | // extraBedFacility
175 | // }
176 | // image{
177 | // url
178 | // }
179 | // location{
180 | // id
181 | // lat
182 | // lng
183 | // formattedAddress
184 | // zipcode
185 | // city
186 | // state_long
187 | // state_short
188 | // country_long
189 | // country_short
190 | // }
191 | // gallery{
192 | // url
193 | // }
194 | // categories{
195 | // id
196 | // slug
197 | // name
198 | // image
199 | // {
200 | // id
201 | // url
202 | // }
203 | // }
204 | // createdAt
205 | // updatedAt
206 | // }`;
207 | // return await this.prisma.client
208 | // .user({ id: userId })
209 | // .favourite_post()
210 | // .$fragment(fragment);
211 | // }
212 | @ResolveField()
213 | async review_liked(@Parent() user: Reviews[]) {
214 | console.log(user);
215 | }
216 | @ResolveField()
217 | async review_disliked(@Parent() { id }: User) {
218 | console.log(id);
219 | }
220 | @Query()
221 | async userPosts(@Args('id') id: ID_Input) {
222 | const fragment = `fragment fragFavourite on User
223 | {
224 | listed_posts{
225 | id
226 | title
227 | content
228 | slug
229 | price
230 | status
231 | isNegotiable
232 | propertyType
233 | condition
234 | contactNumber
235 | rating
236 | termsAndCondition
237 | amenities{
238 | id
239 | guestRoom
240 | bedRoom
241 | wifiAvailability
242 | parkingAvailability
243 | poolAvailability
244 | airCondition
245 | extraBedFacility
246 | }
247 | image{
248 | url
249 | }
250 | location{
251 | id
252 | lat
253 | lng
254 | formattedAddress
255 | zipcode
256 | city
257 | state_long
258 | state_short
259 | country_long
260 | country_short
261 | }
262 | gallery{
263 | url
264 | }
265 | categories{
266 | id
267 | slug
268 | name
269 | image
270 | {
271 | id
272 | url
273 | }
274 | }
275 | createdAt
276 | updatedAt
277 | }
278 | favourite_post{
279 | id
280 | title
281 | content
282 | slug
283 | price
284 | status
285 | isNegotiable
286 | propertyType
287 | condition
288 | contactNumber
289 | termsAndCondition
290 | amenities{
291 | id
292 | guestRoom
293 | bedRoom
294 | wifiAvailability
295 | parkingAvailability
296 | poolAvailability
297 | airCondition
298 | extraBedFacility
299 | }
300 | image{
301 | url
302 | }
303 | location{
304 | id
305 | lat
306 | lng
307 | formattedAddress
308 | zipcode
309 | city
310 | state_long
311 | state_short
312 | country_long
313 | country_short
314 | }
315 | gallery{
316 | url
317 | }
318 | categories{
319 | id
320 | slug
321 | name
322 | image
323 | {
324 | id
325 | url
326 | }
327 | }
328 | createdAt
329 | updatedAt
330 | }
331 | }`;
332 | return this.prisma.client.user({ id }).$fragment(fragment);
333 | }
334 | @Query()
335 | async favouritePosts(@Args('id') id) {
336 | return this.prisma.client.user({ id }).favourite_post();
337 | }
338 | @Query()
339 | async getUserGallery(@Args('id') id) {
340 | return this.prisma.client.user({ id }).gallery();
341 | }
342 | @UseGuards(GqlAuthGuard)
343 | @Query()
344 | async favouritePostsHeart(@Args('id') id, @GqlUser() user: User) {
345 | console.log(user.id);
346 | return await this.prisma.client.user({ id: user.id }).favourite_post({
347 | where: {
348 | id: id,
349 | },
350 | });
351 | }
352 | @Query()
353 | async getUserInfo(@Args('id') id) {
354 | return this.prisma.client.user({ id });
355 | }
356 | @Query()
357 | async getUserReviews(@Args('id') id) {
358 | return this.prisma.client.user({ id }).reviews_maked();
359 | }
360 | @Query()
361 | async getUserNotification(@Args('id') id) {
362 | return this.prisma.client
363 | .user({ id })
364 | .notification({ orderBy: 'createdAt_DESC' });
365 | }
366 | @Query()
367 | async getUserUnreadNotification(@Args('id') id) {
368 | const fragment = `fragment getUnreadNotificationNumber on User{
369 | unreadNotification
370 | }`;
371 | return this.prisma.client.user({ id }).$fragment(fragment);
372 | }
373 | @Query()
374 | async getReviewsLikeDislike(@Args('id') id) {
375 | const fragment = `fragment likeAndDislike on User{
376 | reviewID
377 | peopleLiked {
378 | id
379 | }
380 | peopleDisliked {
381 | id
382 | }
383 | }`;
384 | return this.prisma.client.reviews({ reviewID: id }).$fragment(fragment);
385 | }
386 | @Query()
387 | async getVendorStripeId(@Args('id') id) {
388 | const fragment = `fragment vendorStripeIdFrag on User{
389 | stripeId
390 | }`;
391 | return await this.prisma.client.user({ id }).$fragment(fragment);
392 | }
393 | @Mutation()
394 | @UseGuards(GqlAuthGuard)
395 | async likeHotel(@GqlUser() user: User, @Args('id') id) {
396 | console.log(user.id + 'OK');
397 | console.log(id);
398 | return await this.prisma.client.updateHotel({
399 | where: {
400 | id,
401 | },
402 | data: {
403 | peopleLiked: {
404 | connect: { id: user.id },
405 | },
406 | },
407 | });
408 | }
409 | @Mutation()
410 | @UseGuards(GqlAuthGuard)
411 | async dislikeHotel(@GqlUser() user: User, @Args('id') id) {
412 | console.log(user.id + 'dislike');
413 | return this.prisma.client.updateHotel({
414 | where: {
415 | id,
416 | },
417 | data: {
418 | peopleLiked: {
419 | disconnect: { id: user.id },
420 | },
421 | },
422 | });
423 | }
424 | @Mutation()
425 | @UseGuards(GqlAuthGuard)
426 | async updateProfile(
427 | @GqlUser() user: User,
428 | @Args('profile')
429 | {
430 | first_name,
431 | last_name,
432 | date_of_birth,
433 | gender,
434 | email,
435 | content,
436 | cellNumber,
437 | }: UpdateProfileInput,
438 | @Args('location')
439 | locationInput: LocationInput,
440 | @Args('social')
441 | socialInput: SocialInput,
442 | ) {
443 | return await this.prisma.client.updateUser({
444 | where: {
445 | id: user.id,
446 | },
447 | data: {
448 | first_name,
449 | last_name,
450 | date_of_birth,
451 | gender,
452 | email,
453 | content,
454 | cellNumber,
455 | agent_location: {
456 | create: {
457 | ...locationInput,
458 | },
459 | },
460 | social_profile: {
461 | create: {
462 | ...socialInput,
463 | },
464 | },
465 | },
466 | });
467 | }
468 | @Mutation()
469 | @UseGuards(GqlAuthGuard)
470 | async updatePhotos(
471 | @GqlUser() user: User,
472 | @Args('photos') url: UpdatePhotosInput[],
473 | ) {
474 | return await this.prisma.client.updateUser({
475 | where: {
476 | id: user.id,
477 | },
478 | data: {
479 | gallery: {
480 | create: url,
481 | },
482 | // cover_pic: {
483 | // create: photos.map(i => ({
484 | // url: i.cover_pic.url,
485 | // })),
486 | // },
487 | // profile_pic: {
488 | // create: photos.map(i => ({
489 | // url: i.profile_pic.url,
490 | // })),
491 | // },
492 | // profile_pic_main: profile_pic[0].url,
493 | // cover_pic: {
494 | // create: cover_pic.map(i => ({
495 | // url: i.url,
496 | // })),
497 | // },
498 | // profile_pic: {
499 | // create: profile_pic.map(i => ({
500 | // url: i.url,
501 | // })),
502 | // },
503 | },
504 | });
505 | }
506 | @Mutation()
507 | @UseGuards(GqlAuthGuard)
508 | async deletePhotos(
509 | @GqlUser() user: User,
510 | @Args('photos') id: DeletePhotosInput[],
511 | ) {
512 | console.log(id);
513 | id.map(async i => {
514 | return await this.prisma.client.updateUser({
515 | where: {
516 | id: user.id,
517 | },
518 | data: {
519 | gallery: {
520 | delete: {
521 | id: i.id,
522 | },
523 | },
524 | },
525 | });
526 | });
527 | }
528 | @Mutation()
529 | @UseGuards(GqlAuthGuard)
530 | async setProfilePic(@GqlUser() user: User, @Args('url') url) {
531 | return this.prisma.client.updateUser({
532 | where: {
533 | id: user.id,
534 | },
535 | data: {
536 | profile_pic_main: url,
537 | },
538 | });
539 | }
540 | @Mutation()
541 | @UseGuards(GqlAuthGuard)
542 | async setCoverPic(@GqlUser() user: User, @Args('url') url) {
543 | return this.prisma.client.updateUser({
544 | where: {
545 | id: user.id,
546 | },
547 | data: {
548 | cover_pic_main: url,
549 | },
550 | });
551 | }
552 | @Mutation()
553 | @UseGuards(GqlAuthGuard)
554 | async sendContact(
555 | @GqlUser() user: User,
556 | @Args('contact') { message, cellNumber, subject }: ContactInput,
557 | ) {
558 | return this.mailService.sendContact(
559 | subject,
560 | user.email,
561 | message,
562 | cellNumber,
563 | );
564 | }
565 | @Mutation()
566 | @UseGuards(GqlAuthGuard)
567 | async likeOrDislikeReview(
568 | @GqlUser() user: User,
569 | @Args('id') id,
570 | @Args('type') type,
571 | ) {
572 | const checkUserLiked = await this.prisma.client.$exists.reviews({
573 | reviewID: id,
574 | peopleLiked_some: {
575 | id: user.id,
576 | },
577 | });
578 | const checkUserDisliked = await this.prisma.client.$exists.reviews({
579 | reviewID: id,
580 | peopleDisliked_some: {
581 | id: user.id,
582 | },
583 | });
584 | const fragment = `fragment likeAndDislikeRealtime on Reviews{
585 | reviewID
586 | peopleLiked{
587 | id
588 | }
589 | peopleDisliked{
590 | id
591 | }
592 | }`;
593 | // console.log('Check like');
594 | // console.log(checkUserLiked);
595 | // console.log('Check dislike');
596 | // console.log(checkUserDisliked);
597 | // Like lần đầu
598 |
599 | if (type === 1 && !checkUserDisliked) {
600 | await this.prisma.client.updateReviews({
601 | where: {
602 | reviewID: id,
603 | },
604 | data: {
605 | peopleLiked: {
606 | connect: {
607 | id: user.id,
608 | },
609 | },
610 | },
611 | });
612 | const likeAndDislikePayloads = await this.prisma.client
613 | .reviews({
614 | reviewID: id,
615 | })
616 | .$fragment(fragment);
617 | return this.pubSub.publish('realtimeLikeDislike', likeAndDislikePayloads);
618 | }
619 | // Dislike lần đầu
620 | if (type === 2 && !checkUserLiked) {
621 | await this.prisma.client.updateReviews({
622 | where: {
623 | reviewID: id,
624 | },
625 | data: {
626 | peopleDisliked: {
627 | connect: {
628 | id: user.id,
629 | },
630 | },
631 | },
632 | });
633 | const likeAndDislikePayloads = await this.prisma.client
634 | .reviews({
635 | reviewID: id,
636 | })
637 | .$fragment(fragment);
638 | return this.pubSub.publish('realtimeLikeDislike', likeAndDislikePayloads);
639 | }
640 | // Like và bỏ dislike
641 | if (type == 1 && checkUserDisliked) {
642 | await this.prisma.client.updateReviews({
643 | where: {
644 | reviewID: id,
645 | },
646 | data: {
647 | peopleLiked: {
648 | connect: {
649 | id: user.id,
650 | },
651 | },
652 | peopleDisliked: {
653 | disconnect: {
654 | id: user.id,
655 | },
656 | },
657 | },
658 | });
659 | const likeAndDislikePayloads = await this.prisma.client
660 | .reviews({
661 | reviewID: id,
662 | })
663 | .$fragment(fragment);
664 | return this.pubSub.publish('realtimeLikeDislike', likeAndDislikePayloads);
665 | }
666 | // Dislike và bỏ like
667 | if (type == 2 && checkUserLiked) {
668 | await this.prisma.client.updateReviews({
669 | where: {
670 | reviewID: id,
671 | },
672 | data: {
673 | peopleLiked: {
674 | disconnect: {
675 | id: user.id,
676 | },
677 | },
678 | peopleDisliked: {
679 | connect: {
680 | id: user.id,
681 | },
682 | },
683 | },
684 | });
685 | const likeAndDislikePayloads = await this.prisma.client
686 | .reviews({
687 | reviewID: id,
688 | })
689 | .$fragment(fragment);
690 | return this.pubSub.publish('realtimeLikeDislike', likeAndDislikePayloads);
691 | }
692 | }
693 | @Mutation()
694 | async forgetPassword(@Args('email') email: string, @ResGql() res: Response) {
695 | console.log('Email input is ', email);
696 | const user = await this.prisma.client.user({ email });
697 | if (!user) {
698 | throw Error('Email not exists');
699 | }
700 | const code = uuidv4();
701 | res.cookie('reset-password', code, {
702 | domain: '.hotel-prisma.ml',
703 | httpOnly: false,
704 | sameSite: 'none',
705 | secure: true,
706 | });
707 | return this.mailService.sendEmail(email, code);
708 | }
709 | @Mutation()
710 | async checkNotification(@Args('id') id) {
711 | await this.prisma.client.updateManyNotifications({
712 | where: {
713 | userNotificationId: id,
714 | },
715 | data: {
716 | old: true,
717 | },
718 | });
719 | return await this.prisma.client.updateUser({
720 | where: {
721 | id,
722 | },
723 | data: {
724 | unreadNotification: 0,
725 | },
726 | });
727 | }
728 | @Mutation()
729 | async readNotification(@Args('id') query) {
730 | return await this.prisma.client.updateManyNotifications({
731 | where: {
732 | query,
733 | },
734 | data: {
735 | read: true,
736 | },
737 | });
738 | }
739 | @Mutation()
740 | async deleteAllNotifications(@Args('id') id) {
741 | return await this.prisma.client.deleteManyNotifications({
742 | userNotificationId: id,
743 | });
744 | }
745 | @Mutation()
746 | @UseGuards(GqlAuthGuard)
747 | async createCoupon(
748 | @GqlUser() user: User,
749 | @Args('coupon') coupon: CouponInput,
750 | @Args('type') type: number,
751 | @Args('hotelsId') hotelsId: any[],
752 | ) {
753 | console.log(type);
754 | // Tạo cho tất cả hotel
755 | if (type === 1) {
756 | const hotels = await this.prisma.client.hotels({
757 | where: {
758 | agentId: user.id,
759 | },
760 | });
761 | const coupons = await this.prisma.client.createCoupon({
762 | couponName: coupon.couponName,
763 | couponDescription: coupon.couponDescription,
764 | couponQuantity: coupon.couponQuantity,
765 | couponAuthor: {
766 | connect: {
767 | id: user.id,
768 | },
769 | },
770 | couponAuthorId: user.id,
771 | couponType: coupon.couponType,
772 | couponValue: coupon.couponValue,
773 | couponStartDate: coupon.couponStartDate,
774 | couponEndDate: coupon.couponEndDate,
775 | });
776 | return hotels.map(async i => {
777 | return await this.prisma.client.updateHotel({
778 | where: {
779 | id: i.id,
780 | },
781 | data: {
782 | couponsAvailable: {
783 | connect: {
784 | couponId: coupons.couponId,
785 | },
786 | },
787 | },
788 | });
789 | });
790 | }
791 | // Tạo cho hotels có id trong hotelId
792 | if (type == 2) {
793 | let isValid = true;
794 | await Promise.all(
795 | hotelsId.map(async i => {
796 | if (!isMongoId(i)) {
797 | isValid = false;
798 | throw Error(`'${i}' is not a valid MongoID`);
799 | }
800 | const hotelExists = await this.prisma.client
801 | .user({ id: user.id })
802 | .listed_posts({ where: { id: i } });
803 | if (hotelExists && hotelExists.length === 0) {
804 | isValid = false;
805 | throw Error(`'${i}' is not one of your Hotels`);
806 | }
807 | }),
808 | );
809 | if (isValid) {
810 | const coupons = await this.prisma.client.createCoupon({
811 | couponName: coupon.couponName,
812 | couponDescription: coupon.couponDescription,
813 | couponQuantity: coupon.couponQuantity,
814 | couponAuthor: {
815 | connect: {
816 | id: user.id,
817 | },
818 | },
819 | couponAuthorId: user.id,
820 | couponType: coupon.couponType,
821 | couponValue: coupon.couponValue,
822 | couponStartDate: coupon.couponStartDate,
823 | couponEndDate: coupon.couponEndDate,
824 | });
825 |
826 | return hotelsId.map(async i => {
827 | // console.log(i);
828 | // console.log(isValid);
829 | // if (!isValid)
830 | // throw Error(`ID '${i}' is not exists in your current Hotels`);
831 | return await this.prisma.client.updateHotel({
832 | where: {
833 | id: i,
834 | },
835 | data: {
836 | couponsAvailable: {
837 | connect: {
838 | couponId: coupons.couponId,
839 | },
840 | },
841 | },
842 | });
843 | });
844 | }
845 | }
846 | }
847 | @Mutation()
848 | @UseGuards(GqlAuthGuard)
849 | async makeReviews(
850 | @GqlUser() user: User,
851 | @Args('hotelId') id: ID_Input,
852 | @Args('reviews') review: ReviewInput,
853 | ) {
854 | // console.log(id);
855 | console.log(user.id + ' Id nguoi reviewed');
856 | const hotelManagerId = await this.prisma.client.hotel({ id }).agentId();
857 | const reviewedHotelName = await this.prisma.client.hotel({ id }).title();
858 | const slug = await this.prisma.client.hotel({ id }).slug();
859 | const updateRatingCount = await this.prisma.client
860 | .reviewsesConnection({
861 | where: {
862 | reviewedHotelId: id,
863 | },
864 | })
865 | .aggregate()
866 | .count();
867 | console.log(updateRatingCount);
868 | const query = `post/${slug}/${id}#reviews`;
869 | // const unreadNotification = await this.prisma.client
870 | // .notificationsConnection({
871 | // where: {
872 | // userNotificationId: user.id,
873 | // old: false,
874 | // },
875 | // })
876 | // .aggregate()
877 | // .count();
878 | // console.log(unreadNotification);
879 | const peopleReviewedArr = await this.prisma.client
880 | .hotel({ id })
881 | .peopleReviewed();
882 | // console.log(JSON.stringify(peopleReviewedArr) + 'First');
883 | const hotelMangerIndex =
884 | peopleReviewedArr.findIndex(id => hotelManagerId === id.id) === -1
885 | ? 1
886 | : 0;
887 | if (hotelMangerIndex === 1) {
888 | console.log('updated hotel manager');
889 | await this.prisma.client.updateHotel({
890 | where: {
891 | id,
892 | },
893 | data: {
894 | peopleReviewed: {
895 | connect: {
896 | id: hotelManagerId,
897 | },
898 | },
899 | },
900 | });
901 | }
902 | const index =
903 | peopleReviewedArr.findIndex(id => user.id === id.id) === -1 ? 1 : 0;
904 | console.log(index);
905 | if (index === 1) {
906 | console.log('updated');
907 | await this.prisma.client.updateHotel({
908 | where: {
909 | id,
910 | },
911 | data: {
912 | peopleReviewed: {
913 | connect: {
914 | id: user.id,
915 | },
916 | },
917 | },
918 | });
919 | }
920 | this.pubSub.publish('notificationBell', {
921 | notificationBell: {
922 | reviewAuthorName: user.first_name + ' ' + user.last_name,
923 | reviewedHotelName,
924 | reviewedAuthorId: user.id,
925 | reviewTitle: review.reviewTitle,
926 | reviewText: review.reviewText,
927 | reviewAuthorProfilePic: user.profile_pic_main,
928 | peopleReviewedQuantity: peopleReviewedArr.length,
929 | query,
930 | read: false,
931 | peopleReviewedArr,
932 | },
933 | });
934 | peopleReviewedArr && peopleReviewedArr.length > 0
935 | ? peopleReviewedArr.map(async i => {
936 | if (i.id !== user.id) {
937 | const unreadNotification = await this.prisma.client
938 | .notificationsConnection({
939 | where: {
940 | userNotificationId: i.id,
941 | old: false,
942 | },
943 | })
944 | .aggregate()
945 | .count();
946 | // console.log(i.id + unreadNotification);
947 | this.pubSub.publish('unreadNotification', {
948 | unreadNotification: {
949 | reviewedAuthorId: i.id,
950 | unreadNotification: unreadNotification + 1,
951 | },
952 | });
953 | console.log(unreadNotification, i.id);
954 | await this.prisma.client.updateUser({
955 | where: {
956 | id: i.id,
957 | },
958 | data: {
959 | notification: {
960 | create: {
961 | reviewAuthorName: user.first_name + ' ' + user.last_name,
962 | reviewedHotelName,
963 | reviewTitle: review.reviewTitle,
964 | reviewText: review.reviewText,
965 | userNotificationId: i.id,
966 | query,
967 | reviewAuthorProfilePic: user.profile_pic_main,
968 | peopleReviewedQuantity: peopleReviewedArr.length,
969 | },
970 | },
971 | unreadNotification: unreadNotification + 1,
972 | },
973 | });
974 | }
975 | })
976 | : '';
977 | await this.prisma.client.updateHotel({
978 | where: { id },
979 | data: {
980 | ratingCount: updateRatingCount + 1,
981 | },
982 | });
983 | // console.log(JSON.stringify(peopleReviewedArr) + 'Second');
984 | const reviewSub = await this.prisma.client.createReviews({
985 | reviewOverall: review.reviewOverall,
986 | reviewTitle: review.reviewTitle,
987 | reviewText: review.reviewText,
988 | sortOfTrip: review.sortOfTrip,
989 | reviewTips: review.reviewTips,
990 | reviewAuthorId: {
991 | connect: {
992 | id: user.id,
993 | },
994 | },
995 | reviewAuthorEmail: user.email,
996 | reviewAuthorFirstName: user.first_name,
997 | reviewAuthorLastName: user.last_name,
998 | reviewAuthorPic: user.profile_pic_main,
999 | reviewFields: {
1000 | create: review.reviewFieldInput,
1001 | },
1002 | reviewPics: {
1003 | create: review.reviewPics,
1004 | },
1005 | reviewedHotel: {
1006 | connect: {
1007 | id: id,
1008 | },
1009 | },
1010 | reviewedHotelId: id,
1011 | reviewOptional: {
1012 | create: review.reviewOptionals,
1013 | },
1014 | });
1015 | // console.log(reviewToSubscribe);
1016 | const fragment = `fragment someFrag on Reviews {
1017 | reviewID
1018 | reviewTitle
1019 | reviewText
1020 | sortOfTrip
1021 | reviewAuthorId {
1022 | id
1023 | }
1024 | reviewAuthorFirstName
1025 | reviewAuthorLastName
1026 | reviewAuthorEmail
1027 | reviewOverall
1028 | reviewAuthorPic
1029 | reviewedHotelId
1030 | reviewTips
1031 | reviewPics {
1032 | url
1033 | }
1034 | reviewDate
1035 | reviewOptional {
1036 | option
1037 | optionField
1038 | }
1039 | reviewFields {
1040 | rating
1041 | ratingFieldName
1042 | }
1043 | }`;
1044 | const reviewToSubscribe = await this.prisma.client
1045 | .reviews({
1046 | reviewID: reviewSub.reviewID,
1047 | })
1048 | .$fragment(fragment);
1049 | this.pubSub.publish('realtimeReviews', reviewToSubscribe);
1050 | // console.log(reviewToSubscribe);
1051 | return reviewSub;
1052 | }
1053 | @Mutation()
1054 | @UseGuards(GqlAuthGuard)
1055 | async updateStripeId(
1056 | @GqlUser() user: User,
1057 | @Args('stripeId') stripeId,
1058 | @Args('type') type,
1059 | ) {
1060 | if (user.role === 'Normal') {
1061 | return await this.prisma.client.updateUser({
1062 | where: {
1063 | id: user.id,
1064 | },
1065 | data: {
1066 | stripeId,
1067 | role: type || 'Normal',
1068 | },
1069 | });
1070 | }
1071 | return await this.prisma.client.updateUser({
1072 | where: {
1073 | id: user.id,
1074 | },
1075 | data: {
1076 | role: type,
1077 | },
1078 | });
1079 | }
1080 | @Subscription(returns => Unread, {
1081 | // So data và biến filter channel
1082 | // filter(payloads, variables) {
1083 | // console.log('Nguoi comment' + payloads.notification.reviewAuthorId);
1084 | // // return variables.channelId[1].userId === '5f2b5c8aa7b11b00078d09b1';
1085 | // return _.forEach( variables.channelId, function(i){
1086 | // return i.userId !==payloads.notification.reviewAuthorId
1087 | // })
1088 | // // return variables.channelId.forEach(i => {
1089 | // // console.log(i.userId)
1090 | // // return i.userId === '5f2b5c8aa7b11b00078d09b1';
1091 | // // });
1092 | // console.log(_.find(payloads.notification.peopleReviewedArr, {
1093 | // id: variables.channelId,
1094 | // }))
1095 | // console.log(_.find(payloads.notification.peopleReviewedArr, {
1096 | // id: variables.channelId,
1097 | // }) !== undefined)
1098 | // console.log( _.find(payloads.notification.peopleReviewedArr, {
1099 | // id: variables.channelId,
1100 | // }) !== 'undefined' &&
1101 | // variables.channelId !== payloads.notification.reviewAuthorId)
1102 | // return true;
1103 | // },
1104 | filter: (payloads, variables) =>
1105 | // Special
1106 | // _.find(payloads.unreadNotification.peopleReviewedArr, {
1107 | // id: variables.channelId,
1108 | // }) !== undefined &&
1109 | // variables.channelId !== payloads.unreadNotification.reviewedAuthorId,
1110 | payloads.unreadNotification.reviewedAuthorId === variables.channelId,
1111 | // payloads.notification.peopleReviewedArr.map(id => {
1112 | // console.log(payloads.notification.peopleReviewedArr)
1113 | // console.log(variables.channelId + 'Id lang nghe');
1114 | // console.log(payloads.notification.reviewAuthorId + ' id nguoi comment');
1115 | // console.log(
1116 | // id.id === variables.channelId &&
1117 | // id.id !== payloads.notification.reviewAuthorId + ' True false',
1118 | // );
1119 | // console.log(id.id);
1120 | // const test = _.find(payloads.notification.peopleReviewedArr, {id:variables.channelId});
1121 | // console.log(test);
1122 | // (id.id === variables.channelId &&
1123 | // id.id !== payloads.notification.reviewAuthorId);
1124 | // }),
1125 | })
1126 | // Hứng
1127 | unreadNotification() {
1128 | return this.pubSub.asyncIterator('unreadNotification');
1129 | }
1130 |
1131 | @Subscription(returns => Notification, {
1132 | // resolve(this: PrismaService, payloads, variables, anotherSub: PubSubEngine) {
1133 | // console.log(payloads)
1134 | // console.log(variables.channelId)
1135 | // },
1136 | // filter(payloads, variables) {
1137 | // // console.log(payloads);
1138 | // // console.log(variables);
1139 | // console.log(
1140 | // _.find(payloads.notificationBell.peopleReviewedArr, {
1141 | // id: variables.channelId,
1142 | // }) !== undefined &&
1143 | // variables.channelId !== payloads.notificationBell.reviewAuthorId,
1144 | // );
1145 | // return (
1146 | // _.find(payloads.notificationBell.peopleReviewedArr, {
1147 | // id: variables.channelId,
1148 | // }) !== undefined &&
1149 | // variables.channelId !== payloads.notificationBell.reviewAuthorId
1150 | // );
1151 | // },
1152 | filter: (payloads, variables) =>
1153 | _.find(payloads.notificationBell.peopleReviewedArr, {
1154 | id: variables.channelId,
1155 | }) !== undefined &&
1156 | variables.channelId !== payloads.notificationBell.reviewedAuthorId,
1157 | })
1158 | notificationBell() {
1159 | return this.pubSub.asyncIterator('notificationBell');
1160 | }
1161 |
1162 | @Subscription(returns => Reviews, {
1163 | resolve(this: PrismaService, payloads, variables) {
1164 | return payloads;
1165 | },
1166 | filter: (payloads, variables) =>
1167 | payloads.reviewedHotelId === variables.hotelId,
1168 | })
1169 | realtimeReviews() {
1170 | return this.pubSub.asyncIterator('realtimeReviews');
1171 | }
1172 | @Subscription(returns => Reviews, {
1173 | resolve: payloads => payloads,
1174 | filter: (payloads, variables) => payloads.reviewID === variables.reviewID,
1175 | })
1176 | realtimeLikeDislike() {
1177 | return this.pubSub.asyncIterator('realtimeLikeDislike');
1178 | }
1179 | }
1180 |
--------------------------------------------------------------------------------
/src/utils/FilesAndMockUpload.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Controller,
3 | Post,
4 | UploadedFiles,
5 | UseInterceptors,
6 | Body,
7 | Res,
8 | Req,
9 | } from '@nestjs/common';
10 | import { Response, Request } from 'express';
11 | import { AwsService } from 'src/aws/aws.service';
12 | import { FilesInterceptor } from '@nestjs/platform-express';
13 | import * as fs from 'fs';
14 | import * as jwt2 from 'jsonwebtoken';
15 |
16 | import { StripeService } from './stripe';
17 |
18 | @Controller('api')
19 | export class UploadController {
20 | constructor(private aws: AwsService, private stripeApi: StripeService) {}
21 | @Post('uploadFile')
22 | @UseInterceptors(FilesInterceptor('files[]'))
23 | async uploadFile(@UploadedFiles() files) {
24 | // console.log(files)
25 | return await this.aws.uploadFile(files);
26 | }
27 | @Post('mock-payment')
28 | async mockPayment(@Body() data, @Res() res: Response) {
29 | // console.log(amount.amount);
30 | this.stripeApi.handleClientCard(data, res);
31 | }
32 | @Post('mock-stripe')
33 | async mockStripe(@Body() plan, @Res() res: Response, @Req() req: Request) {
34 | this.stripeApi.createStripeAccount(plan, res, req);
35 | }
36 | @Post('access-mock-stripe')
37 | async accessMockStripe(@Body() account, @Res() res: Response) {
38 | this.stripeApi.accessStripeDashboardConnected(account, res);
39 | }
40 | @Post('mock')
41 | async mock(@Body() receivedMockData: string[]) {
42 | console.log(receivedMockData);
43 | fs.writeFile('mock.ts', JSON.stringify(receivedMockData, null, 4), err => {
44 | if (err) throw err;
45 | });
46 | console.log('MockData exported');
47 | // Có thể viết class lấy hàm như aws làm hoặc execute prisma seed
48 | }
49 | // @Post('mock-mail-jet')
50 | // async mockMailjet(@Body() content) {
51 | // const request = mailjet.post('send', { version: 'v3.1' }).request({
52 | // Messages: [
53 | // {
54 | // From: {
55 | // Email: '19520854@gm.uit.edu.vn',
56 | // Name: 'Palace TripFinder',
57 | // },
58 | // To: [
59 | // {
60 | // Email: 'passenger1@example.com',
61 | // Name: 'passenger 1',
62 | // },
63 | // ],
64 | // TemplateID: 1645231,
65 | // TemplateLanguage: true,
66 | // Subject: 'Palace',
67 | // Variables: {
68 | // firstname: 'Default value',
69 | // startDate: '10-01-2001',
70 | // endDate: '20-01-2001',
71 | // hotelName: '',
72 | // guest: '1',
73 | // room: '1',
74 | // couponName: 'name',
75 | // total_price: 'Default value',
76 | // order_date: 'Default value',
77 | // order_id: 'Default value',
78 | // },
79 | // },
80 | // ],
81 | // });
82 | // request
83 | // .then(result => {
84 | // console.log(result.body);
85 | // })
86 | // .catch(err => {
87 | // console.log(err.statusCode);
88 | // });
89 | // }
90 | }
91 |
--------------------------------------------------------------------------------
/src/utils/seed.ts:
--------------------------------------------------------------------------------
1 | import { mockData, ClientPrisma } from './data';
2 | import * as _ from 'lodash';
3 | // constructor
4 | // const hotel = new Prisma({
5 | // endpoint: 'http://localhost:4466/abcdef/devxyz',
6 | // secret: 'secreetttttt',
7 | // }) //custom endpoint
8 |
9 | // Có thể hoàn toàn ko cần làm file seed.ts
10 | // Call truyền đối số data vào hàm và gọi luôn
11 | // Nhưng chủ yếu để biết chạy chạy seed mongodb graphql bằng cli (prisma seed)
12 |
13 | const format = s =>
14 | s
15 | .toLowerCase()
16 | .split(/\s|%20/)
17 | .filter(Boolean)
18 | .join('-');
19 | const setup = async (dataMock: any[]) => {
20 | const hotel = new ClientPrisma();
21 | dataMock.forEach(async i => {
22 | hotel
23 | .createHotel({
24 | agentId: '5f9e75ed51915100072a4e1d',
25 | connectId: {
26 | connect: {
27 | email: 'duyminhpham1201@gmail.com',
28 | },
29 | },
30 | title: i.title,
31 | agentEmail: "phucpham1301@gmail.com",
32 | agentName: "Phuc Pham",
33 | slug: format(i.slug),
34 | content: i.content,
35 | price: parseInt(i.price),
36 | isNegotiable: i.isNegotiable,
37 | propertyType: i.propertyType,
38 | condition: i.condition,
39 | termsAndCondition: i.termsAndCondition,
40 | contactNumber: i.contactNumber,
41 | rating: i.rating,
42 | ratingCount: i.ratingCount,
43 | status: i.status,
44 | image: {
45 | create: _.omit(i.image, 'id'),
46 | },
47 | location: {
48 | create: _.omit(i.location, 'id'),
49 | },
50 | gallery: {
51 | create: i.gallery.map(v => ({
52 | url: v.url,
53 | })),
54 | },
55 | amenities: {
56 | create: {
57 | guestRoom: i.amenities[0].guestRoom,
58 | bedRoom: i.amenities[1].bedRoom,
59 | wifiAvailability: i.amenities[2].wifiAvailability,
60 | parkingAvailability: i.amenities[3].parkingAvailability,
61 | poolAvailability: i.amenities[4].poolAvailability,
62 | airCondition: i.amenities[5].airCondition,
63 | extraBedFacility: i.amenities[6].extraBedFacility,
64 | },
65 | },
66 | // categories: {
67 | // create: i.categories,
68 | // },
69 | })
70 | .catch(e => console.log(e));
71 | });
72 | };
73 | setup(mockData);
74 | // data:[{
75 | // connectId:"123id"
76 | // title: "abc"
77 | // name:"123abccc"
78 | // },
79 | // {
80 | // connectId:"vcvcc"
81 | // title: "xyz"
82 | // name:"223"
83 | // }]
84 |
--------------------------------------------------------------------------------
/src/utils/stripe.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Res, Req } from '@nestjs/common';
2 | import { Response, Request } from 'express';
3 | import Stripe from 'stripe';
4 | // import { stripe } from './data';
5 | @Injectable()
6 | export class StripeService {
7 | // Stripe ko cung cấp injectable service
8 | // Có thể tạo DTO
9 | // Session flow url cho mỗi stripe
10 | // Sử dụng validation pipe
11 | // construct events với webhook
12 |
13 | private readonly stripe: Stripe = new Stripe(process.env.STRIPE_SECRET, {
14 | apiVersion: '2020-08-27',
15 | });
16 |
17 | public async createStripeAccount(
18 | plan,
19 | @Res() res: Response,
20 | @Req() req: Request,
21 | ): Promise {
22 | const data = req.body;
23 | try {
24 | const account = await this.stripe.accounts.create({
25 | type: 'express',
26 | country: 'US',
27 | email: plan.email,
28 | capabilities: {
29 | card_payments: { requested: true },
30 | transfers: { requested: true },
31 | },
32 |
33 | business_type: 'individual', // trong test mode + Chặn payment bằng cách đổi qua country qua NZ
34 | individual: { // Allow all bằng cách tạo account có các field thủ công hoặc tạo xong và bỏ field SSN
35 | id_number: '000000000',
36 | ssn_last_4: '0000',
37 | },
38 | // external_account: data.external_account,
39 | // tos_acceptance:{
40 | // date: Math.round((new Date()).getTime()/1000),
41 | // ip: req.ip,
42 |
43 | // },
44 | // business_profile: {
45 | // url: data.url,
46 | // mcc: '7623',
47 | // },
48 | // company: {
49 | // name: data.name,
50 | // phone: data.phone,
51 | // tax_id: data.tax_id,
52 | // address: {
53 | // line1: data.line1,
54 | // line2: data.line2,
55 | // state: data.state,
56 | // postal_code: data.postal_code,
57 | // },
58 | // },
59 | });
60 | const accountLink = await this.stripe.accountLinks.create({
61 | account: account.id,
62 | refresh_url: 'https://vercel-v2.hotel-prisma.ml/error', // Khi link ko còn valid
63 | return_url: `https://vercel-v2.hotel-prisma.ml/processing?accountId=${account.id}&plan=${plan.type}`, // redirect //Dev mode thì đang http
64 | type: 'account_onboarding',
65 | });
66 | res.status(200).json({ accountLink });
67 | } catch (e) {
68 | res.status(400).json({ code: 'error', message: e.message });
69 | }
70 | }
71 | public async accessStripeDashboardConnected(account, @Res() res: Response) {
72 | try {
73 | const link = await this.stripe.accounts.createLoginLink(account.id);
74 | res.status(200).json({ link });
75 | } catch (e) {
76 | throw Error(e.message);
77 | }
78 | }
79 | public async handleClientCard(data, @Res() res: Response): Promise {
80 | console.log(data);
81 | try {
82 | const paymentIntents = await this.stripe.paymentIntents.create(
83 | {
84 | amount: data.amount,
85 | currency: 'usd',
86 | },
87 | {
88 | stripeAccount: data.stripeId || '',
89 | },
90 | );
91 | res.status(200).json({ client_secret: paymentIntents.client_secret });
92 | } catch (e) {
93 | res.status(500).json({ statusCode: 500, message: e.message });
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/utils/upload.input.ts:
--------------------------------------------------------------------------------
1 | import { InputType, Field } from "@nestjs/graphql";
2 | import { Upload } from "./upload.scalar";
3 |
4 | @InputType()
5 | export class UploadUserProfilePicInput {
6 | @Field()
7 | file : Upload
8 | }
--------------------------------------------------------------------------------
/src/utils/upload.scalar.ts:
--------------------------------------------------------------------------------
1 | import { Scalar } from "@nestjs/graphql";
2 | import { GraphQLUpload } from 'graphql-upload';
3 |
4 | @Scalar('Upload')
5 | export class Upload {
6 | description = 'File upload scalar type'
7 |
8 | parseValue(value){
9 | return GraphQLUpload.parseValue(value);
10 | }
11 |
12 | serialize(value){
13 | return GraphQLUpload.serialize(value);
14 | }
15 |
16 | parseLiteral(ast){
17 | return GraphQLUpload.parseLiteral(ast, ast.value);
18 | }
19 | }
--------------------------------------------------------------------------------
/src/utils/utils.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UploadController } from './FilesAndMockUpload';
3 | import { AwsService } from 'src/aws/aws.service';
4 | import { PrismaService } from 'src/prisma/prisma.service';
5 | import { StripeService } from './stripe';
6 |
7 | @Module({
8 | controllers: [UploadController],
9 | providers: [AwsService, PrismaService, StripeService],
10 | exports:[StripeService],
11 | })
12 | export class UtilsModule {}
13 |
--------------------------------------------------------------------------------
/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 | }
15 | }
16 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "nestjs-now",
4 | "builds": [
5 | {
6 | "src": "dist/src/main.js",
7 | "use": "@vercel/node"
8 | }
9 | ],
10 | "routes": [
11 | {
12 | "src": "/(.*)",
13 | "dest": "dist/src/main.js"
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------