From 2b81da1f5e11d3daa2299b2d1a5da3b08dc42561 Mon Sep 17 00:00:00 2001 From: Renolation Date: Thu, 22 May 2025 03:51:58 +0700 Subject: [PATCH 1/9] aa --- package-lock.json | 277 +++++++++++++++++- package.json | 2 + src/posts/postsSearch.service.ts | 42 +++ src/posts/types/postSearchBody.interface.ts | 6 + src/posts/types/postSearchResult.interface.ts | 9 + src/search/search.module.ts | 22 ++ 6 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 src/posts/postsSearch.service.ts create mode 100644 src/posts/types/postSearchBody.interface.ts create mode 100644 src/posts/types/postSearchResult.interface.ts create mode 100644 src/search/search.module.ts diff --git a/package-lock.json b/package-lock.json index a2dc9a9..faa3f06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@elastic/elasticsearch": "^9.0.2", "@hapi/joi": "^17.1.1", "@neondatabase/serverless": "^1.0.0", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/elasticsearch": "^11.1.0", "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", @@ -777,6 +779,38 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@elastic/elasticsearch": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-9.0.2.tgz", + "integrity": "sha512-uKA0PuPSND3OhHH9UFqnKZfxifAg/8mQW4VnrQ+sUtusTbPhGuErs5NeWCPyd/RLgruBWBmLSv1zzEv5GS+UnA==", + "license": "Apache-2.0", + "dependencies": { + "@elastic/transport": "^9.0.1", + "apache-arrow": "18.x - 19.x", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@elastic/transport": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-9.0.1.tgz", + "integrity": "sha512-6jVZQzAe2iTRsZA6I/wkO2BjzJFD9BHTASo2YgGfbcoV95ey/8D/ABRhpgfg35LIDrmialIGJBizunSwxsRDLg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "1.x", + "debug": "^4.4.0", + "hpagent": "^1.2.0", + "ms": "^2.1.3", + "secure-json-parse": "^3.0.2", + "tslib": "^2.8.1", + "undici": "^7.2.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -2675,6 +2709,17 @@ } } }, + "node_modules/@nestjs/elasticsearch": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/elasticsearch/-/elasticsearch-11.1.0.tgz", + "integrity": "sha512-NwMakVs8LeXUksaSNp0ejhv223yVCK4w9iqMBrsonKj2gl4sBIBrAgJq/aXhD9bJCNLYb+waoRAsxuuPxYcjXw==", + "license": "MIT", + "peerDependencies": { + "@elastic/elasticsearch": "^7.4.0 || ^8.0.0 || ^9.0.0", + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.2.0" + } + }, "node_modules/@nestjs/jwt": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", @@ -2978,6 +3023,15 @@ "npm": ">=5.10.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@paralleldrive/cuid2": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", @@ -3381,6 +3435,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@swc/types": { "version": "0.1.21", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", @@ -3520,6 +3583,18 @@ "@types/node": "*" } }, + "node_modules/@types/command-line-args": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", + "license": "MIT" + }, + "node_modules/@types/command-line-usage": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", + "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==", + "license": "MIT" + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -5106,6 +5181,41 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/apache-arrow": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-19.0.1.tgz", + "integrity": "sha512-APmMLzS4qbTivLrPdQXexGM4JRr+0g62QDaobzEvip/FdQIrv2qLy0mD5Qdmw4buydtVJgbFeKR8f59I6PPGDg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/command-line-args": "^5.2.3", + "@types/command-line-usage": "^5.0.4", + "@types/node": "^20.13.0", + "command-line-args": "^6.0.1", + "command-line-usage": "^7.0.1", + "flatbuffers": "^24.3.25", + "json-bignum": "^0.0.3", + "tslib": "^2.6.2" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.js" + } + }, + "node_modules/apache-arrow/node_modules/@types/node": { + "version": "20.17.50", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz", + "integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/apache-arrow/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/app-root-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", @@ -5190,6 +5300,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/array-timsort": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", @@ -5820,7 +5939,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -5833,6 +5951,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -6097,6 +6230,44 @@ "node": ">= 0.8" } }, + "node_modules/command-line-args": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "find-replace": "^5.0.2", + "lodash.camelcase": "^4.3.0", + "typical": "^7.2.0" + }, + "engines": { + "node": ">=12.20" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/command-line-usage": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.3.tgz", + "integrity": "sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.0", + "typical": "^7.1.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -7429,6 +7600,23 @@ "node": ">= 0.8" } }, + "node_modules/find-replace": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -7476,6 +7664,12 @@ "node": ">=16" } }, + "node_modules/flatbuffers": { + "version": "24.12.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz", + "integrity": "sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==", + "license": "Apache-2.0" + }, "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", @@ -7975,7 +8169,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8048,6 +8241,15 @@ "node": ">= 0.4" } }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -9298,6 +9500,14 @@ "node": ">=6" } }, + "node_modules/json-bignum": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", + "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -9520,6 +9730,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -11406,6 +11622,22 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/secure-json-parse": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-3.0.2.tgz", + "integrity": "sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/seek-bzip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", @@ -12087,7 +12319,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -12145,6 +12376,19 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -13006,6 +13250,15 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", @@ -13041,6 +13294,15 @@ "through": "^2.3.8" } }, + "node_modules/undici": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -13522,6 +13784,15 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrapjs": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", + "integrity": "sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index 6c10482..ad190cf 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,13 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@elastic/elasticsearch": "^9.0.2", "@hapi/joi": "^17.1.1", "@neondatabase/serverless": "^1.0.0", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/elasticsearch": "^11.1.0", "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", diff --git a/src/posts/postsSearch.service.ts b/src/posts/postsSearch.service.ts new file mode 100644 index 0000000..dbc9acd --- /dev/null +++ b/src/posts/postsSearch.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { ElasticsearchService } from '@nestjs/elasticsearch'; +import Post from "./entities/post.entity"; +import {PostSearchResult} from "./types/postSearchResult.interface"; +import {PostSearchBody} from "./types/postSearchBody.interface"; + +@Injectable() +export default class PostsSearchService { + index = 'posts' + + constructor( + private readonly elasticsearchService: ElasticsearchService + ) {} + + async indexPost(post: Post) { + return this.elasticsearchService.index({ + index: this.index, + body: { + id: post.id, + title: post.title, + content: post.content, + authorId: post.author.id + } + }) + } + + async search(text: string) { + const response = await this.elasticsearchService.search({ + index: this.index, + body: { + query: { + multi_match: { + query: text, + fields: ['title', 'content'] + } + } + } + }) + const hits = body.hits.hits; + return hits.map((item) => item._source); + } +} \ No newline at end of file diff --git a/src/posts/types/postSearchBody.interface.ts b/src/posts/types/postSearchBody.interface.ts new file mode 100644 index 0000000..61dacc3 --- /dev/null +++ b/src/posts/types/postSearchBody.interface.ts @@ -0,0 +1,6 @@ +export interface PostSearchBody { + id: number, + title: string, + content: string, + authorId: number +} \ No newline at end of file diff --git a/src/posts/types/postSearchResult.interface.ts b/src/posts/types/postSearchResult.interface.ts new file mode 100644 index 0000000..2a720f9 --- /dev/null +++ b/src/posts/types/postSearchResult.interface.ts @@ -0,0 +1,9 @@ + +export interface PostSearchResult { + hits: { + total: number; + hits: Array<{ + _source: PostSearchBody; + }>; + }; +} \ No newline at end of file diff --git a/src/search/search.module.ts b/src/search/search.module.ts new file mode 100644 index 0000000..5f5c6d9 --- /dev/null +++ b/src/search/search.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ElasticsearchModule } from '@nestjs/elasticsearch'; + +@Module({ + imports: [ + ConfigModule, + ElasticsearchModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + node: configService.get('ELASTICSEARCH_NODE'), + auth: { + username: configService.get('ELASTICSEARCH_USERNAME'), + password: configService.get('ELASTICSEARCH_PASSWORD'), + } + }), + inject: [ConfigService], + }), + ], + exports: [ElasticsearchModule] +}) +export class SearchModule {} \ No newline at end of file From efea3d541195f3b17b325a06fa69d54c0959d970 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Thu, 22 May 2025 11:35:14 +0700 Subject: [PATCH 2/9] add elastic search --- src/posts/posts.controller.ts | 24 +++- src/posts/posts.service.ts | 39 ++++++- src/posts/postsSearch.service.ts | 103 ++++++++++++------ src/posts/types/postSearchResult.interface.ts | 1 + 4 files changed, 130 insertions(+), 37 deletions(-) diff --git a/src/posts/posts.controller.ts b/src/posts/posts.controller.ts index ae42f03..72c8a92 100644 --- a/src/posts/posts.controller.ts +++ b/src/posts/posts.controller.ts @@ -1,4 +1,16 @@ -import {Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Req} from '@nestjs/common'; +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseGuards, + Req, + Query, + ClassSerializerInterceptor, UseInterceptors +} from '@nestjs/common'; import {PostsService} from './posts.service'; import {CreatePostDto} from './dto/create-post.dto'; import {UpdatePostDto} from './dto/update-post.dto'; @@ -6,6 +18,7 @@ import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"; import RequestWithUser from "../authentication/requestWithUser.interface"; @Controller('posts') +@UseInterceptors(ClassSerializerInterceptor) export class PostsController { constructor(private readonly postsService: PostsService) { } @@ -16,6 +29,15 @@ export class PostsController { return this.postsService.createPost(post, req.user); } + + @Get() + async getPosts(@Query('search') search: string) { + if (search) { + return this.postsService.searchForPosts(search); + } + return this.postsService.getAllPosts(); + } + @Get() findAll() { return this.postsService.findAll(); diff --git a/src/posts/posts.service.ts b/src/posts/posts.service.ts index 59f3da4..f24cca1 100644 --- a/src/posts/posts.service.ts +++ b/src/posts/posts.service.ts @@ -6,12 +6,15 @@ import {InjectRepository} from "@nestjs/typeorm"; import {Repository} from "typeorm"; import Post from './entities/post.entity'; import {PostNotFoundException} from "./exception/postNotFound.exception"; +import PostsSearchService from "./postsSearch.service"; +import {In} from "typeorm"; @Injectable() export class PostsService { constructor( @InjectRepository(Post) private repo: Repository, + private postsSearchService: PostsSearchService ) { } @@ -24,6 +27,19 @@ export class PostsService { return newPost; } + async searchForPosts(text: string) { + const results = await this.postsSearchService.search(text); + const ids = results.flatMap(result => result.hits.hits.map(hit => hit._source.id)); + if (!ids.length) { + return []; + } + return this.repo + .find({ + where: {id: In(ids)} + }); + } + + getAllPosts() { return this.repo.find({relations: ['author']}); @@ -64,11 +80,26 @@ export class PostsService { return `This action returns a #${id} post`; } - update(id: number, updatePostDto: UpdatePostDto) { - return `This action updates a #${id} post`; + async update(id: number, post: UpdatePostDto) { + await this.repo.update(id, post); + const updatedPost = await this.repo.findOne({ + where: {id}, + relations: { + author: true, + } + }); + if (updatedPost) { + await this.postsSearchService.update(updatedPost); + return updatedPost; + } + throw new PostNotFoundException(id); } - remove(id: number) { - return `This action removes a #${id} post`; + async remove(id: number) { + const deleteResponse = await this.repo.delete(id); + if (!deleteResponse.affected) { + throw new PostNotFoundException(id); + } + await this.postsSearchService.remove(id); } } diff --git a/src/posts/postsSearch.service.ts b/src/posts/postsSearch.service.ts index dbc9acd..91f7940 100644 --- a/src/posts/postsSearch.service.ts +++ b/src/posts/postsSearch.service.ts @@ -1,42 +1,81 @@ -import { Injectable } from '@nestjs/common'; -import { ElasticsearchService } from '@nestjs/elasticsearch'; +import {Injectable} from '@nestjs/common'; +import {ElasticsearchService} from '@nestjs/elasticsearch'; import Post from "./entities/post.entity"; import {PostSearchResult} from "./types/postSearchResult.interface"; import {PostSearchBody} from "./types/postSearchBody.interface"; @Injectable() export default class PostsSearchService { - index = 'posts' + index = 'posts'; - constructor( - private readonly elasticsearchService: ElasticsearchService - ) {} + constructor( + private readonly elasticsearchService: ElasticsearchService + ) { + } - async indexPost(post: Post) { - return this.elasticsearchService.index({ - index: this.index, - body: { - id: post.id, - title: post.title, - content: post.content, - authorId: post.author.id - } - }) - } + async indexPost(post: Post) { + return this.elasticsearchService.index({ + index: this.index, + document: { + id: post.id, + title: post.title, + content: post.content, + authorId: post.author.id + } + }); + } + + async search(text: string) { + const result = await this.elasticsearchService.search({ + index: this.index, + query: { + multi_match: { + query: text, + fields: ['title', 'content'], + }, + }, + }); + + const hits = result.hits.hits; + return hits.map((item) => item._source); + } + + + async remove(postId: number) { + await this.elasticsearchService.deleteByQuery({ + index: this.index, + query: { + match: { + id: postId, + } + } + + }) + } + + async update(post: Post) { + const newBody: PostSearchBody = { + id: post.id, + title: post.title, + content: post.content, + authorId: post.author.id + }; + + const script = Object.entries(newBody).reduce((result, [key, value]) => { + return `${result} ctx._source.${key}='${value}';`; + }, ''); + + await this.elasticsearchService.updateByQuery({ + index: this.index, + query: { + match: { + id: post.id, + } + }, + script: { + source: script + } + }); + } - async search(text: string) { - const response = await this.elasticsearchService.search({ - index: this.index, - body: { - query: { - multi_match: { - query: text, - fields: ['title', 'content'] - } - } - } - }) - const hits = body.hits.hits; - return hits.map((item) => item._source); - } } \ No newline at end of file diff --git a/src/posts/types/postSearchResult.interface.ts b/src/posts/types/postSearchResult.interface.ts index 2a720f9..96a9afb 100644 --- a/src/posts/types/postSearchResult.interface.ts +++ b/src/posts/types/postSearchResult.interface.ts @@ -1,3 +1,4 @@ +import {PostSearchBody} from "./postSearchBody.interface"; export interface PostSearchResult { hits: { From 38d1bbf6476709c97ceac0afb4a7366286bb9dd2 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Thu, 22 May 2025 11:48:57 +0700 Subject: [PATCH 3/9] fix --- src/main.ts | 2 ++ src/posts/posts.module.ts | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index cad4dc4..87f28f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,3 +1,5 @@ +import 'reflect-metadata'; + import {HttpAdapterHost, NestFactory, Reflector} from '@nestjs/core'; import {AppModule} from './app.module'; import * as cookieParser from 'cookie-parser'; diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index d4bf308..8f5c6ad 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -4,12 +4,14 @@ import {PostsController} from './posts.controller'; import {TypeOrmModule} from "@nestjs/typeorm"; import User from "../users/entities/user.entity"; import Post from "./entities/post.entity"; +import PostsSearchService from "./postsSearch.service"; +import {SearchModule} from "../search/search.module"; @Module({ controllers: [PostsController], - imports: [TypeOrmModule.forFeature([Post])], - providers: [PostsService], - exports: [PostsService], + imports: [TypeOrmModule.forFeature([Post]), SearchModule], + providers: [PostsService, PostsSearchService], + exports: [PostsService, PostsSearchService], }) export class PostsModule { } From b2a4cd3f4fe908c727a849dc2b12db909f34aec4 Mon Sep 17 00:00:00 2001 From: Renolation Date: Thu, 22 May 2025 22:02:54 +0700 Subject: [PATCH 4/9] add jwt --- src/app.module.ts | 21 ++++++++----- .../authentication.controller.ts | 14 +++++++-- .../jwt-refresh-token.strategy.ts | 30 +++++++++++++++++++ src/authentication/jwtRefreshGuard.guard.ts | 5 ++++ src/users/user.service.ts | 14 ++++++++- 5 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 src/authentication/jwt-refresh-token.strategy.ts create mode 100644 src/authentication/jwtRefreshGuard.guard.ts diff --git a/src/app.module.ts b/src/app.module.ts index 02b112e..bc3a029 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,13 +1,13 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import {Module} from '@nestjs/common'; +import {AppController} from './app.controller'; +import {AppService} from './app.service'; +import {TypeOrmModule} from '@nestjs/typeorm'; +import {ConfigModule, ConfigService} from '@nestjs/config'; import * as Joi from 'joi'; import {UsersModule} from "./users/user.module"; import {AuthenticationModule} from "./authentication/authentication.module"; -import { PostsModule } from './posts/posts.module'; -import { CategoriesModule } from './categories/categories.module'; +import {PostsModule} from './posts/posts.module'; +import {CategoriesModule} from './categories/categories.module'; import {FilesModule} from "./files/file.module"; @Module({ @@ -36,6 +36,10 @@ import {FilesModule} from "./files/file.module"; S3_BUCKET: Joi.string().required(), S3_ACCESS_KEY: Joi.string().required(), S3_ENDPOINT: Joi.string().required(), + JWT_ACCESS_TOKEN_SECRET: Joi.string().required(), + JWT_ACCESS_TOKEN_EXPIRATION_TIME: Joi.string().required(), + JWT_REFRESH_TOKEN_SECRET: Joi.string().required(), + JWT_REFRESH_TOKEN_EXPIRATION_TIME: Joi.string().required(), }) }), UsersModule, @@ -47,4 +51,5 @@ import {FilesModule} from "./files/file.module"; controllers: [AppController], providers: [AppService], }) -export class AppModule {} \ No newline at end of file +export class AppModule { +} \ No newline at end of file diff --git a/src/authentication/authentication.controller.ts b/src/authentication/authentication.controller.ts index 373e955..df36392 100644 --- a/src/authentication/authentication.controller.ts +++ b/src/authentication/authentication.controller.ts @@ -17,12 +17,12 @@ import JwtAuthenticationGuard from './jwt-authentication.guard'; // import {EmailConfirmationService} from '../emailConfirmation/emailConfirmation.service'; import {ApiBody} from '@nestjs/swagger'; import LogInDto from './dto/logIn.dto'; -import { UsersService } from 'src/users/user.service'; +import {UsersService} from 'src/users/user.service'; @Controller('authentication') @UseInterceptors(ClassSerializerInterceptor) @SerializeOptions({ - strategy: 'excludeAll' + strategy: 'excludeAll' }) export class AuthenticationController { constructor( @@ -87,4 +87,14 @@ export class AuthenticationController { return user; } + @UseGuards(JwtRefreshGuard) + @Get('refresh') + refresh(@Req() request: RequestWithUser) { + const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(request.user.id); + + request.res.setHeader('Set-Cookie', accessTokenCookie); + return request.user; + } + + } \ No newline at end of file diff --git a/src/authentication/jwt-refresh-token.strategy.ts b/src/authentication/jwt-refresh-token.strategy.ts new file mode 100644 index 0000000..abf9006 --- /dev/null +++ b/src/authentication/jwt-refresh-token.strategy.ts @@ -0,0 +1,30 @@ +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { PassportStrategy } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Request } from 'express'; +import {UsersService} from "../users/user.service"; + +@Injectable() +export class JwtRefreshTokenStrategy extends PassportStrategy( + Strategy, + 'jwt-refresh-token' +) { + constructor( + private readonly configService: ConfigService, + private readonly userService: UsersService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => { + return request?.cookies?.Refresh; + }]), + secretOrKey: configService.get('JWT_REFRESH_TOKEN_SECRET'), + passReqToCallback: true, + }); + } + + async validate(request: Request, payload: TokenPayload) { + const refreshToken = request.cookies?.Refresh; + return this.userService.getUserIfRefreshTokenMatches(refreshToken, payload.userId); + } +} \ No newline at end of file diff --git a/src/authentication/jwtRefreshGuard.guard.ts b/src/authentication/jwtRefreshGuard.guard.ts new file mode 100644 index 0000000..2b7317e --- /dev/null +++ b/src/authentication/jwtRefreshGuard.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export default class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {} \ No newline at end of file diff --git a/src/users/user.service.ts b/src/users/user.service.ts index 907635b..0328168 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -34,6 +34,19 @@ export class UsersService { throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND); } + async getUserIfRefreshTokenMatches(refreshToken: string, userId: number) { + const user = await this.getById(userId); + + const isRefreshTokenMatching = await bcrypt.compare( + refreshToken, + user.currentHashedRefreshToken + ); + + if (isRefreshTokenMatching) { + return user; + } + } + async addAvatar(userId: number, imageBuffer: Buffer, filename: string) { const avatar = await this.filesService.uploadPublicFile(imageBuffer, filename); const user = await this.getById(userId); @@ -62,5 +75,4 @@ export class UsersService { currentHashedRefreshToken: null, }); } - } From 17ba8b1c7db862e0c251e13d0e6355e9cbed1e44 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Fri, 23 May 2025 10:20:25 +0700 Subject: [PATCH 5/9] transaction --- .../authentication.controller.ts | 1 + src/users/user.service.ts | 38 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/authentication/authentication.controller.ts b/src/authentication/authentication.controller.ts index df36392..e7658db 100644 --- a/src/authentication/authentication.controller.ts +++ b/src/authentication/authentication.controller.ts @@ -18,6 +18,7 @@ import JwtAuthenticationGuard from './jwt-authentication.guard'; import {ApiBody} from '@nestjs/swagger'; import LogInDto from './dto/logIn.dto'; import {UsersService} from 'src/users/user.service'; +import JwtRefreshGuard from "./jwtRefreshGuard.guard"; @Controller('authentication') @UseInterceptors(ClassSerializerInterceptor) diff --git a/src/users/user.service.ts b/src/users/user.service.ts index 0328168..139f105 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -1,6 +1,6 @@ -import {HttpException, HttpStatus, Injectable} from '@nestjs/common'; +import {HttpException, HttpStatus, Injectable, InternalServerErrorException} from '@nestjs/common'; import {InjectRepository} from '@nestjs/typeorm'; -import {Repository} from 'typeorm'; +import {Repository, Connection} from 'typeorm'; import User from './entities/user.entity'; import CreateUserDto from './dto/createUser.dto'; import * as bcrypt from 'bcrypt'; @@ -11,7 +11,8 @@ export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository, - private readonly filesService: FilesService + private readonly filesService: FilesService, + private connection: Connection, ) { } @@ -57,12 +58,38 @@ export class UsersService { return avatar; } + async deleteAvatar(userId: number) { + const queryRunner = this.connection.createQueryRunner(); + + const user = await this.getById(userId); + const fileId = user.avatar?.id; + if (fileId) { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + await queryRunner.manager.update(User, userId, { + ...user, + avatar: null + }); + await this.filesService.deletePublicFileWithQueryRunner(fileId, queryRunner); + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(); + } finally { + await queryRunner.release(); + } + + } + } + async create(userData: CreateUserDto) { const newUser = this.usersRepository.create(userData); await this.usersRepository.save(newUser); return newUser; } - + //region token async setCurrentRefreshToken(refreshToken: string, userId: number) { const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10); await this.usersRepository.update(userId, { @@ -75,4 +102,7 @@ export class UsersService { currentHashedRefreshToken: null, }); } + //endregion + + } From 4c51aad94e05108c19f75d1578a749e41fb7ca89 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Fri, 23 May 2025 11:03:38 +0700 Subject: [PATCH 6/9] search n count, offset --- src/posts/dto/create-post.dto.ts | 8 +++++- src/posts/entities/post.entity.ts | 3 ++ src/posts/posts.controller.ts | 10 +++++-- src/posts/posts.service.ts | 32 +++++++++++++++++++--- src/posts/postsSearch.service.ts | 17 ++++++++++++ src/posts/types/postCountBody.interface.ts | 5 ++++ src/utils/types/paginationParams.ts | 23 ++++++++++++++++ 7 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 src/posts/types/postCountBody.interface.ts create mode 100644 src/utils/types/paginationParams.ts diff --git a/src/posts/dto/create-post.dto.ts b/src/posts/dto/create-post.dto.ts index 1a2b3c5..9af05b1 100644 --- a/src/posts/dto/create-post.dto.ts +++ b/src/posts/dto/create-post.dto.ts @@ -1 +1,7 @@ -export class CreatePostDto {} +import {IsNotEmpty, IsString} from "class-validator"; + +export class CreatePostDto { + @IsString({ each: true }) + @IsNotEmpty() + paragraphs: string[]; +} diff --git a/src/posts/entities/post.entity.ts b/src/posts/entities/post.entity.ts index eaa5bdc..a80187a 100644 --- a/src/posts/entities/post.entity.ts +++ b/src/posts/entities/post.entity.ts @@ -13,6 +13,9 @@ class Post { @Column() public content: string; + @Column('text', { array: true }) + public paragraphs: string[]; + @Column({nullable: true}) public category?: string; diff --git a/src/posts/posts.controller.ts b/src/posts/posts.controller.ts index 72c8a92..dfd0c83 100644 --- a/src/posts/posts.controller.ts +++ b/src/posts/posts.controller.ts @@ -16,6 +16,7 @@ import {CreatePostDto} from './dto/create-post.dto'; import {UpdatePostDto} from './dto/update-post.dto'; import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"; import RequestWithUser from "../authentication/requestWithUser.interface"; +import {PaginationParams} from "../utils/types/paginationParams"; @Controller('posts') @UseInterceptors(ClassSerializerInterceptor) @@ -31,11 +32,14 @@ export class PostsController { @Get() - async getPosts(@Query('search') search: string) { + async getPosts( + @Query('search') search: string, + @Query() { offset, limit }: PaginationParams + ) { if (search) { - return this.postsService.searchForPosts(search); + return this.postsService.searchForPosts(search, offset, limit); } - return this.postsService.getAllPosts(); + return this.postsService.getAllPosts(offset, limit); } @Get() diff --git a/src/posts/posts.service.ts b/src/posts/posts.service.ts index f24cca1..05ffc4c 100644 --- a/src/posts/posts.service.ts +++ b/src/posts/posts.service.ts @@ -3,7 +3,7 @@ import {CreatePostDto} from './dto/create-post.dto'; import {UpdatePostDto} from './dto/update-post.dto'; import User from "../users/entities/user.entity"; import {InjectRepository} from "@nestjs/typeorm"; -import {Repository} from "typeorm"; +import {FindManyOptions, MoreThan, Repository} from "typeorm"; import Post from './entities/post.entity'; import {PostNotFoundException} from "./exception/postNotFound.exception"; import PostsSearchService from "./postsSearch.service"; @@ -27,7 +27,7 @@ export class PostsService { return newPost; } - async searchForPosts(text: string) { + async searchForPosts(text: string, offset?: number, limit?: number) { const results = await this.postsSearchService.search(text); const ids = results.flatMap(result => result.hits.hits.map(hit => hit._source.id)); if (!ids.length) { @@ -39,10 +39,34 @@ export class PostsService { }); } + + + async getAllPosts(offset?: number, limit?: number, startId = 0) { + + const where: FindManyOptions['where'] = {}; + let separateCount = 0; + if (startId) { + where.id = MoreThan(startId); + separateCount = await this.repo.count(); + } - getAllPosts() { - return this.repo.find({relations: ['author']}); + const [items, count] = await this.repo.findAndCount({ + where, + relations: { + author: true + }, + order: { + id: 'ASC' + }, + skip: offset, + take: limit + }); + + return { + items, + count: startId ? separateCount : count + } } async getPostById(id: number) { diff --git a/src/posts/postsSearch.service.ts b/src/posts/postsSearch.service.ts index 91f7940..4d765b8 100644 --- a/src/posts/postsSearch.service.ts +++ b/src/posts/postsSearch.service.ts @@ -3,6 +3,7 @@ import {ElasticsearchService} from '@nestjs/elasticsearch'; import Post from "./entities/post.entity"; import {PostSearchResult} from "./types/postSearchResult.interface"; import {PostSearchBody} from "./types/postSearchBody.interface"; +import PostCountResult from "./types/postCountBody.interface"; @Injectable() export default class PostsSearchService { @@ -25,6 +26,22 @@ export default class PostsSearchService { }); } + async count(query: string, fields: string[]) { + const result = await this.elasticsearchService.count({ + index: this.index, + query: { + multi_match: { + query, + fields, + }, + }, + }); + + return result.count; + } + + + async search(text: string) { const result = await this.elasticsearchService.search({ index: this.index, diff --git a/src/posts/types/postCountBody.interface.ts b/src/posts/types/postCountBody.interface.ts new file mode 100644 index 0000000..19e322f --- /dev/null +++ b/src/posts/types/postCountBody.interface.ts @@ -0,0 +1,5 @@ +interface PostCountResult { + count: number; +} + +export default PostCountResult; \ No newline at end of file diff --git a/src/utils/types/paginationParams.ts b/src/utils/types/paginationParams.ts new file mode 100644 index 0000000..39600b5 --- /dev/null +++ b/src/utils/types/paginationParams.ts @@ -0,0 +1,23 @@ +import { IsNumber, Min, IsOptional } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class PaginationParams { + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + startId?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + offset?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + limit?: number; +} \ No newline at end of file From a6cd9595312c927ab2dbf9e277aaf1b32050ab01 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Fri, 23 May 2025 13:59:21 +0700 Subject: [PATCH 7/9] 22: json --- src/posts/posts.service.ts | 2 +- .../productCategory.entity.ts | 19 +++++++++++++++ src/products/product.entity.ts | 23 +++++++++++++++++++ .../types/bookProperties.interface.ts | 4 ++++ src/products/types/carProperties.interface.ts | 7 ++++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/productCategories/productCategory.entity.ts create mode 100644 src/products/product.entity.ts create mode 100644 src/products/types/bookProperties.interface.ts create mode 100644 src/products/types/carProperties.interface.ts diff --git a/src/posts/posts.service.ts b/src/posts/posts.service.ts index 05ffc4c..4d787b3 100644 --- a/src/posts/posts.service.ts +++ b/src/posts/posts.service.ts @@ -39,7 +39,7 @@ export class PostsService { }); } - + async getAllPosts(offset?: number, limit?: number, startId = 0) { diff --git a/src/productCategories/productCategory.entity.ts b/src/productCategories/productCategory.entity.ts new file mode 100644 index 0000000..1c41f4a --- /dev/null +++ b/src/productCategories/productCategory.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; +import Product from '../products/product.entity'; + +@Entity() +class ProductCategory { + @PrimaryGeneratedColumn() + public id: number; + + @Column() + public name: string; + + @OneToMany( + () => Product, + (product: Product) => product.category, + ) + public products: Product[]; +} + +export default ProductCategory; \ No newline at end of file diff --git a/src/products/product.entity.ts b/src/products/product.entity.ts new file mode 100644 index 0000000..9a6587e --- /dev/null +++ b/src/products/product.entity.ts @@ -0,0 +1,23 @@ +import { Column, Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; +import ProductCategory from '../productCategories/productCategory.entity'; +import { CarProperties } from './types/carProperties.interface'; +import { BookProperties } from './types/bookProperties.interface'; + +@Entity() +class Product { + @PrimaryGeneratedColumn() + public id: number; + + @Column() + public name: string; + + @ManyToOne(() => ProductCategory, (category: ProductCategory) => category.products) + public category: ProductCategory; + + @Column({ + type: 'jsonb' + }) + public properties: CarProperties | BookProperties; +} + +export default Product; \ No newline at end of file diff --git a/src/products/types/bookProperties.interface.ts b/src/products/types/bookProperties.interface.ts new file mode 100644 index 0000000..b3dae4e --- /dev/null +++ b/src/products/types/bookProperties.interface.ts @@ -0,0 +1,4 @@ +export interface BookProperties { + authors: string[]; + publicationYear: string; +} \ No newline at end of file diff --git a/src/products/types/carProperties.interface.ts b/src/products/types/carProperties.interface.ts new file mode 100644 index 0000000..a1c4c28 --- /dev/null +++ b/src/products/types/carProperties.interface.ts @@ -0,0 +1,7 @@ +export interface CarProperties { + brand: string; + engine: { + fuel: string; + numberOfCylinders: number; + } +} \ No newline at end of file From d03e502f2f14faaaa2894b853f3308a34ef5f383 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Fri, 23 May 2025 14:55:22 +0700 Subject: [PATCH 8/9] 23: cache --- package-lock.json | 86 +++++++++++++++++++++++++++-- package.json | 2 + src/posts/httpCache.interceptor.ts | 19 +++++++ src/posts/posts.controller.ts | 6 +- src/posts/posts.module.ts | 12 +++- src/posts/posts.service.ts | 25 ++++++++- src/posts/postsCacheKey.constant.ts | 1 + 7 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 src/posts/httpCache.interceptor.ts create mode 100644 src/posts/postsCacheKey.constant.ts diff --git a/package-lock.json b/package-lock.json index faa3f06..a15641f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@elastic/elasticsearch": "^9.0.2", "@hapi/joi": "^17.1.1", "@neondatabase/serverless": "^1.0.0", + "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", @@ -29,6 +30,7 @@ "@types/uuid": "^10.0.0", "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", + "cache-manager": "^6.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "cookie-parser": "^1.4.7", @@ -2008,6 +2010,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/serialize": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", + "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@keyv/serialize/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -2407,6 +2442,19 @@ "node": ">=19.0.0" } }, + "node_modules/@nestjs/cache-manager": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.1.tgz", + "integrity": "sha512-4UxTnR0fsmKL5YDalU2eLFVnL+OBebWUpX+hEduKGncrVKH4PPNoiRn1kXyOCjmzb0UvWgqubpssNouc8e0MCw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", + "cache-manager": ">=6", + "keyv": ">=5", + "rxjs": "^7.8.1" + } + }, "node_modules/@nestjs/cli": { "version": "11.0.7", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.7.tgz", @@ -5818,6 +5866,15 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-6.4.3.tgz", + "integrity": "sha512-VV5eq/QQ5rIVix7/aICO4JyvSeEv9eIQuKL5iFwgM2BrcYoE0A/D1mNsAHJAsB0WEbNdBlKkn6Tjz6fKzh/cKQ==", + "license": "MIT", + "dependencies": { + "keyv": "^5.3.3" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -5847,6 +5904,16 @@ "node": ">=14.16" } }, + "node_modules/cacheable-request/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -7664,6 +7731,16 @@ "node": ">=16" } }, + "node_modules/flat-cache/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/flatbuffers": { "version": "24.12.23", "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz", @@ -9613,13 +9690,12 @@ } }, "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz", + "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==", "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.0.3" } }, "node_modules/kind-of": { diff --git a/package.json b/package.json index ad190cf..f205ba9 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@elastic/elasticsearch": "^9.0.2", "@hapi/joi": "^17.1.1", "@neondatabase/serverless": "^1.0.0", + "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", @@ -40,6 +41,7 @@ "@types/uuid": "^10.0.0", "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", + "cache-manager": "^6.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "cookie-parser": "^1.4.7", diff --git a/src/posts/httpCache.interceptor.ts b/src/posts/httpCache.interceptor.ts new file mode 100644 index 0000000..4f77122 --- /dev/null +++ b/src/posts/httpCache.interceptor.ts @@ -0,0 +1,19 @@ +import { CACHE_KEY_METADATA, CacheInterceptor, } from '@nestjs/cache-manager'; +import {ExecutionContext, Injectable} from "@nestjs/common"; + +@Injectable() +export class HttpCacheInterceptor extends CacheInterceptor { + trackBy(context: ExecutionContext): string | undefined { + const cacheKey = this.reflector.get( + CACHE_KEY_METADATA, + context.getHandler(), + ); + + if (cacheKey) { + const request = context.switchToHttp().getRequest(); + return `${cacheKey}-${request._parsedUrl.query}`; + } + + return super.trackBy(context); + } +} \ No newline at end of file diff --git a/src/posts/posts.controller.ts b/src/posts/posts.controller.ts index dfd0c83..1e5ce07 100644 --- a/src/posts/posts.controller.ts +++ b/src/posts/posts.controller.ts @@ -17,6 +17,8 @@ import {UpdatePostDto} from './dto/update-post.dto'; import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"; import RequestWithUser from "../authentication/requestWithUser.interface"; import {PaginationParams} from "../utils/types/paginationParams"; +import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; +import {GET_POSTS_CACHE_KEY} from "./postsCacheKey.constant"; @Controller('posts') @UseInterceptors(ClassSerializerInterceptor) @@ -30,7 +32,9 @@ export class PostsController { return this.postsService.createPost(post, req.user); } - + @UseInterceptors(CacheInterceptor) + @CacheKey(GET_POSTS_CACHE_KEY) + @CacheTTL(120) @Get() async getPosts( @Query('search') search: string, diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index 8f5c6ad..c7dfae3 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -6,10 +6,20 @@ import User from "../users/entities/user.entity"; import Post from "./entities/post.entity"; import PostsSearchService from "./postsSearch.service"; import {SearchModule} from "../search/search.module"; +import { CacheModule } from '@nestjs/cache-manager'; @Module({ controllers: [PostsController], - imports: [TypeOrmModule.forFeature([Post]), SearchModule], + imports: [ + CacheModule.register( + { + ttl: 5, + max: 100 + } + ), + TypeOrmModule.forFeature([Post]), + SearchModule + ], providers: [PostsService, PostsSearchService], exports: [PostsService, PostsSearchService], }) diff --git a/src/posts/posts.service.ts b/src/posts/posts.service.ts index 4d787b3..5b1dc39 100644 --- a/src/posts/posts.service.ts +++ b/src/posts/posts.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@nestjs/common'; +import {Inject, Injectable} from '@nestjs/common'; import {CreatePostDto} from './dto/create-post.dto'; import {UpdatePostDto} from './dto/update-post.dto'; import User from "../users/entities/user.entity"; @@ -8,22 +8,39 @@ import Post from './entities/post.entity'; import {PostNotFoundException} from "./exception/postNotFound.exception"; import PostsSearchService from "./postsSearch.service"; import {In} from "typeorm"; +import { Cache } from 'cache-manager'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import {GET_POSTS_CACHE_KEY} from "./postsCacheKey.constant"; @Injectable() export class PostsService { constructor( @InjectRepository(Post) private repo: Repository, - private postsSearchService: PostsSearchService + private postsSearchService: PostsSearchService, + @Inject(CACHE_MANAGER) private cacheManager: Cache ) { } + + async clearCache() { + // const keys: string[] = this.cacheManager.stores.keys(); + // + // await Promise.all( + // keys + // .filter((key) => key.startsWith(GET_POSTS_CACHE_KEY)) + // .map((key) => this.cacheManager.del(key)) // note: 'del', not 'delete' + // ); + } + async createPost(post: CreatePostDto, user: User) { const newPost = this.repo.create({ ...post, author: user }); await this.repo.save(newPost); + await this.postsSearchService.indexPost(newPost); + await this.clearCache(); return newPost; } @@ -91,6 +108,8 @@ export class PostsService { }); if (updatedPost) { + await this.postsSearchService.update(updatedPost); + await this.clearCache(); return updatedPost } throw new PostNotFoundException(id); @@ -114,6 +133,7 @@ export class PostsService { }); if (updatedPost) { await this.postsSearchService.update(updatedPost); + await this.clearCache(); return updatedPost; } throw new PostNotFoundException(id); @@ -125,5 +145,6 @@ export class PostsService { throw new PostNotFoundException(id); } await this.postsSearchService.remove(id); + await this.clearCache(); } } diff --git a/src/posts/postsCacheKey.constant.ts b/src/posts/postsCacheKey.constant.ts new file mode 100644 index 0000000..78140f6 --- /dev/null +++ b/src/posts/postsCacheKey.constant.ts @@ -0,0 +1 @@ +export const GET_POSTS_CACHE_KEY = 'GET_POSTS_CACHE'; \ No newline at end of file From 44cd4f291148879cb08ec3375147e031e4dbafc8 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Fri, 23 May 2025 15:48:33 +0700 Subject: [PATCH 9/9] 24: socket.io chat --- package-lock.json | 375 +++++++++++++++++++++++++++++++++++++- package.json | 5 + src/chat/chat.gateway.ts | 44 +++++ src/chat/chat.service.ts | 47 +++++ src/chat/mesage.entity.ts | 16 ++ 5 files changed, 483 insertions(+), 4 deletions(-) create mode 100644 src/chat/chat.gateway.ts create mode 100644 src/chat/chat.service.ts create mode 100644 src/chat/mesage.entity.ts diff --git a/package-lock.json b/package-lock.json index a15641f..3f73d23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,19 +20,24 @@ "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-socket.io": "^11.1.1", "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", + "@nestjs/websockets": "^11.1.1", "@types/bcrypt": "^5.0.2", + "@types/cookie": "^0.6.0", "@types/cookie-parser": "^1.4.8", "@types/hapi__joi": "^17.1.15", "@types/passport-jwt": "^4.0.1", "@types/passport-local": "^1.0.38", + "@types/socket.io": "^3.0.1", "@types/uuid": "^10.0.0", "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", "cache-manager": "^6.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "joi": "^17.13.3", "multer": "^1.4.5-lts.2", @@ -2832,6 +2837,25 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/platform-socket.io": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.1.tgz", + "integrity": "sha512-Bsc8ouysUFasWiO8RKEvppqYM5LNkHfbyIJQTy3V6+PUdYhblkvmOq8QtjuHpv6DiBI4siUcxACx/90/CdXLkQ==", + "license": "MIT", + "dependencies": { + "socket.io": "4.8.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "rxjs": "^7.1.0" + } + }, "node_modules/@nestjs/schematics": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.5.tgz", @@ -3004,6 +3028,29 @@ "typeorm": "^0.3.0" } }, + "node_modules/@nestjs/websockets": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.1.tgz", + "integrity": "sha512-gxwQoGx5bW5IvparzrX1UOGXz87eqY0fK5Y6yb14z6tSSubQTciNjCDm5osDEkRyRCG6ZB0F+eXF6dRUjwTlBQ==", + "license": "MIT", + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/platform-socket.io": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -3189,6 +3236,12 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@sqltools/formatter": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", @@ -3652,6 +3705,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, "node_modules/@types/cookie-parser": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.8.tgz", @@ -3668,6 +3727,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/cors": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.18.tgz", + "integrity": "sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -3919,6 +3987,15 @@ "@types/send": "*" } }, + "node_modules/@types/socket.io": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.1.tgz", + "integrity": "sha512-XSma2FhVD78ymvoxYV4xGXrIH/0EKQ93rR+YR0Y+Kw1xbPzLDCip/UWSejZ08FpxYeYNci/PZPQS9anrvJRqMA==", + "license": "MIT", + "dependencies": { + "socket.io": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -5624,6 +5701,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", @@ -6438,12 +6524,12 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, "node_modules/cookie-parser": { @@ -6459,6 +6545,15 @@ "node": ">= 0.8.0" } }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-parser/node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -6883,6 +6978,104 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -7375,6 +7568,15 @@ "node": ">= 0.6" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -10408,6 +10610,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -11991,6 +12202,141 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -13973,6 +14319,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index f205ba9..c8e4e97 100644 --- a/package.json +++ b/package.json @@ -31,19 +31,24 @@ "@nestjs/jwt": "^11.0.0", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/platform-socket.io": "^11.1.1", "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", + "@nestjs/websockets": "^11.1.1", "@types/bcrypt": "^5.0.2", + "@types/cookie": "^0.6.0", "@types/cookie-parser": "^1.4.8", "@types/hapi__joi": "^17.1.15", "@types/passport-jwt": "^4.0.1", "@types/passport-local": "^1.0.38", + "@types/socket.io": "^3.0.1", "@types/uuid": "^10.0.0", "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", "cache-manager": "^6.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "cookie": "^1.0.2", "cookie-parser": "^1.4.7", "joi": "^17.13.3", "multer": "^1.4.5-lts.2", diff --git a/src/chat/chat.gateway.ts b/src/chat/chat.gateway.ts new file mode 100644 index 0000000..1c10f8c --- /dev/null +++ b/src/chat/chat.gateway.ts @@ -0,0 +1,44 @@ +import { + ConnectedSocket, + MessageBody, OnGatewayConnection, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import {Server, Socket} from 'socket.io'; +import {ChatService} from "./chat.service"; + +@WebSocketGateway() +export class ChatGateway implements OnGatewayConnection { + @WebSocketServer() + server: Server; + + constructor( + private readonly chatService: ChatService + ) { + } + + async handleConnection(socket: Socket) { + await this.chatService.getUserFromSocket(socket); + } + + @SubscribeMessage('send_message') + async listenForMessages(@MessageBody() content: string, @ConnectedSocket() socket: Socket,) { + const author = await this.chatService.getUserFromSocket(socket); + const message = await this.chatService.saveMessage(content, author); + this.server.sockets.emit('receive_message', message); + return message; + } + + @SubscribeMessage('request_all_messages') + async requestAllMessages( + @ConnectedSocket() socket: Socket, + ) { + await this.chatService.getUserFromSocket(socket); + const messages = await this.chatService.getAllMessages(); + + socket.emit('send_all_messages', messages); + } + + +} \ No newline at end of file diff --git a/src/chat/chat.service.ts b/src/chat/chat.service.ts new file mode 100644 index 0000000..17cd034 --- /dev/null +++ b/src/chat/chat.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { AuthenticationService } from '../authentication/authentication.service'; +import { Socket } from 'socket.io'; +import { parse } from 'cookie'; +import { WsException } from '@nestjs/websockets'; +import UserEntity from "../users/entities/user.entity"; +import {InjectRepository} from "@nestjs/typeorm"; +import Message from "./mesage.entity"; +import {Repository} from "typeorm"; + +@Injectable() +export class ChatService { + constructor( + private readonly authenticationService: AuthenticationService, + @InjectRepository(Message) + private messagesRepository: Repository, + ) { + } + + async saveMessage(content: string, author: UserEntity) { + const newMessage = this.messagesRepository.create({ + content, + author + }); + await this.messagesRepository.save(newMessage); + return newMessage; + } + + async getAllMessages() { + return this.messagesRepository.find({ + relations: { + author: true, + } + }); + } + + async getUserFromSocket(socket: Socket) { + const cookie = socket.handshake.headers.cookie; + const { Authentication: authenticationToken } = parse(cookie); + const user = await this.authenticationService.getUserFromAuthenticationToken(authenticationToken); + if (!user) { + throw new WsException('Invalid credentials.'); + } + return user; + } +} + diff --git a/src/chat/mesage.entity.ts b/src/chat/mesage.entity.ts new file mode 100644 index 0000000..65912c0 --- /dev/null +++ b/src/chat/mesage.entity.ts @@ -0,0 +1,16 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import User from '../users/entities/user.entity'; + +@Entity() +class Message { + @PrimaryGeneratedColumn() + public id: number; + + @Column() + public content: string; + + @ManyToOne(() => User) + public author: User; +} + +export default Message; \ No newline at end of file