diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f51cd5d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,39 @@ +**/.classpath +**/.dockerignore +**/.git +**/.env.example +**/.gitignore +**/.project +**/.editorconfig +**/.eslintrc.js +**/.prettierrc +**/jest.config.json +**/nest-cli.json +**/tsconfig.build.json +**/environment.d.ts +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/.next +**/.cache +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/build +**/dist +**/doc +**/public +**/test +LICENSE +README.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ab561f3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.env.example b/.env.example index 8d594af..fe80dbc 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,9 @@ JWT_SECRET_KEY=secret123123 JWT_SECRET_REFRESH_KEY=secret123123 TOKEN_EXPIRE_TIME=1h TOKEN_REFRESH_EXPIRE_TIME=24h + +POSTGRES_USER=myuser +POSTGRES_PASSWORD=mypassword +POSTGRES_DB=home-library +POSTGRES_PORT=5432 +DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public" diff --git a/.gitignore b/.gitignore index 30faeee..f581e17 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,10 @@ lerna-debug.log* !.vscode/launch.json !.vscode/extensions.json -# .env +# compiled app +dist + .env + +dev.db +dev.db-journal diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ba3206e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +ARG NODE_VERSION=20 + +FROM node:${NODE_VERSION}-alpine as build-stage + +WORKDIR /app + +COPY package.json ./ + +RUN npm i --only=prod && npm i @nestjs/cli + +COPY . . + +RUN npx prisma generate + +FROM node:${NODE_VERSION}-alpine as runtime-stage + +WORKDIR /app + +COPY --from=build-stage /app/src ./src +COPY --from=build-stage /app/types ./types +COPY --from=build-stage /app/.env ./ +COPY --from=build-stage /app/tsconfig.json ./ +COPY --from=build-stage /app/node_modules/@prisma ./node_modules/@prisma +COPY --from=build-stage /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=build-stage /app/prisma ./prisma + +COPY package.json ./ + +RUN npm i && npm i @nestjs/cli \ + && npm cache clean --force \ + && rm -rf \ + ./node_modules/.cache \ + ./node_modules/.npm \ + ./node_modules/.yarn \ + ./node_modules/.pnpm + +EXPOSE ${PORT} + +CMD [ "npm", "run", "start:migrate:dev" ] diff --git a/README.md b/README.md index 50324c6..6139417 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ To run the project locally, you would have to download zip file with the reposit ๐Ÿค” What things do you need to do in order to run our project locally? * โšก Use node 20 LTS -* โœŒ๏ธ Installed [.git](https://git-scm.com/) on your computer. +* โœŒ๏ธ Installed [.git](https://git-scm.com/) on your computer. * ๐Ÿ“ Code Editor of your choice. +* ๐Ÿณ Docker. -## ๐Ÿ”ฎ Installation And Preparation +## ๐Ÿ”ฎ Installation And Preparation First make sure you have all the things listed in the previous section. Then clone our repository to your computer: ๐Ÿ‘Œ @@ -41,11 +42,17 @@ JWT_SECRET_KEY=secret123123 JWT_SECRET_REFRESH_KEY=secret123123 TOKEN_EXPIRE_TIME=1h TOKEN_REFRESH_EXPIRE_TIME=24h + +POSTGRES_USER=myuser +POSTGRES_PASSWORD=mypassword +POSTGRES_DB=home-library +POSTGRES_PORT=5432 +DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public" ``` ๐Ÿคฉ Finally run a development server: ``` -npm run start:dev +docker compose up ``` Aaaaand you're done! ๐ŸŽ‰๐Ÿฅณ @@ -100,7 +107,7 @@ Type check the App with `TypeScript`: npm run type-check ``` -## ๐Ÿงช Testing +## ๐Ÿงช Testing After application running open new terminal and enter: @@ -128,32 +135,47 @@ To run only specific test suite with authorization npm run test:auth -- ``` +## ๐Ÿณ Docker +Run application +``` +docker compose up +``` +Run application in watch mode +``` +docker compose watch +``` +Scan docker images for vulnerabilities +``` +npm run docker:scan +``` + # โš™๏ธ Technology Stack ## ๐Ÿฆˆ Developing -* ๐Ÿฆ… **Nest.js** - The Backend Framework +* ๐Ÿฆ… **Nest.js** - The Backend Framework * ๐Ÿ’– **TypeScript** - The Language -* ๐Ÿฆ„ **Prisma** - The ORM -* ๐Ÿ”’ **bcrypt** - The Password Hasher -* ๐ŸŽซ **jsonwebtoken** - The JWT Token Generator -* ๐Ÿ“– **Nest.js/Swagger** - The OpenAPI Documentation +* ๐Ÿฆ„ **Prisma** - The ORM +* ๐Ÿ”’ **bcrypt** - The Password Hasher +* ๐ŸŽซ **jsonwebtoken** - The JWT Token Generator +* ๐Ÿ“– **Nest.js/Swagger** - The OpenAPI Documentation +* ๐Ÿณ **Docker** - The Containerization tool ## ๐Ÿงน Code Quality -* ๐Ÿงช **Jest** - The Test Runner -* ๐Ÿซ‚ **Supertest** - The Testing Framework -* ๐Ÿ”” **ESLint** โ€” Air-bnb base - The Linter -* ๐Ÿ‘ **Prettier** - The Code Formatter -* ๐Ÿ˜Ž **EditorConfig** - The Code Style Enforcer +* ๐Ÿงช **Jest** - The Test Runner +* ๐Ÿซ‚ **Supertest** - The Testing Framework +* ๐Ÿ”” **ESLint** โ€” Air-bnb base - The Linter +* ๐Ÿ‘ **Prettier** - The Code Formatter +* ๐Ÿ˜Ž **EditorConfig** - The Code Style Enforcer -## ๐Ÿ“š External Libraries -* โœŒ๏ธ **dotenv** - The Environment Variables Library -* ๐ŸŒ **cross-env** - The Environment Variables Loader +## ๐Ÿ“š External Libraries +* โœŒ๏ธ **dotenv** - The Environment Variables Library +* ๐ŸŒ **cross-env** - The Environment Variables Loader # ๐Ÿ“ Working with the API ๐Ÿ™ Following the link below, you can find ```Postman``` collection that will make your life easier while working with the API! [postman collection](https://www.postman.com/bold-spaceship-739379/workspace/node-js-service/overview) -![img.png](./public/img.png) +![img.png](public/img.png) ## ๐ŸŒŠ API endpoints The API has the following endpoints: diff --git a/db/albumDB.ts b/db/albumDB.ts deleted file mode 100644 index 6a6948e..0000000 --- a/db/albumDB.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { DB } from './types/interfaces'; -import { CreateAlbumDto } from '../src/album/dto/create-album.dto'; -import { UpdateAlbumDto } from '../src/album/dto/update-album.dto'; -import { Album } from '../src/album/entities/album.entity'; -import { DBTable } from '../types/types'; - -class AlbumDB implements DB { - #table: DBTable = {}; - - findById(id: string): Album | undefined { - return this.#table[id]; - } - - findByArtistId(id: string): Album | undefined { - return Object.values(this.#table).find((album) => album?.artistId === id); - } - - deleteArtist(id: string) { - const album = this.findByArtistId(id); - if (album) album.artistId = null; - } - - findMany(): Album[] { - return Object.values(this.#table); - } - - create({ name, artistId, year }: CreateAlbumDto): Album | undefined { - const artist = new Album(name, year, artistId); - this.#table[artist.id] = artist; - return artist; - } - - delete(id: string): Album | undefined { - const toDeleteAlbum = this.#table[id]; - - if (!toDeleteAlbum) return undefined; - - this.#table = Object.fromEntries( - Object.entries(this.#table).filter( - ([, artist]) => artist !== toDeleteAlbum, - ), - ); - - return toDeleteAlbum; - } - - update(id: string, dto: UpdateAlbumDto): Album | undefined { - const artist = this.#table[id]; - - if (!artist) return undefined; - - const newArtist = { ...artist, ...dto }; - this.#table[id] = newArtist; - return newArtist; - } -} - -export default new AlbumDB(); diff --git a/db/artistDB.ts b/db/artistDB.ts deleted file mode 100644 index 52c8f4e..0000000 --- a/db/artistDB.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { DB } from './types/interfaces'; -import { CreateArtistDto } from '../src/artist/dto/create-artist.dto'; -import { UpdateArtistDto } from '../src/artist/dto/update-artist.dto'; -import { Artist } from '../src/artist/entities/artist.entity'; -import { DBTable } from '../types/types'; - -class ArtistDB implements DB { - #table: DBTable = {}; - - findById(id: string): Artist | undefined { - return this.#table[id]; - } - - findMany(): Artist[] { - return Object.values(this.#table); - } - - create({ name, grammy }: CreateArtistDto): Artist | undefined { - const artist = new Artist(name, grammy); - this.#table[artist.id] = artist; - return artist; - } - - delete(id: string): Artist | undefined { - const toDeleteArtist = this.#table[id]; - - if (!toDeleteArtist) return undefined; - - this.#table = Object.fromEntries( - Object.entries(this.#table).filter( - ([, artist]) => artist !== toDeleteArtist, - ), - ); - - return toDeleteArtist; - } - - update(id: string, dto: UpdateArtistDto): Artist | undefined { - const artist = this.#table[id]; - - if (!artist) return undefined; - - const newArtist = { ...artist, ...dto }; - this.#table[id] = newArtist; - return newArtist; - } -} - -export default new ArtistDB(); diff --git a/db/db.ts b/db/db.ts deleted file mode 100644 index b41ce7d..0000000 --- a/db/db.ts +++ /dev/null @@ -1,17 +0,0 @@ -import AlbumDB from './albumDB'; -import ArtistDB from './artistDB'; -import FavoriteDB from './favorite/favoriteDB'; -import TrackDB from './trackDB'; -import UserDB from './userDB'; - -export class DB { - public user = UserDB; - - public track = TrackDB; - - public artist = ArtistDB; - - public album = AlbumDB; - - public favorite = FavoriteDB; -} diff --git a/db/favorite/favoriteAlbumDB.ts b/db/favorite/favoriteAlbumDB.ts deleted file mode 100644 index 1bb7a8a..0000000 --- a/db/favorite/favoriteAlbumDB.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Album } from '../../src/album/entities/album.entity'; -import { DBTable } from '../../types/types'; -import AlbumDB from '../albumDB'; -import { FavoriteDB } from '../types/interfaces'; - -class FavoriteAlbumDB implements FavoriteDB { - #table: DBTable = {}; - - findMany(): Album[] { - return Object.values(this.#table); - } - - create(id: string): Album | undefined { - const album = AlbumDB.findById(id); - - if (!album) return undefined; - - this.#table[id] = album; - return album; - } - - delete(id: string): Album | undefined { - const toDeleteAlbum = this.#table[id]; - - if (!toDeleteAlbum) return undefined; - - this.#table = Object.fromEntries( - Object.entries(this.#table).filter( - ([, album]) => album !== toDeleteAlbum, - ), - ); - - return toDeleteAlbum; - } -} - -export default new FavoriteAlbumDB(); diff --git a/db/favorite/favoriteArtistDB.ts b/db/favorite/favoriteArtistDB.ts deleted file mode 100644 index bb167e8..0000000 --- a/db/favorite/favoriteArtistDB.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Artist } from '../../src/artist/entities/artist.entity'; -import { DBTable } from '../../types/types'; -import ArtistDB from '../artistDB'; -import { FavoriteDB } from '../types/interfaces'; - -class FavoriteArtistDB implements FavoriteDB { - #table: DBTable = {}; - - findMany(): Artist[] { - return Object.values(this.#table); - } - - create(id: string): Artist | undefined { - const artist = ArtistDB.findById(id); - - if (!artist) return undefined; - - this.#table[id] = artist; - return artist; - } - - delete(id: string): Artist | undefined { - const toDeleteArtist = this.#table[id]; - - if (!toDeleteArtist) return undefined; - - this.#table = Object.fromEntries( - Object.entries(this.#table).filter( - ([, artist]) => artist !== toDeleteArtist, - ), - ); - - return toDeleteArtist; - } -} - -export default new FavoriteArtistDB(); diff --git a/db/favorite/favoriteDB.ts b/db/favorite/favoriteDB.ts deleted file mode 100644 index b0edb2c..0000000 --- a/db/favorite/favoriteDB.ts +++ /dev/null @@ -1,13 +0,0 @@ -import FavoriteAlbumDB from './favoriteAlbumDB'; -import FavoriteArtistDB from './favoriteArtistDB'; -import FavoriteTrackDB from './favoriteTrackDB'; - -class FavoriteDB { - public track = FavoriteTrackDB; - - public album = FavoriteAlbumDB; - - public artist = FavoriteArtistDB; -} - -export default new FavoriteDB(); diff --git a/db/favorite/favoriteTrackDB.ts b/db/favorite/favoriteTrackDB.ts deleted file mode 100644 index 3278a2c..0000000 --- a/db/favorite/favoriteTrackDB.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Track } from '../../src/track/entities/track.entity'; -import { DBTable } from '../../types/types'; -import TrackDB from '../trackDB'; -import { FavoriteDB } from '../types/interfaces'; - -class FavoriteTrackDB implements FavoriteDB { - #table: DBTable = {}; - - findMany(): Track[] { - return Object.values(this.#table); - } - - create(id: string): Track | undefined { - const track = TrackDB.findById(id); - - if (!track) return undefined; - - this.#table[id] = track; - return track; - } - - delete(id: string): Track | undefined { - const toDeleteTrack = this.#table[id]; - - if (!toDeleteTrack) return undefined; - - this.#table = Object.fromEntries( - Object.entries(this.#table).filter( - ([, track]) => track !== toDeleteTrack, - ), - ); - - return toDeleteTrack; - } -} - -export default new FavoriteTrackDB(); diff --git a/db/trackDB.ts b/db/trackDB.ts deleted file mode 100644 index 13896d6..0000000 --- a/db/trackDB.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { DB } from './types/interfaces'; -import { CreateTrackDto } from '../src/track/dto/create-track.dto'; -import { UpdateTrackDto } from '../src/track/dto/update-track.dto'; -import { Track } from '../src/track/entities/track.entity'; -import { DBTable } from '../types/types'; - -class TrackDB implements DB { - #table: DBTable = {}; - - findByArtistId(id: string): Track | undefined { - return Object.values(this.#table).find((track) => track?.artistId === id); - } - - findByAlbumById(id: string): Track | undefined { - return Object.values(this.#table).find((track) => track?.albumId === id); - } - - deleteArtist(id: string) { - const track = this.findByArtistId(id); - if (track) track.artistId = null; - } - - deleteAlbum(id: string) { - const track = this.findByAlbumById(id); - if (track) track.albumId = null; - } - - findById(id: string): Track | undefined { - return this.#table[id]; - } - - findMany(): Track[] { - return Object.values(this.#table); - } - - create({ name, duration, artistId, albumId }: CreateTrackDto): Track { - const track = new Track(name, duration, artistId, albumId); - this.#table[track.id] = track; - return track; - } - - delete(id: string): Track | undefined { - const toDeleteTrack = this.#table[id]; - - if (!toDeleteTrack) return undefined; - - this.#table = Object.fromEntries( - Object.entries(this.#table).filter( - ([, track]) => track !== toDeleteTrack, - ), - ); - - return toDeleteTrack; - } - - update(id: string, dto: UpdateTrackDto): Track | undefined { - const track = this.#table[id]; - - if (!track) return undefined; - - const newTrack = { ...track, ...dto }; - this.#table[id] = newTrack; - return newTrack; - } -} - -export default new TrackDB(); diff --git a/db/types/interfaces.ts b/db/types/interfaces.ts deleted file mode 100644 index 46e35c9..0000000 --- a/db/types/interfaces.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface DB { - findById(id: string): TEntity | undefined; - findMany(): TEntity[]; - create(dto: unknown): TEntity | undefined; - delete(id: string): TEntity | undefined; - update(id: string, dto: unknown): TEntity | undefined; -} - -export interface FavoriteDB - extends Pick, 'findMany' | 'create' | 'delete'> {} diff --git a/db/userDB.ts b/db/userDB.ts deleted file mode 100644 index 873bda7..0000000 --- a/db/userDB.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DB } from './types/interfaces'; -import { CreateUserDto } from '../src/user/dto/create-user.dto'; -import { UpdateUserDto } from '../src/user/dto/update-user.dto'; -import { User } from '../src/user/entities/user.entity'; -import { DBTable } from '../types/types'; - -class UserDB implements DB { - #table: DBTable = {}; - - findById(id: string): User | undefined { - return this.#table[id]; - } - - findMany(): User[] { - return Object.values(this.#table); - } - - create({ login, password }: CreateUserDto): User { - const user = new User(login, password); - this.#table[user.id] = user; - return user; - } - - delete(id: string): User | undefined { - const toDeleteUser = this.#table[id]; - - if (!toDeleteUser) return undefined; - - this.#table = Object.fromEntries( - Object.entries(this.#table).filter(([, user]) => user !== toDeleteUser), - ); - - return toDeleteUser; - } - - update(id: string, dto: UpdateUserDto): User | undefined { - const user = this.#table[id]; - user?.update(dto); - return user; - } -} - -export default new UserDB(); diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b9dd067 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,50 @@ +version: '3.8' + +services: + server: + build: + context: . + ports: + - ${PORT}:${PORT} + depends_on: + db: + condition: service_healthy + networks: + - app-network + develop: + watch: + - path: ./package.json + action: rebuild + - path: ./package-lock.json + action: rebuild + - path: . + target: /app + action: sync + db: + image: postgres:16.2-alpine + restart: always + user: postgres + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + volumes: + - db-data:/var/lib/postgresql/data + - logs:/home/logs + ports: + - ${POSTGRES_PORT}:${POSTGRES_PORT} + healthcheck: + test: [ "CMD", "pg_isready", "-U", "${POSTGRES_USER}", "-d", "${POSTGRES_DB}" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - app-network + +volumes: + db-data: + logs: + +networks: + app-network: + driver: bridge diff --git a/environment.d.ts b/environment.d.ts index 34af0e9..c802d9b 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -6,5 +6,10 @@ namespace NodeJS { JWT_SECRET_REFRESH_KEY: string; TOKEN_EXPIRE_TIME: string; TOKEN_REFRESH_EXPIRE_TIME: string; + POSTGRES_USER:string; + POSTGRES_PASSWORD:string; + POSTGRES_DB:string; + POSTGRES_PORT:string; + DATABASE_URL:string; } } diff --git a/package-lock.json b/package-lock.json index f373f00..68ea5dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,13 @@ "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^10.3.3", + "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.3.3", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "^2.0.5", "@nestjs/platform-express": "^10.3.3", "@nestjs/swagger": "^7.3.0", + "@prisma/client": "^5.11.0", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -53,6 +55,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "jest": "^29.7.0", "prettier": "^3.2.5", + "prisma": "^5.11.0", "source-map-support": "^0.5.21", "supertest": "^6.2.4", "ts-jest": "^29.1.2", @@ -1997,6 +2000,32 @@ } } }, + "node_modules/@nestjs/config": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.0.tgz", + "integrity": "sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==", + "dependencies": { + "dotenv": "16.4.1", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.1" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/@nestjs/core": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.3.tgz", @@ -2247,6 +2276,68 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@prisma/client": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.11.0.tgz", + "integrity": "sha512-SWshvS5FDXvgJKM/a0y9nDC1rqd7KG0Q6ZVzd+U7ZXK5soe73DJxJJgbNBt2GNXOa+ysWB4suTpdK5zfFPhwiw==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.11.0.tgz", + "integrity": "sha512-N6yYr3AbQqaiUg+OgjkdPp3KPW1vMTAgtKX6+BiB/qB2i1TjLYCrweKcUjzOoRM5BriA4idrkTej9A9QqTfl3A==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.11.0.tgz", + "integrity": "sha512-gbrpQoBTYWXDRqD+iTYMirDlF9MMlQdxskQXbhARhG6A/uFQjB7DZMYocMQLoiZXO/IskfDOZpPoZE8TBQKtEw==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.11.0", + "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", + "@prisma/fetch-engine": "5.11.0", + "@prisma/get-platform": "5.11.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102.tgz", + "integrity": "sha512-WXCuyoymvrS4zLz4wQagSsc3/nE6CHy8znyiMv8RKazKymOMd5o9FP5RGwGHAtgoxd+aB/BWqxuP/Ckfu7/3MA==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.11.0.tgz", + "integrity": "sha512-994viazmHTJ1ymzvWugXod7dZ42T2ROeFuH6zHPcUfp/69+6cl5r9u3NFb6bW8lLdNjwLYEVPeu3hWzxpZeC0w==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.11.0", + "@prisma/engines-version": "5.11.0-15.efd2449663b3d73d637ea1fd226bafbcf45b3102", + "@prisma/get-platform": "5.11.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.11.0.tgz", + "integrity": "sha512-rxtHpMLxNTHxqWuGOLzR2QOyQi79rK1u1XYAVLZxDGTLz/A+uoDnjz9veBFlicrpWjwuieM4N6jcnjj/DDoidw==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.11.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4532,6 +4623,14 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "engines": { + "node": ">=12" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -9067,6 +9166,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prisma": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.11.0.tgz", + "integrity": "sha512-KCLiug2cs0Je7kGkQBN9jDWoZ90ogE/kvZTUTgz2h94FEo8pczCkPH7fPNXkD1sGU7Yh65risGGD1HQ5DF3r3g==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.11.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index c8039a0..2a72cf4 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node dist/src/main.js", "lint": "eslint \"{src,apps,libs,test,types,db}/**/*.ts\" --fix", "type-check": "tsc --noEmit", "test": "jest --testPathIgnorePatterns refresh.e2e.spec.ts --noStackTrace --runInBand", @@ -21,18 +21,24 @@ "test:refresh": "cross-env TEST_MODE=auth jest --testPathPattern refresh.e2e.spec.ts --noStackTrace --runInBand", "test:watch": "jest --watch", "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand" + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "docker:scan": "docker login && docker scout cves nodejs2024q1-service-server && docker scout cves postgres:16.2-alpine", + "prisma:migrate:reset": "npx prisma migrate reset --force", + "prisma:migrate": "npx prisma migrate dev --name init", + "start:migrate:dev": "npm run prisma:migrate:reset && npm run prisma:migrate && npm run start:dev" }, "engines": { "node": "20.0.0" }, "dependencies": { "@nestjs/common": "^10.3.3", + "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.3.3", "@nestjs/jwt": "^10.2.0", "@nestjs/mapped-types": "^2.0.5", "@nestjs/platform-express": "^10.3.3", "@nestjs/swagger": "^7.3.0", + "@prisma/client": "^5.11.0", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", @@ -71,6 +77,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "jest": "^29.7.0", "prettier": "^3.2.5", + "prisma": "^5.11.0", "source-map-support": "^0.5.21", "supertest": "^6.2.4", "ts-jest": "^29.1.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..c1b7e43 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,57 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + login String + password String + version Int @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Artist { + id String @id @default(uuid()) + name String + grammy Boolean + tracks Track[] + album Album[] + favorites Favorites? @relation(fields: [favoritesId], references: [id]) + favoritesId String? +} + +model Track { + id String @id @default(uuid()) + name String + artist Artist? @relation(fields: [artistId], references: [id], onDelete: SetNull) + artistId String? + album Album? @relation(fields: [albumId], references: [id], onDelete: SetNull) + albumId String? + duration Int + favorites Favorites? @relation(fields: [favoritesId], references: [id]) + favoritesId String? +} + +model Album { + id String @id @default(uuid()) + name String + year Int + artist Artist? @relation(fields: [artistId], references: [id], onDelete: SetNull) + artistId String? + track Track[] + favorites Favorites? @relation(fields: [favoritesId], references: [id]) + favoritesId String? +} + +model Favorites { + id String @id @default("favs") + artists Artist[] + albums Album[] + tracks Track[] +} diff --git a/src/album/album.controller.ts b/src/album/album.controller.ts index 2855c27..b5dd0fd 100644 --- a/src/album/album.controller.ts +++ b/src/album/album.controller.ts @@ -25,40 +25,38 @@ export class AlbumController { @UsePipes(new ValidationPipe()) @Post() - create(@Body() createAlbumDto: CreateAlbumDto) { + async create(@Body() createAlbumDto: CreateAlbumDto) { return this.albumService.create(createAlbumDto); } @Get() - findAll() { + async findAll() { return this.albumService.findAll(); } @Get(':id') - findOne(@Param('id', ParseUUIDPipe) id: string) { - const album = this.albumService.findOne(id); + async findOne(@Param('id', ParseUUIDPipe) id: string) { + const album = await this.albumService.findOne(id); if (!album) throw new NotFoundException(errorMessage.ALBUM_NOT_FOUND); return album; } @UsePipes(new ValidationPipe()) @Put(':id') - update( + async update( @Param('id', ParseUUIDPipe) id: string, @Body() updateAlbumDto: UpdateAlbumDto, ) { - const updatedAlbum = this.albumService.update(id, updateAlbumDto); - + const updatedAlbum = await this.albumService.update(id, updateAlbumDto); if (!updatedAlbum) throw new NotFoundException(errorMessage.ALBUM_NOT_FOUND); - return updatedAlbum; } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - remove(@Param('id', ParseUUIDPipe) id: string) { - const album = this.albumService.remove(id); + async remove(@Param('id', ParseUUIDPipe) id: string) { + const album = await this.albumService.remove(id); if (!album) throw new NotFoundException(errorMessage.ARTIST_NOT_FOUND); return album; } diff --git a/src/album/album.module.ts b/src/album/album.module.ts index dce2e18..a90b997 100644 --- a/src/album/album.module.ts +++ b/src/album/album.module.ts @@ -2,10 +2,10 @@ import { Module } from '@nestjs/common'; import { AlbumController } from './album.controller'; import { AlbumService } from './album.service'; -import { DatabaseModule } from '../database/database.module'; +import { PrismaModule } from '../prisma/prisma.module'; @Module({ - imports: [DatabaseModule], + imports: [PrismaModule], controllers: [AlbumController], providers: [AlbumService], }) diff --git a/src/album/album.service.ts b/src/album/album.service.ts index c1a963b..ceaa711 100644 --- a/src/album/album.service.ts +++ b/src/album/album.service.ts @@ -2,31 +2,40 @@ import { Injectable } from '@nestjs/common'; import { CreateAlbumDto } from './dto/create-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto'; -import { DatabaseService } from '../database/database.service'; +import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class AlbumService { - constructor(private readonly databaseService: DatabaseService) {} + constructor(private readonly prismaService: PrismaService) {} - create(createAlbumDto: CreateAlbumDto) { - return this.databaseService.album.create(createAlbumDto); + async create(createAlbumDto: CreateAlbumDto) { + return this.prismaService.album.create({ data: createAlbumDto }); } - findAll() { - return this.databaseService.album.findMany(); + async findAll() { + return this.prismaService.album.findMany(); } - findOne(id: string) { - return this.databaseService.album.findById(id); + async findOne(id: string) { + return this.prismaService.album.findUnique({ where: { id } }); } - update(id: string, updateAlbumDto: UpdateAlbumDto) { - return this.databaseService.album.update(id, updateAlbumDto); + async update(id: string, updateAlbumDto: UpdateAlbumDto) { + try { + return await this.prismaService.album.update({ + where: { id }, + data: updateAlbumDto, + }); + } catch (e) { + return null; + } } - remove(id: string) { - this.databaseService.track.deleteAlbum(id); - this.databaseService.favorite.album.delete(id); - return this.databaseService.album.delete(id); + async remove(id: string) { + try { + return await this.prismaService.album.delete({ where: { id } }); + } catch (e) { + return null; + } } } diff --git a/src/app.module.ts b/src/app.module.ts index 4d4fce5..1a80393 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,22 +1,24 @@ import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; import { AlbumModule } from './album/album.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ArtistModule } from './artist/artist.module'; -import { DatabaseModule } from './database/database.module'; import { FavoriteModule } from './favorite/favorite.module'; +import { PrismaModule } from './prisma/prisma.module'; import { TrackModule } from './track/track.module'; import { UserModule } from './user/user.module'; @Module({ imports: [ UserModule, - DatabaseModule, TrackModule, ArtistModule, AlbumModule, FavoriteModule, + ConfigModule.forRoot(), + PrismaModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/artist/artist.controller.ts b/src/artist/artist.controller.ts index 2c9b905..8b67c35 100644 --- a/src/artist/artist.controller.ts +++ b/src/artist/artist.controller.ts @@ -25,40 +25,38 @@ export class ArtistController { @UsePipes(new ValidationPipe()) @Post() - create(@Body() createArtistDto: CreateArtistDto) { + async create(@Body() createArtistDto: CreateArtistDto) { return this.artistService.create(createArtistDto); } @Get() - findAll() { + async findAll() { return this.artistService.findAll(); } @Get(':id') - findOne(@Param('id', ParseUUIDPipe) id: string) { - const artist = this.artistService.findOne(id); + async findOne(@Param('id', ParseUUIDPipe) id: string) { + const artist = await this.artistService.findOne(id); if (!artist) throw new NotFoundException(errorMessage.ARTIST_NOT_FOUND); return artist; } @UsePipes(new ValidationPipe()) @Put(':id') - update( + async update( @Param('id', ParseUUIDPipe) id: string, @Body() updateArtistDto: UpdateArtistDto, ) { - const updatedArtist = this.artistService.update(id, updateArtistDto); - + const updatedArtist = await this.artistService.update(id, updateArtistDto); if (!updatedArtist) throw new NotFoundException(errorMessage.ARTIST_NOT_FOUND); - return updatedArtist; } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - remove(@Param('id', ParseUUIDPipe) id: string) { - const artist = this.artistService.remove(id); + async remove(@Param('id', ParseUUIDPipe) id: string) { + const artist = await this.artistService.remove(id); if (!artist) throw new NotFoundException(errorMessage.ARTIST_NOT_FOUND); return artist; } diff --git a/src/artist/artist.module.ts b/src/artist/artist.module.ts index 5b521b0..de47f48 100644 --- a/src/artist/artist.module.ts +++ b/src/artist/artist.module.ts @@ -2,10 +2,10 @@ import { Module } from '@nestjs/common'; import { ArtistController } from './artist.controller'; import { ArtistService } from './artist.service'; -import { DatabaseModule } from '../database/database.module'; +import { PrismaModule } from '../prisma/prisma.module'; @Module({ - imports: [DatabaseModule], + imports: [PrismaModule], controllers: [ArtistController], providers: [ArtistService], }) diff --git a/src/artist/artist.service.ts b/src/artist/artist.service.ts index b4c4cac..642b626 100644 --- a/src/artist/artist.service.ts +++ b/src/artist/artist.service.ts @@ -2,32 +2,40 @@ import { Injectable } from '@nestjs/common'; import { CreateArtistDto } from './dto/create-artist.dto'; import { UpdateArtistDto } from './dto/update-artist.dto'; -import { DatabaseService } from '../database/database.service'; +import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class ArtistService { - constructor(private readonly databaseService: DatabaseService) {} + constructor(private readonly prismaService: PrismaService) {} - create(createArtistDto: CreateArtistDto) { - return this.databaseService.artist.create(createArtistDto); + async create(createArtistDto: CreateArtistDto) { + return this.prismaService.artist.create({ data: createArtistDto }); } - findAll() { - return this.databaseService.artist.findMany(); + async findAll() { + return this.prismaService.artist.findMany(); } - findOne(id: string) { - return this.databaseService.artist.findById(id); + async findOne(id: string) { + return this.prismaService.artist.findUnique({ where: { id } }); } - update(id: string, updateArtistDto: UpdateArtistDto) { - return this.databaseService.artist.update(id, updateArtistDto); + async update(id: string, updateArtistDto: UpdateArtistDto) { + try { + return await this.prismaService.artist.update({ + where: { id }, + data: updateArtistDto, + }); + } catch (e) { + return null; + } } - remove(id: string) { - this.databaseService.track.deleteArtist(id); - this.databaseService.album.deleteArtist(id); - this.databaseService.favorite.artist.delete(id); - return this.databaseService.artist.delete(id); + async remove(id: string) { + try { + return await this.prismaService.artist.delete({ where: { id } }); + } catch (e) { + return null; + } } } diff --git a/src/database/database.module.ts b/src/database/database.module.ts deleted file mode 100644 index 53280ec..0000000 --- a/src/database/database.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { DatabaseService } from './database.service'; - -@Module({ - providers: [DatabaseService], - exports: [DatabaseService], -}) -export class DatabaseModule {} diff --git a/src/database/database.service.ts b/src/database/database.service.ts deleted file mode 100644 index 373bd47..0000000 --- a/src/database/database.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { DB } from '../../db/db'; - -// TODO: change to use Prisma client -@Injectable() -export class DatabaseService extends DB {} diff --git a/src/favorite/favorite.controller.ts b/src/favorite/favorite.controller.ts index 46cc52c..651171c 100644 --- a/src/favorite/favorite.controller.ts +++ b/src/favorite/favorite.controller.ts @@ -21,8 +21,8 @@ export class FavoriteController { @UsePipes(new ValidationPipe()) @Post('track/:id') - createTrack(@Param('id', ParseUUIDPipe) id: string) { - const track = this.favoriteService.createTrack(id); + async createTrack(@Param('id', ParseUUIDPipe) id: string) { + const track = await this.favoriteService.createTrack(id); if (!track) throw new UnprocessableEntityException(errorMessage.TRACK_NOT_FOUND); return track; @@ -30,8 +30,8 @@ export class FavoriteController { @UsePipes(new ValidationPipe()) @Post('album/:id') - createAlbum(@Param('id', ParseUUIDPipe) id: string) { - const album = this.favoriteService.createAlbum(id); + async createAlbum(@Param('id', ParseUUIDPipe) id: string) { + const album = await this.favoriteService.createAlbum(id); if (!album) throw new UnprocessableEntityException(errorMessage.ALBUM_NOT_FOUND); return album; @@ -39,25 +39,22 @@ export class FavoriteController { @UsePipes(new ValidationPipe()) @Post('artist/:id') - createArtist(@Param('id', ParseUUIDPipe) id: string) { - const artist = this.favoriteService.createArtist(id); + async createArtist(@Param('id', ParseUUIDPipe) id: string) { + const artist = await this.favoriteService.createArtist(id); if (!artist) throw new UnprocessableEntityException(errorMessage.ARTIST_NOT_FOUND); return artist; } @Get() - findAll() { - const tracks = this.favoriteService.findAllTracks(); - const albums = this.favoriteService.findAllAlbums(); - const artists = this.favoriteService.findAllArtists(); - return { tracks, albums, artists }; + async findAll() { + return this.favoriteService.findAll(); } @Delete('track/:id') @HttpCode(HttpStatus.NO_CONTENT) - removeTrack(@Param('id', ParseUUIDPipe) id: string) { - const track = this.favoriteService.removeTrack(id); + async removeTrack(@Param('id', ParseUUIDPipe) id: string) { + const track = await this.favoriteService.removeTrack(id); if (!track) throw new UnprocessableEntityException(errorMessage.TRACK_NOT_FOUND); return track; @@ -65,8 +62,8 @@ export class FavoriteController { @Delete('album/:id') @HttpCode(HttpStatus.NO_CONTENT) - removeAlbum(@Param('id', ParseUUIDPipe) id: string) { - const album = this.favoriteService.removeAlbum(id); + async removeAlbum(@Param('id', ParseUUIDPipe) id: string) { + const album = await this.favoriteService.removeAlbum(id); if (!album) throw new UnprocessableEntityException(errorMessage.ALBUM_NOT_FOUND); return album; @@ -74,8 +71,8 @@ export class FavoriteController { @Delete('artist/:id') @HttpCode(HttpStatus.NO_CONTENT) - removeArtist(@Param('id', ParseUUIDPipe) id: string) { - const artist = this.favoriteService.removeArtist(id); + async removeArtist(@Param('id', ParseUUIDPipe) id: string) { + const artist = await this.favoriteService.removeArtist(id); if (!artist) throw new UnprocessableEntityException(errorMessage.ARTIST_NOT_FOUND); return artist; diff --git a/src/favorite/favorite.module.ts b/src/favorite/favorite.module.ts index 37f29bb..b7ac9f6 100644 --- a/src/favorite/favorite.module.ts +++ b/src/favorite/favorite.module.ts @@ -2,10 +2,10 @@ import { Module } from '@nestjs/common'; import { FavoriteController } from './favorite.controller'; import { FavoriteService } from './favorite.service'; -import { DatabaseModule } from '../database/database.module'; +import { PrismaModule } from '../prisma/prisma.module'; @Module({ - imports: [DatabaseModule], + imports: [PrismaModule], controllers: [FavoriteController], providers: [FavoriteService], }) diff --git a/src/favorite/favorite.service.ts b/src/favorite/favorite.service.ts index b43760e..c259948 100644 --- a/src/favorite/favorite.service.ts +++ b/src/favorite/favorite.service.ts @@ -1,44 +1,92 @@ import { Injectable } from '@nestjs/common'; -import { DatabaseService } from '../database/database.service'; +import exclude from '../lib/shared/exclude'; +import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class FavoriteService { - constructor(private readonly databaseService: DatabaseService) {} + constructor(private readonly prismaService: PrismaService) {} - createTrack(id: string) { - return this.databaseService.favorite.track.create(id); + async createTrack(id: string) { + try { + return await this.prismaService.track.update({ + where: { id }, + data: { favoritesId: 'favs' }, + }); + } catch (e) { + return null; + } } - createAlbum(id: string) { - return this.databaseService.favorite.album.create(id); + async createAlbum(id: string) { + try { + return await this.prismaService.album.update({ + where: { id }, + data: { favoritesId: 'favs' }, + }); + } catch (e) { + return null; + } } - createArtist(id: string) { - return this.databaseService.favorite.artist.create(id); + async createArtist(id: string) { + try { + return await this.prismaService.artist.update({ + where: { id }, + data: { favoritesId: 'favs' }, + }); + } catch (e) { + return null; + } } - findAllTracks() { - return this.databaseService.favorite.track.findMany(); - } + async findAll() { + const data = await this.prismaService.favorites.findMany({ + where: { id: 'favs' }, + select: { artists: true, albums: true, tracks: true }, + }); - findAllAlbums() { - return this.databaseService.favorite.album.findMany(); - } + const clearData = { + albums: data['0']?.albums.map((el) => exclude(el, ['favoritesId'])), + artists: data['0']?.artists.map((el) => exclude(el, ['favoritesId'])), + tracks: data['0']?.tracks.map((el) => exclude(el, ['favoritesId'])), + }; - findAllArtists() { - return this.databaseService.favorite.artist.findMany(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return { artists: [], albums: [], tracks: [], ...clearData }; } - removeTrack(id: string) { - return this.databaseService.favorite.track.delete(id); + async removeTrack(id: string) { + try { + return await this.prismaService.track.update({ + where: { id }, + data: { favoritesId: null }, + }); + } catch (e) { + return null; + } } - removeAlbum(id: string) { - return this.databaseService.favorite.album.delete(id); + async removeAlbum(id: string) { + try { + return await this.prismaService.album.update({ + where: { id }, + data: { favoritesId: null }, + }); + } catch (e) { + return null; + } } - removeArtist(id: string) { - return this.databaseService.favorite.artist.delete(id); + async removeArtist(id: string) { + try { + return await this.prismaService.artist.update({ + where: { id }, + data: { favoritesId: null }, + }); + } catch (e) { + return null; + } } } diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index 3359cbf..3e0719b 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -14,3 +14,5 @@ export const SWAGGER_CONFIG = new DocumentBuilder() .setVersion('1.0') .addTag('Home Library') .build(); + +export const FAVS_TABLE_ID = 'favs'; diff --git a/src/lib/shared/exclude.ts b/src/lib/shared/exclude.ts new file mode 100644 index 0000000..5497f33 --- /dev/null +++ b/src/lib/shared/exclude.ts @@ -0,0 +1,11 @@ +function exclude( + entity: TEntity, + keys: Key[], +): Omit { + return Object.fromEntries( + Object.entries(entity as { [k: string]: unknown }).filter( + ([key]) => !keys.includes(key as Key), + ), + ) as Omit; +} +export default exclude; diff --git a/src/lib/shared/formatUserDate.ts b/src/lib/shared/formatUserDate.ts new file mode 100644 index 0000000..98be080 --- /dev/null +++ b/src/lib/shared/formatUserDate.ts @@ -0,0 +1,9 @@ +import { User } from '@prisma/client'; + +const formatUserDate = (user: User) => ({ + ...user, + createdAt: new Date(user.createdAt).getTime(), + updatedAt: new Date(user.updatedAt).getTime(), +}); + +export default formatUserDate; diff --git a/src/main.ts b/src/main.ts index e4158e3..183ee74 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,3 @@ -import 'dotenv/config'; - import { NestFactory } from '@nestjs/core'; import { SwaggerModule } from '@nestjs/swagger'; diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts new file mode 100644 index 0000000..54327ba --- /dev/null +++ b/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { PrismaService } from './prisma.service'; + +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts new file mode 100644 index 0000000..6b6f8c6 --- /dev/null +++ b/src/prisma/prisma.service.ts @@ -0,0 +1,37 @@ +import { + INestApplication, + Injectable, + OnModuleDestroy, + OnModuleInit, +} from '@nestjs/common'; +import { Prisma, PrismaClient } from '@prisma/client'; + +import { FAVS_TABLE_ID } from '../lib/const/const'; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + async onModuleInit() { + await this.$connect(); + + const favorites = await this.favorites.findUnique({ + where: { id: FAVS_TABLE_ID }, + }); + + if (!favorites) { + await this.favorites.create({ + data: { id: FAVS_TABLE_ID }, + }); + } + } + + async onModuleDestroy() { + await this.$disconnect(); + } + + async enableShutdownHooks(app: INestApplication) { + this.$on('beforeExit', async () => app.close()); + } +} diff --git a/src/track/track.controller.ts b/src/track/track.controller.ts index 19644d9..b62d37a 100644 --- a/src/track/track.controller.ts +++ b/src/track/track.controller.ts @@ -25,41 +25,38 @@ export class TrackController { @UsePipes(new ValidationPipe()) @Post() - create(@Body() createTrackDto: CreateTrackDto) { - const track = this.trackService.create(createTrackDto); - return track; + async create(@Body() createTrackDto: CreateTrackDto) { + return this.trackService.create(createTrackDto); } @Get() - findAll() { + async findAll() { return this.trackService.findAll(); } @Get(':id') - findOne(@Param('id', ParseUUIDPipe) id: string) { - const track = this.trackService.findOne(id); + async findOne(@Param('id', ParseUUIDPipe) id: string) { + const track = await this.trackService.findOne(id); if (!track) throw new NotFoundException(errorMessage.TRACK_NOT_FOUND); return track; } @UsePipes(new ValidationPipe()) @Put(':id') - update( + async update( @Param('id', ParseUUIDPipe) id: string, @Body() updateTrackDto: UpdateTrackDto, ) { - const updatedTrack = this.trackService.update(id, updateTrackDto); - + const updatedTrack = await this.trackService.update(id, updateTrackDto); if (!updatedTrack) throw new NotFoundException(errorMessage.TRACK_NOT_FOUND); - return updatedTrack; } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - remove(@Param('id', ParseUUIDPipe) id: string) { - const track = this.trackService.remove(id); + async remove(@Param('id', ParseUUIDPipe) id: string) { + const track = await this.trackService.remove(id); if (!track) throw new NotFoundException(errorMessage.TRACK_NOT_FOUND); return track; } diff --git a/src/track/track.module.ts b/src/track/track.module.ts index 60347df..21be25c 100644 --- a/src/track/track.module.ts +++ b/src/track/track.module.ts @@ -2,10 +2,10 @@ import { Module } from '@nestjs/common'; import { TrackController } from './track.controller'; import { TrackService } from './track.service'; -import { DatabaseModule } from '../database/database.module'; +import { PrismaModule } from '../prisma/prisma.module'; @Module({ - imports: [DatabaseModule], + imports: [PrismaModule], controllers: [TrackController], providers: [TrackService], }) diff --git a/src/track/track.service.ts b/src/track/track.service.ts index e7e3cd3..66b0c40 100644 --- a/src/track/track.service.ts +++ b/src/track/track.service.ts @@ -2,30 +2,40 @@ import { Injectable } from '@nestjs/common'; import { CreateTrackDto } from './dto/create-track.dto'; import { UpdateTrackDto } from './dto/update-track.dto'; -import { DatabaseService } from '../database/database.service'; +import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class TrackService { - constructor(private readonly databaseService: DatabaseService) {} + constructor(private readonly prismaService: PrismaService) {} - create(createTrackDto: CreateTrackDto) { - return this.databaseService.track.create(createTrackDto); + async create(createTrackDto: CreateTrackDto) { + return this.prismaService.track.create({ data: createTrackDto }); } - findAll() { - return this.databaseService.track.findMany(); + async findAll() { + return this.prismaService.track.findMany(); } - findOne(id: string) { - return this.databaseService.track.findById(id); + async findOne(id: string) { + return this.prismaService.track.findUnique({ where: { id } }); } - update(id: string, updateTrackDto: UpdateTrackDto) { - return this.databaseService.track.update(id, updateTrackDto); + async update(id: string, updateTrackDto: UpdateTrackDto) { + try { + return await this.prismaService.track.update({ + where: { id }, + data: updateTrackDto, + }); + } catch (e) { + return null; + } } - remove(id: string) { - this.databaseService.favorite.track.delete(id); - return this.databaseService.track.delete(id); + async remove(id: string) { + try { + return await this.prismaService.track.delete({ where: { id } }); + } catch (e) { + return null; + } } } diff --git a/src/user/entities/user.entity.ts b/src/user/entities/user.entity.ts index 9e8b739..e830e15 100644 --- a/src/user/entities/user.entity.ts +++ b/src/user/entities/user.entity.ts @@ -1,8 +1,6 @@ import { Exclude } from 'class-transformer'; import * as uuid from 'uuid'; -import { UpdateUserDto } from '../dto/update-user.dto'; - export class User { public id: string; @@ -22,13 +20,7 @@ export class User { this.login = login; this.password = password; this.version = 1; - this.createdAt = Date.now(); + this.createdAt = 123; this.updatedAt = this.createdAt; } - - update({ newPassword }: UpdateUserDto) { - this.password = newPassword; - this.version += 1; - this.updatedAt = Date.now(); - } } diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 7fbf0cb..34f9c39 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -1,6 +1,5 @@ import { Body, - ClassSerializerInterceptor, Controller, Delete, ForbiddenException, @@ -12,51 +11,55 @@ import { ParseUUIDPipe, Post, Put, - UseInterceptors, UsePipes, ValidationPipe, } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; +import { User } from './entities/user.entity'; import { UserService } from './user.service'; import { errorMessage } from '../lib/const/const'; +import exclude from '../lib/shared/exclude'; +import formatUserDate from '../lib/shared/formatUserDate'; @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} - @UseInterceptors(ClassSerializerInterceptor) @UsePipes(new ValidationPipe()) @Post() - create(@Body() createUserDto: CreateUserDto) { - return this.userService.create(createUserDto); + async create(@Body() createUserDto: CreateUserDto) { + const user = await this.userService.create(createUserDto); + const updUser = formatUserDate(user); + return exclude(updUser!, ['password']); } - @UseInterceptors(ClassSerializerInterceptor) @Get() - findAll() { - return this.userService.findAll(); + async findAll() { + const users = await this.userService.findAll(); + const updUsers = users.map(formatUserDate); + return updUsers.map((user) => + exclude(user!, ['password']), + ); } - @UseInterceptors(ClassSerializerInterceptor) @Get(':id') - findOne(@Param('id', ParseUUIDPipe) id: string) { - const user = this.userService.findOne(id); - + async findOne(@Param('id', ParseUUIDPipe) id: string) { + const user = await this.userService.findOne(id); if (!user) throw new NotFoundException(errorMessage.USER_NOT_FOUND); - return user; + const updUser = formatUserDate(user); + return exclude(updUser!, ['password']); } - @UseInterceptors(ClassSerializerInterceptor) @UsePipes(new ValidationPipe()) @Put(':id') - update( + async update( @Param('id', ParseUUIDPipe) id: string, @Body() updateUserDto: UpdateUserDto, ) { const { oldPassword } = updateUserDto; - const user = this.userService.findOne(id); + const user = await this.userService.findOne(id); const isSamePassword = oldPassword === user?.password; if (!user) throw new NotFoundException(errorMessage.USER_NOT_FOUND); @@ -64,17 +67,17 @@ export class UserController { if (!isSamePassword) throw new ForbiddenException(errorMessage.INVALID_PASSWORD); - const updatedUser = this.userService.update(id, updateUserDto); - return updatedUser; + const updatedUser = await this.userService.update(id, updateUserDto); + const updUser = formatUserDate(updatedUser!); + return exclude(updUser!, ['password']); } - @UseInterceptors(ClassSerializerInterceptor) @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - remove(@Param('id', ParseUUIDPipe) id: string) { - const user = this.userService.remove(id); - + async remove(@Param('id', ParseUUIDPipe) id: string) { + const user = await this.userService.remove(id); if (!user) throw new NotFoundException(errorMessage.USER_NOT_FOUND); - return user; + const updUser = formatUserDate(user); + return exclude(updUser!, ['password']); } } diff --git a/src/user/user.module.ts b/src/user/user.module.ts index ba1ec7e..139fcab 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -2,10 +2,10 @@ import { Module } from '@nestjs/common'; import { UserController } from './user.controller'; import { UserService } from './user.service'; -import { DatabaseModule } from '../database/database.module'; +import { PrismaModule } from '../prisma/prisma.module'; @Module({ - imports: [DatabaseModule], + imports: [PrismaModule], controllers: [UserController], providers: [UserService], }) diff --git a/src/user/user.service.ts b/src/user/user.service.ts index f90090e..3316540 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -2,29 +2,45 @@ import { Injectable } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; -import { DatabaseService } from '../database/database.service'; +import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class UserService { - constructor(private readonly databaseService: DatabaseService) {} + constructor(private readonly prismaService: PrismaService) {} - create(createUserDto: CreateUserDto) { - return this.databaseService.user.create(createUserDto); + async create(createUserDto: CreateUserDto) { + return this.prismaService.user.create({ + data: createUserDto, + }); } - findAll() { - return this.databaseService.user.findMany(); + async findAll() { + return this.prismaService.user.findMany(); } - findOne(id: string) { - return this.databaseService.user.findById(id); + async findOne(id: string) { + return this.prismaService.user.findUnique({ where: { id } }); } - update(id: string, updateUserDto: UpdateUserDto) { - return this.databaseService.user.update(id, updateUserDto); + async update(id: string, { newPassword }: UpdateUserDto) { + try { + return await this.prismaService.user.update({ + where: { id }, + data: { + password: newPassword, + version: { increment: 1 }, + }, + }); + } catch (e) { + return null; + } } - remove(id: string) { - return this.databaseService.user.delete(id); + async remove(id: string) { + try { + return await this.prismaService.user.delete({ where: { id } }); + } catch (e) { + return null; + } } }