├── .env.example ├── .eslintrc.json ├── .gitignore ├── .node-version ├── .prettierrc ├── migrations ├── 1663235388383_Create-table-albums.js ├── 1663235603533_Create-table-songs.js ├── 1663425150620_Create-table-users.js ├── 1663434554291_Create-table-autentications.js ├── 1663518879766_Create-table-playlists.js ├── 1663529261522_Create-table-playlist_songs.js ├── 1663620848797_Add-updatedAt-column-to-songs.js ├── 1663623777260_Create-table-collaborations.js ├── 1663700232364_Create-table-playlist-song-activities.js ├── 1663861319926_Add-cover-column-to-albums.js └── 1663870090628_Create-table-user-album-likes.js ├── package.json ├── src ├── api │ ├── albums │ │ ├── handler.js │ │ ├── index.js │ │ └── routes.js │ ├── authentications │ │ ├── handler.js │ │ ├── index.js │ │ └── routes.js │ ├── collaborations │ │ ├── handler.js │ │ ├── index.js │ │ └── routes.js │ ├── exports │ │ ├── handler.js │ │ ├── index.js │ │ └── routes.js │ ├── playlists │ │ ├── handler.js │ │ ├── index.js │ │ └── routes.js │ ├── songs │ │ ├── handler.js │ │ ├── index.js │ │ └── routes.js │ └── users │ │ ├── handler.js │ │ ├── index.js │ │ └── routes.js ├── exceptions │ ├── AuthenticationError.js │ ├── AuthorizationError.js │ ├── ClientError.js │ ├── InvariantError.js │ └── NotFoundError.js ├── server.js ├── services │ ├── _types │ │ ├── AlbumsServiceType.js │ │ ├── AuthenticationsServiceType.js │ │ ├── CollaborationsServiceType.js │ │ ├── PlaylistSongActivitiesServiceType.js │ │ ├── PlaylistSongsServiceType.js │ │ ├── PlaylistsServiceType.js │ │ ├── SongsServiceType.js │ │ ├── UserAlbumLikesType.js │ │ └── UsersServiceType.js │ ├── postgres │ │ ├── AlbumsService.js │ │ ├── AuthenticationsService.js │ │ ├── CollaborationsService.js │ │ ├── PlaylistSongActivitiesService.js │ │ ├── PlaylistSongsService.js │ │ ├── PlaylistsService.js │ │ ├── SongsService.js │ │ ├── UserAlbumLikes.js │ │ └── UsersService.js │ ├── rabbitmq │ │ └── ProducerService.js │ ├── redis │ │ └── CacheService.js │ └── storage │ │ └── StorageService.js ├── tokenize │ └── TokenManager.js ├── utils │ ├── HapiErrorHandler.js │ └── config.js └── validator │ ├── albums │ ├── index.js │ └── scheme.js │ ├── authentications │ ├── index.js │ └── scheme.js │ ├── collaborations │ ├── index.js │ └── scheme.js │ ├── exports │ ├── index.js │ └── scheme.js │ ├── playlists │ ├── index.js │ └── scheme.js │ ├── songs │ ├── index.js │ └── scheme.js │ └── users │ ├── index.js │ └── scheme.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # server configuration 2 | HOST=localhost 3 | PORT=5000 4 | 5 | # postgres configuration 6 | PGUSER=postgres 7 | PGHOST=localhost 8 | PGPASSWORD=password 9 | PGDATABASE=openmusic 10 | PGPORT=5432 11 | 12 | # jwt configuration 13 | ACCESS_TOKEN_KEY=444c5f50e629298398f16616b9396a44233e0c0d605269b80092289cd194c3c9127cb3a3072aad0185724dca4cfeba1453dbab94a6f859bb7a31df080d023b1f 14 | REFRESH_TOKEN_KEY=b4e3dd21b34e8855d68cbbbb5ac14802d10d25d88c524f72f5ac9111a5c73bcd496f073add90014a35faf5d7da4775ffc10e3ccd89c0bb0472c50e2f2bcf406c 15 | ACCESS_TOKEN_AGE=1800 16 | 17 | # rabbitmq configuration 18 | RABBITMQ_SERVER=amqp://localhost 19 | 20 | # redis configuration 21 | REDIS_SERVER=localhost -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "airbnb-base", 9 | "plugin:jsdoc/recommended", 10 | "plugin:prettier/recommended" 11 | ], 12 | "plugins": ["prettier", "sort-requires"], 13 | "overrides": [], 14 | "parserOptions": { 15 | "ecmaVersion": "latest" 16 | }, 17 | "settings": { 18 | "jsdoc": { 19 | "mode": "typescript" 20 | } 21 | }, 22 | "rules": { 23 | "class-methods-use-this": "off", 24 | "no-console": "off", 25 | "no-underscore-dangle": "off", 26 | "jsdoc/require-param-description": "off", 27 | "jsdoc/require-property-description": "off", 28 | "jsdoc/require-returns-description": "off", 29 | "sort-requires/sort-requires": "error" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .env 3 | node_modules 4 | src/api/albums/file -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v16.17.0 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /migrations/1663235388383_Create-table-albums.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable camelcase */ 3 | 4 | /** 5 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 6 | */ 7 | 8 | const table_name = 'albums'; 9 | 10 | /** 11 | * @param {MigrationBuilder} pgm migration builder type interface 12 | */ 13 | exports.up = (pgm) => { 14 | pgm.createTable(table_name, { 15 | id: { 16 | type: 'varchar(22)', 17 | primaryKey: true, 18 | }, 19 | name: { 20 | type: 'varchar(50)', 21 | notNull: true, 22 | }, 23 | year: { 24 | type: 'smallint', 25 | notNull: true, 26 | }, 27 | createdAt: { 28 | type: 'timestamp', 29 | notNull: true, 30 | default: pgm.func('current_timestamp'), 31 | }, 32 | updatedAt: { 33 | type: 'timestamp', 34 | }, 35 | }); 36 | }; 37 | 38 | /** 39 | * @param {MigrationBuilder} pgm migration builder type interface 40 | */ 41 | exports.down = (pgm) => { 42 | pgm.dropTable(table_name); 43 | }; 44 | -------------------------------------------------------------------------------- /migrations/1663235603533_Create-table-songs.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | /** 4 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 5 | */ 6 | 7 | const table_name = 'songs'; 8 | 9 | /** 10 | * @param {MigrationBuilder} pgm 11 | */ 12 | exports.up = (pgm) => { 13 | pgm.createTable(table_name, { 14 | id: { 15 | type: 'varchar(21)', 16 | primaryKey: true, 17 | }, 18 | title: { 19 | type: 'varchar(50)', 20 | notNull: true, 21 | }, 22 | year: { 23 | type: 'smallint', 24 | notNull: true, 25 | }, 26 | performer: { 27 | type: 'varchar(50)', 28 | notNull: true, 29 | }, 30 | genre: { 31 | type: 'varchar(32)', 32 | notNull: true, 33 | }, 34 | duration: { 35 | type: 'smallint', 36 | }, 37 | albumId: { 38 | type: 'varchar(22)', 39 | references: '"albums"', 40 | onUpdate: 'cascade', 41 | onDelete: 'cascade', 42 | }, 43 | createdAt: { 44 | type: 'timestamp', 45 | notNull: true, 46 | default: pgm.func('current_timestamp'), 47 | }, 48 | updatedAt: { 49 | type: 'timestamp', 50 | }, 51 | }); 52 | }; 53 | 54 | /** 55 | * @param {MigrationBuilder} pgm 56 | */ 57 | exports.down = (pgm) => { 58 | pgm.dropTable(table_name); 59 | }; 60 | -------------------------------------------------------------------------------- /migrations/1663425150620_Create-table-users.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | /** 4 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 5 | */ 6 | 7 | const table_name = 'users'; 8 | 9 | /** 10 | * @param {MigrationBuilder} pgm 11 | */ 12 | exports.up = (pgm) => { 13 | pgm.createTable(table_name, { 14 | id: { 15 | type: 'varchar(21)', 16 | primaryKey: true, 17 | }, 18 | username: { 19 | type: 'varchar(50)', 20 | unique: true, 21 | notNull: true, 22 | }, 23 | password: { 24 | type: 'text', 25 | notNull: true, 26 | }, 27 | fullname: { 28 | type: 'text', 29 | notNull: true, 30 | }, 31 | createdAt: { 32 | type: 'timestamp', 33 | notNull: true, 34 | default: pgm.func('current_timestamp'), 35 | }, 36 | updatedAt: { 37 | type: 'timestamp', 38 | }, 39 | }); 40 | }; 41 | 42 | /** 43 | * @param {MigrationBuilder} pgm 44 | */ 45 | exports.down = (pgm) => { 46 | pgm.dropTable(table_name); 47 | }; 48 | -------------------------------------------------------------------------------- /migrations/1663434554291_Create-table-autentications.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-disable camelcase */ 3 | 4 | /** 5 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 6 | */ 7 | 8 | const table_name = 'authentications'; 9 | 10 | /** 11 | * @param {MigrationBuilder} pgm 12 | */ 13 | exports.up = (pgm) => { 14 | pgm.createTable(table_name, { 15 | token: { 16 | type: 'text', 17 | notNull: true, 18 | }, 19 | }); 20 | }; 21 | 22 | /** 23 | * @param {MigrationBuilder} pgm 24 | */ 25 | exports.down = (pgm) => { 26 | pgm.dropTable(table_name); 27 | }; 28 | -------------------------------------------------------------------------------- /migrations/1663518879766_Create-table-playlists.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | /** 4 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 5 | */ 6 | 7 | const table_name = 'playlists'; 8 | 9 | /** 10 | * @param {MigrationBuilder} pgm 11 | */ 12 | exports.up = (pgm) => { 13 | pgm.createTable(table_name, { 14 | id: { 15 | type: 'varchar(25)', 16 | primaryKey: true, 17 | }, 18 | name: { 19 | type: 'varchar(50)', 20 | notNull: true, 21 | }, 22 | owner: { 23 | type: 'varchar(21)', 24 | notNull: true, 25 | references: 'users(id)', 26 | onUpdate: 'cascade', 27 | onDelete: 'cascade', 28 | }, 29 | createdAt: { 30 | type: 'timestamp', 31 | notNull: true, 32 | default: pgm.func('current_timestamp'), 33 | }, 34 | updatedAt: { 35 | type: 'timestamp', 36 | }, 37 | }); 38 | }; 39 | 40 | /** 41 | * @param {MigrationBuilder} pgm 42 | */ 43 | exports.down = (pgm) => { 44 | pgm.dropTable(table_name); 45 | }; 46 | -------------------------------------------------------------------------------- /migrations/1663529261522_Create-table-playlist_songs.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | /** 4 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 5 | */ 6 | 7 | const table_name = 'playlist_songs'; 8 | 9 | /** 10 | * @param {MigrationBuilder} pgm 11 | */ 12 | exports.up = (pgm) => { 13 | pgm.createTable(table_name, { 14 | id: { 15 | type: 'varchar(30)', 16 | primaryKey: true, 17 | }, 18 | playlist_id: { 19 | type: 'varchar(25)', 20 | notNull: true, 21 | references: 'playlists(id)', 22 | onUpdate: 'cascade', 23 | onDelete: 'cascade', 24 | }, 25 | song_id: { 26 | type: 'varchar(21)', 27 | notNull: true, 28 | references: 'songs(id)', 29 | onUpdate: 'cascade', 30 | onDelete: 'cascade', 31 | }, 32 | createdAt: { 33 | type: 'timestamp', 34 | notNull: true, 35 | default: pgm.func('current_timestamp'), 36 | }, 37 | updatedAt: { 38 | type: 'timestamp', 39 | }, 40 | }); 41 | 42 | pgm.addConstraint( 43 | table_name, 44 | `unique_${table_name}_playlist_id_and_song_id`, 45 | 'UNIQUE(playlist_id, song_id)' 46 | ); 47 | }; 48 | 49 | /** 50 | * @param {MigrationBuilder} pgm 51 | */ 52 | exports.down = (pgm) => { 53 | pgm.dropTable(table_name); 54 | }; 55 | -------------------------------------------------------------------------------- /migrations/1663620848797_Add-updatedAt-column-to-songs.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | /** 4 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 5 | */ 6 | 7 | /** 8 | * @param {MigrationBuilder} pgm 9 | */ 10 | exports.up = (pgm) => { 11 | pgm.addColumn('songs', { 12 | deletedAt: { 13 | type: 'timestamp', 14 | default: null, 15 | }, 16 | }); 17 | }; 18 | 19 | /** 20 | * @param {MigrationBuilder} pgm 21 | */ 22 | exports.down = (pgm) => { 23 | pgm.dropColumn('songs', 'deletedAt'); 24 | }; 25 | -------------------------------------------------------------------------------- /migrations/1663623777260_Create-table-collaborations.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | /** 4 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 5 | */ 6 | 7 | const table_name = 'collaborations'; 8 | 9 | /** 10 | * @param {MigrationBuilder} pgm 11 | */ 12 | exports.up = (pgm) => { 13 | pgm.createTable(table_name, { 14 | id: { 15 | type: 'varchar(23)', 16 | primaryKey: true, 17 | }, 18 | playlist_id: { 19 | type: 'varchar(25)', 20 | notNull: true, 21 | references: 'playlists(id)', 22 | onUpdate: 'cascade', 23 | onDelete: 'cascade', 24 | }, 25 | user_id: { 26 | type: 'varchar(21)', 27 | notNull: true, 28 | references: 'users(id)', 29 | onUpdate: 'cascade', 30 | onDelete: 'cascade', 31 | }, 32 | createdAt: { 33 | type: 'timestamp', 34 | notNull: true, 35 | default: pgm.func('current_timestamp'), 36 | }, 37 | updatedAt: { 38 | type: 'timestamp', 39 | }, 40 | }); 41 | 42 | pgm.addConstraint( 43 | table_name, 44 | `unique_${table_name}_playlist_id_and_user_id`, 45 | 'UNIQUE(playlist_id, user_id)' 46 | ); 47 | }; 48 | 49 | /** 50 | * 51 | * @param {MigrationBuilder} pgm 52 | */ 53 | exports.down = (pgm) => { 54 | pgm.dropTable(table_name); 55 | }; 56 | -------------------------------------------------------------------------------- /migrations/1663700232364_Create-table-playlist-song-activities.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | /** 4 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 5 | */ 6 | 7 | const table_name = 'playlist_song_activities'; 8 | 9 | /** 10 | * @param {MigrationBuilder} pgm 11 | */ 12 | exports.up = (pgm) => { 13 | pgm.createTable(table_name, { 14 | id: { 15 | type: 'varchar(20)', 16 | primaryKey: true, 17 | }, 18 | playlist_id: { 19 | type: 'varchar(25)', 20 | notNull: true, 21 | references: 'playlists(id)', 22 | onUpdate: 'cascade', 23 | onDelete: 'cascade', 24 | }, 25 | song_id: { 26 | type: 'varchar(21)', 27 | notNull: true, 28 | }, 29 | user_id: { 30 | type: 'varchar(21)', 31 | notNull: true, 32 | }, 33 | action: { 34 | type: 'text', 35 | notNull: true, 36 | }, 37 | time: { 38 | type: 'timestamp', 39 | notNull: true, 40 | default: pgm.func('current_timestamp'), 41 | }, 42 | }); 43 | }; 44 | 45 | /** 46 | * @param {MigrationBuilder} pgm 47 | */ 48 | exports.down = (pgm) => { 49 | pgm.dropTable(table_name); 50 | }; 51 | -------------------------------------------------------------------------------- /migrations/1663861319926_Add-cover-column-to-albums.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | /** 4 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 5 | */ 6 | 7 | /** 8 | * @param {MigrationBuilder} pgm 9 | */ 10 | exports.up = (pgm) => { 11 | pgm.addColumn('albums', { 12 | cover: { 13 | type: 'text', 14 | default: null, 15 | }, 16 | }); 17 | }; 18 | 19 | /** 20 | * @param {MigrationBuilder} pgm 21 | */ 22 | exports.down = (pgm) => { 23 | pgm.dropColumn('albums', 'cover'); 24 | }; 25 | -------------------------------------------------------------------------------- /migrations/1663870090628_Create-table-user-album-likes.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | /** 4 | * @typedef {import('node-pg-migrate').MigrationBuilder} MigrationBuilder 5 | */ 6 | 7 | const table_name = 'user_album_likes'; 8 | 9 | /** 10 | * @param {MigrationBuilder} pgm 11 | */ 12 | exports.up = (pgm) => { 13 | pgm.createTable(table_name, { 14 | id: { 15 | type: 'varchar(20)', 16 | primaryKey: true, 17 | }, 18 | user_id: { 19 | type: 'varchar(21)', 20 | notNull: true, 21 | references: 'users(id)', 22 | onUpdate: 'cascade', 23 | onDelete: 'cascade', 24 | }, 25 | album_id: { 26 | type: 'varchar(22)', 27 | notNull: true, 28 | references: 'albums(id)', 29 | onUpdate: 'cascade', 30 | onDelete: 'cascade', 31 | }, 32 | }); 33 | 34 | pgm.addConstraint( 35 | table_name, 36 | `unique_${table_name}_album_id_and_user_id`, 37 | `UNIQUE(album_id, user_id)` 38 | ); 39 | }; 40 | 41 | /** 42 | * @param {MigrationBuilder} pgm 43 | */ 44 | exports.down = (pgm) => { 45 | pgm.dropTable(table_name); 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openmusic-api", 3 | "version": "1.0.0", 4 | "description": "Kriteria Proyek OpenMusic API versi 1", 5 | "main": "src/server.js", 6 | "author": "Dhiki Indryanto", 7 | "license": "MIT", 8 | "scripts": { 9 | "start-prod": "NODE_ENV=production node ./src/server.js", 10 | "start-dev": "nodemon -w src/ ./src/server.js", 11 | "lint": "eslint ./src", 12 | "migrate": "node-pg-migrate" 13 | }, 14 | "dependencies": { 15 | "@hapi/boom": "^10.0.0", 16 | "@hapi/hapi": "^20.2.2", 17 | "@hapi/inert": "^7.0.0", 18 | "@hapi/jwt": "^3.0.0", 19 | "amqplib": "^0.10.3", 20 | "auto-bind": "^4.0.0", 21 | "bcrypt": "^5.0.1", 22 | "dotenv": "^16.0.2", 23 | "joi": "^17.6.0", 24 | "nanoid": "^3.3.4", 25 | "node-pg-migrate": "^6.2.2", 26 | "pg": "^8.8.0", 27 | "redis": "^4.3.1" 28 | }, 29 | "devDependencies": { 30 | "@types/hapi__hapi": "^20.0.12", 31 | "eslint": "^8.23.1", 32 | "eslint-config-airbnb-base": "^15.0.0", 33 | "eslint-config-prettier": "^8.5.0", 34 | "eslint-plugin-import": "^2.25.2", 35 | "eslint-plugin-jsdoc": "^39.3.6", 36 | "eslint-plugin-prettier": "^4.2.1", 37 | "eslint-plugin-sort-requires": "^2.1.0", 38 | "nodemon": "^2.0.19", 39 | "prettier": "^2.7.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/api/albums/handler.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const autoBind = require('auto-bind'); 4 | 5 | const config = require('../../utils/config'); 6 | 7 | /** 8 | * @typedef {import('@hapi/hapi').Request} Request 9 | * @typedef {import('@hapi/hapi').ResponseToolkit} ResponseToolkit 10 | * @typedef {import('../../services/_types/AlbumsServiceType').IAlbumEntity} IAlbumEntity 11 | * @typedef {import('../../services/_types/AlbumsServiceType').IAlbumsService} IAlbumsService 12 | * @typedef {import('../../services/_types/UserAlbumLikesType').IUserAlbumLikes} IUserAlbumLikes 13 | * @typedef {import('../../services/storage/StorageService')} StorageService 14 | */ 15 | 16 | class AlbumsHandler { 17 | /** 18 | * @readonly 19 | * @private 20 | */ 21 | _albumsService; 22 | 23 | /** 24 | * @readonly 25 | * @private 26 | */ 27 | _ualService; 28 | 29 | /** 30 | * @readonly 31 | * @private 32 | */ 33 | _storageService; 34 | 35 | /** 36 | * @readonly 37 | * @private 38 | */ 39 | _validator; 40 | 41 | /** 42 | * @param {IAlbumsService} albumsService 43 | * @param {IUserAlbumLikes} ualService 44 | * @param {StorageService} storageService 45 | * @param {import('../../validator/albums')} validator 46 | */ 47 | constructor(albumsService, ualService, storageService, validator) { 48 | this._albumsService = albumsService; 49 | this._ualService = ualService; 50 | this._storageService = storageService; 51 | this._validator = validator; 52 | 53 | autoBind(this); 54 | } 55 | 56 | /** 57 | * @param {Request} request 58 | * @param {ResponseToolkit} h 59 | */ 60 | async postAlbumHandler(request, h) { 61 | this._validator.validateAlbumPayload(request.payload); 62 | 63 | const { name, year } = /** @type {IAlbumEntity} */ (request.payload); 64 | 65 | const albumId = await this._albumsService.addAlbum({ name, year }); 66 | 67 | const response = h.response({ 68 | status: 'success', 69 | message: 'Album berhasil ditambahkan', 70 | data: { 71 | albumId, 72 | }, 73 | }); 74 | response.code(201); 75 | 76 | return response; 77 | } 78 | 79 | /** 80 | * @param {Request} request 81 | * @param {ResponseToolkit} h 82 | */ 83 | async postUploadAlbumCoverHandler(request, h) { 84 | const { cover } = /** @type {{cover: object}} */ (request.payload); 85 | const { id } = /** @type {{id: string}} */ (request.params); 86 | this._validator.validateAlbumCoverHeader(cover.hapi.headers); 87 | 88 | const filename = await this._storageService.writeFile(cover, cover.hapi); 89 | await this._albumsService.editAlbumCoverById( 90 | id, 91 | `http://${config.app.host}:${config.app.port}/albums/covers/${filename}` 92 | ); 93 | 94 | const response = h.response({ 95 | status: 'success', 96 | message: 'Cover album berhasil diupload', 97 | }); 98 | response.code(201); 99 | 100 | return response; 101 | } 102 | 103 | /** 104 | * @param {Request} request 105 | */ 106 | async getAlbumByIdHandler(request) { 107 | const { id } = /** @type {{id: string}} */ (request.params); 108 | 109 | const album = await this._albumsService.getAlbumById(id); 110 | 111 | return { 112 | status: 'success', 113 | data: { 114 | album, 115 | }, 116 | }; 117 | } 118 | 119 | /** 120 | * @param {Request} request 121 | */ 122 | async putAlbumByIdHandler(request) { 123 | this._validator.validateAlbumPayload(request.payload); 124 | 125 | const { id } = /** @type {{id: string}} */ (request.params); 126 | const { name, year } = /** @type {IAlbumEntity} */ (request.payload); 127 | 128 | await this._albumsService.editAlbumById(id, { name, year }); 129 | 130 | return { 131 | status: 'success', 132 | message: 'Album berhasil diperbarui', 133 | }; 134 | } 135 | 136 | /** 137 | * @param {Request} request 138 | */ 139 | async deleteAlbumByIdHandler(request) { 140 | const { id } = /** @type {{id: string}} */ (request.params); 141 | 142 | await this._albumsService.deleteAlbumById(id); 143 | 144 | return { 145 | status: 'success', 146 | message: 'Album berhasil dihapus', 147 | }; 148 | } 149 | 150 | /** 151 | * @param {Request} request 152 | * @param {ResponseToolkit} h 153 | */ 154 | async postAlbumLikeOrUnlikeByIdHandler(request, h) { 155 | const { id } = /** @type {{id: string}} */ (request.params); 156 | const { userId } = /** @type {{userId: string}} */ ( 157 | request.auth.credentials 158 | ); 159 | 160 | const action = await this._ualService.likeOrUnlikeAlbum({ 161 | albumId: id, 162 | userId, 163 | }); 164 | 165 | const response = h.response({ 166 | status: 'success', 167 | message: `Album berhasil ${action}`, 168 | }); 169 | response.code(201); 170 | 171 | return response; 172 | } 173 | 174 | /** 175 | * @param {Request} request 176 | * @param {ResponseToolkit} h 177 | */ 178 | async getAlbumTotalLikesByIdHandler(request, h) { 179 | const { id } = /** @type {{id: string}} */ (request.params); 180 | 181 | const { cached, count } = await this._ualService.getAlbumTotalLikes(id); 182 | 183 | const response = h.response({ 184 | status: 'success', 185 | data: { 186 | likes: count, 187 | }, 188 | }); 189 | 190 | if (cached) response.header('X-Data-Source', 'cache'); 191 | 192 | return response; 193 | } 194 | } 195 | 196 | module.exports = AlbumsHandler; 197 | -------------------------------------------------------------------------------- /src/api/albums/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const AlbumsHandler = require('./handler'); 4 | const routes = require('./routes'); 5 | 6 | /** 7 | * @typedef {import('../../services/_types/AlbumsServiceType').IAlbumsService} IAlbumsService 8 | * @typedef {import('../../services/_types/UserAlbumLikesType').IUserAlbumLikes} IUserAlbumLikes 9 | * @typedef {import('../../services/storage/StorageService')} StorageService 10 | * @typedef {import('../../validator/albums')} AlbumsValidator 11 | */ 12 | 13 | /** 14 | * @typedef {object} IAlbumsPlugin 15 | * @property {IAlbumsService} albumsService 16 | * @property {IUserAlbumLikes} ualService 17 | * @property {StorageService} storageService 18 | * @property {AlbumsValidator} validator 19 | */ 20 | 21 | /** 22 | * @type {import('@hapi/hapi').Plugin} 23 | */ 24 | const albumHapiPlugin = { 25 | name: 'Album Plugin', 26 | version: '1.0.0', 27 | 28 | register: async ( 29 | server, 30 | { albumsService, ualService, storageService, validator } 31 | ) => { 32 | const albumsHandler = new AlbumsHandler( 33 | albumsService, 34 | ualService, 35 | storageService, 36 | validator 37 | ); 38 | 39 | server.route(routes(albumsHandler)); 40 | }, 41 | }; 42 | 43 | module.exports = albumHapiPlugin; 44 | -------------------------------------------------------------------------------- /src/api/albums/routes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const path = require('path'); 4 | 5 | /** 6 | * @param {import('./handler')} handler 7 | * @returns {import('@hapi/hapi').ServerRoute[]} 8 | */ 9 | const routes = (handler) => [ 10 | { 11 | method: 'POST', 12 | path: '/albums', 13 | handler: handler.postAlbumHandler, 14 | }, 15 | { 16 | method: 'POST', 17 | path: '/albums/{id}/covers', 18 | handler: handler.postUploadAlbumCoverHandler, 19 | options: { 20 | payload: { 21 | allow: 'multipart/form-data', 22 | maxBytes: 512000, 23 | multipart: { 24 | output: 'stream', 25 | }, 26 | output: 'stream', 27 | }, 28 | }, 29 | }, 30 | { 31 | method: 'GET', 32 | path: '/albums/{id}', 33 | handler: handler.getAlbumByIdHandler, 34 | }, 35 | { 36 | method: 'PUT', 37 | path: '/albums/{id}', 38 | handler: handler.putAlbumByIdHandler, 39 | }, 40 | { 41 | method: 'DELETE', 42 | path: '/albums/{id}', 43 | handler: handler.deleteAlbumByIdHandler, 44 | }, 45 | { 46 | method: 'POST', 47 | path: '/albums/{id}/likes', 48 | handler: handler.postAlbumLikeOrUnlikeByIdHandler, 49 | options: { 50 | auth: 'openmusic_jwt', 51 | }, 52 | }, 53 | { 54 | method: 'GET', 55 | path: '/albums/{id}/likes', 56 | handler: handler.getAlbumTotalLikesByIdHandler, 57 | }, 58 | { 59 | method: 'GET', 60 | path: '/albums/covers/{param*}', 61 | handler: { 62 | directory: { 63 | path: path.resolve(__dirname, 'file/covers'), 64 | }, 65 | }, 66 | }, 67 | ]; 68 | 69 | module.exports = routes; 70 | -------------------------------------------------------------------------------- /src/api/authentications/handler.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const autoBind = require('auto-bind'); 4 | 5 | /** 6 | * @typedef {import('@hapi/hapi').Request} Request 7 | * @typedef {import('@hapi/hapi').ResponseToolkit} ResponseToolkit 8 | * @typedef {import('../../services/_types/AuthenticationsServiceType').IAuthenticationsService} IAuthenticationsService 9 | * @typedef {import('../../services/_types/UsersServiceType').IUsersService} IUsersService 10 | * @typedef {import('../../tokenize/TokenManager')} TokenManager 11 | * @typedef {import('../../validator/authentications')} AuthenticationsValidator 12 | */ 13 | 14 | class AuthenticationsHandler { 15 | /** 16 | * @readonly 17 | * @private 18 | */ 19 | _authenticationsService; 20 | 21 | /** 22 | * @readonly 23 | * @private 24 | */ 25 | _usersService; 26 | 27 | /** 28 | * @readonly 29 | * @private 30 | */ 31 | _tokenManager; 32 | 33 | /** 34 | * @readonly 35 | * @private 36 | */ 37 | _validator; 38 | 39 | /** 40 | * @param {IAuthenticationsService} authenticationsService 41 | * @param {IUsersService} usersService 42 | * @param {TokenManager} tokenManager 43 | * @param {AuthenticationsValidator} validator 44 | */ 45 | constructor(authenticationsService, usersService, tokenManager, validator) { 46 | this._authenticationsService = authenticationsService; 47 | this._usersService = usersService; 48 | this._tokenManager = tokenManager; 49 | this._validator = validator; 50 | 51 | autoBind(this); 52 | } 53 | 54 | /** 55 | * @param {Request} request 56 | * @param {ResponseToolkit} h 57 | */ 58 | async postAuthenticationHandler(request, h) { 59 | this._validator.validatePostAuthentication(request.payload); 60 | 61 | const { username, password } = 62 | /** @type {{username: string, password: string}} */ (request.payload); 63 | 64 | const userId = await this._usersService.verifyUserCredential( 65 | username, 66 | password 67 | ); 68 | 69 | const accessToken = this._tokenManager.generateAccessToken({ userId }); 70 | const refreshToken = this._tokenManager.generateRefreshToken({ userId }); 71 | 72 | await this._authenticationsService.addRefreshToken(refreshToken); 73 | 74 | const response = h.response({ 75 | status: 'success', 76 | message: 'Authentication behasil ditambahkan', 77 | data: { 78 | accessToken, 79 | refreshToken, 80 | }, 81 | }); 82 | response.code(201); 83 | 84 | return response; 85 | } 86 | 87 | /** 88 | * @param {Request} request 89 | */ 90 | async putAuthenticationHandler(request) { 91 | this._validator.validatePutAuthencation(request.payload); 92 | 93 | const { refreshToken } = /** @type {{refreshToken: string}} */ ( 94 | request.payload 95 | ); 96 | await this._authenticationsService.verifyRefreshToken(refreshToken); 97 | const { userId } = this._tokenManager.verifyRefreshToken(refreshToken); 98 | 99 | const accessToken = this._tokenManager.generateAccessToken({ userId }); 100 | 101 | return { 102 | status: 'success', 103 | message: 'Access Token berhasil diperbarui', 104 | data: { 105 | accessToken, 106 | }, 107 | }; 108 | } 109 | 110 | /** 111 | * @param {Request} request 112 | */ 113 | async deleteAuthenticationHandler(request) { 114 | this._validator.validateDeleteAuthentication(request.payload); 115 | 116 | const { refreshToken } = /** @type {{refreshToken: string}} */ ( 117 | request.payload 118 | ); 119 | await this._authenticationsService.verifyRefreshToken(refreshToken); 120 | await this._authenticationsService.deleteRefreshToken(refreshToken); 121 | 122 | return { 123 | status: 'success', 124 | message: 'Refresh token berhasil dihapus', 125 | }; 126 | } 127 | } 128 | 129 | module.exports = AuthenticationsHandler; 130 | -------------------------------------------------------------------------------- /src/api/authentications/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const AuthenticationsHandler = require('./handler'); 4 | const routes = require('./routes'); 5 | 6 | /** 7 | * @typedef {import('../../services/_types/AuthenticationsServiceType').IAuthenticationsService} IAuthenticationsService 8 | * @typedef {import('../../services/_types/UsersServiceType').IUsersService} IUsersService 9 | * @typedef {import('../../tokenize/TokenManager')} TokenManager 10 | * @typedef {import('../../validator/authentications')} AuthenticationsValidator 11 | */ 12 | 13 | /** 14 | * @typedef {object} IAuthenticationsPlugin 15 | * @property {IAuthenticationsService} authenticationsService 16 | * @property {IUsersService} usersService 17 | * @property {TokenManager} tokenManager 18 | * @property {AuthenticationsValidator} validator 19 | */ 20 | 21 | /** 22 | * @type {import('@hapi/hapi').Plugin} 23 | */ 24 | const authenticationHapiPlugin = { 25 | name: 'Authentication Plugin', 26 | version: '1.0.0', 27 | 28 | register: async ( 29 | server, 30 | { authenticationsService, usersService, tokenManager, validator } 31 | ) => { 32 | const authenticationsHandler = new AuthenticationsHandler( 33 | authenticationsService, 34 | usersService, 35 | tokenManager, 36 | validator 37 | ); 38 | 39 | server.route(routes(authenticationsHandler)); 40 | }, 41 | }; 42 | 43 | module.exports = authenticationHapiPlugin; 44 | -------------------------------------------------------------------------------- /src/api/authentications/routes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import('./handler')} handler 5 | * @returns {import('@hapi/hapi').ServerRoute[]} 6 | */ 7 | const routes = (handler) => [ 8 | { 9 | method: 'POST', 10 | path: '/authentications', 11 | handler: handler.postAuthenticationHandler, 12 | }, 13 | { 14 | method: 'PUT', 15 | path: '/authentications', 16 | handler: handler.putAuthenticationHandler, 17 | }, 18 | { 19 | method: 'DELETE', 20 | path: '/authentications', 21 | handler: handler.deleteAuthenticationHandler, 22 | }, 23 | ]; 24 | 25 | module.exports = routes; 26 | -------------------------------------------------------------------------------- /src/api/collaborations/handler.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const autoBind = require('auto-bind'); 4 | 5 | /** 6 | * @typedef {import('@hapi/hapi').Request} Request 7 | * @typedef {import('@hapi/hapi').ResponseToolkit} ResponseToolkit 8 | * @typedef {import('../../services/_types/CollaborationsServiceType').ICollaborationsService} ICollaborationsService 9 | * @typedef {import('../../services/_types/CollaborationsServiceType').ICollaborationEntity} ICollaborationEntity 10 | * @typedef {import('../../validator/collaborations')} CollaborationsValidator 11 | * @typedef {import('../../services/_types/PlaylistsServiceType').IPlaylistsService} IPlaylistsService 12 | */ 13 | 14 | class CollaborationsHandler { 15 | /** 16 | * @readonly 17 | * @private 18 | */ 19 | _collaborationsService; 20 | 21 | /** 22 | * @readonly 23 | * @private 24 | */ 25 | _playlistsService; 26 | 27 | /** 28 | * @readonly 29 | * @private 30 | */ 31 | _validator; 32 | 33 | /** 34 | * @param {ICollaborationsService} collaborationsService 35 | * @param {IPlaylistsService} playlistsService 36 | * @param {CollaborationsValidator} validator 37 | */ 38 | constructor(collaborationsService, playlistsService, validator) { 39 | this._collaborationsService = collaborationsService; 40 | this._playlistsService = playlistsService; 41 | this._validator = validator; 42 | 43 | autoBind(this); 44 | } 45 | 46 | /** 47 | * @param {Request} request 48 | * @param {ResponseToolkit} h 49 | */ 50 | async postCollaborationHandler(request, h) { 51 | this._validator.validatePostCollaboration(request.payload); 52 | 53 | const { playlistId, userId } = /** @type {ICollaborationEntity} */ ( 54 | request.payload 55 | ); 56 | const { userId: owner } = /** @type {{userId: string}} */ ( 57 | request.auth.credentials 58 | ); 59 | 60 | await this._playlistsService.verifyPlaylistAccess({ 61 | playlistId, 62 | userId: owner, 63 | }); 64 | const collaborationId = await this._collaborationsService.addCollaboration({ 65 | playlistId, 66 | userId, 67 | }); 68 | 69 | const response = h.response({ 70 | status: 'success', 71 | data: { 72 | collaborationId, 73 | }, 74 | }); 75 | response.code(201); 76 | 77 | return response; 78 | } 79 | 80 | /** 81 | * @param {Request} request 82 | */ 83 | async deleteCollaborationHandler(request) { 84 | this._validator.validatePostCollaboration(request.payload); 85 | 86 | const { playlistId, userId } = /** @type {ICollaborationEntity} */ ( 87 | request.payload 88 | ); 89 | const { userId: owner } = /** @type {{userId: string}} */ ( 90 | request.auth.credentials 91 | ); 92 | 93 | await this._playlistsService.verifyPlaylistOwner({ 94 | owner, 95 | id: playlistId, 96 | }); 97 | await this._collaborationsService.deleteCollaboration({ 98 | playlistId, 99 | userId, 100 | }); 101 | 102 | return { 103 | status: 'success', 104 | message: 'Collaborator berhasil dihapus', 105 | }; 106 | } 107 | } 108 | 109 | module.exports = CollaborationsHandler; 110 | -------------------------------------------------------------------------------- /src/api/collaborations/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const CollaborationsHandler = require('./handler'); 4 | const routes = require('./routes'); 5 | 6 | /** 7 | * @typedef {import('../../services/_types/CollaborationsServiceType').ICollaborationsService} ICollaborationsService 8 | * @typedef {import('../../services/_types/PlaylistsServiceType').IPlaylistsService} IPlaylistsService 9 | * @typedef {import('../../validator/collaborations')} CollaborationsValidator 10 | */ 11 | 12 | /** 13 | * @typedef {object} ICollaborationsPlugin 14 | * @property {ICollaborationsService} collaborationsService 15 | * @property {IPlaylistsService} playlistsService 16 | * @property {CollaborationsValidator} validator 17 | */ 18 | 19 | /** 20 | * @type {import('@hapi/hapi').Plugin} 21 | */ 22 | const collaborationHapiPlugin = { 23 | name: 'Collaborations Plugin', 24 | version: '1.0.0', 25 | 26 | register: async ( 27 | server, 28 | { collaborationsService, playlistsService, validator } 29 | ) => { 30 | const collaborationsHandler = new CollaborationsHandler( 31 | collaborationsService, 32 | playlistsService, 33 | validator 34 | ); 35 | 36 | server.route(routes(collaborationsHandler)); 37 | }, 38 | }; 39 | 40 | module.exports = collaborationHapiPlugin; 41 | -------------------------------------------------------------------------------- /src/api/collaborations/routes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import('./handler')} handler 5 | * @returns {import('@hapi/hapi').ServerRoute[]} 6 | */ 7 | const routes = (handler) => [ 8 | { 9 | method: 'POST', 10 | path: '/collaborations', 11 | handler: handler.postCollaborationHandler, 12 | options: { 13 | auth: 'openmusic_jwt', 14 | }, 15 | }, 16 | { 17 | method: 'DELETE', 18 | path: '/collaborations', 19 | handler: handler.deleteCollaborationHandler, 20 | options: { 21 | auth: 'openmusic_jwt', 22 | }, 23 | }, 24 | ]; 25 | 26 | module.exports = routes; 27 | -------------------------------------------------------------------------------- /src/api/exports/handler.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const autoBind = require('auto-bind'); 4 | 5 | /** 6 | * @typedef {import('@hapi/hapi').Request} Request 7 | * @typedef {import('@hapi/hapi').ResponseToolkit} ResponseToolkit 8 | * @typedef {import('../../services/_types/PlaylistsServiceType').IPlaylistsService} IPlaylistsService 9 | */ 10 | 11 | class ExportsHandler { 12 | /** 13 | * @readonly 14 | * @private 15 | */ 16 | _producerService; 17 | 18 | /** 19 | * @readonly 20 | * @private 21 | */ 22 | _playlistsService; 23 | 24 | /** 25 | * @readonly 26 | * @private 27 | */ 28 | _validator; 29 | 30 | /** 31 | * @readonly 32 | * @private 33 | */ 34 | _queueName = 'export:playlist'; 35 | 36 | /** 37 | * @param {import('../../services/rabbitmq/ProducerService')} producerService 38 | * @param {IPlaylistsService} playlistsService 39 | * @param {import('../../validator/exports')} validator 40 | */ 41 | constructor(producerService, playlistsService, validator) { 42 | this._producerService = producerService; 43 | this._playlistsService = playlistsService; 44 | this._validator = validator; 45 | 46 | autoBind(this); 47 | } 48 | 49 | /** 50 | * @param {Request} request 51 | * @param {ResponseToolkit} h 52 | */ 53 | async postExportPlaylistHandler(request, h) { 54 | this._validator.ValidateExportPlaylist(request.payload); 55 | 56 | const { playlistId } = /** @type {{playlistId: string}} */ (request.params); 57 | const { targetEmail } = /** @type {{targetEmail: string}} */ ( 58 | request.payload 59 | ); 60 | const { userId } = /** @type {{userId: string}} */ ( 61 | request.auth.credentials 62 | ); 63 | 64 | await this._playlistsService.verifyPlaylistOwner({ 65 | owner: userId, 66 | id: playlistId, 67 | }); 68 | 69 | const message = { 70 | targetEmail, 71 | playlistId, 72 | }; 73 | 74 | await this._producerService.sendMessage( 75 | this._queueName, 76 | JSON.stringify(message) 77 | ); 78 | 79 | const response = h.response({ 80 | status: 'success', 81 | message: 'Permintaan Anda sedang kami proses', 82 | }); 83 | response.code(201); 84 | 85 | return response; 86 | } 87 | } 88 | 89 | module.exports = ExportsHandler; 90 | -------------------------------------------------------------------------------- /src/api/exports/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const ExportsHandler = require('./handler'); 4 | const routes = require('./routes'); 5 | 6 | /** 7 | * @typedef {import('../../services/rabbitmq/ProducerService')} ProducerService 8 | * @typedef {import('../../services/_types/PlaylistsServiceType').IPlaylistsService} IPlaylistsService 9 | * @typedef {import('../../validator/exports')} ExportsValidator 10 | */ 11 | 12 | /** 13 | * @typedef {object} IExportsPlugin 14 | * @property {ProducerService} producerService 15 | * @property {IPlaylistsService} playlistsService 16 | * @property {ExportsValidator} validator 17 | */ 18 | 19 | /** 20 | * @type {import('@hapi/hapi').Plugin} 21 | */ 22 | const exportHapiPlugin = { 23 | name: 'Exports Plugin', 24 | version: '1.0.0', 25 | 26 | register: async ( 27 | server, 28 | { producerService, playlistsService, validator } 29 | ) => { 30 | const exportsHandler = new ExportsHandler( 31 | producerService, 32 | playlistsService, 33 | validator 34 | ); 35 | 36 | server.route(routes(exportsHandler)); 37 | }, 38 | }; 39 | 40 | module.exports = exportHapiPlugin; 41 | -------------------------------------------------------------------------------- /src/api/exports/routes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import('./handler')} handler 5 | * @returns {import('@hapi/hapi').ServerRoute[]} 6 | */ 7 | const routes = (handler) => [ 8 | { 9 | method: 'POST', 10 | path: '/export/playlists/{playlistId}', 11 | handler: handler.postExportPlaylistHandler, 12 | options: { 13 | auth: 'openmusic_jwt', 14 | }, 15 | }, 16 | ]; 17 | 18 | module.exports = routes; 19 | -------------------------------------------------------------------------------- /src/api/playlists/handler.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const autoBind = require('auto-bind'); 4 | 5 | /** 6 | * @typedef {import('@hapi/hapi').Request} Request 7 | * @typedef {import('@hapi/hapi').ResponseToolkit} ResponseToolkit 8 | * @typedef {import('../../services/_types/PlaylistsServiceType').IPlaylistsService} IPlaylistsService 9 | * @typedef {import('../../services/_types/PlaylistSongsServiceType').IPlaylistSongsService} IPlaylistSongsService 10 | * @typedef {import('../../services/_types/PlaylistSongActivitiesServiceType').IPSAService} IPSAService 11 | */ 12 | 13 | class PlaylistsHandler { 14 | /** 15 | * @readonly 16 | * @private 17 | */ 18 | _playlistsService; 19 | 20 | /** 21 | * @readonly 22 | * @private 23 | */ 24 | _playlistSongsService; 25 | 26 | /** 27 | * @readonly 28 | * @private 29 | */ 30 | _psaService; 31 | 32 | /** 33 | * @readonly 34 | * @private 35 | */ 36 | _validator; 37 | 38 | /** 39 | * @param {IPlaylistsService} playlistsService 40 | * @param {IPlaylistSongsService} playlistSongsService 41 | * @param {IPSAService} psaService 42 | * @param {import('../../validator/playlists')} validator 43 | */ 44 | constructor(playlistsService, playlistSongsService, psaService, validator) { 45 | this._playlistsService = playlistsService; 46 | this._playlistSongsService = playlistSongsService; 47 | this._psaService = psaService; 48 | this._validator = validator; 49 | 50 | autoBind(this); 51 | } 52 | 53 | /** 54 | * @param {Request} request 55 | * @param {ResponseToolkit} h 56 | */ 57 | async postPlaylistHandler(request, h) { 58 | this._validator.validateCreatePlaylist(request.payload); 59 | 60 | const { name } = /** @type {{name: string}} */ (request.payload); 61 | const { userId: owner } = /** @type {{userId: string}} */ ( 62 | request.auth.credentials 63 | ); 64 | 65 | const playlistId = await this._playlistsService.addPlaylist({ 66 | owner, 67 | name, 68 | }); 69 | 70 | const response = h.response({ 71 | status: 'success', 72 | message: 'Playlist berhasil ditambahkan', 73 | data: { 74 | playlistId, 75 | }, 76 | }); 77 | response.code(201); 78 | 79 | return response; 80 | } 81 | 82 | /** 83 | * @param {Request} request 84 | */ 85 | async getPlaylistsHandler(request) { 86 | const { userId: owner } = /** @type {{userId: string}} */ ( 87 | request.auth.credentials 88 | ); 89 | 90 | const playlists = await this._playlistsService.getPlaylists({ owner }); 91 | 92 | return { 93 | status: 'success', 94 | data: { 95 | playlists, 96 | }, 97 | }; 98 | } 99 | 100 | /** 101 | * @param {Request} request 102 | */ 103 | async deletePlaylistByIdHandler(request) { 104 | const { id } = /** @type {{id: string}} */ (request.params); 105 | const { userId: owner } = /** @type {{userId: string}} */ ( 106 | request.auth.credentials 107 | ); 108 | 109 | await this._playlistsService.verifyPlaylistOwner({ 110 | owner, 111 | id, 112 | }); 113 | await this._playlistsService.deletePlaylistById({ id }); 114 | 115 | return { 116 | status: 'success', 117 | message: 'Playlist berhasil dihapus', 118 | }; 119 | } 120 | 121 | /** 122 | * @param {Request} request 123 | * @param {ResponseToolkit} h 124 | */ 125 | async postPlaylistSongHandler(request, h) { 126 | this._validator.validateAddPlaylistSong(request.payload); 127 | 128 | const { id: playlistId } = /** @type {{id: string}} */ (request.params); 129 | const { songId } = /** @type {{songId: string}} */ (request.payload); 130 | const { userId } = /** @type {{userId: string}} */ ( 131 | request.auth.credentials 132 | ); 133 | 134 | await this._playlistsService.verifyPlaylistAccess({ userId, playlistId }); 135 | await this._playlistSongsService.addPlaylistSong({ 136 | playlistId, 137 | songId, 138 | userId, 139 | }); 140 | 141 | const response = h.response({ 142 | status: 'success', 143 | message: 'Song berhasil ditambahkan ke playlist', 144 | }); 145 | response.code(201); 146 | 147 | return response; 148 | } 149 | 150 | /** 151 | * @param {Request} request 152 | */ 153 | async getPlaylistByIdHandler(request) { 154 | const { id } = /** @type {{id: string}} */ (request.params); 155 | const { userId } = /** @type {{userId: string}} */ ( 156 | request.auth.credentials 157 | ); 158 | 159 | await this._playlistsService.verifyPlaylistAccess({ 160 | userId, 161 | playlistId: id, 162 | }); 163 | const playlist = await this._playlistsService.getPlaylistById({ id }); 164 | 165 | return { 166 | status: 'success', 167 | data: { 168 | playlist, 169 | }, 170 | }; 171 | } 172 | 173 | /** 174 | * @param {Request} request 175 | */ 176 | async deletePlaylistSongByIdHandler(request) { 177 | this._validator.validateAddPlaylistSong(request.payload); 178 | 179 | const { id: playlistId } = /** @type {{id: string}} */ (request.params); 180 | const { userId } = /** @type {{userId: string}} */ ( 181 | request.auth.credentials 182 | ); 183 | const { songId } = /** @type {{songId: string}} */ (request.payload); 184 | 185 | await this._playlistsService.verifyPlaylistAccess({ userId, playlistId }); 186 | await this._playlistSongsService.deletePlaylistSongById({ 187 | playlistId, 188 | songId, 189 | userId, 190 | }); 191 | 192 | return { 193 | status: 'success', 194 | message: 'Song berhasil dihapus dari palylist', 195 | }; 196 | } 197 | 198 | /** 199 | * @param {Request} request 200 | */ 201 | async getPlaylistActivitiesByIdHandler(request) { 202 | const { id: playlistId } = /** @type {{id: string}} */ (request.params); 203 | const { userId } = /** @type {{userId: string}} */ ( 204 | request.auth.credentials 205 | ); 206 | 207 | await this._playlistsService.verifyPlaylistAccess({ userId, playlistId }); 208 | 209 | const activities = await this._psaService.getActivitiesByPlaylistId({ 210 | playlistId, 211 | }); 212 | 213 | return { 214 | status: 'success', 215 | data: { 216 | playlistId, 217 | activities, 218 | }, 219 | }; 220 | } 221 | } 222 | 223 | module.exports = PlaylistsHandler; 224 | -------------------------------------------------------------------------------- /src/api/playlists/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const PlaylistsHandler = require('./handler'); 4 | const routes = require('./routes'); 5 | 6 | /** 7 | * @typedef {import('../../services/_types/PlaylistsServiceType').IPlaylistsService} IPlaylistsService 8 | * @typedef {import('../../services/_types/PlaylistSongsServiceType').IPlaylistSongsService} IPlaylistSongsService 9 | * @typedef {import('../../services/_types/PlaylistSongActivitiesServiceType').IPSAService} IPSAService 10 | * @typedef {import('../../validator/playlists')} PlaylistsValidator 11 | */ 12 | 13 | /** 14 | * @typedef {object} IPlaylistsPlugin 15 | * @property {IPlaylistsService} playlistsService 16 | * @property {IPlaylistSongsService} playlistSongsService 17 | * @property {IPSAService} psaService 18 | * @property {PlaylistsValidator} validator 19 | */ 20 | 21 | /** 22 | * @type {import('@hapi/hapi').Plugin} 23 | */ 24 | const playlistHapiPlugin = { 25 | name: 'Playlist Plugin', 26 | version: '1.0.0', 27 | 28 | register: async ( 29 | server, 30 | { playlistsService, playlistSongsService, psaService, validator } 31 | ) => { 32 | const playlistsHandler = new PlaylistsHandler( 33 | playlistsService, 34 | playlistSongsService, 35 | psaService, 36 | validator 37 | ); 38 | 39 | server.route(routes(playlistsHandler)); 40 | }, 41 | }; 42 | 43 | module.exports = playlistHapiPlugin; 44 | -------------------------------------------------------------------------------- /src/api/playlists/routes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import('./handler')} handler 5 | * @returns {import('@hapi/hapi').ServerRoute[]} 6 | */ 7 | const routes = (handler) => [ 8 | { 9 | method: 'POST', 10 | path: '/playlists', 11 | handler: handler.postPlaylistHandler, 12 | options: { 13 | auth: 'openmusic_jwt', 14 | }, 15 | }, 16 | { 17 | method: 'GET', 18 | path: '/playlists', 19 | handler: handler.getPlaylistsHandler, 20 | options: { 21 | auth: 'openmusic_jwt', 22 | }, 23 | }, 24 | { 25 | method: 'DELETE', 26 | path: '/playlists/{id}', 27 | handler: handler.deletePlaylistByIdHandler, 28 | options: { 29 | auth: 'openmusic_jwt', 30 | }, 31 | }, 32 | { 33 | method: 'POST', 34 | path: '/playlists/{id}/songs', 35 | handler: handler.postPlaylistSongHandler, 36 | options: { 37 | auth: 'openmusic_jwt', 38 | }, 39 | }, 40 | { 41 | method: 'GET', 42 | path: '/playlists/{id}/songs', 43 | handler: handler.getPlaylistByIdHandler, 44 | options: { 45 | auth: 'openmusic_jwt', 46 | }, 47 | }, 48 | { 49 | method: 'DELETE', 50 | path: '/playlists/{id}/songs', 51 | handler: handler.deletePlaylistSongByIdHandler, 52 | options: { 53 | auth: 'openmusic_jwt', 54 | }, 55 | }, 56 | { 57 | method: 'GET', 58 | path: '/playlists/{id}/activities', 59 | handler: handler.getPlaylistActivitiesByIdHandler, 60 | options: { 61 | auth: 'openmusic_jwt', 62 | }, 63 | }, 64 | ]; 65 | 66 | module.exports = routes; 67 | -------------------------------------------------------------------------------- /src/api/songs/handler.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const autoBind = require('auto-bind'); 4 | 5 | /** 6 | * @typedef {import('@hapi/hapi').Request} Request 7 | * @typedef {import('@hapi/hapi').ResponseToolkit} ResponseToolkit 8 | * @typedef {import('../../services/_types/SongsServiceType').ISongEntity} ISongEntity 9 | * @typedef {import('../../services/_types/SongsServiceType').ISongsService} ISongsService 10 | * @typedef {import('../../services/_types/SongsServiceType').ISongsGetQuery} ISongsGetQuery 11 | */ 12 | 13 | class SongsHandler { 14 | /** 15 | * @readonly 16 | * @private 17 | */ 18 | _service; 19 | 20 | /** 21 | * @readonly 22 | * @private 23 | */ 24 | _validator; 25 | 26 | /** 27 | * 28 | * @param {ISongsService} service 29 | * @param {import('../../validator/songs')} validator 30 | */ 31 | constructor(service, validator) { 32 | this._service = service; 33 | this._validator = validator; 34 | 35 | autoBind(this); 36 | } 37 | 38 | /** 39 | * @param {Request} request 40 | * @param {ResponseToolkit} h 41 | */ 42 | async postSongHandler(request, h) { 43 | this._validator.validateSongPayload(request.payload); 44 | 45 | const songId = await this._service.addSong( 46 | /** @type {ISongEntity} */ (request.payload) 47 | ); 48 | 49 | const response = h.response({ 50 | status: 'success', 51 | message: 'Song berhasil ditambahkan', 52 | data: { 53 | songId, 54 | }, 55 | }); 56 | response.code(201); 57 | 58 | return response; 59 | } 60 | 61 | /** 62 | * @param {Request} request 63 | */ 64 | async getSongsHandler(request) { 65 | const { title, performer } = /** @type {ISongsGetQuery} */ (request.query); 66 | 67 | const songs = await this._service.getSongs({ title, performer }); 68 | 69 | return { 70 | status: 'success', 71 | data: { 72 | songs, 73 | }, 74 | }; 75 | } 76 | 77 | /** 78 | * @param {Request} request 79 | */ 80 | async getSongByIdHandler(request) { 81 | const { id } = /** @type {{id: string}} */ (request.params); 82 | 83 | const song = await this._service.getSongById(id); 84 | 85 | return { 86 | status: 'success', 87 | data: { 88 | song, 89 | }, 90 | }; 91 | } 92 | 93 | /** 94 | * @param {Request} request 95 | */ 96 | async putSongByIdHandler(request) { 97 | this._validator.validateSongPayload(request.payload); 98 | const { id } = /** @type {{id:string}} */ (request.params); 99 | 100 | await this._service.editSongById( 101 | id, 102 | /** @type {ISongEntity} */ (request.payload) 103 | ); 104 | 105 | return { 106 | status: 'success', 107 | message: 'Song berhasil diperbarui', 108 | }; 109 | } 110 | 111 | /** 112 | * @param {Request} request 113 | */ 114 | async deleteAlbumByIdHandler(request) { 115 | const { id } = /** @type {{id: string}} */ (request.params); 116 | 117 | await this._service.deleteSongById(id); 118 | 119 | return { 120 | status: 'success', 121 | message: 'Song berhasil dihapus', 122 | }; 123 | } 124 | } 125 | 126 | module.exports = SongsHandler; 127 | -------------------------------------------------------------------------------- /src/api/songs/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const routes = require('./routes'); 4 | const SongsHandler = require('./handler'); 5 | 6 | /** 7 | * @typedef {import('../../services/_types/SongsServiceType').ISongsService} ISongsService 8 | * @typedef {import('../../validator/songs')} SongsValidator 9 | */ 10 | 11 | /** 12 | * @typedef {object} ISongsPlugin 13 | * @property {ISongsService} service 14 | * @property {SongsValidator} validator 15 | */ 16 | 17 | /** 18 | * @type {import('@hapi/hapi').Plugin} 19 | */ 20 | const songHapiPlugin = { 21 | name: 'Song Plugin', 22 | version: '1.0.0', 23 | 24 | register: async (server, { service, validator }) => { 25 | const songsHandler = new SongsHandler(service, validator); 26 | 27 | server.route(routes(songsHandler)); 28 | }, 29 | }; 30 | 31 | module.exports = songHapiPlugin; 32 | -------------------------------------------------------------------------------- /src/api/songs/routes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import('./handler')} handler 5 | * @returns {import('@hapi/hapi').ServerRoute[]} 6 | */ 7 | const routes = (handler) => [ 8 | { 9 | method: 'POST', 10 | path: '/songs', 11 | handler: handler.postSongHandler, 12 | }, 13 | { 14 | method: 'GET', 15 | path: '/songs', 16 | handler: handler.getSongsHandler, 17 | }, 18 | { 19 | method: 'GET', 20 | path: '/songs/{id}', 21 | handler: handler.getSongByIdHandler, 22 | }, 23 | { 24 | method: 'PUT', 25 | path: '/songs/{id}', 26 | handler: handler.putSongByIdHandler, 27 | }, 28 | { 29 | method: 'DELETE', 30 | path: '/songs/{id}', 31 | handler: handler.deleteAlbumByIdHandler, 32 | }, 33 | ]; 34 | 35 | module.exports = routes; 36 | -------------------------------------------------------------------------------- /src/api/users/handler.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const autoBind = require('auto-bind'); 4 | 5 | /** 6 | * @typedef {import('@hapi/hapi').Request} Request 7 | * @typedef {import('@hapi/hapi').ResponseToolkit} ResponseToolkit 8 | * @typedef {import('../../services/_types/UsersServiceType').IUserEntity} IUserEntity 9 | * @typedef {import('../../services/_types/UsersServiceType').IUsersService} IUsersService 10 | */ 11 | 12 | class UsersHandler { 13 | /** 14 | * @readonly 15 | * @private 16 | */ 17 | _service; 18 | 19 | /** 20 | * @readonly 21 | * @private 22 | */ 23 | _validator; 24 | 25 | /** 26 | * @param {IUsersService} service 27 | * @param {import('../../validator/users')} validator 28 | */ 29 | constructor(service, validator) { 30 | this._service = service; 31 | this._validator = validator; 32 | 33 | autoBind(this); 34 | } 35 | 36 | /** 37 | * @param {Request} request 38 | * @param {ResponseToolkit} h 39 | */ 40 | async postUserHandler(request, h) { 41 | this._validator.validateUserPayload(request.payload); 42 | 43 | const { username, password, fullname } = /** @type {IUserEntity} */ ( 44 | request.payload 45 | ); 46 | 47 | const userId = await this._service.addUser({ 48 | username, 49 | password, 50 | fullname, 51 | }); 52 | 53 | const response = h.response({ 54 | status: 'success', 55 | message: 'User berhasil ditambahkan', 56 | data: { 57 | userId, 58 | }, 59 | }); 60 | response.code(201); 61 | 62 | return response; 63 | } 64 | 65 | /** 66 | * @param {Request} request 67 | */ 68 | async getUserByIdHandler(request) { 69 | const { id } = /** @type {{id: string}} */ (request.params); 70 | 71 | const user = await this._service.getUserById(id); 72 | 73 | return { 74 | status: 'success', 75 | data: { 76 | user, 77 | }, 78 | }; 79 | } 80 | } 81 | 82 | module.exports = UsersHandler; 83 | -------------------------------------------------------------------------------- /src/api/users/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const routes = require('./routes'); 4 | const UsersHandler = require('./handler'); 5 | 6 | /** 7 | * @typedef {import('../../services/_types/UsersServiceType').IUsersService} IUsersService 8 | * @typedef {import('../../validator/users')} UsersValidator 9 | */ 10 | 11 | /** 12 | * @typedef {object} IUsersPlugin 13 | * @property {IUsersService} service 14 | * @property {UsersValidator} validator 15 | */ 16 | 17 | /** 18 | * @type {import('@hapi/hapi').Plugin} 19 | */ 20 | const userHapiPlugin = { 21 | name: 'User Plugin', 22 | version: '1.0.0', 23 | 24 | register: async (server, { service, validator }) => { 25 | const usersHandler = new UsersHandler(service, validator); 26 | 27 | server.route(routes(usersHandler)); 28 | }, 29 | }; 30 | 31 | module.exports = userHapiPlugin; 32 | -------------------------------------------------------------------------------- /src/api/users/routes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @param {import('./handler')} handler 5 | * @returns {import('@hapi/hapi').ServerRoute[]} 6 | */ 7 | const routes = (handler) => [ 8 | { 9 | method: 'POST', 10 | path: '/users', 11 | handler: handler.postUserHandler, 12 | }, 13 | { 14 | method: 'GET', 15 | path: '/users/{id}', 16 | handler: handler.getUserByIdHandler, 17 | }, 18 | ]; 19 | 20 | module.exports = routes; 21 | -------------------------------------------------------------------------------- /src/exceptions/AuthenticationError.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const ClientError = require('./ClientError'); 4 | 5 | class AuthenticationError extends ClientError { 6 | /** @param {string} message */ 7 | constructor(message) { 8 | super(message, 401); 9 | this.name = 'Authentication Error'; 10 | } 11 | } 12 | 13 | module.exports = AuthenticationError; 14 | -------------------------------------------------------------------------------- /src/exceptions/AuthorizationError.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const ClientError = require('./ClientError'); 4 | 5 | class AuthorizationError extends ClientError { 6 | /** @param {string} message */ 7 | constructor(message) { 8 | super(message, 403); 9 | this.name = 'Authorization Error'; 10 | } 11 | } 12 | 13 | module.exports = AuthorizationError; 14 | -------------------------------------------------------------------------------- /src/exceptions/ClientError.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | class ClientError extends Error { 4 | /** 5 | * @param {string} message 6 | * @param {number} statusCode 7 | */ 8 | constructor(message, statusCode = 400) { 9 | super(message); 10 | this.statusCode = statusCode; 11 | this.name = 'Client Error'; 12 | } 13 | } 14 | 15 | module.exports = ClientError; 16 | -------------------------------------------------------------------------------- /src/exceptions/InvariantError.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const ClientError = require('./ClientError'); 4 | 5 | class InvariantError extends ClientError { 6 | /** @param {string} message */ 7 | constructor(message) { 8 | super(message); 9 | this.name = 'Invariant Error'; 10 | } 11 | } 12 | 13 | module.exports = InvariantError; 14 | -------------------------------------------------------------------------------- /src/exceptions/NotFoundError.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const ClientError = require('./ClientError'); 4 | 5 | class NotFoundError extends ClientError { 6 | /** @param {string} message */ 7 | constructor(message) { 8 | super(message, 404); 9 | this.name = 'Not Found Error'; 10 | } 11 | } 12 | 13 | module.exports = NotFoundError; 14 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | require('dotenv').config(); 4 | 5 | const Hapi = require('@hapi/hapi'); 6 | const Inert = require('@hapi/inert'); 7 | const Jwt = require('@hapi/jwt'); 8 | const path = require('path'); 9 | 10 | const config = require('./utils/config'); 11 | 12 | const albumHapiPlugin = require('./api/albums'); 13 | const AlbumsService = require('./services/postgres/AlbumsService'); 14 | const AlbumsValidator = require('./validator/albums'); 15 | 16 | const authenticationsHapiPlugin = require('./api/authentications'); 17 | const AuthenticationsService = require('./services/postgres/AuthenticationsService'); 18 | const AuthenticationsValidator = require('./validator/authentications'); 19 | 20 | const CacheService = require('./services/redis/CacheService'); 21 | 22 | const collaborationHapiPlugin = require('./api/collaborations'); 23 | const CollaborationsService = require('./services/postgres/CollaborationsService'); 24 | const CollaborationsValidator = require('./validator/collaborations'); 25 | 26 | const exportHapiPlugin = require('./api/exports'); 27 | const ExportsValidator = require('./validator/exports'); 28 | 29 | const playlistHapiPlugin = require('./api/playlists'); 30 | const PlaylistSongsService = require('./services/postgres/PlaylistSongsService'); 31 | const PlaylistsService = require('./services/postgres/PlaylistsService'); 32 | const PlaylistsValidator = require('./validator/playlists'); 33 | 34 | const ProducerService = require('./services/rabbitmq/ProducerService'); 35 | 36 | const PSAService = require('./services/postgres/PlaylistSongActivitiesService'); 37 | 38 | const songHapiPlugin = require('./api/songs'); 39 | const SongsService = require('./services/postgres/SongsService'); 40 | const SongsValidator = require('./validator/songs'); 41 | 42 | const StorageService = require('./services/storage/StorageService'); 43 | 44 | const UALService = require('./services/postgres/UserAlbumLikes'); 45 | 46 | const userHapiPlugin = require('./api/users'); 47 | const UsersService = require('./services/postgres/UsersService'); 48 | const UsersValidator = require('./validator/users'); 49 | 50 | const { hapiErrorHandler } = require('./utils/HapiErrorHandler'); 51 | 52 | const TokenManager = require('./tokenize/TokenManager'); 53 | 54 | const init = async () => { 55 | const server = Hapi.server({ 56 | host: config.app.host, 57 | port: config.app.port, 58 | routes: { 59 | cors: { 60 | origin: ['*'], 61 | }, 62 | }, 63 | }); 64 | 65 | const albumsService = new AlbumsService(); 66 | const cacheService = new CacheService(); 67 | const ualService = new UALService(cacheService); 68 | const songsService = new SongsService(); 69 | const usersService = new UsersService(); 70 | const authenticationsService = new AuthenticationsService(); 71 | const collaborationsService = new CollaborationsService(); 72 | const psaService = new PSAService(); 73 | const playlistsService = new PlaylistsService(collaborationsService); 74 | const playlistSongsService = new PlaylistSongsService(psaService); 75 | const storageService = new StorageService( 76 | path.resolve(__dirname, 'api/albums/file/covers') 77 | ); 78 | 79 | await server.register([Jwt.plugin, Inert]); 80 | 81 | server.auth.strategy('openmusic_jwt', 'jwt', { 82 | keys: config.jwtToken.access_token_key, 83 | verify: { 84 | aud: false, 85 | iss: false, 86 | sub: false, 87 | maxAgeSec: config.jwtToken.access_token_age, 88 | }, 89 | validate: (artifacts) => { 90 | return { 91 | isValid: true, 92 | credentials: { 93 | userId: artifacts.decoded.payload.userId, 94 | }, 95 | }; 96 | }, 97 | }); 98 | 99 | await server.register([ 100 | { 101 | plugin: albumHapiPlugin, 102 | options: { 103 | albumsService, 104 | ualService, 105 | storageService, 106 | validator: AlbumsValidator, 107 | }, 108 | }, 109 | { 110 | plugin: songHapiPlugin, 111 | options: { 112 | service: songsService, 113 | validator: SongsValidator, 114 | }, 115 | }, 116 | { 117 | plugin: userHapiPlugin, 118 | options: { 119 | service: usersService, 120 | validator: UsersValidator, 121 | }, 122 | }, 123 | { 124 | plugin: authenticationsHapiPlugin, 125 | options: { 126 | authenticationsService, 127 | usersService, 128 | tokenManager: TokenManager, 129 | validator: AuthenticationsValidator, 130 | }, 131 | }, 132 | { 133 | plugin: playlistHapiPlugin, 134 | options: { 135 | playlistsService, 136 | playlistSongsService, 137 | psaService, 138 | validator: PlaylistsValidator, 139 | }, 140 | }, 141 | { 142 | plugin: collaborationHapiPlugin, 143 | options: { 144 | collaborationsService, 145 | playlistsService, 146 | validator: CollaborationsValidator, 147 | }, 148 | }, 149 | { 150 | plugin: exportHapiPlugin, 151 | options: { 152 | playlistsService, 153 | producerService: ProducerService, 154 | validator: ExportsValidator, 155 | }, 156 | }, 157 | ]); 158 | 159 | server.ext('onPreResponse', (request, h) => { 160 | if (request.response instanceof Error) { 161 | return hapiErrorHandler(h, request.response); 162 | } 163 | 164 | return h.continue; 165 | }); 166 | 167 | await server.start(); 168 | console.log(`Server running on ${server.info.uri}`); 169 | }; 170 | 171 | init(); 172 | -------------------------------------------------------------------------------- /src/services/_types/AlbumsServiceType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Album entity interface 5 | * 6 | * @typedef {object} IAlbumEntity 7 | * @property {string} name 8 | * @property {number} year 9 | */ 10 | 11 | /** 12 | * Albums Service interface 13 | * 14 | * @typedef {object} IAlbumsService 15 | * @property {function(IAlbumEntity): Promise | string} addAlbum 16 | * @property {function(): Promise | Array} getAlbums 17 | * @property {function(string): Promise | any} getAlbumById 18 | * @property {function(string, IAlbumEntity): Promise | void} editAlbumById 19 | * @property {function(string, string): Promise | void} editAlbumCoverById 20 | * @property {function(string): Promise | void} deleteAlbumById 21 | */ 22 | 23 | module.exports = {}; 24 | -------------------------------------------------------------------------------- /src/services/_types/AuthenticationsServiceType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {object} IAuthenticationsService 5 | * @property {function(string): Promise | void} addRefreshToken 6 | * @property {function(string): Promise | void} verifyRefreshToken 7 | * @property {function(string): Promise | void} deleteRefreshToken 8 | */ 9 | 10 | module.exports = {}; 11 | -------------------------------------------------------------------------------- /src/services/_types/CollaborationsServiceType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {object} ICollaborationEntity 5 | * @property {string} playlistId 6 | * @property {string} userId 7 | */ 8 | 9 | /** 10 | * @typedef {object} ICollaborationsService 11 | * @property {function(ICollaborationEntity): Promise | string} addCollaboration 12 | * @property {function(ICollaborationEntity): Promise | void} deleteCollaboration 13 | * @property {function(ICollaborationEntity): Promise | void} verifyCollaboration 14 | */ 15 | 16 | module.exports = {}; 17 | -------------------------------------------------------------------------------- /src/services/_types/PlaylistSongActivitiesServiceType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {object} IActivityParam 5 | * @property {string} playlistId 6 | * @property {string} songId 7 | * @property {string} userId 8 | * @property {'add'|'delete'} action 9 | */ 10 | 11 | /** 12 | * @typedef {object} IPSAService 13 | * @property {function(IActivityParam): Promise | void} addActivity 14 | * @property {function({playlistId: string}): Promise | any[]} getActivitiesByPlaylistId 15 | */ 16 | 17 | module.exports = {}; 18 | -------------------------------------------------------------------------------- /src/services/_types/PlaylistSongsServiceType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {object} IPlaylistSongsService 5 | * @property {function({playlistId: string, songId: string, userId: string}): Promise | string} addPlaylistSong 6 | * @property {function({playlistId: string, songId: string, userId: string}): Promise | void} deletePlaylistSongById 7 | */ 8 | 9 | module.exports = {}; 10 | -------------------------------------------------------------------------------- /src/services/_types/PlaylistsServiceType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {object} IPlaylistsService 5 | * @property {function({owner: string, name: string}): Promise | any} addPlaylist 6 | * @property {function({owner: string}): Promise | any[]} getPlaylists 7 | * @property {function({id: string}): Promise | any} getPlaylistById 8 | * @property {function({id: string}): Promise | void} deletePlaylistById 9 | * @property {function({owner: string, id: string}): Promise | void} verifyPlaylistOwner 10 | * @property {function({playlistId: string, userId: string}): Promise | void} verifyPlaylistAccess 11 | */ 12 | 13 | module.exports = {}; 14 | -------------------------------------------------------------------------------- /src/services/_types/SongsServiceType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Songs entity interface 5 | * 6 | * @typedef {object} ISongEntity 7 | * @property {string} title 8 | * @property {number} year 9 | * @property {string} performer 10 | * @property {string} genre 11 | * @property {number|null} duration 12 | * @property {string|null} albumId 13 | */ 14 | 15 | /** 16 | * Songs Service interface 17 | * 18 | * @typedef {object} ISongsService 19 | * @property {function(ISongEntity): Promise | string} addSong 20 | * @property {function(ISongsGetQuery): Promise> | Array} getSongs 21 | * @property {function(string): Promise | any} getSongById 22 | * @property {function(string, ISongEntity): Promise | void} editSongById 23 | * @property {function(string): Promise | void} deleteSongById 24 | */ 25 | 26 | /** 27 | * Songs Get Filter Query 28 | * 29 | * @typedef {object} ISongsGetQuery 30 | * @property {string} title 31 | * @property {string} performer 32 | */ 33 | 34 | module.exports = {}; 35 | -------------------------------------------------------------------------------- /src/services/_types/UserAlbumLikesType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {object} IUserAlbumLikes 5 | * @property {function({albumId: string, userId: string}): Promise | string} likeOrUnlikeAlbum 6 | * @property {function(string): Promise<{cached: boolean, count: number}> | {cached: boolean, count: number}} getAlbumTotalLikes 7 | */ 8 | 9 | module.exports = {}; 10 | -------------------------------------------------------------------------------- /src/services/_types/UsersServiceType.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * User entity interface 5 | * 6 | * @typedef {object} IUserEntity 7 | * @property {string} username 8 | * @property {string} password 9 | * @property {string} fullname 10 | */ 11 | 12 | /** 13 | * Users Service interface 14 | * 15 | * @typedef {object} IUsersService 16 | * @property {function(string): Promise | void} verifyNewUsername 17 | * @property {function(IUserEntity): Promise | string} addUser 18 | * @property {function(string): Promise | any} getUserById 19 | * @property {function(string,string): Promise | string} verifyUserCredential 20 | */ 21 | 22 | module.exports = {}; 23 | -------------------------------------------------------------------------------- /src/services/postgres/AlbumsService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { nanoid } = require('nanoid'); 4 | const { Pool } = require('pg'); 5 | 6 | const InvariantError = require('../../exceptions/InvariantError'); 7 | const NotFoundError = require('../../exceptions/NotFoundError'); 8 | 9 | /** 10 | * @typedef {import('../_types/AlbumsServiceType').IAlbumEntity} IAlbumEntity 11 | * @typedef {import('../_types/AlbumsServiceType').IAlbumsService} IAlbumsService 12 | */ 13 | 14 | /** 15 | * @implements {IAlbumsService} 16 | */ 17 | class AlbumsService { 18 | /** 19 | * @readonly 20 | * @private 21 | */ 22 | _pool; 23 | 24 | /** 25 | * @readonly 26 | * @private 27 | */ 28 | _tableName = 'albums'; 29 | 30 | /** 31 | * @readonly 32 | * @private 33 | */ 34 | _prefixId = 'album-'; 35 | 36 | constructor() { 37 | this._pool = new Pool(); 38 | } 39 | 40 | /** 41 | * @param {IAlbumEntity} param0 42 | * @returns {Promise} 43 | */ 44 | async addAlbum({ name, year }) { 45 | const id = this._prefixId + nanoid(16); 46 | const createdAt = new Date().toISOString(); 47 | 48 | const query = { 49 | text: ` 50 | INSERT INTO ${this._tableName} 51 | VALUES($1, $2, $3, $4, $4) 52 | RETURNING id 53 | `, 54 | values: [id, name, year, createdAt], 55 | }; 56 | 57 | const result = await this._pool.query(query); 58 | 59 | if (result.rowCount === 0) { 60 | throw new InvariantError('Album gagal ditambahkan'); 61 | } 62 | 63 | return result.rows[0].id; 64 | } 65 | 66 | /** 67 | * @returns {Promise>} 68 | */ 69 | async getAlbums() { 70 | return []; 71 | } 72 | 73 | /** 74 | * @param {string} id 75 | * @returns {Promise} 76 | */ 77 | async getAlbumById(id) { 78 | const query = { 79 | text: ` 80 | SELECT a.id, a.name, a.year, cover as "coverUrl", COALESCE( 81 | json_agg( 82 | json_build_object( 83 | 'id', s.id, 84 | 'title', s.title, 85 | 'performer', s.performer 86 | ) 87 | ) FILTER ( 88 | WHERE s.id IS NOT NULL 89 | AND s."deletedAt" IS NULL 90 | ), 91 | '[]') as songs 92 | FROM ${this._tableName} a 93 | LEFT JOIN songs s ON a.id = "albumId" 94 | WHERE a.id = $1 95 | GROUP BY a.id 96 | `, 97 | values: [id], 98 | }; 99 | 100 | const result = await this._pool.query(query); 101 | 102 | if (!result.rowCount) { 103 | throw new NotFoundError('Album tidak ditemukan'); 104 | } 105 | 106 | return result.rows[0]; 107 | } 108 | 109 | /** 110 | * @param {string} id 111 | * @param {IAlbumEntity} param1 112 | */ 113 | async editAlbumById(id, { name, year }) { 114 | const updatedAt = new Date().toISOString(); 115 | const query = { 116 | text: ` 117 | UPDATE ${this._tableName} 118 | SET name = $1, year = $2, "updatedAt" = $3 119 | WHERE id = $4 120 | RETURNING id 121 | `, 122 | values: [name, year, updatedAt, id], 123 | }; 124 | 125 | const result = await this._pool.query(query); 126 | 127 | if (!result.rowCount) { 128 | throw new NotFoundError('Gagal memperbarui album. Id tidak ditemukan'); 129 | } 130 | } 131 | 132 | /** 133 | * @param {string} id 134 | * @param {string} filename 135 | */ 136 | async editAlbumCoverById(id, filename) { 137 | const updatedAt = new Date().toISOString(); 138 | const query = { 139 | text: ` 140 | UPDATE ${this._tableName} 141 | SET cover = $1, "updatedAt" = $2 142 | WHERE id = $3 143 | RETURNING id 144 | `, 145 | values: [filename, updatedAt, id], 146 | }; 147 | 148 | const result = await this._pool.query(query); 149 | 150 | if (!result.rowCount) { 151 | throw new NotFoundError( 152 | 'Gagal memperbarui cover album. Id tidak ditemukan' 153 | ); 154 | } 155 | } 156 | 157 | /** 158 | * @param {string} id 159 | */ 160 | async deleteAlbumById(id) { 161 | const query = { 162 | text: ` 163 | DELETE FROM ${this._tableName} 164 | WHERE id = $1 165 | RETURNING id 166 | `, 167 | values: [id], 168 | }; 169 | 170 | const result = await this._pool.query(query); 171 | 172 | if (!result.rowCount) { 173 | throw new NotFoundError('Album gagal dihapus. Id tidak ditemukan'); 174 | } 175 | } 176 | } 177 | 178 | module.exports = AlbumsService; 179 | -------------------------------------------------------------------------------- /src/services/postgres/AuthenticationsService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { Pool } = require('pg'); 4 | 5 | const InvariantError = require('../../exceptions/InvariantError'); 6 | 7 | /** 8 | * @typedef {import('../_types/AuthenticationsServiceType').IAuthenticationsService} IAuthenticationsService 9 | */ 10 | 11 | /** 12 | * @implements {IAuthenticationsService} 13 | */ 14 | class AuthenticationsService { 15 | /** 16 | * @readonly 17 | * @private 18 | */ 19 | _pool; 20 | 21 | /** 22 | * @readonly 23 | * @private 24 | */ 25 | _tableName = 'authentications'; 26 | 27 | constructor() { 28 | this._pool = new Pool(); 29 | } 30 | 31 | /** 32 | * @param {string} token 33 | */ 34 | async addRefreshToken(token) { 35 | const query = { 36 | text: `INSERT INTO ${this._tableName} VALUES($1)`, 37 | values: [token], 38 | }; 39 | 40 | await this._pool.query(query); 41 | } 42 | 43 | /** 44 | * @param {string} token 45 | */ 46 | async verifyRefreshToken(token) { 47 | const query = { 48 | text: ` 49 | SELECT token 50 | FROM ${this._tableName} 51 | WHERE token = $1 52 | `, 53 | values: [token], 54 | }; 55 | 56 | const result = await this._pool.query(query); 57 | 58 | if (!result.rowCount) { 59 | throw new InvariantError('Refresh token tidak valid'); 60 | } 61 | } 62 | 63 | /** 64 | * @param {string} token 65 | */ 66 | async deleteRefreshToken(token) { 67 | const query = { 68 | text: ` 69 | DELETE FROM ${this._tableName} 70 | WHERE token = $1 71 | `, 72 | values: [token], 73 | }; 74 | 75 | await this._pool.query(query); 76 | } 77 | } 78 | 79 | module.exports = AuthenticationsService; 80 | -------------------------------------------------------------------------------- /src/services/postgres/CollaborationsService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { nanoid } = require('nanoid'); 4 | const { Pool } = require('pg'); 5 | 6 | const InvariantError = require('../../exceptions/InvariantError'); 7 | const NotFoundError = require('../../exceptions/NotFoundError'); 8 | 9 | /** 10 | * @typedef {import('../_types/CollaborationsServiceType').ICollaborationsService} ICollaborationsService 11 | * @typedef {import('../_types/CollaborationsServiceType').ICollaborationEntity} ICollaborationEntity 12 | */ 13 | 14 | /** 15 | * @implements {ICollaborationsService} 16 | */ 17 | class CollaborationsService { 18 | /** 19 | * @readonly 20 | * @private 21 | */ 22 | _pool; 23 | 24 | /** 25 | * @readonly 26 | * @private 27 | */ 28 | _tableName = 'collaborations'; 29 | 30 | /** 31 | * @readonly 32 | * @private 33 | */ 34 | _prefixId = 'collab-'; 35 | 36 | constructor() { 37 | this._pool = new Pool(); 38 | } 39 | 40 | /** 41 | * @param {ICollaborationEntity} param0 42 | */ 43 | async addCollaboration({ playlistId, userId }) { 44 | const id = this._prefixId + nanoid(16); 45 | const createdAt = new Date().toISOString(); 46 | 47 | const query = { 48 | text: ` 49 | INSERT INTO ${this._tableName} 50 | SELECT $1, $2, $3, $4, $4 51 | WHERE EXISTS ( 52 | SELECT 1 FROM users 53 | WHERE "id" = $5 54 | ) 55 | RETURNING id 56 | `, 57 | values: [id, playlistId, userId, createdAt, userId], 58 | }; 59 | 60 | const result = await this._pool.query(query); 61 | 62 | if (!result.rowCount) { 63 | throw new NotFoundError( 64 | 'Collaboration gagal ditambahkan. Id user tidak ditemukan' 65 | ); 66 | } 67 | 68 | return result.rows[0].id; 69 | } 70 | 71 | /** 72 | * 73 | * @param {ICollaborationEntity} param0 74 | */ 75 | async deleteCollaboration({ playlistId, userId }) { 76 | const query = { 77 | text: ` 78 | DELETE FROM ${this._tableName} 79 | WHERE playlist_id = $1 80 | AND user_id = $2 81 | RETURNING id 82 | `, 83 | values: [playlistId, userId], 84 | }; 85 | 86 | const result = await this._pool.query(query); 87 | 88 | if (!result.rowCount) { 89 | throw new NotFoundError('Collaboration gagal dihapus.'); 90 | } 91 | } 92 | 93 | /** 94 | * @param {ICollaborationEntity} param0 95 | */ 96 | async verifyCollaboration({ playlistId, userId }) { 97 | const query = { 98 | text: ` 99 | SELECT id FROM ${this._tableName} 100 | WHERE playlist_id = $1 101 | AND user_id = $2 102 | `, 103 | values: [playlistId, userId], 104 | }; 105 | 106 | const result = await this._pool.query(query); 107 | 108 | if (!result.rowCount) { 109 | throw new InvariantError('Kolaborasi gagal diverfikasi'); 110 | } 111 | } 112 | } 113 | 114 | module.exports = CollaborationsService; 115 | -------------------------------------------------------------------------------- /src/services/postgres/PlaylistSongActivitiesService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { nanoid } = require('nanoid'); 4 | const { Pool } = require('pg'); 5 | 6 | const NotFoundError = require('../../exceptions/NotFoundError'); 7 | 8 | /** 9 | * @typedef {import('../_types/PlaylistSongActivitiesServiceType').IPSAService} IPSAService 10 | * @typedef {import('../_types/PlaylistSongActivitiesServiceType').IActivityParam} IActivityParam 11 | */ 12 | 13 | /** 14 | * @implements {IPSAService} 15 | */ 16 | class PSAService { 17 | /** 18 | * @readonly 19 | * @private 20 | */ 21 | _pool; 22 | 23 | /** 24 | * @readonly 25 | * @private 26 | */ 27 | _tableName = 'playlist_song_activities'; 28 | 29 | /** 30 | * @readonly 31 | * @private 32 | */ 33 | _prefixId = 'psa-'; 34 | 35 | constructor() { 36 | this._pool = new Pool(); 37 | } 38 | 39 | /** 40 | * @param {IActivityParam} param0 41 | */ 42 | async addActivity({ playlistId, songId, userId, action }) { 43 | const id = this._prefixId + nanoid(16); 44 | 45 | const query = { 46 | text: ` 47 | INSERT INTO ${this._tableName} 48 | VALUES($1, $2, $3, $4, $5) 49 | RETURNING id 50 | `, 51 | values: [id, playlistId, songId, userId, action], 52 | }; 53 | 54 | await this._pool.query(query); 55 | } 56 | 57 | /** 58 | * @param {{playlistId: string}} param0 59 | */ 60 | async getActivitiesByPlaylistId({ playlistId }) { 61 | const query = { 62 | text: ` 63 | SELECT u.username, s.title, psa.action, psa.time 64 | FROM ${this._tableName} psa 65 | LEFT JOIN users u ON psa.user_id = u.id 66 | LEFT JOIN songs s ON psa.song_id = s.id 67 | WHERE psa.playlist_id = $1 68 | ORDER BY psa.time ASC 69 | `, 70 | values: [playlistId], 71 | }; 72 | 73 | const result = await this._pool.query(query); 74 | 75 | if (!result.rowCount) { 76 | throw new NotFoundError('Playlist tidak ditemukan'); 77 | } 78 | 79 | return result.rows; 80 | } 81 | } 82 | 83 | module.exports = PSAService; 84 | -------------------------------------------------------------------------------- /src/services/postgres/PlaylistSongsService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { nanoid } = require('nanoid'); 4 | const { Pool } = require('pg'); 5 | 6 | const NotFoundError = require('../../exceptions/NotFoundError'); 7 | 8 | /** 9 | * @typedef {import('../_types/PlaylistSongsServiceType').IPlaylistSongsService} IPlaylistSongsService 10 | * @typedef {import('../_types/PlaylistSongActivitiesServiceType').IPSAService} IPSAService 11 | */ 12 | 13 | /** 14 | * @implements {IPlaylistSongsService} 15 | */ 16 | class PlaylistSongsService { 17 | /** 18 | * @readonly 19 | * @private 20 | */ 21 | _pool; 22 | 23 | /** 24 | * @readonly 25 | * @private 26 | */ 27 | _tableName = 'playlist_songs'; 28 | 29 | /** 30 | * @readonly 31 | * @private 32 | */ 33 | _prefixId = 'playlist_song-'; 34 | 35 | /** 36 | * @readonly 37 | * @private 38 | */ 39 | _psaService; 40 | 41 | /** 42 | * @param {IPSAService} psaService 43 | */ 44 | constructor(psaService) { 45 | this._pool = new Pool(); 46 | this._psaService = psaService; 47 | } 48 | 49 | /** 50 | * @param {{playlistId: string, songId: string, userId: string}} param0 51 | */ 52 | async addPlaylistSong({ playlistId, songId, userId }) { 53 | const id = this._prefixId + nanoid(16); 54 | const createdAt = new Date().toISOString(); 55 | 56 | const query = { 57 | text: ` 58 | INSERT INTO ${this._tableName} 59 | SELECT $1, $2, $3, $4, $4 60 | WHERE EXISTS ( 61 | SELECT 1 FROM songs 62 | WHERE "deletedAt" IS NULL 63 | AND "id" = $5 64 | ) 65 | RETURNING id 66 | `, 67 | values: [id, playlistId, songId, createdAt, songId], 68 | }; 69 | 70 | const result = await this._pool.query(query); 71 | 72 | if (!result.rowCount) { 73 | throw new NotFoundError( 74 | 'Song gagal ditambahkan ke playlist. Id song tidak ditemukan' 75 | ); 76 | } 77 | 78 | await this._psaService.addActivity({ 79 | playlistId, 80 | songId, 81 | userId, 82 | action: 'add', 83 | }); 84 | 85 | return result.rows[0].id; 86 | } 87 | 88 | /** 89 | * @param {{playlistId: string, songId: string, userId: string}} param0 90 | */ 91 | async deletePlaylistSongById({ playlistId, songId, userId }) { 92 | const query = { 93 | text: ` 94 | DELETE FROM ${this._tableName} 95 | WHERE playlist_id = $1 AND song_id = $2 96 | RETURNING id 97 | `, 98 | values: [playlistId, songId], 99 | }; 100 | 101 | const result = await this._pool.query(query); 102 | 103 | if (!result.rowCount) { 104 | throw new NotFoundError( 105 | 'Song gagal dihapus dari playlist. Id song tidak ditemukan' 106 | ); 107 | } 108 | 109 | await this._psaService.addActivity({ 110 | playlistId, 111 | songId, 112 | userId, 113 | action: 'delete', 114 | }); 115 | } 116 | } 117 | 118 | module.exports = PlaylistSongsService; 119 | -------------------------------------------------------------------------------- /src/services/postgres/PlaylistsService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { nanoid } = require('nanoid'); 4 | const { Pool } = require('pg'); 5 | 6 | const AuthroizationError = require('../../exceptions/AuthorizationError'); 7 | const InvariantError = require('../../exceptions/InvariantError'); 8 | const NotFoundError = require('../../exceptions/NotFoundError'); 9 | 10 | /** 11 | * @typedef {import('../_types/PlaylistsServiceType').IPlaylistsService} IPlaylistsService 12 | * @typedef {import('../_types/CollaborationsServiceType').ICollaborationsService} ICollaborationsService 13 | */ 14 | 15 | /** 16 | * @implements {IPlaylistsService} 17 | */ 18 | class PlaylistsService { 19 | /** 20 | * @readonly 21 | * @private 22 | */ 23 | _pool; 24 | 25 | /** 26 | * @readonly 27 | * @private 28 | */ 29 | _tableName = 'playlists'; 30 | 31 | /** 32 | * @readonly 33 | * @private 34 | */ 35 | _prefixId = 'playlist-'; 36 | 37 | /** 38 | * @readonly 39 | * @private 40 | */ 41 | _collaborationsService; 42 | 43 | /** 44 | * @param {ICollaborationsService} collaborationsService 45 | */ 46 | constructor(collaborationsService) { 47 | this._pool = new Pool(); 48 | this._collaborationsService = collaborationsService; 49 | } 50 | 51 | /** 52 | * @param {{owner: string, name: string}} param0 53 | */ 54 | async addPlaylist({ owner, name }) { 55 | const id = this._prefixId + nanoid(16); 56 | const createdAt = new Date().toISOString(); 57 | 58 | const query = { 59 | text: ` 60 | INSERT INTO ${this._tableName} 61 | VALUES($1, $2, $3, $4, $4) 62 | RETURNING id 63 | `, 64 | values: [id, name, owner, createdAt], 65 | }; 66 | 67 | const result = await this._pool.query(query); 68 | 69 | if (!result.rowCount) { 70 | throw new InvariantError('Playlist gagal ditambahkan'); 71 | } 72 | 73 | return result.rows[0].id; 74 | } 75 | 76 | /** 77 | * @param {{owner: string}} param0 78 | */ 79 | async getPlaylists({ owner }) { 80 | const query = { 81 | text: ` 82 | SELECT p.id, p.name, u.username 83 | FROM ${this._tableName} p 84 | LEFT JOIN users u ON p.owner = u.id 85 | LEFT JOIN collaborations c ON c.playlist_id = p.id 86 | WHERE p.owner = $1 87 | OR c.user_id = $1 88 | `, 89 | values: [owner], 90 | }; 91 | 92 | const { rows } = await this._pool.query(query); 93 | 94 | return rows; 95 | } 96 | 97 | /** 98 | * @param {{id: string}} param0 99 | */ 100 | async getPlaylistById({ id }) { 101 | const query = { 102 | text: ` 103 | SELECT p.id, p.name, u.username, COALESCE( 104 | json_agg( 105 | json_build_object( 106 | 'id', s.id, 107 | 'title', s.title, 108 | 'performer', s.performer 109 | ) 110 | ) FILTER ( 111 | WHERE s."deletedAt" IS NULL 112 | AND s.id IS NOT NULL 113 | ), 114 | '[]') as songs 115 | FROM ${this._tableName} p 116 | LEFT JOIN users u ON p.owner = u.id 117 | LEFT JOIN playlist_songs ps ON ps.playlist_id = p.id 118 | LEFT JOIN songs s ON s.id = ps.song_id 119 | WHERE p.id = $1 120 | GROUP BY p.id, u.username 121 | `, 122 | values: [id], 123 | }; 124 | 125 | const result = await this._pool.query(query); 126 | 127 | if (!result.rowCount) { 128 | throw new NotFoundError('Playlist tidak ditemukan'); 129 | } 130 | 131 | return result.rows[0]; 132 | } 133 | 134 | /** 135 | * 136 | * @param {{id: string}} param0 137 | */ 138 | async deletePlaylistById({ id }) { 139 | const query = { 140 | text: ` 141 | DELETE FROM ${this._tableName} 142 | WHERE id = $1 143 | RETURNING id 144 | `, 145 | values: [id], 146 | }; 147 | 148 | const result = await this._pool.query(query); 149 | 150 | if (!result.rowCount) { 151 | throw new NotFoundError('Playlist gagal dihapus. Id tidak ditemukan'); 152 | } 153 | } 154 | 155 | /** 156 | * @param {{id: string, owner: string}} param0 157 | */ 158 | async verifyPlaylistOwner({ id, owner }) { 159 | const query = { 160 | text: ` 161 | SELECT owner 162 | FROM ${this._tableName} 163 | WHERE id = $1 164 | `, 165 | values: [id], 166 | }; 167 | 168 | const result = await this._pool.query(query); 169 | 170 | if (!result.rowCount) { 171 | throw new NotFoundError('Playlist tidak ditemukan'); 172 | } 173 | 174 | const playlist = /** @type {{owner: string}} */ (result.rows[0]); 175 | 176 | if (playlist.owner !== owner) { 177 | throw new AuthroizationError('Anda tidak berhak mengakses resource ini'); 178 | } 179 | } 180 | 181 | /** 182 | * 183 | * @param {{playlistId: string, userId: string}} param0 184 | */ 185 | async verifyPlaylistAccess({ playlistId, userId }) { 186 | try { 187 | await this.verifyPlaylistOwner({ id: playlistId, owner: userId }); 188 | } catch (error) { 189 | if (error instanceof NotFoundError) { 190 | throw error; 191 | } 192 | 193 | try { 194 | await this._collaborationsService.verifyCollaboration({ 195 | playlistId, 196 | userId, 197 | }); 198 | } catch { 199 | throw error; 200 | } 201 | } 202 | } 203 | } 204 | 205 | module.exports = PlaylistsService; 206 | -------------------------------------------------------------------------------- /src/services/postgres/SongsService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { nanoid } = require('nanoid'); 4 | const { Pool } = require('pg'); 5 | 6 | const InvariantError = require('../../exceptions/InvariantError'); 7 | const NotFoundError = require('../../exceptions/NotFoundError'); 8 | 9 | /** 10 | * @typedef {import('../_types/SongsServiceType').ISongEntity} ISongEntity 11 | * @typedef {import('../_types/SongsServiceType').ISongsService} ISongsService 12 | * @typedef {import('../_types/SongsServiceType').ISongsGetQuery} ISongsGetQuery 13 | */ 14 | 15 | /** 16 | * @implements {ISongsService} 17 | */ 18 | class SongsService { 19 | /** 20 | * @readonly 21 | * @private 22 | */ 23 | _pool; 24 | 25 | /** 26 | * @readonly 27 | * @private 28 | */ 29 | _tableName = 'songs'; 30 | 31 | /** 32 | * @readonly 33 | * @private 34 | */ 35 | _prefixId = 'song-'; 36 | 37 | constructor() { 38 | this._pool = new Pool(); 39 | } 40 | 41 | /** 42 | * @param {ISongEntity} param0 43 | * @returns {Promise} 44 | */ 45 | async addSong({ title, year, genre, performer, duration, albumId }) { 46 | const id = this._prefixId + nanoid(16); 47 | const createdAt = new Date().toISOString(); 48 | 49 | const query = { 50 | text: ` 51 | INSERT INTO ${this._tableName} 52 | VALUES($1, $2, $3, $4, $5, $6, $7, $8, $8) 53 | RETURNING id 54 | `, 55 | values: [id, title, year, performer, genre, duration, albumId, createdAt], 56 | }; 57 | 58 | const result = await this._pool.query(query); 59 | 60 | if (!result.rowCount) { 61 | throw new InvariantError('Song gagal ditambahkan'); 62 | } 63 | 64 | return result.rows[0].id; 65 | } 66 | 67 | /** 68 | * @param {ISongsGetQuery} param0 69 | * @returns {Promise} 70 | */ 71 | async getSongs({ title = '', performer = '' }) { 72 | const query = { 73 | text: ` 74 | SELECT id, title, performer 75 | FROM ${this._tableName} 76 | WHERE "deletedAt" IS NULL 77 | AND title ILIKE $1 78 | AND performer ILIKE $2 79 | `, 80 | values: [`%${title}%`, `%${performer}%`], 81 | }; 82 | 83 | const { rows } = await this._pool.query(query); 84 | 85 | return rows; 86 | } 87 | 88 | /** 89 | * @param {string} id 90 | */ 91 | async getSongById(id) { 92 | const query = { 93 | text: ` 94 | SELECT id, title, year, performer, genre, duration, "albumId" 95 | FROM ${this._tableName} 96 | WHERE "deletedAt" IS NULL 97 | AND id = $1 98 | `, 99 | values: [id], 100 | }; 101 | 102 | const result = await this._pool.query(query); 103 | 104 | if (!result.rowCount) { 105 | throw new NotFoundError('Song tidak ditemukan'); 106 | } 107 | 108 | return result.rows[0]; 109 | } 110 | 111 | /** 112 | * @param {string} id 113 | * @param {ISongEntity} param1 114 | */ 115 | async editSongById(id, { title, year, performer, genre, duration, albumId }) { 116 | const updatedAt = new Date().toISOString(); 117 | const query = { 118 | text: ` 119 | UPDATE ${this._tableName} 120 | SET title = $1, year = $2, performer = $3, genre = $4, 121 | duration = $5, "albumId" = $6, "updatedAt" = $7 122 | WHERE "deletedAt" IS NULL 123 | AND id = $8 124 | RETURNING id 125 | `, 126 | values: [title, year, performer, genre, duration, albumId, updatedAt, id], 127 | }; 128 | 129 | const result = await this._pool.query(query); 130 | 131 | if (!result.rowCount) { 132 | throw new NotFoundError('Gagal memperbarui song. Id tidak ditemukan'); 133 | } 134 | } 135 | 136 | /** 137 | * @param {string} id 138 | */ 139 | async deleteSongById(id) { 140 | const deletedAt = new Date().toISOString(); 141 | 142 | const query = { 143 | // text: `DELETE FROM ${this._tableName} WHERE id = $1 RETURNING id`, 144 | text: ` 145 | UPDATE ${this._tableName} 146 | SET "deletedAt" = $1 147 | WHERE "deletedAt" IS NULL 148 | AND id = $2 149 | RETURNING id 150 | `, 151 | values: [deletedAt, id], 152 | }; 153 | 154 | const result = await this._pool.query(query); 155 | 156 | if (!result.rowCount) { 157 | throw new NotFoundError('Song gagal dihapus. Id tidak ditemukan'); 158 | } 159 | } 160 | } 161 | 162 | module.exports = SongsService; 163 | -------------------------------------------------------------------------------- /src/services/postgres/UserAlbumLikes.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { nanoid } = require('nanoid'); 4 | const { Pool } = require('pg'); 5 | 6 | const InvariantError = require('../../exceptions/InvariantError'); 7 | const NotFoundError = require('../../exceptions/NotFoundError'); 8 | 9 | /** 10 | * @typedef {import('../_types/UserAlbumLikesType').IUserAlbumLikes} IUserAlbumLikes 11 | * @typedef {import('../redis/CacheService')} CacheService 12 | */ 13 | 14 | /** 15 | * @implements {IUserAlbumLikes} 16 | */ 17 | class UALService { 18 | /** 19 | * @readonly 20 | * @private 21 | */ 22 | _pool; 23 | 24 | /** 25 | * @readonly 26 | * @private 27 | */ 28 | _cacheService; 29 | 30 | /** 31 | * @readonly 32 | * @private 33 | */ 34 | _tableName = 'user_album_likes'; 35 | 36 | /** 37 | * @readonly 38 | * @private 39 | */ 40 | _prefixId = 'ual-'; 41 | 42 | /** 43 | * @param {CacheService} cacheService 44 | */ 45 | constructor(cacheService) { 46 | this._pool = new Pool(); 47 | this._cacheService = cacheService; 48 | } 49 | 50 | /** 51 | * @param {{albumId: string, userId: string}} param0 52 | */ 53 | async likeOrUnlikeAlbum({ albumId, userId }) { 54 | try { 55 | const id = this._prefixId + nanoid(16); 56 | 57 | let result = await this._pool.query({ 58 | text: ` 59 | INSERT INTO ${this._tableName} 60 | SELECT $1, $2, $3 61 | WHERE NOT EXISTS ( 62 | SELECT 1 FROM ${this._tableName} 63 | WHERE user_id = $4 64 | AND album_id = $5 65 | ) 66 | RETURNING TRUE 67 | `, 68 | values: [id, userId, albumId, userId, albumId], 69 | }); 70 | 71 | if (!result.rowCount) { 72 | result = await this._pool.query({ 73 | text: ` 74 | DELETE FROM ${this._tableName} 75 | WHERE user_id = $1 76 | AND album_id = $2 77 | RETURNING FALSE 78 | `, 79 | values: [userId, albumId], 80 | }); 81 | 82 | if (!result.rowCount) { 83 | throw new InvariantError('error'); 84 | } 85 | } 86 | 87 | await this._cacheService.delete(`${this._tableName}:${albumId}`); 88 | 89 | return result.rows[0].bool ? 'like' : 'unlike'; 90 | } catch (error) { 91 | if (error.code === '23503') { 92 | throw new NotFoundError( 93 | 'Gagal like/unlike album. Album id tidak ditemukan.' 94 | ); 95 | } 96 | 97 | throw new InvariantError(error.message); 98 | } 99 | } 100 | 101 | /** 102 | * @param {string} albumId 103 | * @returns {Promise<{cached: boolean, count: number}>} 104 | */ 105 | async getAlbumTotalLikes(albumId) { 106 | const cacheKey = `${this._tableName}:${albumId}`; 107 | 108 | let cached = false; 109 | let count = 0; 110 | 111 | try { 112 | const result = await this._cacheService.get(cacheKey); 113 | 114 | count = JSON.parse(result).count; 115 | cached = true; 116 | } catch { 117 | const query = { 118 | text: ` 119 | SELECT id 120 | FROM ${this._tableName} 121 | WHERE album_id = $1 122 | `, 123 | values: [albumId], 124 | }; 125 | 126 | const { rowCount } = await this._pool.query(query); 127 | 128 | await this._cacheService.set( 129 | cacheKey, 130 | JSON.stringify({ count: rowCount }) 131 | ); 132 | 133 | count = rowCount; 134 | } 135 | 136 | return { 137 | cached, 138 | count, 139 | }; 140 | } 141 | } 142 | 143 | module.exports = UALService; 144 | -------------------------------------------------------------------------------- /src/services/postgres/UsersService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const bcrypt = require('bcrypt'); 4 | const { nanoid } = require('nanoid'); 5 | const { Pool } = require('pg'); 6 | 7 | const AuthenticationError = require('../../exceptions/AuthenticationError'); 8 | const InvariantError = require('../../exceptions/InvariantError'); 9 | const NotFoundError = require('../../exceptions/NotFoundError'); 10 | 11 | /** 12 | * @typedef {import('../_types/UsersServiceType').IUserEntity} IUserEntity 13 | * @typedef {import('../_types/UsersServiceType').IUsersService} IUsersService 14 | */ 15 | 16 | /** 17 | * @implements {IUsersService} 18 | */ 19 | class UsersService { 20 | /** 21 | * @readonly 22 | * @private 23 | */ 24 | _pool; 25 | 26 | /** 27 | * @readonly 28 | * @private 29 | */ 30 | _tableName = 'users'; 31 | 32 | /** 33 | * @readonly 34 | * @private 35 | */ 36 | _prefixId = 'user-'; 37 | 38 | constructor() { 39 | this._pool = new Pool(); 40 | } 41 | 42 | /** 43 | * @param {string} username 44 | */ 45 | async verifyNewUsername(username) { 46 | const query = { 47 | text: ` 48 | SELECT username 49 | FROM ${this._tableName} 50 | WHERE username = $1 51 | `, 52 | values: [username], 53 | }; 54 | 55 | const result = await this._pool.query(query); 56 | 57 | if (result.rowCount > 0) { 58 | throw new InvariantError( 59 | 'Gagal menambahkan User. Username sudah digunakan.' 60 | ); 61 | } 62 | } 63 | 64 | /** 65 | * @param {IUserEntity} param0 66 | * @returns {Promise} 67 | */ 68 | async addUser({ username, password, fullname }) { 69 | await this.verifyNewUsername(username); 70 | 71 | const id = this._prefixId + nanoid(16); 72 | const hashedPassword = await bcrypt.hash(password, 10); 73 | 74 | const query = { 75 | text: ` 76 | INSERT INTO ${this._tableName} 77 | VALUES($1, $2, $3, $4) 78 | RETURNING id 79 | `, 80 | values: [id, username, hashedPassword, fullname], 81 | }; 82 | 83 | const result = await this._pool.query(query); 84 | 85 | if (!result.rowCount) { 86 | throw new InvariantError('User gagal ditambahkan'); 87 | } 88 | 89 | return result.rows[0].id; 90 | } 91 | 92 | /** 93 | * 94 | * @param {string} id 95 | * @returns {Promise} 96 | */ 97 | async getUserById(id) { 98 | const query = { 99 | text: ` 100 | SELECT id, username, fullname 101 | FROM ${this._tableName} 102 | WHERE id = $id 103 | `, 104 | values: [id], 105 | }; 106 | 107 | const result = await this._pool.query(query); 108 | 109 | if (!result.rowCount) { 110 | throw new NotFoundError('User tidak ditemukan'); 111 | } 112 | 113 | return result.rows[0]; 114 | } 115 | 116 | /** 117 | * @param {string} username 118 | * @param {string} password 119 | */ 120 | async verifyUserCredential(username, password) { 121 | const query = { 122 | text: ` 123 | SELECT id, password 124 | FROM ${this._tableName} 125 | WHERE username = $1 126 | `, 127 | values: [username], 128 | }; 129 | 130 | const result = await this._pool.query(query); 131 | 132 | if (!result.rowCount) { 133 | throw new AuthenticationError('Kredential yang anda berikan salah'); 134 | } 135 | 136 | const { id, password: hashedPassword } = 137 | /** @type {{id: string, password: string}} */ (result.rows[0]); 138 | 139 | const match = await bcrypt.compare(password, hashedPassword); 140 | 141 | if (!match) { 142 | throw new AuthenticationError('Kredential yang anda berikan salah'); 143 | } 144 | 145 | return id; 146 | } 147 | } 148 | 149 | module.exports = UsersService; 150 | -------------------------------------------------------------------------------- /src/services/rabbitmq/ProducerService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const amqp = require('amqplib'); 4 | 5 | const config = require('../../utils/config'); 6 | 7 | const ProducerService = { 8 | /** 9 | * @param {string} queue 10 | * @param {string} message 11 | */ 12 | sendMessage: async (queue, message) => { 13 | const connection = await amqp.connect(config.rabbitMq.server); 14 | const channel = await connection.createChannel(); 15 | 16 | await channel.assertQueue(queue, { 17 | durable: true, 18 | }); 19 | 20 | await channel.sendToQueue(queue, Buffer.from(message)); 21 | 22 | setTimeout(() => { 23 | connection.close(); 24 | }, 1000); 25 | }, 26 | }; 27 | 28 | module.exports = ProducerService; 29 | -------------------------------------------------------------------------------- /src/services/redis/CacheService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const redis = require('redis'); 4 | 5 | const config = require('../../utils/config'); 6 | 7 | /** 8 | * @typedef {import('@redis/client/dist/lib/commands').RedisCommandArgument} RedisCommandArgument 9 | */ 10 | 11 | class CacheService { 12 | /** 13 | * @readonly 14 | * @private 15 | */ 16 | _client; 17 | 18 | constructor() { 19 | this._client = redis.createClient({ 20 | socket: { 21 | host: config.redis.host, 22 | }, 23 | }); 24 | 25 | this._client.on('error', (error) => { 26 | console.log(error); 27 | }); 28 | 29 | this._client.connect(); 30 | } 31 | 32 | /** 33 | * @param {RedisCommandArgument} key 34 | * @param {RedisCommandArgument} value 35 | * @param {number} exp 36 | */ 37 | async set(key, value, exp = 1800) { 38 | await this._client.set(key, value, { 39 | EX: exp, 40 | }); 41 | } 42 | 43 | /** 44 | * @param {RedisCommandArgument} key 45 | * @returns {Promise} 46 | */ 47 | async get(key) { 48 | const result = await this._client.get(key); 49 | 50 | if (result === null) throw new Error('Cache tidak ditemukan'); 51 | 52 | return result; 53 | } 54 | 55 | /** 56 | * @param {RedisCommandArgument} key 57 | * @returns {Promise} 58 | */ 59 | async delete(key) { 60 | return this._client.del(key); 61 | } 62 | } 63 | 64 | module.exports = CacheService; 65 | -------------------------------------------------------------------------------- /src/services/storage/StorageService.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const fs = require('fs'); 4 | 5 | class StorageService { 6 | /** 7 | * @readonly 8 | * @private 9 | */ 10 | _folder; 11 | 12 | /** 13 | * @param {string} folder 14 | */ 15 | constructor(folder) { 16 | this._folder = folder; 17 | 18 | if (!fs.existsSync(this._folder)) { 19 | fs.mkdirSync(this._folder, { recursive: true }); 20 | } 21 | } 22 | 23 | /** 24 | * @param {any} file 25 | * @param {any} meta 26 | * @returns {Promise} 27 | */ 28 | writeFile(file, meta) { 29 | const filename = +new Date() + meta.filename; 30 | const path = `${this._folder}/${filename}`; 31 | 32 | const fileStream = fs.createWriteStream(path); 33 | 34 | return new Promise((resolve, reject) => { 35 | fileStream.on('error', (error) => reject(error)); 36 | file.pipe(fileStream); 37 | file.on('end', () => resolve(filename)); 38 | }); 39 | } 40 | } 41 | 42 | module.exports = StorageService; 43 | -------------------------------------------------------------------------------- /src/tokenize/TokenManager.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const Jwt = require('@hapi/jwt'); 4 | 5 | const config = require('../utils/config'); 6 | 7 | const InvariantError = require('../exceptions/InvariantError'); 8 | 9 | /** 10 | * @typedef {{userId: string}} JwtPayload 11 | */ 12 | 13 | const TokenManager = { 14 | generateAccessToken: (/** @type {JwtPayload} */ payload) => { 15 | return Jwt.token.generate(payload, config.jwtToken.access_token_key); 16 | }, 17 | generateRefreshToken: (/** @type {JwtPayload} */ payload) => { 18 | return Jwt.token.generate(payload, config.jwtToken.refresh_token_key); 19 | }, 20 | verifyRefreshToken: (/** @type {string} */ refreshToken) => { 21 | try { 22 | const artifacts = Jwt.token.decode(refreshToken); 23 | Jwt.token.verifySignature(artifacts, config.jwtToken.refresh_token_key); 24 | 25 | const { payload } = artifacts.decoded; 26 | 27 | return /** @type {JwtPayload} */ (payload); 28 | } catch (error) { 29 | throw new InvariantError('Refresh token tidak valid'); 30 | } 31 | }, 32 | }; 33 | 34 | module.exports = TokenManager; 35 | -------------------------------------------------------------------------------- /src/utils/HapiErrorHandler.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { Boom } = require('@hapi/boom'); 4 | 5 | const ClientError = require('../exceptions/ClientError'); 6 | // const NotFoundError = require('../exceptions/NotFoundError'); 7 | 8 | /** 9 | * 10 | * @param {import('@hapi/hapi').ResponseToolkit} h 11 | * @param {Error} error 12 | * @returns {import('@hapi/hapi').ResponseObject} 13 | */ 14 | const hapiErrorHandler = (h, error) => { 15 | const responseData = { 16 | status: 'error', 17 | message: 'Maaf, terjadi kegagala pada server kami.', 18 | }; 19 | let code = 500; 20 | 21 | if (error instanceof ClientError) { 22 | responseData.status = 'fail'; 23 | responseData.message = error.message; 24 | code = error.statusCode; 25 | } else if (error instanceof Boom) { 26 | responseData.status = 'fail'; 27 | responseData.message = error.message; 28 | code = error.output.statusCode; 29 | } else { 30 | console.log(error.message); 31 | } 32 | 33 | const response = h.response(responseData); 34 | response.code(code); 35 | 36 | return response; 37 | }; 38 | 39 | module.exports = { hapiErrorHandler }; 40 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | app: { 3 | host: process.env.HOST, 4 | port: process.env.PORT, 5 | }, 6 | jwtToken: { 7 | access_token_key: process.env.ACCESS_TOKEN_KEY 8 | ? process.env.ACCESS_TOKEN_KEY 9 | : '1fd3b1cf932a77dc', 10 | refresh_token_key: process.env.REFRESH_TOKEN_KEY 11 | ? process.env.REFRESH_TOKEN_KEY 12 | : 'ddebe603fe57b33b', 13 | access_token_age: process.env.ACCESS_TOKEN_AGE, 14 | }, 15 | rabbitMq: { 16 | server: process.env.RABBITMQ_SERVER 17 | ? process.env.RABBITMQ_SERVER 18 | : 'amqp://localhost', 19 | }, 20 | redis: { 21 | host: process.env.REDIS_SERVER, 22 | }, 23 | }; 24 | 25 | module.exports = config; 26 | -------------------------------------------------------------------------------- /src/validator/albums/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { 4 | AlbumPayloadScheme, 5 | UploadAlbumCoverHeaderScheme, 6 | } = require('./scheme'); 7 | 8 | const InvariantError = require('../../exceptions/InvariantError'); 9 | 10 | const AlbumsValidator = { 11 | validateAlbumPayload: (payload) => { 12 | const validationResult = AlbumPayloadScheme.validate(payload); 13 | if (validationResult.error) { 14 | throw new InvariantError(validationResult.error.message); 15 | } 16 | }, 17 | validateAlbumCoverHeader: (headers) => { 18 | const validationResult = UploadAlbumCoverHeaderScheme.validate(headers); 19 | if (validationResult.error) { 20 | throw new InvariantError(validationResult.error.message); 21 | } 22 | }, 23 | }; 24 | 25 | module.exports = AlbumsValidator; 26 | -------------------------------------------------------------------------------- /src/validator/albums/scheme.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const Joi = require('joi'); 4 | 5 | const AlbumPayloadScheme = Joi.object({ 6 | name: Joi.string().max(50).required(), 7 | year: Joi.number() 8 | .integer() 9 | .min(1900) 10 | .max(new Date().getFullYear()) 11 | .required(), 12 | }); 13 | 14 | const UploadAlbumCoverHeaderScheme = Joi.object({ 15 | 'content-type': Joi.string() 16 | .valid( 17 | 'image/apng', 18 | 'image/avif', 19 | 'image/gif', 20 | 'image/jpeg', 21 | 'image/png', 22 | 'image/webp' 23 | ) 24 | .required(), 25 | }).unknown(); 26 | 27 | module.exports = { AlbumPayloadScheme, UploadAlbumCoverHeaderScheme }; 28 | -------------------------------------------------------------------------------- /src/validator/authentications/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { 4 | DeleteAuthenticationPayloadScheme, 5 | PostAuthenticationPayloadScheme, 6 | PutAuthenticationPayloadScheme, 7 | } = require('./scheme'); 8 | 9 | const InvariantError = require('../../exceptions/InvariantError'); 10 | 11 | const AuthenticationsValidator = { 12 | validatePostAuthentication: (payload) => { 13 | const validationResult = PostAuthenticationPayloadScheme.validate(payload); 14 | if (validationResult.error) { 15 | throw new InvariantError(validationResult.error.message); 16 | } 17 | }, 18 | validatePutAuthencation: (payload) => { 19 | const validationResult = PutAuthenticationPayloadScheme.validate(payload); 20 | if (validationResult.error) { 21 | throw new InvariantError(validationResult.error.message); 22 | } 23 | }, 24 | validateDeleteAuthentication: (payload) => { 25 | const validationResult = 26 | DeleteAuthenticationPayloadScheme.validate(payload); 27 | if (validationResult.error) { 28 | throw new InvariantError(validationResult.error.message); 29 | } 30 | }, 31 | }; 32 | 33 | module.exports = AuthenticationsValidator; 34 | -------------------------------------------------------------------------------- /src/validator/authentications/scheme.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const Joi = require('joi'); 4 | 5 | const PostAuthenticationPayloadScheme = Joi.object({ 6 | username: Joi.string().max(50).required(), 7 | password: Joi.string().required(), 8 | }); 9 | 10 | const PutAuthenticationPayloadScheme = Joi.object({ 11 | refreshToken: Joi.string().required(), 12 | }); 13 | 14 | const DeleteAuthenticationPayloadScheme = Joi.object({ 15 | refreshToken: Joi.string().required(), 16 | }); 17 | 18 | module.exports = { 19 | PostAuthenticationPayloadScheme, 20 | PutAuthenticationPayloadScheme, 21 | DeleteAuthenticationPayloadScheme, 22 | }; 23 | -------------------------------------------------------------------------------- /src/validator/collaborations/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const InvariantError = require('../../exceptions/InvariantError'); 4 | 5 | const { PostCollaborationPayloadScheme } = require('./scheme'); 6 | 7 | const CollaborationsValidator = { 8 | validatePostCollaboration: (payload) => { 9 | const validationResult = PostCollaborationPayloadScheme.validate(payload); 10 | if (validationResult.error) { 11 | throw new InvariantError(validationResult.error.message); 12 | } 13 | }, 14 | }; 15 | 16 | module.exports = CollaborationsValidator; 17 | -------------------------------------------------------------------------------- /src/validator/collaborations/scheme.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const Joi = require('joi'); 4 | 5 | const PostCollaborationPayloadScheme = Joi.object({ 6 | playlistId: Joi.string().max(25).required(), 7 | userId: Joi.string().max(21).required(), 8 | }); 9 | 10 | module.exports = { 11 | PostCollaborationPayloadScheme, 12 | }; 13 | -------------------------------------------------------------------------------- /src/validator/exports/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const InvariantError = require('../../exceptions/InvariantError'); 4 | 5 | const { ExportPlaylistPayloadScheme } = require('./scheme'); 6 | 7 | const ExportsValidator = { 8 | ValidateExportPlaylist: (payload) => { 9 | const validationResult = ExportPlaylistPayloadScheme.validate(payload); 10 | if (validationResult.error) { 11 | throw new InvariantError(validationResult.error.message); 12 | } 13 | }, 14 | }; 15 | 16 | module.exports = ExportsValidator; 17 | -------------------------------------------------------------------------------- /src/validator/exports/scheme.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const Joi = require('joi'); 4 | 5 | const ExportPlaylistPayloadScheme = Joi.object({ 6 | targetEmail: Joi.string().email().required(), 7 | }); 8 | 9 | module.exports = { 10 | ExportPlaylistPayloadScheme, 11 | }; 12 | -------------------------------------------------------------------------------- /src/validator/playlists/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { 4 | PostAddPlaylistSongPayloadScheme, 5 | PostCreatePlaylistPayloadScheme, 6 | } = require('./scheme'); 7 | 8 | const InvariantError = require('../../exceptions/InvariantError'); 9 | 10 | const PlaylistsValidator = { 11 | validateCreatePlaylist: (payload) => { 12 | const validationResult = PostCreatePlaylistPayloadScheme.validate(payload); 13 | if (validationResult.error) { 14 | throw new InvariantError(validationResult.error.message); 15 | } 16 | }, 17 | validateAddPlaylistSong: (payload) => { 18 | const validationResult = PostAddPlaylistSongPayloadScheme.validate(payload); 19 | if (validationResult.error) { 20 | throw new InvariantError(validationResult.error.message); 21 | } 22 | }, 23 | }; 24 | 25 | module.exports = PlaylistsValidator; 26 | -------------------------------------------------------------------------------- /src/validator/playlists/scheme.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const Joi = require('joi'); 4 | 5 | const PostCreatePlaylistPayloadScheme = Joi.object({ 6 | name: Joi.string().max(50).required(), 7 | }); 8 | 9 | const PostAddPlaylistSongPayloadScheme = Joi.object({ 10 | songId: Joi.string().max(21).required(), 11 | }); 12 | 13 | module.exports = { 14 | PostCreatePlaylistPayloadScheme, 15 | PostAddPlaylistSongPayloadScheme, 16 | }; 17 | -------------------------------------------------------------------------------- /src/validator/songs/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const InvariantError = require('../../exceptions/InvariantError'); 4 | 5 | const { SongPayloadScheme } = require('./scheme'); 6 | 7 | const SongsValidator = { 8 | validateSongPayload: (payload) => { 9 | const validationResult = SongPayloadScheme.validate(payload); 10 | if (validationResult.error) { 11 | throw new InvariantError(validationResult.error.message); 12 | } 13 | }, 14 | }; 15 | 16 | module.exports = SongsValidator; 17 | -------------------------------------------------------------------------------- /src/validator/songs/scheme.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const Joi = require('joi'); 4 | 5 | const SongPayloadScheme = Joi.object({ 6 | title: Joi.string().max(50).required(), 7 | year: Joi.number() 8 | .integer() 9 | .min(1900) 10 | .max(new Date().getFullYear()) 11 | .required(), 12 | genre: Joi.string().max(32).required(), 13 | performer: Joi.string().max(50).required(), 14 | duration: Joi.number(), 15 | albumId: Joi.string().max(22), 16 | }); 17 | 18 | module.exports = { SongPayloadScheme }; 19 | -------------------------------------------------------------------------------- /src/validator/users/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const InvariantError = require('../../exceptions/InvariantError'); 4 | 5 | const { UserPayloadScheme } = require('./scheme'); 6 | 7 | const UsersValidator = { 8 | validateUserPayload: (payload) => { 9 | const validationResult = UserPayloadScheme.validate(payload); 10 | 11 | if (validationResult.error) { 12 | throw new InvariantError(validationResult.error.message); 13 | } 14 | }, 15 | }; 16 | 17 | module.exports = UsersValidator; 18 | -------------------------------------------------------------------------------- /src/validator/users/scheme.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const Joi = require('joi'); 4 | 5 | const UserPayloadScheme = Joi.object({ 6 | username: Joi.string().max(50).required(), 7 | password: Joi.string().required(), 8 | fullname: Joi.string().required(), 9 | }); 10 | 11 | module.exports = { UserPayloadScheme }; 12 | --------------------------------------------------------------------------------