├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .husky ├── .gitignore ├── post-merge ├── pre-commit ├── pre-merge └── pre-push ├── .nvmrc ├── LICENCE.md ├── README.md ├── TECH-DEBT.md ├── docker ├── dev │ ├── .docker.env │ └── configureDatabase │ │ ├── initDatabase.js │ │ ├── postsDataToBePersisted.js │ │ └── usersDataToBePersisted.js ├── docker-compose.yml └── test │ ├── .docker.env │ └── configureDatabase │ └── initDatabase.js ├── docs ├── README_ES.md └── insomnia │ └── graphql.json ├── env ├── .env.dev ├── .env.test ├── README.md └── dotenv-test.ts ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── app.ts ├── common │ ├── errors │ │ ├── ApiError.ts │ │ ├── AuthenticationErrors │ │ │ ├── 400.bad.request │ │ │ │ ├── LoginDataError.ts │ │ │ │ ├── TokenFormatError.ts │ │ │ │ └── index.ts │ │ │ ├── 401.unauthorized │ │ │ │ ├── TokenExpiredError.ts │ │ │ │ ├── WrongPasswordError.ts │ │ │ │ ├── WrongUsernameError.ts │ │ │ │ └── index.ts │ │ │ ├── 403.forbidden │ │ │ │ ├── RequiredTokenNotProvidedError.ts │ │ │ │ └── index.ts │ │ │ ├── 500.internal.server.error │ │ │ │ ├── CheckingPasswordError.ts │ │ │ │ ├── CheckingTokenError.ts │ │ │ │ ├── GettingTokenError.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── CommonErrors │ │ │ ├── InternalServerError.ts │ │ │ └── index.ts │ │ ├── PostErrors │ │ │ ├── 400.bad.request │ │ │ │ ├── NewPostCommentError.ts │ │ │ │ ├── NewPostError.ts │ │ │ │ ├── PostCommentError.ts │ │ │ │ ├── PostDislikeUserError.ts │ │ │ │ ├── PostIdentificationError.ts │ │ │ │ └── index.ts │ │ │ ├── 401.unauthorized │ │ │ │ ├── UnauthorizedPostCommentDeletingError.ts │ │ │ │ ├── UnauthorizedPostDeletingError.ts │ │ │ │ └── index.ts │ │ │ ├── 404.not.found │ │ │ │ ├── PostCommentNotFoundError.ts │ │ │ │ ├── PostNotFoundError.ts │ │ │ │ └── index.ts │ │ │ ├── 500.internal.server.error │ │ │ │ ├── CreatingPostCommentError.ts │ │ │ │ ├── CreatingPostError.ts │ │ │ │ ├── DeletingPostCommentError.ts │ │ │ │ ├── DeletingPostError.ts │ │ │ │ ├── DeletingPostLikeError.ts │ │ │ │ ├── GettingPostCommentError.ts │ │ │ │ ├── GettingPostError.ts │ │ │ │ ├── LikingPostError.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── UserErrors │ │ │ ├── 400.bad.request │ │ │ │ ├── EmptyProfileDataError.ts │ │ │ │ ├── NewUserAlreadyExistsError.ts │ │ │ │ ├── ProfileDataError.ts │ │ │ │ ├── SigninDataError.ts │ │ │ │ ├── UserDoesNotExistError.ts │ │ │ │ └── index.ts │ │ │ ├── 500.internal.server.error │ │ │ │ ├── CreatingUserError.ts │ │ │ │ ├── GettingUserError.ts │ │ │ │ ├── GettingUserProfileError.ts │ │ │ │ ├── UpdatingUserError.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── httpCodes.ts │ │ └── index.ts │ ├── index.ts │ ├── logger │ │ ├── index.ts │ │ └── log4js │ │ │ ├── config.ts │ │ │ └── index.ts │ └── utils │ │ ├── index.ts │ │ └── timestamp │ │ ├── getUtcTimestampIsoString.ts │ │ ├── index.ts │ │ └── test │ │ └── getUtcTimestampIsoString.test.ts ├── domain │ ├── index.ts │ ├── models │ │ ├── index.ts │ │ ├── post.domain.model.ts │ │ └── user.domain.models.ts │ └── services │ │ ├── authentication.services.ts │ │ ├── hash.services.ts │ │ ├── index.ts │ │ ├── post.services.ts │ │ ├── test │ │ ├── authentication.services │ │ │ ├── checkToken.test.ts │ │ │ ├── login.test.ts │ │ │ └── logout.test.ts │ │ ├── hash.servives │ │ │ ├── checkPassword.test.ts │ │ │ └── hashPassword.test.ts │ │ ├── post.services │ │ │ ├── createPost.test.ts │ │ │ ├── createPostComment.test.ts │ │ │ ├── deletePost.test.ts │ │ │ ├── deletePostComment.test.ts │ │ │ ├── dislikePost.test.ts │ │ │ ├── getExtendedPostById.test.ts │ │ │ ├── getExtendedPosts.test.ts │ │ │ ├── getPostById.test.ts │ │ │ ├── getPostComment.test.ts │ │ │ ├── getPosts.test.ts │ │ │ └── likePost.test.ts │ │ └── user.services │ │ │ ├── createUser.test.ts │ │ │ ├── getUserByToken.test.ts │ │ │ ├── getUserByUsername.test.ts │ │ │ ├── getUserProfile.test.ts │ │ │ ├── updateUserLoginData.test.ts │ │ │ ├── updateUserLogoutData.test.ts │ │ │ └── updateUserProfile.test.ts │ │ └── user.services.ts ├── infrastructure │ ├── authentication │ │ ├── index.ts │ │ └── token │ │ │ ├── index.ts │ │ │ └── jwt │ │ │ ├── decodeJwt.ts │ │ │ ├── encodeJwt.ts │ │ │ ├── index.ts │ │ │ └── test │ │ │ ├── decodeJwt.test.ts │ │ │ └── encodeJwt.test.ts │ ├── dataSources │ │ ├── index.ts │ │ ├── post.datasource.ts │ │ └── user.datasource.ts │ ├── dtos │ │ ├── index.ts │ │ ├── post.dtos.ts │ │ └── user.dtos.ts │ ├── mappers │ │ ├── index.ts │ │ ├── post.mappers.ts │ │ ├── test │ │ │ ├── post.mappers │ │ │ │ ├── mapOwnerFromDtoToDomainModel.test.ts │ │ │ │ ├── mapPostCommentFromDtoToDomainModel.test.ts │ │ │ │ ├── mapPostFromDtoToDomainModel.test.ts │ │ │ │ └── mapPostOwnerFromDomainModelToDto.test.ts │ │ │ └── user.mappers │ │ │ │ ├── mapNewUserFromDtoToDomain.test.ts │ │ │ │ ├── mapUserFromDtoToDomainModel.test.ts │ │ │ │ └── mapUserFromDtoToProfileDomainModel.test.ts │ │ └── user.mappers.ts │ ├── orm │ │ ├── index.ts │ │ └── mongoose │ │ │ ├── core │ │ │ ├── config.ts │ │ │ ├── connect.ts │ │ │ ├── disconnect.ts │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── post.mongodb.model.ts │ │ │ └── user.mongodb.model.ts │ │ │ └── requests │ │ │ ├── index.ts │ │ │ ├── post.mongodb.requests.ts │ │ │ ├── test │ │ │ ├── post.requests │ │ │ │ ├── create.test.ts │ │ │ │ ├── createComment.test.ts │ │ │ │ ├── deleteComment.test.ts │ │ │ │ ├── deletePost.test.ts │ │ │ │ ├── dislike.test.ts │ │ │ │ ├── getAll.test.ts │ │ │ │ ├── getById.test.ts │ │ │ │ ├── getComment.test.ts │ │ │ │ └── like.test.ts │ │ │ └── user.requests │ │ │ │ ├── create.test.ts │ │ │ │ ├── getByToken.test.ts │ │ │ │ ├── getByUsername.test.ts │ │ │ │ ├── getProfileById.test.ts │ │ │ │ └── updateById.test.ts │ │ │ └── user.mongodb.requests.ts │ ├── server │ │ ├── apidoc │ │ │ ├── components │ │ │ │ ├── authentication │ │ │ │ │ ├── AuthenticatedUser.ts │ │ │ │ │ ├── LoginInputParams.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── common │ │ │ │ │ ├── Error.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── post │ │ │ │ │ ├── Comment.ts │ │ │ │ │ ├── CommentArray.ts │ │ │ │ │ ├── EmptyArray.ts │ │ │ │ │ ├── ExtendedComment.ts │ │ │ │ │ ├── ExtendedCommentArray.ts │ │ │ │ │ ├── ExtendedPost.ts │ │ │ │ │ ├── ExtendedPostArray.ts │ │ │ │ │ ├── LikeArray.ts │ │ │ │ │ ├── NewPost.ts │ │ │ │ │ ├── Owner.ts │ │ │ │ │ ├── Post.ts │ │ │ │ │ ├── PostArray.ts │ │ │ │ │ └── index.ts │ │ │ │ └── user │ │ │ │ │ ├── NewRegisteredUser.ts │ │ │ │ │ ├── NewUserInput.ts │ │ │ │ │ ├── NewUserProfileDataInput.ts │ │ │ │ │ ├── UserProfile.ts │ │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ └── paths │ │ │ │ ├── authentication │ │ │ │ ├── index.ts │ │ │ │ ├── postLogin.path.ts │ │ │ │ └── postLogout.path.ts │ │ │ │ ├── index.ts │ │ │ │ ├── posts │ │ │ │ ├── createPost.path.ts │ │ │ │ ├── createPostComment.path.ts │ │ │ │ ├── createPostLike.path.ts │ │ │ │ ├── deletePost.path.ts │ │ │ │ ├── deletePostComment.path.ts │ │ │ │ ├── deletePostLike.path.ts │ │ │ │ ├── getAllExtendedPosts.path.ts │ │ │ │ ├── getAllPosts.path.ts │ │ │ │ ├── getExtendedPostById.path.ts │ │ │ │ ├── getPostById.path.ts │ │ │ │ └── index.ts │ │ │ │ └── users │ │ │ │ ├── getProfile.path.ts │ │ │ │ ├── index.ts │ │ │ │ ├── signin.path.ts │ │ │ │ └── updateProfile.path.ts │ │ ├── index.ts │ │ ├── middlewares │ │ │ ├── ensureAuthenticated.middleware.ts │ │ │ ├── handleHttpError.middleware.ts │ │ │ ├── index.ts │ │ │ ├── validateLogin.middleware.ts │ │ │ ├── validateNewPost.middleware.ts │ │ │ ├── validateNewPostComment.middleware.ts │ │ │ ├── validatePost.middleware.ts │ │ │ ├── validatePostComment.middleware.ts │ │ │ ├── validatePostLike.middleware.ts │ │ │ ├── validateProfileData.middleware.ts │ │ │ └── validateSignin.middleware.ts │ │ ├── routes │ │ │ ├── authentication │ │ │ │ ├── index.ts │ │ │ │ ├── login.routes.ts │ │ │ │ ├── logout.routes.ts │ │ │ │ └── test │ │ │ │ │ ├── login.route.test.ts │ │ │ │ │ └── logout.route.test.ts │ │ │ ├── index.ts │ │ │ ├── manifest │ │ │ │ ├── index.ts │ │ │ │ └── manifest.routes.ts │ │ │ ├── posts │ │ │ │ ├── index.ts │ │ │ │ ├── post.comment.routes.ts │ │ │ │ ├── post.general.routes.ts │ │ │ │ ├── post.like.routes.ts │ │ │ │ └── test │ │ │ │ │ ├── createPost.route.test.ts │ │ │ │ │ ├── createPostComment.route.test.ts │ │ │ │ │ ├── createPostLike.route.test.ts │ │ │ │ │ ├── deletePost.route.test.ts │ │ │ │ │ ├── deletePostComment.route.test.ts │ │ │ │ │ ├── deletePostLike.route.test.ts │ │ │ │ │ ├── getAllPosts.route.test.ts │ │ │ │ │ ├── getExtendedPostById.route.test.ts │ │ │ │ │ ├── getExtendedPosts.route.test.ts │ │ │ │ │ └── getPostById.route.test.ts │ │ │ └── user │ │ │ │ ├── index.ts │ │ │ │ ├── profile.routes.ts │ │ │ │ ├── signin.routes.ts │ │ │ │ └── test │ │ │ │ ├── getProfile.route.test.ts │ │ │ │ ├── signin.route.test.ts │ │ │ │ └── updateProfile.route.test.ts │ │ ├── server.ts │ │ ├── serverDtos │ │ │ ├── express.dto.ts │ │ │ └── index.ts │ │ └── validators │ │ │ ├── authentication │ │ │ ├── index.ts │ │ │ ├── login.validator.ts │ │ │ ├── test │ │ │ │ ├── validateLoginParams.test.ts │ │ │ │ └── validateToken.test.ts │ │ │ └── token.validator.ts │ │ │ ├── index.ts │ │ │ ├── posts │ │ │ ├── comment.validator.ts │ │ │ ├── index.ts │ │ │ ├── like.validator.ts │ │ │ ├── post.validator.ts │ │ │ └── test │ │ │ │ ├── validateNewPost.test.ts │ │ │ │ ├── validateNewPostComment.test.ts │ │ │ │ ├── validatePostComment.test.ts │ │ │ │ ├── validatePostLikeParams.test.ts │ │ │ │ └── validatePostParams.test.ts │ │ │ ├── user │ │ │ ├── index.ts │ │ │ ├── profile.validator.ts │ │ │ ├── signin.validator.ts │ │ │ └── test │ │ │ │ ├── validateProfileParams.test.ts │ │ │ │ └── validateSigninParams.test.ts │ │ │ └── validation.rules.ts │ └── types │ │ ├── authentication.types.ts │ │ └── index.ts ├── preset.ts └── test │ └── fixtures │ ├── assets │ ├── authentication.json │ └── avatarUrls.json │ ├── authentication.fixtures.ts │ ├── index.ts │ ├── mockers │ ├── runMockers.ts │ ├── testingAuthenticationMockFactory.ts │ ├── testingPostMockFactory.ts │ ├── testingUserMockFactory.ts │ └── utils.ts │ ├── mongodb │ ├── index.ts │ ├── posts.ts │ └── users.ts │ ├── post.fixtures.ts │ ├── types │ ├── authentication.fixture.types.ts │ ├── index.ts │ ├── post.fixture.types.ts │ └── user.fixture.types.ts │ ├── user.fixtures.ts │ └── utils.fixtures.ts ├── tsconfig.json ├── tsconfig.paths.json └── webpack ├── tsconfig.webpack.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | docker -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "standard" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "indent": 0, 20 | "@typescript-eslint/indent": [ 21 | "error", 22 | 2 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | dist 4 | coverage 5 | src/test/fixtures/assets/users.json 6 | src/test/fixtures/assets/posts.json 7 | 8 | .env 9 | manifest.json -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm install && npm install --package-lock-only && npm run deps -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.husky/pre-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | 6 | npm --no-git-tag-version version patch 7 | git add package.json package-lock.json 8 | git commit -m "package version $(node -pe "require('./package.json').version")" -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.16.0 2 | -------------------------------------------------------------------------------- /TECH-DEBT.md: -------------------------------------------------------------------------------- 1 | # Technical debt 2 | 3 | ## @types/node outdated 4 | 5 | - Recorded by: Dailos Rafael Díaz Lara 6 | - Observed at: 2021, August 4 7 | - Impact (_if this tech debt affected your work somehow, add a +1 here with a date and optionally a note_): 8 | - +1 Jane Doe 2021, March 26 (_this is an example of +1_) 9 | 10 | ### Updates 11 | 12 | No updates. 13 | 14 | ### Problem 15 | 16 | During a rutinary modules updating process, the [@types/node](https://www.npmjs.com/package/@types/node) module was updated to [v16.4.12](https://www.npmjs.com/package/@types/node/v/16.4.12). 17 | 18 | After the update, several typing errors appeared in the Express server file, boud with the use of Helmet and several methods of the Body Parser module. 19 | 20 | A research unveiled the issue was bound with this module and the quick solution was to downgrade to v14.14.45. This action removed the whole errors. 21 | 22 | The issue open in the @types/node repository is this one: [Helmet + Express + Typescript = No overload matches this call error](https://github.com/helmetjs/helmet/issues/325). 23 | 24 | ### Why it was done this way? 25 | 26 | Due to it's a third party library issue and based on it's a development module, to downgrade it was the easier option. 27 | 28 | ### Why this way is problematic? 29 | 30 | Based on this situation only affects to development tasks, it doesn't really represent a threat to the project performance. 31 | 32 | ### What the solution might be? 33 | 34 | To check periodically it the types module has been updated in order to include the new defined features and if it's so, proceed with the modules updates. 35 | 36 | ### Why we aren't already doing the above? 37 | 38 | Because the types module is not updated yet. 39 | 40 | ### Next steps 41 | 42 | - Take a look to the modules list shown in the dependencies checking process. 43 | - Update all available modules except this one. 44 | - Verify that the types modules alredy includes the new features. 45 | - If the new features are included in the types module, proceed with the full updating process. 46 | 47 | ### Other notes 48 | 49 | There are no additional notes. 50 | -------------------------------------------------------------------------------- /docker/dev/.docker.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME="ts_course_mongodb_dev" 2 | CONTAINER_NAME="ts_course_mongodb_dev" 3 | 4 | ROOT_USERNAME="root" 5 | ROOT_PASSWORD="root" 6 | ROOT_DATABASE="admin" 7 | 8 | EXTERNAL_PORT=23032 9 | INTERNAL_PORT=27017 10 | 11 | VOLUME_NAME="ts_course_mongodb_dev_data" 12 | CONFIGURATION_PATH="./dev/configureDatabase" -------------------------------------------------------------------------------- /docker/dev/configureDatabase/initDatabase.js: -------------------------------------------------------------------------------- 1 | load('/docker-entrypoint-initdb.d/usersDataToBePersisted.js') 2 | load('/docker-entrypoint-initdb.d/postsDataToBePersisted.js') 3 | 4 | const DATABASE_NAME = 'ts-course-dev'; 5 | 6 | const apiDatabases = [ 7 | { 8 | dbName: DATABASE_NAME, 9 | dbUsers: [ 10 | { 11 | username: 'tsdev', 12 | password: 'tsdev', 13 | roles: [ 14 | { 15 | role: 'readWrite', 16 | db: DATABASE_NAME, 17 | } 18 | ] 19 | } 20 | ], 21 | dbData: [ 22 | { 23 | collection: 'users', 24 | data: usersDataToBePersisted 25 | }, 26 | { 27 | collection: 'posts', 28 | data: postsDataToBePersisted 29 | } 30 | ] 31 | } 32 | ] 33 | 34 | const collections = { 35 | users: (db, userData) => db.users.insert(userData), 36 | posts: (db, postData) => db.posts.insert(postData) 37 | } 38 | 39 | const createDatabaseUsers = (db, dbName, users) => { 40 | users.map(({ username, password, roles }) => { 41 | print(`[TRACE] Creating new user '${username}' into the '${dbName}' database...`) 42 | 43 | db.createUser({ 44 | user: username, 45 | pwd: password, 46 | roles 47 | }) 48 | 49 | print(`[INFO ] The user '${username}' has been created successfully.`) 50 | }) 51 | } 52 | 53 | const populateDatabase = (db, data) => { 54 | if (data !== null && data.length > 0) { 55 | data.map((setOfData) => { 56 | print(`[TRACE] Persisting data of collection '${setOfData.collection}'...`) 57 | setOfData.data.map((document) => collections[setOfData.collection](db, document)) 58 | }) 59 | } 60 | } 61 | 62 | try { 63 | apiDatabases.map(({ dbName, dbUsers, dbData }) => { 64 | db = db.getSiblingDB(dbName) 65 | 66 | print(`[TRACE] Switching to '${dbName}' database...`) 67 | createDatabaseUsers(db, dbName, dbUsers) 68 | populateDatabase(db, dbData) 69 | }) 70 | } catch ({ message }) { 71 | print(`[ERROR ] ${message}`) 72 | } 73 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | ts_course_db: 5 | image: mongo:latest 6 | container_name: ${CONTAINER_NAME} 7 | environment: 8 | MONGO_INITDB_ROOT_USERNAME: ${ROOT_USERNAME} 9 | MONGO_INITDB_ROOT_PASSWORD: ${ROOT_PASSWORD} 10 | MONGO_INITDB_DATABASE: ${ROOT_DATABASE} 11 | ports: 12 | - ${EXTERNAL_PORT}:${INTERNAL_PORT} 13 | volumes: 14 | - data_volume:/data/db 15 | - ${CONFIGURATION_PATH}:/docker-entrypoint-initdb.d:rw 16 | 17 | volumes: 18 | data_volume: 19 | name: ${VOLUME_NAME} -------------------------------------------------------------------------------- /docker/test/.docker.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME="ts_course_mongodb_test" 2 | CONTAINER_NAME="ts_course_mongodb_test" 3 | 4 | ROOT_USERNAME="root" 5 | ROOT_PASSWORD="root" 6 | ROOT_DATABASE="admin" 7 | 8 | EXTERNAL_PORT=32023 9 | INTERNAL_PORT=27017 10 | 11 | VOLUME_NAME="ts_course_mongodb_test_data" 12 | CONFIGURATION_PATH="./test/configureDatabase" -------------------------------------------------------------------------------- /docker/test/configureDatabase/initDatabase.js: -------------------------------------------------------------------------------- 1 | const DATABASE_NAME = 'ts-course-test'; 2 | 3 | const apiDatabases = [ 4 | { 5 | dbName: DATABASE_NAME, 6 | dbUsers: [ 7 | { 8 | username: 'tstest', 9 | password: 'tstest', 10 | roles: [ 11 | { 12 | role: 'readWrite', 13 | db: DATABASE_NAME, 14 | } 15 | ] 16 | } 17 | ], 18 | dbData: [] 19 | } 20 | ] 21 | 22 | const createDatabaseUsers = (db, dbName, users) => { 23 | users.map(({ username, password, roles }) => { 24 | print(`[TRACE] Creating new user '${username}' into the '${dbName}' database...`) 25 | 26 | db.createUser({ 27 | user: username, 28 | pwd: password, 29 | roles 30 | }) 31 | 32 | print(`[INFO ] The user '${username}' has been created successfully.`) 33 | }) 34 | } 35 | 36 | try { 37 | apiDatabases.map(({ dbName, dbUsers }) => { 38 | db = db.getSiblingDB(dbName) 39 | 40 | print(`[TRACE] Switching to '${dbName}' database...`) 41 | createDatabaseUsers(db, dbName, dbUsers) 42 | }) 43 | } catch ({ message }) { 44 | print(`[ERROR ] ${message}`) 45 | } 46 | -------------------------------------------------------------------------------- /env/.env.dev: -------------------------------------------------------------------------------- 1 | NODE_ENV="development" 2 | 3 | SERVER_PORT=3600 4 | 5 | LOGGER_LEVEL="all" 6 | 7 | MONGO_USER='tsdev' 8 | MONGO_PASS='tsdev' 9 | MONGO_HOST='localhost' 10 | MONGO_PORT='23032' 11 | MONGO_DB='ts-course-dev' 12 | 13 | BCRYPT_SALT=3 14 | 15 | JWT_KEY='developmentkey' 16 | JWT_ALGORITHM='HS512' 17 | JWT_EXPIRING_TIME_IN_SECONDS=3600 -------------------------------------------------------------------------------- /env/.env.test: -------------------------------------------------------------------------------- 1 | NODE_ENV="test" 2 | 3 | SERVER_PORT=4000 4 | 5 | LOGGER_LEVEL="off" 6 | 7 | MONGO_USER='tstest' 8 | MONGO_PASS='tstest' 9 | MONGO_HOST='localhost' 10 | MONGO_PORT='32023' 11 | MONGO_DB='ts-course-test' 12 | 13 | BCRYPT_SALT=3 14 | 15 | JWT_KEY='testingkey' 16 | JWT_ALGORITHM='HS512' 17 | JWT_EXPIRING_TIME_IN_SECONDS=60 -------------------------------------------------------------------------------- /env/README.md: -------------------------------------------------------------------------------- 1 | # Environment configuration 2 | 3 | ## ⚠️ WARNING ⚠️ 4 | 5 | **THESE FILES MUST NEVER BEEN PUSHED TO ANY KIND OF REPOSITORY DUE TO THEY CAN CONTAIN SENSIBLE INFORATION FOR YOUR PROJECT** 6 | 7 | In oreder to work with this project, you are going to need to define tree differente environment files: 8 | 9 | - `.env` for production/deployment code. 10 | - `.env.dev` for development tasks. 11 | - `.env.test` for testing tasks. 12 | 13 | The basic content of these files must be like the next examples: 14 | 15 | ```sh 16 | NODE_ENV="production" | "development" | "test" 17 | 18 | # Set the port number that best matches for your environment. 19 | SERVER_PORT=4000 20 | 21 | # Set the logging level that best matches for your environment. 22 | LOGGER_LEVEL="off" | "fatal" | "error" | "warn" | "info" | "debug" | "trace" | "all" 23 | 24 | # Set the database configuration that best matches for your environment. 25 | MONGO_USER='tstest' 26 | MONGO_PASS='tstest' 27 | MONGO_HOST='localhost' 28 | MONGO_PORT='32023' 29 | MONGO_DB='ts-course-test' 30 | 31 | # Set the encryption configuration that best matches for your environment. 32 | BCRYPT_SALT=3 33 | 34 | # Set the token configuration that best matches for your environment. 35 | JWT_KEY='testingkey' 36 | JWT_ALGORITHM='HS512' 37 | JWT_EXPIRING_TIME_IN_SECONDS=60 38 | 39 | # ⚠️ Just for being includedn into '.env.test' file 40 | PLAIN_PASSWORD='123456' 41 | WRONG_PASSWORD='wrongpassword' 42 | # ⚠️ Just for being includedn into '.env.test' file 43 | 44 | # Rest of the environment variables here. 45 | ``` 46 | -------------------------------------------------------------------------------- /env/dotenv-test.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { config } from 'dotenv' 3 | 4 | module.exports = async () => { 5 | config({ path: resolve(__dirname, '../env/.env.test') }) 6 | } 7 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { appLogger } from '@logger' 2 | 3 | import { runServer, stopServer } from '@infrastructure/server' 4 | import { runOrm, stopOrm } from '@infrastructure/orm' 5 | import { checkStartup } from './preset' 6 | 7 | const startApplication = async () => { 8 | try { 9 | await runOrm() 10 | runServer() 11 | } catch ({ message }) { 12 | appLogger('error', 'Application stating error') 13 | } 14 | } 15 | 16 | const closeApplication = async () => { 17 | await stopOrm() 18 | stopServer() 19 | appLogger('info', 'Service successfully closed.') 20 | } 21 | 22 | const requiredEnvVariables = [ 23 | 'SERVER_PORT', 24 | 'LOGGER_LEVEL', 25 | 'MONGO_USER', 26 | 'MONGO_PASS', 27 | 'MONGO_HOST', 28 | 'MONGO_PORT', 29 | 'MONGO_DB', 30 | 'BCRYPT_SALT', 31 | 'JWT_KEY', 32 | 'JWT_ALGORITHM', 33 | 'JWT_EXPIRING_TIME_IN_SECONDS' 34 | ] 35 | 36 | checkStartup(requiredEnvVariables) 37 | 38 | process.on('SIGINT', async () => closeApplication()) 39 | process.on('SIGTERM', async () => closeApplication()) 40 | 41 | if (process.env.NODE_ENV !== 'test') { startApplication() } 42 | -------------------------------------------------------------------------------- /src/common/errors/ApiError.ts: -------------------------------------------------------------------------------- 1 | interface IApiError { 2 | status: number 3 | message: string 4 | description?: string 5 | } 6 | 7 | export class ApiError extends Error implements IApiError { 8 | constructor (public status: number, public message: string, public description?: string) { 9 | super() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/400.bad.request/LoginDataError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'Wrong login data' 4 | 5 | export class LoginDataError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/400.bad.request/TokenFormatError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'Wrong token format' 4 | 5 | export class TokenFormatError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/400.bad.request/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LoginDataError' 2 | export * from './TokenFormatError' 3 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/401.unauthorized/TokenExpiredError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, UNAUTHORIZED } from '@errors' 2 | 3 | const message = 'Token expired' 4 | 5 | export class TokenExpiredError extends ApiError { 6 | constructor (description?: string) { 7 | super(UNAUTHORIZED, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/401.unauthorized/WrongPasswordError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, UNAUTHORIZED } from '@errors' 2 | 3 | const message = 'Password not valid' 4 | 5 | export class WrongPasswordError extends ApiError { 6 | constructor (description?: string) { 7 | super(UNAUTHORIZED, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/401.unauthorized/WrongUsernameError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, UNAUTHORIZED } from '@errors' 2 | 3 | const message = 'Username not valid' 4 | 5 | export class WrongUsernameError extends ApiError { 6 | constructor (description?: string) { 7 | super(UNAUTHORIZED, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/401.unauthorized/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TokenExpiredError' 2 | export * from './WrongPasswordError' 3 | export * from './WrongUsernameError' 4 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/403.forbidden/RequiredTokenNotProvidedError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, FORBIDDEN } from '@errors' 2 | 3 | const message = 'Required token was not provided' 4 | 5 | export class RequiredTokenNotProvidedError extends ApiError { 6 | constructor (description?: string) { 7 | super(FORBIDDEN, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/403.forbidden/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RequiredTokenNotProvidedError' 2 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/500.internal.server.error/CheckingPasswordError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class CheckingPasswordError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/500.internal.server.error/CheckingTokenError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class CheckingTokenError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/500.internal.server.error/GettingTokenError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class GettingTokenError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/500.internal.server.error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CheckingPasswordError' 2 | export * from './CheckingTokenError' 3 | export * from './GettingTokenError' 4 | -------------------------------------------------------------------------------- /src/common/errors/AuthenticationErrors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './400.bad.request' 2 | export * from './401.unauthorized' 3 | export * from './403.forbidden' 4 | export * from './500.internal.server.error' 5 | -------------------------------------------------------------------------------- /src/common/errors/CommonErrors/InternalServerError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, INTERNAL_SERVER_ERROR } from '@errors' 2 | 3 | const message = 'Internal Server Error' 4 | 5 | export class InternalServerError extends ApiError { 6 | constructor (description?: string) { 7 | super(INTERNAL_SERVER_ERROR, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/CommonErrors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './InternalServerError' 2 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/400.bad.request/NewPostCommentError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'New post comment data error.' 4 | 5 | export class NewPostCommentError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/400.bad.request/NewPostError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'New post data error.' 4 | 5 | export class NewPostError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/400.bad.request/PostCommentError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'Post comment data error.' 4 | 5 | export class PostCommentError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/400.bad.request/PostDislikeUserError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'User must like a post before dislike it' 4 | 5 | export class PostDislikeUserError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/400.bad.request/PostIdentificationError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'Post identification not valid' 4 | 5 | export class PostIdentificationError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/400.bad.request/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PostIdentificationError' 2 | export * from './PostCommentError' 3 | export * from './PostDislikeUserError' 4 | export * from './NewPostError' 5 | export * from './NewPostCommentError' 6 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/401.unauthorized/UnauthorizedPostCommentDeletingError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, UNAUTHORIZED } from '@errors' 2 | 3 | const message = 'User not authorized to delete this comment' 4 | 5 | export class UnauthorizedPostCommentDeletingError extends ApiError { 6 | constructor (description?: string) { 7 | super(UNAUTHORIZED, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/401.unauthorized/UnauthorizedPostDeletingError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, UNAUTHORIZED } from '@errors' 2 | 3 | const message = 'User not authorized to delete this post' 4 | 5 | export class UnauthorizedPostDeletingError extends ApiError { 6 | constructor (description?: string) { 7 | super(UNAUTHORIZED, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/401.unauthorized/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UnauthorizedPostDeletingError' 2 | export * from './UnauthorizedPostCommentDeletingError' 3 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/404.not.found/PostCommentNotFoundError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, NOT_FOUND } from '@errors' 2 | 3 | const message = 'Post comment not found' 4 | 5 | export class PostCommentNotFoundError extends ApiError { 6 | constructor (description?: string) { 7 | super(NOT_FOUND, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/404.not.found/PostNotFoundError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, NOT_FOUND } from '@errors' 2 | 3 | const message = 'Post not found' 4 | 5 | export class PostNotFoundError extends ApiError { 6 | constructor (description?: string) { 7 | super(NOT_FOUND, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/404.not.found/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PostNotFoundError' 2 | export * from './PostCommentNotFoundError' 3 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/500.internal.server.error/CreatingPostCommentError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class CreatingPostCommentError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/500.internal.server.error/CreatingPostError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class CreatingPostError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/500.internal.server.error/DeletingPostCommentError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class DeletingPostCommentError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/500.internal.server.error/DeletingPostError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class DeletingPostError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/500.internal.server.error/DeletingPostLikeError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class DeletingPostLikeError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/500.internal.server.error/GettingPostCommentError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class GettingPostCommentError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/500.internal.server.error/GettingPostError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class GettingPostError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/500.internal.server.error/LikingPostError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class LikingPostError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/500.internal.server.error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CreatingPostError' 2 | export * from './CreatingPostCommentError' 3 | export * from './GettingPostError' 4 | export * from './GettingPostCommentError' 5 | export * from './DeletingPostError' 6 | export * from './DeletingPostCommentError' 7 | export * from './DeletingPostLikeError' 8 | export * from './LikingPostError' 9 | -------------------------------------------------------------------------------- /src/common/errors/PostErrors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './400.bad.request' 2 | export * from './401.unauthorized' 3 | export * from './404.not.found' 4 | export * from './500.internal.server.error' 5 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/400.bad.request/EmptyProfileDataError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'Empty profile data not allowed' 4 | 5 | export class EmptyProfileDataError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/400.bad.request/NewUserAlreadyExistsError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'User already exists' 4 | 5 | export class NewUserAlreadyExistsError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/400.bad.request/ProfileDataError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'Profile data error' 4 | 5 | export class ProfileDataError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/400.bad.request/SigninDataError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'Signin data error' 4 | 5 | export class SigninDataError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/400.bad.request/UserDoesNotExistError.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, BAD_REQUEST } from '@errors' 2 | 3 | const message = 'User does not exist' 4 | 5 | export class UserDoesNotExistError extends ApiError { 6 | constructor (description?: string) { 7 | super(BAD_REQUEST, message, description) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/400.bad.request/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UserDoesNotExistError' 2 | export * from './NewUserAlreadyExistsError' 3 | export * from './SigninDataError' 4 | export * from './ProfileDataError' 5 | export * from './EmptyProfileDataError' 6 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/500.internal.server.error/CreatingUserError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class CreatingUserError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/500.internal.server.error/GettingUserError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class GettingUserError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/500.internal.server.error/GettingUserProfileError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class GettingUserProfileError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/500.internal.server.error/UpdatingUserError.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@errors' 2 | 3 | export class UpdatingUserError extends InternalServerError {} 4 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/500.internal.server.error/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GettingUserError' 2 | export * from './GettingUserProfileError' 3 | export * from './CreatingUserError' 4 | export * from './UpdatingUserError' 5 | -------------------------------------------------------------------------------- /src/common/errors/UserErrors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './400.bad.request' 2 | export * from './500.internal.server.error' 3 | -------------------------------------------------------------------------------- /src/common/errors/httpCodes.ts: -------------------------------------------------------------------------------- 1 | export const OK = 200 2 | export const CREATED = 201 3 | export const BAD_REQUEST = 400 4 | export const UNAUTHORIZED = 401 5 | export const FORBIDDEN = 403 6 | export const NOT_FOUND = 404 7 | export const CONFLICT = 409 8 | export const INTERNAL_SERVER_ERROR = 500 9 | export const NOT_IMPLEMENTED = 501 10 | -------------------------------------------------------------------------------- /src/common/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ApiError' 2 | export * from './httpCodes' 3 | export * from './CommonErrors' 4 | export * from './AuthenticationErrors' 5 | export * from './UserErrors' 6 | export * from './PostErrors' 7 | -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils' 2 | export * from './logger' 3 | -------------------------------------------------------------------------------- /src/common/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log4js' 2 | -------------------------------------------------------------------------------- /src/common/logger/log4js/config.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'log4js' 2 | 3 | const config = { 4 | appenders: { 5 | console: { 6 | type: 'console', 7 | layout: { 8 | type: 'pattern', 9 | pattern: '[%[%5.5p%]] - %m%' 10 | } 11 | } 12 | }, 13 | categories: { 14 | default: { 15 | appenders: ['console'], 16 | level: process.env.LOGGER_LEVEL || 'all' 17 | } 18 | } 19 | } 20 | 21 | export const logger = configure(config).getLogger() 22 | -------------------------------------------------------------------------------- /src/common/logger/log4js/index.ts: -------------------------------------------------------------------------------- 1 | import { logger } from './config' 2 | 3 | type LogTypes = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'mark' 4 | 5 | export const appLogger = (logType: LogTypes, message: string) => logger[logType](`[app] - ${message}`) 6 | export const serverLogger = (logType: LogTypes, message: string) => logger[logType](`[server] - ${message}`) 7 | export const mongooseLogger = (logType: LogTypes, message: string) => logger[logType](`[mongoose] - ${message}`) 8 | export const authEndpointsLogger = (logType: LogTypes, message: string) => logger[logType](`[auth.endpoints] - ${message}`) 9 | export const postEndpointsLogger = (logType: LogTypes, message: string) => logger[logType](`[post.endpoints] - ${message}`) 10 | export const userEndpointsLogger = (logType: LogTypes, message: string) => logger[logType](`[user.endpoints] - ${message}`) 11 | -------------------------------------------------------------------------------- /src/common/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './timestamp' 2 | -------------------------------------------------------------------------------- /src/common/utils/timestamp/getUtcTimestampIsoString.ts: -------------------------------------------------------------------------------- 1 | export const getUtcTimestampIsoString = () => (new Date((new Date()).toUTCString())).toISOString() 2 | -------------------------------------------------------------------------------- /src/common/utils/timestamp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getUtcTimestampIsoString' 2 | -------------------------------------------------------------------------------- /src/common/utils/timestamp/test/getUtcTimestampIsoString.test.ts: -------------------------------------------------------------------------------- 1 | import { getUtcTimestampIsoString } from '../getUtcTimestampIsoString' 2 | 3 | describe('[UTILS] Timestamp', () => { 4 | describe('getUtcTimestampIsoString', () => { 5 | it('must provide a valid UTC ISO timestamp string', () => { 6 | const timestamp = getUtcTimestampIsoString() 7 | 8 | expect(timestamp).toMatch(/^\d{4}(-\d{2}){2}T\d{2}(:\d{2}){2}\.\d{3}Z$/) 9 | }) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './services' 2 | -------------------------------------------------------------------------------- /src/domain/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post.domain.model' 2 | export * from './user.domain.models' 3 | -------------------------------------------------------------------------------- /src/domain/models/post.domain.model.ts: -------------------------------------------------------------------------------- 1 | import { UserDomainModel } from './user.domain.models' 2 | 3 | interface DatabaseSpecificStructure { 4 | id?: string 5 | createdAt?: string 6 | updatedAt?: string 7 | } 8 | 9 | interface BasicContentStructure extends DatabaseSpecificStructure { 10 | body: string 11 | } 12 | 13 | export type OwnerBasicStructure = Pick 14 | 15 | export type PostOwnerDomainModel = OwnerBasicStructure & Pick 16 | export type PostCommentOwnerDomainModel = PostOwnerDomainModel 17 | 18 | export type PostCommentDomainModel = BasicContentStructure & { 19 | owner: PostCommentOwnerDomainModel 20 | } 21 | 22 | export type PostLikeDomainModel = PostOwnerDomainModel 23 | 24 | export interface PostDomainModel extends BasicContentStructure { 25 | owner: PostOwnerDomainModel 26 | comments: PostCommentDomainModel[] 27 | likes: PostLikeDomainModel[] 28 | } 29 | 30 | export interface ExtendedPostDomainModel extends PostDomainModel { 31 | userIsOwner?: boolean 32 | userHasLiked?: boolean 33 | comments: (PostCommentDomainModel & { userIsOwner?: boolean })[] 34 | } 35 | -------------------------------------------------------------------------------- /src/domain/models/user.domain.models.ts: -------------------------------------------------------------------------------- 1 | export interface UserDomainModel { 2 | id: string 3 | username: string 4 | password: string 5 | email: string 6 | name: string 7 | surname: string 8 | avatar: string 9 | token: string 10 | enabled: boolean 11 | deleted: boolean 12 | lastLoginAt: string 13 | createdAt: string 14 | updatedAt: string 15 | } 16 | 17 | export type NewUserDomainModel = Pick 18 | export type RegisteredUserDomainModel = Pick & { fullName: string } 19 | export type UpdateUserPayloadDomainModel = Omit, 'id' | 'username' | 'email' | 'createdAt' | 'updatedAt'> 20 | export type UserProfileDomainModel = Pick 21 | export type NewUserProfileDomainModel = Partial> 22 | export type AuthenticatedUserDomainModel = Pick 23 | -------------------------------------------------------------------------------- /src/domain/services/authentication.services.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedUserDomainModel } from '@domainModels' 2 | import { getUserByUsername, updateUserLoginData, updateUserLogoutData, checkPassword } from '@domainServices' 3 | import { decodeToken, generateToken } from '@infrastructure/authentication' 4 | import { DecodedJwtToken } from '@infrastructure/types' 5 | import { GettingTokenError, WrongUsernameError, WrongPasswordError, TokenExpiredError, CheckingTokenError } from '@errors' 6 | 7 | const getToken = (userId: string, username: string): string => { 8 | try { 9 | return generateToken(userId, username) 10 | } catch ({ message }) { 11 | throw new GettingTokenError(`Error getting token for username '${username}'. ${message}`) 12 | } 13 | } 14 | 15 | export const checkToken = (token: string): DecodedJwtToken => { 16 | try { 17 | return decodeToken(token) 18 | } catch ({ message }) { 19 | throw message.match(/expired/) 20 | ? new TokenExpiredError(`Token '${token}' expired`) 21 | : new CheckingTokenError(`Error ckecking token '${token}'. ${message}`) 22 | } 23 | } 24 | 25 | export const login = async (username: string, password: string): Promise => { 26 | const persistedUser = await getUserByUsername(username) 27 | if (!persistedUser) { 28 | throw new WrongUsernameError(`User with username '${username}' doesn't exist in login process.`) 29 | } 30 | const validPassword = await checkPassword(password, persistedUser.password) 31 | if (!validPassword) { 32 | throw new WrongPasswordError(`Password missmatches for username '${username}' in login process.`) 33 | } 34 | const token = getToken(persistedUser.id, username) 35 | await updateUserLoginData(persistedUser.id, token) 36 | 37 | return { 38 | token 39 | } 40 | } 41 | 42 | export const logout = async (userId: string): Promise => { 43 | await updateUserLogoutData(userId) 44 | } 45 | -------------------------------------------------------------------------------- /src/domain/services/hash.services.ts: -------------------------------------------------------------------------------- 1 | import { hash, compare } from 'bcrypt' 2 | import { CheckingPasswordError } from '@errors' 3 | 4 | const bcryptSalt = parseInt(process.env.BCRYPT_SALT!, 10) 5 | 6 | export const hashPassword = async (password: string): Promise => hash(password, bcryptSalt) 7 | export const checkPassword = async (plainPassword: string, hashedPassword: string): Promise => { 8 | try { 9 | return await compare(plainPassword, hashedPassword) 10 | } catch ({ message }) { 11 | throw new CheckingPasswordError(`Error checking password. ${message}`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/domain/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hash.services' 2 | export * from './authentication.services' 3 | export * from './user.services' 4 | export * from './post.services' 5 | -------------------------------------------------------------------------------- /src/domain/services/test/authentication.services/checkToken.test.ts: -------------------------------------------------------------------------------- 1 | import { TokenExpiredError, CheckingTokenError } from '@errors' 2 | import { testingExpiredJwtToken, testingMalformedJwtToken, testingUsers } from '@testingFixtures' 3 | 4 | import { checkToken } from '@domainServices' 5 | 6 | const [{ id: userId, token: validToken, username }] = testingUsers 7 | 8 | describe('[SERVICES] Authentication - checkToken', () => { 9 | it('must return the token content decoded', () => { 10 | const token = validToken 11 | const result = checkToken(token) 12 | const expectedFields = ['exp', 'iat', 'sub', 'username'] 13 | const retrievedTokenFields = Object.keys(result).sort() 14 | expect(retrievedTokenFields.sort()).toEqual(expectedFields.sort()) 15 | 16 | expect(result.exp).toBeGreaterThan(0) 17 | expect(result.iat).toBeGreaterThan(0) 18 | expect(result.sub).toBe(userId) 19 | expect(result.username).toBe(username) 20 | }) 21 | 22 | it('must throw an UNAUTHORIZED (401) error when the token is expired', () => { 23 | const token = testingExpiredJwtToken 24 | const expectedError = new TokenExpiredError(`Token '${token}' expired`) 25 | 26 | expect(() => checkToken(token)).toThrowError(expectedError) 27 | }) 28 | 29 | it('must throw an INTERNAL_SERVER_ERROR (500) when the verification process fails', async () => { 30 | const token = testingMalformedJwtToken 31 | const errorMessage = 'invalid token' 32 | const expectedError = new CheckingTokenError(`Error ckecking token '${token}'. ${errorMessage}`) 33 | 34 | expect(() => checkToken(token)).toThrowError(expectedError) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/domain/services/test/authentication.services/logout.test.ts: -------------------------------------------------------------------------------- 1 | import { userDataSource } from '@infrastructure/dataSources' 2 | import { mongodb } from '@infrastructure/orm' 3 | import { UpdatingUserError } from '@errors' 4 | import { NewUserDomainModel } from '@domainModels' 5 | import { testingUsers, cleanUsersCollectionFixture, saveUserFixture, getUserByUsernameFixture } from '@testingFixtures' 6 | 7 | import { logout } from '@domainServices' 8 | 9 | describe('[SERVICES] Authentication - logout', () => { 10 | const { connect, disconnect } = mongodb 11 | const errorMessage = 'Testing Error' 12 | const [{ username, password, email, name, surname, avatar, token }] = testingUsers 13 | const mockedUserData: NewUserDomainModel & { token: string } = { 14 | username, 15 | password, 16 | email, 17 | name, 18 | surname, 19 | avatar, 20 | token 21 | } 22 | 23 | beforeAll(async () => { 24 | await connect() 25 | await cleanUsersCollectionFixture() 26 | }) 27 | 28 | beforeEach(async () => { 29 | await cleanUsersCollectionFixture() 30 | await saveUserFixture(mockedUserData) 31 | }) 32 | 33 | afterAll(async () => { 34 | await disconnect() 35 | }) 36 | 37 | it('must logout the user and remove the persisted token', async () => { 38 | const { username } = mockedUserData 39 | const authenticatedUser = (await getUserByUsernameFixture(username))! 40 | expect(authenticatedUser.token).toBe(token) 41 | 42 | const { _id: userId } = authenticatedUser 43 | await logout(userId) 44 | 45 | const unauthenticatedUser = (await getUserByUsernameFixture(username))! 46 | 47 | expect(unauthenticatedUser.token).toBe('') 48 | }) 49 | 50 | it('must throw an INTERNAL_SERVER_ERROR (500) when the updating logout user data process fails', async () => { 51 | jest.spyOn(userDataSource, 'updateUserById').mockImplementation(() => { 52 | throw new Error(errorMessage) 53 | }) 54 | 55 | const { username } = mockedUserData 56 | const { _id: userId } = (await getUserByUsernameFixture(username))! 57 | const expectedError = new UpdatingUserError(`Error updating user '${userId}' logout data. ${errorMessage}`) 58 | 59 | try { 60 | await logout(userId) 61 | } catch (error) { 62 | expect(error.status).toBe(expectedError.status) 63 | expect(error.message).toBe(expectedError.message) 64 | expect(error.description).toBe(expectedError.description) 65 | } 66 | 67 | jest.spyOn(userDataSource, 'updateUserById').mockRestore() 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/domain/services/test/hash.servives/checkPassword.test.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | 3 | import { testingValidHashedPassword, testingValidPlainPassword, testingWrongPlainPassword } from '@testingFixtures' 4 | import { checkPassword } from '@domainServices' 5 | import { CheckingPasswordError } from '@errors' 6 | 7 | describe('[SERVICES] Hash - checkPassword', () => { 8 | const hashedPassword = testingValidHashedPassword 9 | 10 | it('must return TRUE if the provided password and the hashed one are equivalent', async () => { 11 | const plainPassword = testingValidPlainPassword 12 | 13 | expect((await checkPassword(plainPassword, hashedPassword))).toBeTruthy() 14 | }) 15 | 16 | it('must return FALSE if the provided password and the hashed one are NOT equivalent', async () => { 17 | const plainPassword = testingWrongPlainPassword 18 | 19 | expect((await checkPassword(plainPassword, hashedPassword))).toBeFalsy() 20 | }) 21 | 22 | it('must throw an INTERNAL_SERVER_ERROR (500) when the bcrypt lib fails', async () => { 23 | jest.mock('bcrypt') 24 | 25 | const plainPassword = testingValidPlainPassword 26 | const errorMessage = 'Testing Error' 27 | const expectedError = new CheckingPasswordError(`Error checking password. ${errorMessage}`) 28 | 29 | bcrypt.compare = jest.fn().mockImplementationOnce(() => { throw expectedError }) 30 | 31 | await expect(checkPassword(plainPassword, hashedPassword)).rejects.toThrowError(expectedError) 32 | 33 | jest.mock('bcrypt').resetAllMocks() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/domain/services/test/hash.servives/hashPassword.test.ts: -------------------------------------------------------------------------------- 1 | import { compare } from 'bcrypt' 2 | import { hashPassword } from '@domainServices' 3 | import { testingValidPlainPassword } from '@testingFixtures' 4 | 5 | describe('[SERVICES] Hash - hashPassword', () => { 6 | it('must return a valid hash of the provided password', async () => { 7 | const plainPassword = testingValidPlainPassword 8 | const hashedPassword = await hashPassword(plainPassword) 9 | 10 | expect(hashedPassword).toMatch(/^\$[$/.\w\d]{59}$/) 11 | expect((await compare(plainPassword, hashedPassword))).toBeTruthy() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/domain/services/test/post.services/getPostById.test.ts: -------------------------------------------------------------------------------- 1 | import { mongodb } from '@infrastructure/orm' 2 | import { postDataSource } from '@infrastructure/dataSources' 3 | import { PostDomainModel } from '@domainModels' 4 | import { 5 | testingLikedAndCommentedPersistedDtoPosts, 6 | testingLikedAndCommentedPersistedDomainModelPosts, 7 | savePostsFixture, 8 | cleanPostsCollectionFixture, 9 | testingNonValidPostId 10 | } from '@testingFixtures' 11 | 12 | import { getPostById } from '@domainServices' 13 | import { GettingPostError, PostNotFoundError } from '@errors' 14 | 15 | describe('[SERVICES] Post - getPostById', () => { 16 | const { connect, disconnect } = mongodb 17 | const errorMessage = 'Testing Error' 18 | const mockedPosts = testingLikedAndCommentedPersistedDtoPosts 19 | const resultPosts = testingLikedAndCommentedPersistedDomainModelPosts 20 | const [selectedPost] = resultPosts 21 | const { id: selectedPostId } = selectedPost 22 | const nonValidPostId = testingNonValidPostId 23 | 24 | beforeAll(async () => { 25 | await connect() 26 | await savePostsFixture(mockedPosts) 27 | }) 28 | 29 | afterAll(async () => { 30 | await cleanPostsCollectionFixture() 31 | await disconnect() 32 | }) 33 | 34 | it('must retrieve the persisted posts based on the provided ID', async () => { 35 | const postId = selectedPostId 36 | 37 | const persistedPost = await getPostById(postId) 38 | 39 | expect(persistedPost).toStrictEqual(selectedPost) 40 | }) 41 | 42 | it('must throw an NOT_FOUND (404) when the selected post does not exist', async () => { 43 | const postId = nonValidPostId 44 | const expectedError = new PostNotFoundError(`Post with id '${postId}' doesn't exist.`) 45 | 46 | await expect(getPostById(postId)).rejects.toThrowError(expectedError) 47 | }) 48 | 49 | it('must throw an INTERNAL_SERVER_ERROR (500) when the datasource throws an unexpected error', async () => { 50 | jest.spyOn(postDataSource, 'getPostById').mockImplementation(() => { 51 | throw new Error(errorMessage) 52 | }) 53 | 54 | const postId = selectedPostId 55 | const expectedError = new GettingPostError(`Error retereaving post '${postId}'. ${errorMessage}`) 56 | 57 | try { 58 | await getPostById(postId) 59 | } catch (error) { 60 | expect(error.status).toBe(expectedError.status) 61 | expect(error.message).toBe(expectedError.message) 62 | expect(error.description).toBe(expectedError.description) 63 | } 64 | 65 | jest.spyOn(postDataSource, 'getPostById').mockRestore() 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/domain/services/test/post.services/getPosts.test.ts: -------------------------------------------------------------------------------- 1 | import { mongodb } from '@infrastructure/orm' 2 | import { postDataSource } from '@infrastructure/dataSources' 3 | import { testingLikedAndCommentedPersistedDtoPosts, testingLikedAndCommentedPersistedDomainModelPosts, savePostsFixture, cleanPostsCollectionFixture } from '@testingFixtures' 4 | 5 | import { getPosts } from '@domainServices' 6 | import { GettingPostError } from '@errors' 7 | 8 | const mockedPosts = testingLikedAndCommentedPersistedDtoPosts 9 | const resultPosts = testingLikedAndCommentedPersistedDomainModelPosts 10 | 11 | describe('[SERVICES] Post - getPosts', () => { 12 | const { connect, disconnect } = mongodb 13 | const errorMessage = 'Testing error' 14 | 15 | beforeAll(async () => { 16 | await connect() 17 | }) 18 | 19 | afterEach(async () => { 20 | await cleanPostsCollectionFixture() 21 | }) 22 | 23 | afterAll(async () => { 24 | await cleanPostsCollectionFixture() 25 | await disconnect() 26 | }) 27 | 28 | it('must retrieve an empty array when there are no posts', async () => { 29 | await expect(getPosts()).resolves.toHaveLength(0) 30 | }) 31 | 32 | it('must retrieve the whole persisted posts', async () => { 33 | await savePostsFixture(mockedPosts) 34 | 35 | const persistedPosts = await getPosts() 36 | 37 | expect(persistedPosts).toHaveLength(mockedPosts.length) 38 | 39 | persistedPosts.forEach((post) => { 40 | const expectedFields = ['id', 'body', 'owner', 'comments', 'likes', 'createdAt', 'updatedAt'].sort() 41 | expect(Object.keys(post).sort()).toEqual(expectedFields) 42 | 43 | const expectedPost = resultPosts.find((resultPost) => resultPost.id === post.id?.toString()) 44 | 45 | expect(post).toStrictEqual(expectedPost) 46 | }) 47 | }) 48 | 49 | it('must throw an INTERNAL_SERVER_ERROR (500) when the datasource throws an unexpected error', async () => { 50 | jest.spyOn(postDataSource, 'getPosts').mockImplementation(() => { 51 | throw new Error(errorMessage) 52 | }) 53 | 54 | const expectedError = new GettingPostError(`Error retereaving posts. ${errorMessage}`) 55 | 56 | try { 57 | await getPosts() 58 | } catch (error) { 59 | expect(error.status).toBe(expectedError.status) 60 | expect(error.message).toBe(expectedError.message) 61 | expect(error.description).toBe(expectedError.description) 62 | } 63 | 64 | jest.spyOn(postDataSource, 'getPosts').mockRestore() 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/domain/services/test/user.services/updateUserLogoutData.test.ts: -------------------------------------------------------------------------------- 1 | import { mongodb } from '@infrastructure/orm' 2 | import { userDataSource } from '@infrastructure/dataSources' 3 | import { UpdatingUserError } from '@errors' 4 | import { testingUsers, cleanUsersCollectionFixture, saveUserFixture, getUserByUsernameFixture } from '@testingFixtures' 5 | 6 | import { updateUserLogoutData } from '@domainServices' 7 | 8 | describe('[SERVICES] User - updateUserLogoutData', () => { 9 | const { connect, disconnect } = mongodb 10 | const errorMessage = 'Testing error' 11 | const [{ username, password, email, token }] = testingUsers 12 | const mockedUserData = { 13 | username, 14 | password, 15 | email, 16 | token 17 | } 18 | 19 | beforeAll(async () => { 20 | await connect() 21 | await cleanUsersCollectionFixture() 22 | }) 23 | 24 | beforeEach(async () => { 25 | await cleanUsersCollectionFixture() 26 | await saveUserFixture(mockedUserData) 27 | }) 28 | 29 | afterAll(async () => { 30 | await disconnect() 31 | }) 32 | 33 | it('must update the user record setting the token field content to NULL', async () => { 34 | const { _id: userId, token } = (await getUserByUsernameFixture(username))! 35 | 36 | expect(token).toBe(mockedUserData.token) 37 | 38 | await updateUserLogoutData(userId) 39 | 40 | const updatedUser = (await getUserByUsernameFixture(username))! 41 | 42 | expect(updatedUser.token).toBe('') 43 | }) 44 | 45 | it('must throw an INTERNAL_SERVER_ERROR (500) when the datasource throws an unexpected error', async () => { 46 | jest.spyOn(userDataSource, 'updateUserById').mockImplementation(() => { 47 | throw new Error(errorMessage) 48 | }) 49 | 50 | const { _id: userId } = (await getUserByUsernameFixture(username))! 51 | const expectedError = new UpdatingUserError(`Error updating user '${userId}' logout data. ${errorMessage}`) 52 | 53 | try { 54 | await updateUserLogoutData(userId) 55 | } catch (error) { 56 | expect(error.status).toBe(expectedError.status) 57 | expect(error.message).toBe(expectedError.message) 58 | expect(error.description).toBe(expectedError.description) 59 | } 60 | 61 | jest.spyOn(userDataSource, 'updateUserById').mockRestore() 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/infrastructure/authentication/index.ts: -------------------------------------------------------------------------------- 1 | export * from './token' 2 | -------------------------------------------------------------------------------- /src/infrastructure/authentication/token/index.ts: -------------------------------------------------------------------------------- 1 | import { encodeJwt, decodeJwt } from './jwt' 2 | 3 | const generateToken = encodeJwt 4 | const decodeToken = decodeJwt 5 | 6 | export { 7 | generateToken, 8 | decodeToken 9 | } 10 | -------------------------------------------------------------------------------- /src/infrastructure/authentication/token/jwt/decodeJwt.ts: -------------------------------------------------------------------------------- 1 | import { verify, Secret } from 'jsonwebtoken' 2 | import { DecodedJwtToken } from '@infrastructure/types' 3 | 4 | export const decodeJwt = (encodedToken: string) => { 5 | const secret: Secret = process.env.JWT_KEY! 6 | return verify(encodedToken, secret) as DecodedJwtToken 7 | } 8 | -------------------------------------------------------------------------------- /src/infrastructure/authentication/token/jwt/encodeJwt.ts: -------------------------------------------------------------------------------- 1 | import { sign, Secret, SignOptions, Algorithm } from 'jsonwebtoken' 2 | import { JwtPayload } from '@infrastructure/types' 3 | 4 | export const encodeJwt = (userId: string, username: string): string => { 5 | const payload: JwtPayload = { 6 | sub: userId, 7 | username 8 | } 9 | const secret: Secret = process.env.JWT_KEY! 10 | const options: SignOptions = { 11 | algorithm: process.env.JWT_ALGORITHM as Algorithm ?? 'HS512', 12 | expiresIn: parseInt(process.env.JWT_EXPIRING_TIME_IN_SECONDS ?? '60', 10) 13 | } 14 | 15 | return sign(payload, secret, options) 16 | } 17 | -------------------------------------------------------------------------------- /src/infrastructure/authentication/token/jwt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './encodeJwt' 2 | export * from './decodeJwt' 3 | -------------------------------------------------------------------------------- /src/infrastructure/authentication/token/jwt/test/decodeJwt.test.ts: -------------------------------------------------------------------------------- 1 | import { sign, Secret, SignOptions, Algorithm } from 'jsonwebtoken' 2 | import { decodeJwt } from '../decodeJwt' 3 | import { JwtPayload } from '@infrastructure/types' 4 | 5 | import { testingUsers, testingExpiredJwtToken } from '@testingFixtures' 6 | 7 | const [{ id: testingUserId, username: testingUsername }] = testingUsers 8 | 9 | describe.only('[AUTHENTICATION] Token - JWT', () => { 10 | describe('decodeJwt', () => { 11 | it('must decode a valid token', () => { 12 | const payload: JwtPayload = { 13 | sub: testingUserId, 14 | username: testingUsername 15 | } 16 | const secret: Secret = process.env.JWT_KEY! 17 | const options: SignOptions = { 18 | algorithm: process.env.JWT_ALGORITHM as Algorithm ?? 'HS512', 19 | expiresIn: parseInt(process.env.JWT_EXPIRING_TIME_IN_SECONDS ?? '60', 10) 20 | } 21 | 22 | const token = sign(payload, secret, options) 23 | 24 | const decodedToken = decodeJwt(token) 25 | 26 | const expectedFields = ['exp', 'iat', 'sub', 'username'] 27 | const retrievedTokenFields = Object.keys(decodedToken).sort() 28 | expect(retrievedTokenFields.sort()).toEqual(expectedFields.sort()) 29 | 30 | expect(decodedToken.exp).toBeGreaterThan(0) 31 | expect(decodedToken.iat).toBeGreaterThan(0) 32 | expect(decodedToken.sub).toBe(testingUserId) 33 | expect(decodedToken.username).toBe(testingUsername) 34 | }) 35 | 36 | it('must throw an error when we provide a NON valid token', () => { 37 | const expiredToken = testingExpiredJwtToken 38 | 39 | try { 40 | decodeJwt(expiredToken) 41 | } catch ({ message }) { 42 | expect(message).toBe('jwt expired') 43 | } 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/infrastructure/authentication/token/jwt/test/encodeJwt.test.ts: -------------------------------------------------------------------------------- 1 | import { verify, Secret } from 'jsonwebtoken' 2 | import { encodeJwt } from '../encodeJwt' 3 | import { DecodedJwtToken } from '@infrastructure/types' 4 | 5 | import { testingUsers } from '@testingFixtures' 6 | 7 | const [{ id: userId, username: testingUsername }] = testingUsers 8 | 9 | describe('[AUTHENTICATION] Token - JWT', () => { 10 | describe('encodeJwt', () => { 11 | it('must generate a valid token', () => { 12 | const secret: Secret = process.env.JWT_KEY! 13 | 14 | let obtainedError = null 15 | 16 | try { 17 | const token = encodeJwt(userId, testingUsername) 18 | const verifiedToken = verify(token, secret) as DecodedJwtToken 19 | 20 | const expectedFields = ['exp', 'iat', 'sub', 'username'] 21 | const retrievedTokenFields = Object.keys(verifiedToken).sort() 22 | expect(retrievedTokenFields.sort()).toEqual(expectedFields.sort()) 23 | 24 | expect(verifiedToken.exp).toBeGreaterThan(0) 25 | expect(verifiedToken.iat).toBeGreaterThan(0) 26 | expect(verifiedToken.sub).toBe(userId) 27 | expect(verifiedToken.username).toBe(testingUsername) 28 | } catch (error) { 29 | obtainedError = error 30 | } finally { 31 | expect(obtainedError).toBeNull() 32 | } 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/infrastructure/dataSources/index.ts: -------------------------------------------------------------------------------- 1 | export * as userDataSource from './user.datasource' 2 | export * as postDataSource from './post.datasource' 3 | -------------------------------------------------------------------------------- /src/infrastructure/dataSources/user.datasource.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UserDomainModel, 3 | NewUserDomainModel, 4 | UserProfileDomainModel, 5 | UpdateUserPayloadDomainModel, 6 | NewUserProfileDomainModel, 7 | RegisteredUserDomainModel 8 | } from '@domainModels' 9 | import { 10 | mapUserFromDtoToDomainModel, 11 | mapUserFromDtoToProfileDomainModel, 12 | mapUserFromDtoToRegisteredDomainModel 13 | } from '@infrastructure/mappers' 14 | import { mongodb } from '@infrastructure/orm' 15 | 16 | export const createUser = async (newUserData: NewUserDomainModel): Promise => 17 | mapUserFromDtoToRegisteredDomainModel(await mongodb.requests.user.create(newUserData)) 18 | 19 | export const getUserByUsername = async (username: string): Promise => 20 | mapUserFromDtoToDomainModel(await mongodb.requests.user.getByUsername(username)) 21 | 22 | export const getUserByToken = async (token: string): Promise => 23 | mapUserFromDtoToDomainModel(await mongodb.requests.user.getByToken(token)) 24 | 25 | export const getUserProfileById = async (userId: string): Promise => 26 | mongodb.requests.user.getProfileById(userId) 27 | 28 | export const updateUserById = async (userId: string, updatedUserData: UpdateUserPayloadDomainModel): Promise => 29 | mapUserFromDtoToDomainModel(await mongodb.requests.user.updateById(userId, updatedUserData)) 30 | 31 | export const updateUserProfileById = async (userId: string, newUserProfileData: NewUserProfileDomainModel): Promise => 32 | mapUserFromDtoToProfileDomainModel(await mongodb.requests.user.updateById(userId, newUserProfileData)) 33 | -------------------------------------------------------------------------------- /src/infrastructure/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.dtos' 2 | export * from './post.dtos' 3 | -------------------------------------------------------------------------------- /src/infrastructure/dtos/post.dtos.ts: -------------------------------------------------------------------------------- 1 | import { OwnerBasicStructure } from '@domainModels' 2 | 3 | interface DatabaseSpecificStructure { 4 | _id?: string 5 | createdAt?: string 6 | updatedAt?: string 7 | } 8 | 9 | interface BasicContentStructure extends DatabaseSpecificStructure { 10 | body: string 11 | } 12 | 13 | export type PostOwnerDto = DatabaseSpecificStructure & OwnerBasicStructure & { userId: string } 14 | type PostCommentOwnerDto = PostOwnerDto 15 | 16 | export interface PostCommentDto extends BasicContentStructure { 17 | owner: PostCommentOwnerDto 18 | } 19 | 20 | export type PostLikeDto = PostOwnerDto 21 | 22 | export interface PostDto extends BasicContentStructure { 23 | owner: PostOwnerDto 24 | comments: PostCommentDto[] 25 | likes: PostLikeDto[] 26 | } 27 | -------------------------------------------------------------------------------- /src/infrastructure/dtos/user.dtos.ts: -------------------------------------------------------------------------------- 1 | import { UserDomainModel, NewUserDomainModel, UserProfileDomainModel, NewUserProfileDomainModel, UpdateUserPayloadDomainModel } from '@domainModels' 2 | 3 | export type UserDto = Omit & { _id: string } 4 | export type UserProfileDto = UserProfileDomainModel 5 | export type NewUserProfileDto = NewUserProfileDomainModel 6 | export type NewUserInputDto = Omit 7 | export type NewUserDatabaseDto = NewUserDomainModel 8 | export type UpdateUserPayloadDto = UpdateUserPayloadDomainModel 9 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.mappers' 2 | export * from './post.mappers' 3 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/post.mappers.ts: -------------------------------------------------------------------------------- 1 | import { PostDomainModel, PostOwnerDomainModel, PostCommentDomainModel } from '@domainModels' 2 | import { PostDto, PostOwnerDto, PostCommentDto } from '@infrastructure/dtos' 3 | 4 | export const mapOwnerFromDtoToDomainModel = (owner: PostOwnerDto): PostOwnerDomainModel => { 5 | const { _id, userId, createdAt, updatedAt, ...otherOwnerFields } = owner 6 | return { 7 | id: userId, 8 | ...otherOwnerFields 9 | } 10 | } 11 | 12 | export const mapPostCommentFromDtoToDomainModel = (comment: PostCommentDto): PostCommentDomainModel => { 13 | const { _id, owner, createdAt, updatedAt, ...otherCommentFields } = comment 14 | return { 15 | ...otherCommentFields, 16 | id: _id?.toString(), 17 | owner: mapOwnerFromDtoToDomainModel(owner), 18 | createdAt: createdAt && (new Date(createdAt)).toISOString(), 19 | updatedAt: updatedAt && (new Date(updatedAt)).toISOString() 20 | } 21 | } 22 | 23 | export const mapPostFromDtoToDomainModel = (post: PostDto): PostDomainModel => { 24 | const { _id, owner, comments, likes, createdAt, updatedAt, ...oherPostFields } = post 25 | 26 | const parsedOwner = mapOwnerFromDtoToDomainModel(owner) 27 | const parsedComments = comments.map((comment) => mapPostCommentFromDtoToDomainModel(comment)) 28 | const parsedLikes = likes.map((like) => mapOwnerFromDtoToDomainModel(like)) 29 | return { 30 | ...oherPostFields, 31 | id: _id?.toString(), 32 | owner: parsedOwner, 33 | comments: parsedComments, 34 | likes: parsedLikes, 35 | createdAt: createdAt && (new Date(createdAt)).toISOString(), 36 | updatedAt: updatedAt && (new Date(updatedAt)).toISOString() 37 | } 38 | } 39 | 40 | export const mapPostOwnerFromDomainModelToDto = (owner: PostOwnerDomainModel): PostOwnerDto => { 41 | const { id, ...otherOwnerFields } = owner 42 | return { 43 | ...otherOwnerFields, 44 | userId: id.toString() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/test/post.mappers/mapOwnerFromDtoToDomainModel.test.ts: -------------------------------------------------------------------------------- 1 | import { PostOwnerDomainModel } from '@domainModels' 2 | import { testingDtoPostCommentOwners } from '@testingFixtures' 3 | 4 | import { mapOwnerFromDtoToDomainModel } from '@infrastructure/mappers' 5 | import { PostOwnerDto } from '@infrastructure/dtos' 6 | 7 | describe('[MAPPERS] Post mapper - mapOwnerFromDtoToDomainModel', () => { 8 | it('maps successfully from Domain to DTO', () => { 9 | const [testingPostCommentOwner] = testingDtoPostCommentOwners 10 | const { userId, name, surname, avatar } = testingPostCommentOwner 11 | const originalOwner: PostOwnerDto = { 12 | ...testingPostCommentOwner, 13 | _id: userId, 14 | createdAt: (new Date()).toISOString().replace(/\dZ/, '0Z'), 15 | updatedAt: (new Date()).toISOString().replace(/\dZ/, '0Z') 16 | } 17 | const expectedOwner: PostOwnerDomainModel = { 18 | id: userId, 19 | name, 20 | surname, 21 | avatar 22 | } 23 | 24 | const mappedData = mapOwnerFromDtoToDomainModel(originalOwner) 25 | expect(mappedData).toStrictEqual(expectedOwner) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/test/post.mappers/mapPostCommentFromDtoToDomainModel.test.ts: -------------------------------------------------------------------------------- 1 | import { PostCommentDomainModel } from '@domainModels' 2 | import { testingCommentedPersistedDtoPosts } from '@testingFixtures' 3 | 4 | import { mapPostCommentFromDtoToDomainModel } from '@infrastructure/mappers' 5 | 6 | describe('[MAPPERS] Post mapper - mapPostCommentFromDtoToDomainModel', () => { 7 | it('maps successfully from Domain to DTO', () => { 8 | const [selectedPost] = testingCommentedPersistedDtoPosts 9 | const [orifginalComment] = selectedPost.comments 10 | const { _id: commentId, owner, ...otherCommentFields } = orifginalComment 11 | const { _id: commentOwnerId, userId, createdAt, updatedAt, ...otherCommentOwnerFields } = owner 12 | 13 | const expectedOwner: PostCommentDomainModel = { 14 | ...otherCommentFields, 15 | id: commentId, 16 | owner: { 17 | id: userId, 18 | ...otherCommentOwnerFields 19 | } 20 | } 21 | 22 | const mappedData = mapPostCommentFromDtoToDomainModel(orifginalComment) 23 | expect(mappedData).toStrictEqual(expectedOwner) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/test/post.mappers/mapPostFromDtoToDomainModel.test.ts: -------------------------------------------------------------------------------- 1 | import { testingLikedAndCommentedPersistedDtoPosts, testingLikedAndCommentedPersistedDomainModelPosts } from '@testingFixtures' 2 | 3 | import { mapPostFromDtoToDomainModel } from '@infrastructure/mappers' 4 | 5 | describe('[MAPPERS] Post mapper - mapPostFromDtoToDomainModel', () => { 6 | it('maps successfully from DTO to Domain', () => { 7 | const [originalPost] = testingLikedAndCommentedPersistedDtoPosts 8 | const [expectedPost] = testingLikedAndCommentedPersistedDomainModelPosts 9 | 10 | const mappedData = mapPostFromDtoToDomainModel(originalPost) 11 | expect(mappedData).toStrictEqual(expectedPost) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/test/post.mappers/mapPostOwnerFromDomainModelToDto.test.ts: -------------------------------------------------------------------------------- 1 | import { testingDomainModelPostOwners } from '@testingFixtures' 2 | 3 | import { mapPostOwnerFromDomainModelToDto } from '@infrastructure/mappers' 4 | 5 | describe('[MAPPERS] Post mapper - mapPostOwnerFromDomainModelToDto', () => { 6 | it('maps successfully from Domain to DTO', () => { 7 | const [originalPostOwner] = testingDomainModelPostOwners 8 | const { id, ...otherOriginalPostOwnerFields } = originalPostOwner 9 | const expectedPostOwner = { 10 | ...otherOriginalPostOwnerFields, 11 | userId: id 12 | } 13 | 14 | const mappedData = mapPostOwnerFromDomainModelToDto(originalPostOwner) 15 | expect(mappedData).toStrictEqual(expectedPostOwner) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/test/user.mappers/mapNewUserFromDtoToDomain.test.ts: -------------------------------------------------------------------------------- 1 | import { NewUserInputDto } from '@infrastructure/dtos' 2 | import { NewUserDomainModel } from '@domainModels' 3 | 4 | import { testingUsers } from '@testingFixtures' 5 | 6 | import { mapNewUserFromDtoToDomainModel } from '@infrastructure/mappers' 7 | 8 | const [{ email, password, name, surname, avatar }] = testingUsers 9 | 10 | describe('[MAPPERS] User mapper - mapNewUserFromDtoToDomainModel', () => { 11 | it('maps successfully from DTO to Domain', () => { 12 | const originData: NewUserInputDto = { 13 | email, 14 | password, 15 | name, 16 | surname, 17 | avatar 18 | } 19 | const expectedData: NewUserDomainModel = { 20 | ...originData, 21 | username: email 22 | } 23 | const mappedData = mapNewUserFromDtoToDomainModel(originData) 24 | 25 | expect(mappedData).toEqual(expectedData) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/test/user.mappers/mapUserFromDtoToDomainModel.test.ts: -------------------------------------------------------------------------------- 1 | import { UserDto } from '@infrastructure/dtos' 2 | import { UserDomainModel } from '@domainModels' 3 | 4 | import { testingUsers } from '@testingFixtures' 5 | 6 | import { mapUserFromDtoToDomainModel } from '@infrastructure/mappers' 7 | 8 | const [{ id, ...otherUserFields }] = testingUsers 9 | 10 | describe('[MAPPERS] User mapper - mapUserFromDtoToDomainModel', () => { 11 | it('must return null when we provide null', () => { 12 | expect(mapUserFromDtoToDomainModel(null)).toBeNull() 13 | }) 14 | 15 | it('maps successfully from DTO to Domain when we provide user information', () => { 16 | const originData: UserDto = { 17 | _id: id, 18 | ...otherUserFields, 19 | createdAt: (new Date()).toISOString().replace(/\dZ/, '0Z'), 20 | updatedAt: (new Date()).toISOString().replace(/\dZ/, '0Z') 21 | } 22 | const expectedData: UserDomainModel = { 23 | id, 24 | ...otherUserFields, 25 | createdAt: originData.createdAt, 26 | updatedAt: originData.updatedAt 27 | } 28 | const mappedData = mapUserFromDtoToDomainModel(originData) 29 | 30 | expect(mappedData).toEqual(expectedData) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/test/user.mappers/mapUserFromDtoToProfileDomainModel.test.ts: -------------------------------------------------------------------------------- 1 | import { UserDto } from '@infrastructure/dtos' 2 | import { UserProfileDomainModel } from '@domainModels' 3 | 4 | import { testingUsers } from '@testingFixtures' 5 | 6 | import { mapUserFromDtoToProfileDomainModel } from '@infrastructure/mappers' 7 | 8 | const [{ id, username, email, name, surname, avatar, ...otherUserFields }] = testingUsers 9 | 10 | describe('[MAPPERS] User mapper - mapUserFromDtoToProfileDomainModel', () => { 11 | it('must return null when we provide null', () => { 12 | expect(mapUserFromDtoToProfileDomainModel(null)).toBeNull() 13 | }) 14 | 15 | it('maps successfully from DTO to Domain', () => { 16 | const originData: UserDto = { 17 | _id: id, 18 | username, 19 | email, 20 | name, 21 | surname, 22 | avatar, 23 | ...otherUserFields, 24 | createdAt: (new Date()).toISOString().replace(/\dZ/, '0Z'), 25 | updatedAt: (new Date()).toISOString().replace(/\dZ/, '0Z') 26 | } 27 | const expectedData: UserProfileDomainModel = { 28 | username, 29 | email, 30 | name, 31 | surname, 32 | avatar 33 | } 34 | const mappedData = mapUserFromDtoToProfileDomainModel(originData) 35 | 36 | expect(mappedData).toEqual(expectedData) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/infrastructure/mappers/user.mappers.ts: -------------------------------------------------------------------------------- 1 | import { NewUserInputDto, UserDto } from '@infrastructure/dtos' 2 | import { NewUserDomainModel, RegisteredUserDomainModel, UserDomainModel, UserProfileDomainModel } from '@domainModels' 3 | 4 | export const mapUserFromDtoToRegisteredDomainModel = (user: UserDto): RegisteredUserDomainModel => { 5 | const { username, name, surname, avatar } = user 6 | 7 | return { 8 | username, 9 | fullName: `${name} ${surname}`, 10 | avatar 11 | } 12 | } 13 | 14 | export const mapUserFromDtoToDomainModel = (user: UserDto | null): UserDomainModel | null => { 15 | if (!user) { return user } 16 | const { _id, ...otherUserfields } = user 17 | 18 | return { 19 | id: _id.toString(), 20 | ...otherUserfields 21 | } 22 | } 23 | 24 | export const mapNewUserFromDtoToDomainModel = (newUserDto: NewUserInputDto): NewUserDomainModel => { 25 | const { email } = newUserDto 26 | 27 | return { 28 | ...newUserDto, 29 | username: email 30 | } 31 | } 32 | 33 | export const mapUserFromDtoToProfileDomainModel = (user: UserDto | null): UserProfileDomainModel | null => { 34 | if (!user) { return user } 35 | const { username, email, name, surname, avatar } = user 36 | 37 | return { 38 | username, 39 | email, 40 | name, 41 | surname, 42 | avatar 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/infrastructure/orm/index.ts: -------------------------------------------------------------------------------- 1 | import * as mongodb from './mongoose' 2 | 3 | export { mongodb } 4 | 5 | export const runOrm = async () => { 6 | await mongodb.connect() 7 | } 8 | 9 | export const stopOrm = async () => { 10 | await mongodb.disconnect() 11 | } 12 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/core/config.ts: -------------------------------------------------------------------------------- 1 | import { mongooseLogger } from '@logger' 2 | 3 | import mongoose, { ConnectionOptions } from 'mongoose' 4 | import Bluebird from 'bluebird' 5 | 6 | mongoose.Promise = Bluebird 7 | 8 | mongoose.connection.on('connected', () => { 9 | mongooseLogger('info', `DDBB '${mongoose.connection.db.databaseName}' connection success.`) 10 | }) 11 | 12 | mongoose.connection.on('disconnected', () => { 13 | mongooseLogger('info', 'DDBB connection successfully closed.') 14 | }) 15 | 16 | mongoose.connection.on('error', ({ message }) => { 17 | mongooseLogger('error', `DDBB error. ${message}`) 18 | }) 19 | 20 | const MONGO_USER = process.env.MONGO_USER 21 | const MONGO_PASS = process.env.MONGO_PASS 22 | const MONGO_HOST = process.env.MONGO_HOST 23 | const MONGO_PORT = process.env.MONGO_PORT 24 | const MONGO_DB = process.env.MONGO_DB 25 | 26 | const MONGO_URI = `mongodb://${MONGO_USER}:${MONGO_PASS}@${MONGO_HOST}:${MONGO_PORT}/${MONGO_DB}` 27 | 28 | const MONGO_OPTIONS: ConnectionOptions = { 29 | useCreateIndex: true, 30 | useNewUrlParser: true, 31 | useUnifiedTopology: true, 32 | useFindAndModify: false 33 | } 34 | 35 | const MONGO_SCHEMA_OPTIONS = { 36 | versionKey: false, 37 | timestamps: true, 38 | toJSON: { 39 | versionKey: false 40 | // transform: (doc: unknown, ret: Record, options: unknown) => { 41 | // if (ret._id) { 42 | // ret.id = ret._id 43 | // delete ret._id 44 | // } 45 | // return ret 46 | // } 47 | } 48 | } 49 | 50 | export { mongoose, MONGO_DB, MONGO_URI, MONGO_OPTIONS, MONGO_SCHEMA_OPTIONS } 51 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/core/connect.ts: -------------------------------------------------------------------------------- 1 | import { mongooseLogger } from '@logger' 2 | import { mongoose, MONGO_URI, MONGO_OPTIONS } from './config' 3 | 4 | export const connect = async () => { 5 | try { 6 | const connection = await mongoose.connect(MONGO_URI, MONGO_OPTIONS) 7 | if (!connection) { 8 | mongooseLogger('error', 'DDBB connection failed.') 9 | } 10 | } catch ({ message }) { 11 | mongooseLogger('error', `DDBB connection error. ${message}`) 12 | throw new Error() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/core/disconnect.ts: -------------------------------------------------------------------------------- 1 | import { mongoose } from './config' 2 | 3 | export const disconnect = async () => { 4 | if (mongoose.connection.readyState) { 5 | await mongoose.disconnect() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/core/index.ts: -------------------------------------------------------------------------------- 1 | export { MONGO_SCHEMA_OPTIONS } from './config' 2 | export * from './connect' 3 | export * from './disconnect' 4 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/index.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from './core' 2 | import * as models from './models' 3 | import * as requests from './requests' 4 | 5 | export { 6 | connect, 7 | disconnect, 8 | models, 9 | requests 10 | } 11 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.mongodb.model' 2 | export * from './post.mongodb.model' 3 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/models/post.mongodb.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose' 2 | import { MONGO_SCHEMA_OPTIONS } from '../core' 3 | import { PostOwnerDto, PostCommentDto, PostDto } from '@infrastructure/dtos' 4 | 5 | type PostOwnerMongo = Omit 6 | type PostCommentMongo = Omit 7 | type PostMongo = Omit 8 | 9 | const PostOwnerSchema = new Schema({ 10 | userId: { type: String, required: true }, 11 | name: { type: String, required: true }, 12 | surname: { type: String, default: '' }, 13 | avatar: { type: String, default: null } 14 | }, MONGO_SCHEMA_OPTIONS) 15 | 16 | const PostCommentOwnerSchema = PostOwnerSchema 17 | const PostLikeOwnerSchema = PostOwnerSchema 18 | 19 | const PostCommentSchema = new Schema({ 20 | body: { type: String, required: true }, 21 | owner: { type: PostCommentOwnerSchema, required: true } 22 | }, MONGO_SCHEMA_OPTIONS) 23 | 24 | const PostSchema = new Schema({ 25 | body: { type: String, required: true }, 26 | owner: { type: PostOwnerSchema, required: true }, 27 | comments: { type: [PostCommentSchema], required: true, default: [] }, 28 | likes: { type: [PostLikeOwnerSchema], required: true, default: [] } 29 | }, MONGO_SCHEMA_OPTIONS) 30 | 31 | export const Post = mongoose.model('Post', PostSchema) 32 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/models/user.mongodb.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from 'mongoose' 2 | import { MONGO_SCHEMA_OPTIONS } from '../core' 3 | import { UserDto } from '@infrastructure/dtos' 4 | 5 | type UserMongo = Omit 6 | 7 | const UserSchema = new Schema({ 8 | username: { type: String, required: true, unique: true, immutable: true }, 9 | password: { type: String, required: true }, 10 | email: { type: String, required: true, immutable: true }, 11 | name: { type: String, default: '' }, 12 | surname: { type: String, default: '' }, 13 | avatar: { type: String, default: '' }, 14 | token: { type: String, default: '' }, 15 | enabled: { type: Boolean, required: true, default: true }, 16 | deleted: { type: Boolean, required: true, default: false }, 17 | lastLoginAt: { type: String, default: '' } 18 | }, MONGO_SCHEMA_OPTIONS) 19 | 20 | export const User = model('User', UserSchema) 21 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/index.ts: -------------------------------------------------------------------------------- 1 | export * as user from './user.mongodb.requests' 2 | export * as post from './post.mongodb.requests' 3 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/post.requests/create.test.ts: -------------------------------------------------------------------------------- 1 | import { lorem } from 'faker' 2 | import { connect, disconnect } from '../../../core' 3 | import { PostOwnerDto } from '@infrastructure/dtos' 4 | import { testingDtoPostOwners, cleanPostsCollectionFixture } from '@testingFixtures' 5 | 6 | import { create } from '../../post.mongodb.requests' 7 | 8 | describe('[ORM] MongoDB - Posts - create', () => { 9 | beforeAll(async () => { 10 | await connect() 11 | }) 12 | 13 | beforeEach(async () => { 14 | await cleanPostsCollectionFixture() 15 | }) 16 | 17 | afterAll(async () => { 18 | await cleanPostsCollectionFixture() 19 | await disconnect() 20 | }) 21 | 22 | it('must persist the new user successfully', async () => { 23 | const body = lorem.paragraph() 24 | const [owner] = testingDtoPostOwners 25 | const basicPost = { body, owner, comments: [], likes: [] } 26 | 27 | const createdPost = (await create(basicPost))! 28 | 29 | const expectedFields = ['_id', 'body', 'owner', 'comments', 'likes', 'createdAt', 'updatedAt'] 30 | const createdPostFields = Object.keys(createdPost).sort() 31 | expect(createdPostFields.sort()).toEqual(expectedFields.sort()) 32 | 33 | expect(createdPost._id).not.toBeNull() 34 | expect(createdPost.body).toBe(basicPost.body) 35 | 36 | const expectedPostOwnerFields = ['_id', 'userId', 'name', 'surname', 'avatar', 'createdAt', 'updatedAt'] 37 | const createdOwnerPostFields = Object.keys(createdPost.owner).sort() 38 | expect(createdOwnerPostFields.sort()).toEqual(expectedPostOwnerFields.sort()) 39 | 40 | // NOTE The fiels 'createdAt' and 'updatedAt' are retrived as 'object' from the database and not as 'string'. 41 | expect(JSON.parse(JSON.stringify(createdPost.owner))).toStrictEqual(basicPost.owner) 42 | 43 | expect(createdPost.comments).toStrictEqual(basicPost.comments) 44 | expect(createdPost.likes).toStrictEqual(basicPost.likes) 45 | expect(createdPost.createdAt).not.toBeNull() 46 | expect(createdPost.updatedAt).not.toBeNull() 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/post.requests/deleteComment.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { testingLikedAndCommentedPersistedDtoPosts, savePostsFixture, cleanPostsCollectionFixture } from '@testingFixtures' 3 | 4 | import { deleteComment } from '../../post.mongodb.requests' 5 | 6 | describe('[ORM] MongoDB - Posts - deleteComment', () => { 7 | const mockedPosts = testingLikedAndCommentedPersistedDtoPosts 8 | const [selectedPost] = mockedPosts 9 | const [selectedComment] = selectedPost.comments 10 | 11 | beforeAll(async () => { 12 | await connect() 13 | await savePostsFixture(mockedPosts) 14 | }) 15 | 16 | afterAll(async () => { 17 | await cleanPostsCollectionFixture() 18 | await disconnect() 19 | }) 20 | 21 | it('must delete the selected post comment', async () => { 22 | const postId = selectedPost._id 23 | const commentId = selectedComment._id 24 | 25 | const { comments: updatedComments } = await deleteComment(postId, commentId) 26 | 27 | expect(updatedComments).toHaveLength(selectedPost.comments.length - 1) 28 | expect(updatedComments.map(({ _id }) => _id).includes(commentId)).toBeFalsy() 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/post.requests/deletePost.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { testingLikedAndCommentedPersistedDtoPosts, savePostsFixture, cleanPostsCollectionFixture, getPostByIdFixture } from '@testingFixtures' 3 | 4 | import { deletePost } from '../../post.mongodb.requests' 5 | 6 | describe('[ORM] MongoDB - Posts - deletePost', () => { 7 | const [selectedPost] = testingLikedAndCommentedPersistedDtoPosts 8 | 9 | beforeAll(async () => { 10 | await connect() 11 | await savePostsFixture(testingLikedAndCommentedPersistedDtoPosts) 12 | }) 13 | 14 | afterAll(async () => { 15 | await cleanPostsCollectionFixture() 16 | await disconnect() 17 | }) 18 | 19 | it('must delete the selected post', async () => { 20 | const postId = selectedPost._id 21 | 22 | await deletePost(postId) 23 | 24 | const retrievedPost = await getPostByIdFixture(postId) 25 | 26 | expect(retrievedPost).toBeNull() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/post.requests/dislike.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { testingLikedAndCommentedPersistedDtoPosts, savePostsFixture, cleanPostsCollectionFixture } from '@testingFixtures' 3 | 4 | import { dislike } from '../../post.mongodb.requests' 5 | 6 | describe('[ORM] MongoDB - Posts - dislike', () => { 7 | const [selectedPost] = testingLikedAndCommentedPersistedDtoPosts 8 | const [selectedLike] = selectedPost.likes 9 | 10 | beforeAll(async () => { 11 | await connect() 12 | await savePostsFixture(testingLikedAndCommentedPersistedDtoPosts) 13 | }) 14 | 15 | afterAll(async () => { 16 | await cleanPostsCollectionFixture() 17 | await disconnect() 18 | }) 19 | 20 | it('must delete the selected post like', async () => { 21 | const postId = selectedPost._id 22 | const userId = selectedLike.userId 23 | 24 | const { likes: updatedLikes } = await dislike(postId, userId) 25 | 26 | expect(updatedLikes).toHaveLength(selectedPost.likes.length - 1) 27 | expect(updatedLikes.map(({ userId: updatedUserId }) => updatedUserId).includes(userId)).toBeFalsy() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/post.requests/getAll.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { PostDto } from '@infrastructure/dtos' 3 | import { testingLikedAndCommentedPersistedDtoPosts, savePostsFixture, cleanPostsCollectionFixture } from '@testingFixtures' 4 | 5 | import { getAll } from '../../post.mongodb.requests' 6 | 7 | describe('[ORM] MongoDB - Posts - getAll', () => { 8 | const mockedPosts = testingLikedAndCommentedPersistedDtoPosts 9 | 10 | beforeAll(async () => { 11 | await connect() 12 | await savePostsFixture(mockedPosts) 13 | }) 14 | 15 | afterAll(async () => { 16 | await cleanPostsCollectionFixture() 17 | await disconnect() 18 | }) 19 | 20 | it('must retrieve the whole persisted posts', async () => { 21 | const persistedPosts = (await getAll())! 22 | 23 | expect(persistedPosts).toHaveLength(persistedPosts.length) 24 | 25 | persistedPosts.forEach((post) => { 26 | const expectedFields = ['_id', 'body', 'owner', 'comments', 'likes', 'createdAt', 'updatedAt'] 27 | const getAlldPostFields = Object.keys(post).sort() 28 | expect(getAlldPostFields.sort()).toEqual(expectedFields.sort()) 29 | 30 | const expectedPost = mockedPosts.find((mockedPost) => mockedPost._id === post._id?.toString())! 31 | 32 | // NOTE The fiels 'createdAt' and 'updatedAt' are retrived as 'object' from the database and not as 'string'. 33 | expect(JSON.parse(JSON.stringify(post))).toStrictEqual(expectedPost) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/post.requests/getById.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { PostDto } from '@infrastructure/dtos' 3 | import { 4 | testingLikedAndCommentedPersistedDtoPosts, 5 | savePostsFixture, 6 | cleanPostsCollectionFixture, 7 | testingNonValidPostId 8 | } from '@testingFixtures' 9 | 10 | import { getById } from '../../post.mongodb.requests' 11 | 12 | describe('[ORM] MongoDB - Posts - getById', () => { 13 | const [selectedPost] = testingLikedAndCommentedPersistedDtoPosts 14 | const nonValidPostId = testingNonValidPostId 15 | 16 | beforeAll(async () => { 17 | await connect() 18 | await savePostsFixture(testingLikedAndCommentedPersistedDtoPosts) 19 | }) 20 | 21 | afterAll(async () => { 22 | await cleanPostsCollectionFixture() 23 | await disconnect() 24 | }) 25 | 26 | it('must retrieve the selected post', async () => { 27 | const postId = selectedPost._id 28 | 29 | const persistedPost = (await getById(postId))! 30 | 31 | // NOTE The fiels 'createdAt' and 'updatedAt' are retrived as 'object' from the database and not as 'string'. 32 | expect(JSON.parse(JSON.stringify(persistedPost))).toStrictEqual(selectedPost) 33 | }) 34 | 35 | it('must return NULL when the provided post ID doesn\'t exist', async () => { 36 | const postId = nonValidPostId 37 | 38 | await expect(getById(postId)).resolves.toBeNull() 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/post.requests/getComment.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { PostCommentDto } from '@infrastructure/dtos' 3 | import { testingLikedAndCommentedPersistedDtoPosts, savePostsFixture, cleanPostsCollectionFixture, testingNonValidPostCommentId, testingNonValidPostId } from '@testingFixtures' 4 | 5 | import { getComment } from '../../post.mongodb.requests' 6 | 7 | describe('[ORM] MongoDB - Posts - getComment', () => { 8 | const [selectedPost, completeDtoPostWithNoComments] = testingLikedAndCommentedPersistedDtoPosts 9 | 10 | const { _id: selectedPostId } = selectedPost 11 | const [selectedComment] = selectedPost.comments 12 | const { _id: selectedCommentId } = selectedComment 13 | 14 | const { _id: noCommentsPostId } = completeDtoPostWithNoComments 15 | completeDtoPostWithNoComments.comments = [] 16 | 17 | const mockedNonValidPostId = testingNonValidPostId 18 | const mockedNonValidCommentId = testingNonValidPostCommentId 19 | 20 | beforeAll(async () => { 21 | await connect() 22 | await savePostsFixture([selectedPost, completeDtoPostWithNoComments]) 23 | }) 24 | 25 | afterAll(async () => { 26 | await cleanPostsCollectionFixture() 27 | await disconnect() 28 | }) 29 | 30 | it('must retrieve the selected post comment', async () => { 31 | const postId = selectedPostId! 32 | const commentId = selectedCommentId! 33 | 34 | const persistedComment = (await getComment(postId, commentId))! 35 | 36 | const expectedFields = ['_id', 'body', 'owner', 'createdAt', 'updatedAt'] 37 | const persistedCommentFields = Object.keys(persistedComment).sort() 38 | expect(persistedCommentFields.sort()).toEqual(expectedFields.sort()) 39 | 40 | // NOTE The fiels 'createdAt' and 'updatedAt' are retrived as 'object' from the database and not as 'string'. 41 | expect(JSON.parse(JSON.stringify(persistedComment))).toStrictEqual(selectedComment) 42 | }) 43 | 44 | it('must return NULL when the selected post doesn\'t exist', async () => { 45 | const postId = mockedNonValidPostId 46 | const commentId = selectedCommentId! 47 | 48 | await expect(getComment(postId, commentId)).resolves.toBeNull() 49 | }) 50 | 51 | it('must return NULL when select a post which doesn\'t contain the provided comment', async () => { 52 | const postId = noCommentsPostId! 53 | const commentId = selectedCommentId! 54 | 55 | await expect(getComment(postId, commentId)).resolves.toBeNull() 56 | }) 57 | 58 | it('must return NULL when provide a comment which is not contained into the selected post', async () => { 59 | const postId = selectedPostId! 60 | const commentId = mockedNonValidCommentId 61 | 62 | await expect(getComment(postId, commentId)).resolves.toBeNull() 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/post.requests/like.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { testingLikedAndCommentedPersistedDtoPosts, testingDtoFreeUsers, savePostsFixture, cleanPostsCollectionFixture } from '@testingFixtures' 3 | 4 | import { like } from '../../post.mongodb.requests' 5 | 6 | describe('[ORM] MongoDB - Posts - like', () => { 7 | const [selectedPost] = testingLikedAndCommentedPersistedDtoPosts 8 | 9 | beforeAll(async () => { 10 | await connect() 11 | await savePostsFixture([selectedPost]) 12 | }) 13 | 14 | afterAll(async () => { 15 | await cleanPostsCollectionFixture() 16 | await disconnect() 17 | }) 18 | 19 | it('must persist the new like into the selected post', async () => { 20 | const { _id: postId } = selectedPost 21 | const [likeOwner] = testingDtoFreeUsers 22 | 23 | const updatedPost = await like(postId, likeOwner) 24 | 25 | const expectedFields = ['_id', 'body', 'owner', 'comments', 'likes', 'createdAt', 'updatedAt'] 26 | const updatedPostFields = Object.keys(updatedPost).sort() 27 | expect(updatedPostFields.sort()).toEqual(expectedFields.sort()) 28 | 29 | expect(updatedPost._id?.toString()).toBe(postId) 30 | expect(updatedPost.body).toBe(selectedPost.body) 31 | // NOTE The fiels 'createdAt' and 'updatedAt' are retrived as 'object' from the database and not as 'string'. 32 | expect(JSON.parse(JSON.stringify(updatedPost.owner))).toStrictEqual(selectedPost.owner) 33 | expect(JSON.parse(JSON.stringify(updatedPost.comments))).toStrictEqual(selectedPost.comments) 34 | 35 | expect(updatedPost.likes).toHaveLength(selectedPost.likes.length + 1) 36 | const originalLikesIds = selectedPost.likes.map(({ _id }) => _id?.toString()) 37 | const updatedLikesIds = updatedPost.likes.map(({ _id }) => _id?.toString()) 38 | const newLikeId = updatedLikesIds.find((updatedId) => !originalLikesIds.includes(updatedId)) 39 | const newPersistedLike = updatedPost.likes.find((like) => like._id?.toString() === newLikeId)! 40 | expect(newPersistedLike.userId).toBe(likeOwner.userId) 41 | expect(newPersistedLike.name).toBe(likeOwner.name) 42 | expect(newPersistedLike.surname).toBe(likeOwner.surname) 43 | expect(newPersistedLike.avatar).toBe(likeOwner.avatar) 44 | 45 | expect((new Date(updatedPost.createdAt!)).toISOString()).toBe(selectedPost.createdAt) 46 | expect((new Date(updatedPost.updatedAt!)).toISOString()).not.toBe(selectedPost.updatedAt) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/user.requests/create.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { NewUserDatabaseDto } from '@infrastructure/dtos' 3 | 4 | import { testingUsers, cleanUsersCollectionFixture } from '@testingFixtures' 5 | 6 | import { create } from '../../user.mongodb.requests' 7 | 8 | const [{ username, password, email, name, surname, avatar }] = testingUsers 9 | 10 | describe('[ORM] MongoDB - create', () => { 11 | const mockedUserData: NewUserDatabaseDto = { 12 | username, 13 | password, 14 | email, 15 | name, 16 | surname, 17 | avatar 18 | } 19 | 20 | beforeAll(async () => { 21 | await connect() 22 | }) 23 | 24 | beforeEach(async () => { 25 | await cleanUsersCollectionFixture() 26 | }) 27 | 28 | afterAll(async () => { 29 | await cleanUsersCollectionFixture() 30 | await disconnect() 31 | }) 32 | 33 | it('must persist the new user successfully', async () => { 34 | const newUserData = { ...mockedUserData } 35 | const registeredUser = await create(newUserData) 36 | 37 | const expectedFields = ['_id', 'username', 'password', 'email', 'name', 'surname', 'avatar', 'token', 'enabled', 'deleted', 'lastLoginAt', 'createdAt', 'updatedAt'] 38 | const registeredUserFields = Object.keys(registeredUser).sort() 39 | expect(registeredUserFields.sort()).toEqual(expectedFields.sort()) 40 | 41 | expect(registeredUser._id).not.toBeNull() 42 | expect(registeredUser.username).toBe(mockedUserData.username) 43 | expect(registeredUser.password).toBe(mockedUserData.password) 44 | expect(registeredUser.email).toBe(mockedUserData.email) 45 | expect(registeredUser.name).toBe(mockedUserData.name) 46 | expect(registeredUser.surname).toBe(mockedUserData.surname) 47 | expect(registeredUser.avatar).toBe(mockedUserData.avatar) 48 | expect(registeredUser.enabled).toBeTruthy() 49 | expect(registeredUser.deleted).toBeFalsy() 50 | expect(registeredUser.createdAt).not.toBeNull() 51 | expect(registeredUser.updatedAt).not.toBeNull() 52 | 53 | expect(registeredUser.token).toBe('') 54 | expect(registeredUser.lastLoginAt).toBe('') 55 | }) 56 | 57 | it('must throw an error when we try to persist the same username', async () => { 58 | const newUserData = { ...mockedUserData } 59 | await create(newUserData) 60 | 61 | // NOTE Information obtained when this error happens. 62 | // MongoError: E11000 duplicate key error collection: ts-course-test.users index: username_1 dup key: { username: "test@mail.com" } 63 | await expect(create(newUserData)).rejects.toThrow(/duplicate key error/) 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/user.requests/getByToken.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { testingUsers, testingValidJwtTokenForNonPersistedUser, cleanUsersCollectionFixture, saveUserFixture } from '@testingFixtures' 3 | 4 | import { getByToken } from '../../user.mongodb.requests' 5 | 6 | const [{ username, password, email, name, surname, avatar, token }] = testingUsers 7 | 8 | describe('[ORM] MongoDB - getByToken', () => { 9 | const mockedUserData = { 10 | username, 11 | password, 12 | email, 13 | name, 14 | surname, 15 | avatar, 16 | token 17 | } 18 | 19 | beforeAll(async () => { 20 | await connect() 21 | }) 22 | 23 | beforeEach(async () => { 24 | await cleanUsersCollectionFixture() 25 | }) 26 | 27 | afterAll(async () => { 28 | await cleanUsersCollectionFixture() 29 | await disconnect() 30 | }) 31 | 32 | it('must not retrieve any user', async () => { 33 | const token = testingValidJwtTokenForNonPersistedUser 34 | const retrievedUser = await getByToken(token) 35 | expect(retrievedUser).toBeNull() 36 | }) 37 | 38 | it('must retrieve the persisted user', async () => { 39 | const newUserData = { ...mockedUserData } 40 | await saveUserFixture(newUserData) 41 | 42 | const token = newUserData.token 43 | const retrievedUser = (await getByToken(token))! 44 | 45 | const expectedFields = ['_id', 'username', 'password', 'email', 'name', 'surname', 'avatar', 'token', 'enabled', 'deleted', 'lastLoginAt', 'createdAt', 'updatedAt'] 46 | const retrievedUserFields = Object.keys(retrievedUser).sort() 47 | expect(retrievedUserFields.sort()).toEqual(expectedFields.sort()) 48 | 49 | expect(retrievedUser._id).not.toBeNull() 50 | expect(retrievedUser.username).toBe(newUserData.username) 51 | expect(retrievedUser.password).toBe(newUserData.password) 52 | expect(retrievedUser.email).toBe(newUserData.email) 53 | expect(retrievedUser.name).toBe(newUserData.name) 54 | expect(retrievedUser.surname).toBe(newUserData.surname) 55 | expect(retrievedUser.avatar).toBe(newUserData.avatar) 56 | expect(retrievedUser.token).toBe(newUserData.token) 57 | expect(retrievedUser.enabled).toBeTruthy() 58 | expect(retrievedUser.deleted).toBeFalsy() 59 | expect(retrievedUser.createdAt).not.toBeNull() 60 | expect(retrievedUser.updatedAt).not.toBeNull() 61 | 62 | expect(retrievedUser.lastLoginAt).toBe('') 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/user.requests/getByUsername.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { NewUserDatabaseDto } from '@infrastructure/dtos' 3 | import { testingUsers, testingNonPersistedUsername, cleanUsersCollectionFixture, saveUserFixture } from '@testingFixtures' 4 | 5 | import { getByUsername } from '../../user.mongodb.requests' 6 | 7 | const [{ username, password, email, name, surname, avatar }] = testingUsers 8 | 9 | describe('[ORM] MongoDB - getByUsername', () => { 10 | const mockedUserData: NewUserDatabaseDto = { 11 | username, 12 | password, 13 | email, 14 | name, 15 | surname, 16 | avatar 17 | } 18 | 19 | beforeAll(async () => { 20 | await connect() 21 | }) 22 | 23 | beforeEach(async () => { 24 | await cleanUsersCollectionFixture() 25 | }) 26 | 27 | afterAll(async () => { 28 | await cleanUsersCollectionFixture() 29 | await disconnect() 30 | }) 31 | 32 | it('must not retrieve any user', async () => { 33 | const username = testingNonPersistedUsername 34 | const retrievedUser = await getByUsername(username) 35 | expect(retrievedUser).toBeNull() 36 | }) 37 | 38 | it('must retrieve the persisted user', async () => { 39 | const newUserData: NewUserDatabaseDto = { ...mockedUserData } 40 | await saveUserFixture(newUserData) 41 | 42 | const username = newUserData.username 43 | const retrievedUser = (await getByUsername(username))! 44 | 45 | const expectedFields = ['_id', 'username', 'password', 'email', 'name', 'surname', 'avatar', 'token', 'enabled', 'deleted', 'lastLoginAt', 'createdAt', 'updatedAt'] 46 | const retrievedUserFields = Object.keys(retrievedUser).sort() 47 | expect(retrievedUserFields.sort()).toEqual(expectedFields.sort()) 48 | 49 | expect(retrievedUser._id).not.toBeNull() 50 | expect(retrievedUser.username).toBe(newUserData.username) 51 | expect(retrievedUser.password).toBe(newUserData.password) 52 | expect(retrievedUser.email).toBe(newUserData.email) 53 | expect(retrievedUser.name).toBe(newUserData.name) 54 | expect(retrievedUser.surname).toBe(newUserData.surname) 55 | expect(retrievedUser.avatar).toBe(newUserData.avatar) 56 | expect(retrievedUser.enabled).toBeTruthy() 57 | expect(retrievedUser.deleted).toBeFalsy() 58 | expect(retrievedUser.createdAt).not.toBeNull() 59 | expect(retrievedUser.updatedAt).not.toBeNull() 60 | 61 | expect(retrievedUser.token).toBe('') 62 | expect(retrievedUser.lastLoginAt).toBe('') 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/test/user.requests/getProfileById.test.ts: -------------------------------------------------------------------------------- 1 | import { connect, disconnect } from '../../../core' 2 | import { UserProfileDto } from '@infrastructure/dtos' 3 | 4 | import { getProfileById } from '../../user.mongodb.requests' 5 | import { testingUsers, testingNonValidUserId, cleanUsersCollectionFixture, saveUserFixture, getUserByUsernameFixture } from '@testingFixtures' 6 | 7 | const [{ username, password, email, avatar, name, surname }] = testingUsers 8 | 9 | interface TestingProfileDto extends UserProfileDto { 10 | password: string 11 | } 12 | 13 | describe('[ORM] MongoDB - getProfileById', () => { 14 | const mockedUserData: TestingProfileDto = { 15 | username, 16 | password, 17 | email, 18 | avatar, 19 | name, 20 | surname 21 | } 22 | 23 | beforeAll(async () => { 24 | await connect() 25 | await cleanUsersCollectionFixture() 26 | await saveUserFixture(mockedUserData) 27 | }) 28 | 29 | afterAll(async () => { 30 | await cleanUsersCollectionFixture() 31 | await disconnect() 32 | }) 33 | 34 | it('must not retrieve any provile using a non-existent user id', async () => { 35 | const userId = testingNonValidUserId 36 | const retrievedUserProfile = await getProfileById(userId) 37 | expect(retrievedUserProfile).toBeNull() 38 | }) 39 | 40 | it('must retrieve selected user\'s profile', async () => { 41 | const { _id: userId } = (await getUserByUsernameFixture(username))! 42 | const retrievedUserProfile = (await getProfileById(userId))! 43 | 44 | const expectedFields = ['username', 'email', 'name', 'surname', 'avatar'] 45 | const retrievedUserProfileFields = Object.keys(retrievedUserProfile).sort() 46 | expect(retrievedUserProfileFields.sort()).toEqual(expectedFields.sort()) 47 | 48 | expect(retrievedUserProfile.username).toBe(mockedUserData.username) 49 | expect(retrievedUserProfile.email).toBe(mockedUserData.email) 50 | expect(retrievedUserProfile.name).toBe(mockedUserData.name) 51 | expect(retrievedUserProfile.surname).toBe(mockedUserData.surname) 52 | expect(retrievedUserProfile.avatar).toBe(mockedUserData.avatar) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/infrastructure/orm/mongoose/requests/user.mongodb.requests.ts: -------------------------------------------------------------------------------- 1 | import { QueryOptions } from 'mongoose' 2 | import { User } from '../models' 3 | import { UserDto, NewUserDatabaseDto, UpdateUserPayloadDto, UserProfileDto } from '@infrastructure/dtos' 4 | 5 | export const create = async (newUserData: NewUserDatabaseDto): Promise => { 6 | await (new User(newUserData)).save() 7 | return await User.findOne({ username: newUserData.username }).lean() 8 | } 9 | 10 | export const getByUsername = async (username: string): Promise => 11 | User.findOne({ username }).lean() 12 | 13 | export const getByToken = async (token: string): Promise => 14 | User.findOne({ token }).lean() 15 | 16 | export const getProfileById = async (id: string): Promise => 17 | User.findById(id).select('-_id username email name surname avatar').lean() 18 | 19 | export const updateById = async (id: string, payload: UpdateUserPayloadDto): Promise => { 20 | // NOTE: Besides to define the fields as 'immutable' in the schema definition, it's required to use the 'strict' option 'cos in opposite, the field can be overwriten anyway :( 21 | const update = payload 22 | const options: QueryOptions = { new: true, strict: 'throw' } 23 | 24 | return await User.findByIdAndUpdate(id, update, options).lean() 25 | } 26 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/authentication/AuthenticatedUser.ts: -------------------------------------------------------------------------------- 1 | export const authenticatedUserComponent = { 2 | AuthenticatedUser: { 3 | type: 'object', 4 | required: ['token'], 5 | properties: { 6 | token: { 7 | type: 'string', 8 | example: 'JWT string' 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/authentication/LoginInputParams.ts: -------------------------------------------------------------------------------- 1 | export const loginInputParamsComponent = { 2 | LoginInputParams: { 3 | type: 'object', 4 | required: ['username', 'password'], 5 | properties: { 6 | username: { 7 | type: 'string', 8 | example: 'trenton.kutch@mail.com' 9 | }, 10 | password: { 11 | type: 'string', 12 | example: '123456' 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/authentication/index.ts: -------------------------------------------------------------------------------- 1 | import { loginInputParamsComponent } from './LoginInputParams' 2 | import { authenticatedUserComponent } from './AuthenticatedUser' 3 | 4 | export const authentication = { 5 | ...loginInputParamsComponent, 6 | ...authenticatedUserComponent 7 | } 8 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/common/Error.ts: -------------------------------------------------------------------------------- 1 | export const errorComponent = { 2 | Error400: { 3 | type: 'object', 4 | required: ['error', 'message'], 5 | properties: { 6 | error: { 7 | type: 'boolean', 8 | description: 'When this flag is included and it is set to \'true\', it indicates that an error has occurred.', 9 | example: true 10 | }, 11 | message: { 12 | type: 'string', 13 | description: 'Expanded information about the error.', 14 | example: 'You have sent someting that is now allowed :(' 15 | } 16 | } 17 | }, 18 | Error401: { 19 | type: 'object', 20 | required: ['error', 'message'], 21 | properties: { 22 | error: { 23 | type: 'boolean', 24 | description: 'When this flag is included and it is set to \'true\', it indicates that an error has occurred.', 25 | example: true 26 | }, 27 | message: { 28 | type: 'string', 29 | description: 'Expanded information about the error', 30 | example: 'You shall not pass ' 31 | } 32 | } 33 | }, 34 | Error403: { 35 | type: 'object', 36 | required: ['error', 'message'], 37 | properties: { 38 | error: { 39 | type: 'boolean', 40 | description: 'When this flag is included and it is set to \'true\', it indicates that an error has occurred.', 41 | example: true 42 | }, 43 | message: { 44 | type: 'string', 45 | description: 'Expanded information about the error.', 46 | example: 'You forgot to send something important' 47 | } 48 | } 49 | }, 50 | Error404: { 51 | type: 'object', 52 | required: ['error', 'message'], 53 | properties: { 54 | error: { 55 | type: 'boolean', 56 | description: 'When this flag is included and it is set to \'true\', it indicates that an error has occurred.', 57 | example: true 58 | }, 59 | message: { 60 | type: 'string', 61 | description: 'Expanded information about the error.', 62 | example: 'You are asking for something that is not here' 63 | } 64 | } 65 | }, 66 | Error500: { 67 | type: 'object', 68 | required: ['error', 'message'], 69 | properties: { 70 | error: { 71 | type: 'boolean', 72 | description: 'When this flag is included and it is set to \'true\', it indicates that an error has occurred.', 73 | example: true 74 | }, 75 | message: { 76 | type: 'string', 77 | description: 'Expanded information about the error', 78 | example: 'Internal Server Error' 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/common/index.ts: -------------------------------------------------------------------------------- 1 | // export * from './Error' 2 | import { errorComponent } from './Error' 3 | 4 | export const common = { ...errorComponent } 5 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/index.ts: -------------------------------------------------------------------------------- 1 | import { common } from './common' 2 | import { authentication } from './authentication' 3 | import { user } from './user' 4 | import { posts } from './post' 5 | 6 | export const components = { 7 | ...common, 8 | ...authentication, 9 | ...user, 10 | ...posts 11 | } 12 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/Comment.ts: -------------------------------------------------------------------------------- 1 | export const commentComponent = { 2 | Comment: { 3 | type: 'object', 4 | required: ['id', 'body', 'owner'], 5 | properties: { 6 | id: { 7 | type: 'string', 8 | example: '91739d498840433a8f570029' 9 | }, 10 | body: { 11 | type: 'string', 12 | descriptions: 'Comment content.', 13 | example: 'Expedita error est voluptas suscipit sed et inventore minima. Voluptate in minus est tenetur nihil consequuntur.' 14 | }, 15 | owner: { 16 | $ref: '#/components/schemas/Owner', 17 | description: 'Post owner data.' 18 | }, 19 | createdAt: { 20 | type: 'string', 21 | description: 'Creation timestamp in ISO format.', 22 | example: '2020-11-29T22:29:07.568Z' 23 | }, 24 | updatedAt: { 25 | type: 'string', 26 | description: 'Update timestamp in ISO format.', 27 | example: '2020-11-29T22:29:07.568Z' 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/CommentArray.ts: -------------------------------------------------------------------------------- 1 | export const commentArrayComponent = { 2 | CommentArray: { 3 | type: 'array', 4 | items: { 5 | $ref: '#/components/schemas/Comment' 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/EmptyArray.ts: -------------------------------------------------------------------------------- 1 | export const emptyArrayComponent = { 2 | EmptyArray: { 3 | type: 'array', 4 | items: [] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/ExtendedComment.ts: -------------------------------------------------------------------------------- 1 | export const extendedCommentComponent = { 2 | ExtendedComment: { 3 | type: 'object', 4 | required: ['id', 'body', 'owner'], 5 | properties: { 6 | id: { 7 | type: 'string', 8 | example: '91739d498840433a8f570029' 9 | }, 10 | body: { 11 | type: 'string', 12 | descriptions: 'Comment content.', 13 | example: 'Expedita error est voluptas suscipit sed et inventore minima. Voluptate in minus est tenetur nihil consequuntur.' 14 | }, 15 | owner: { 16 | $ref: '#/components/schemas/Owner', 17 | description: 'Post owner data.' 18 | }, 19 | userIsOwner: { 20 | type: 'boolean', 21 | descriptions: 'Indicates whether the user who run the request is the comment owner.' 22 | }, 23 | createdAt: { 24 | type: 'string', 25 | description: 'Creation timestamp in ISO format.', 26 | example: '2020-11-29T22:29:07.568Z' 27 | }, 28 | updatedAt: { 29 | type: 'string', 30 | description: 'Update timestamp in ISO format.', 31 | example: '2020-11-29T22:29:07.568Z' 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/ExtendedCommentArray.ts: -------------------------------------------------------------------------------- 1 | export const extendedCommentArrayComponent = { 2 | ExtendedCommentArray: { 3 | type: 'array', 4 | items: { 5 | $ref: '#/components/schemas/ExtendedComment' 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/ExtendedPost.ts: -------------------------------------------------------------------------------- 1 | export const extendedPostComponent = { 2 | ExtendedPost: { 3 | type: 'object', 4 | required: ['id', 'body', 'owner', 'userIsOwner', 'userHasLiked'], 5 | properties: { 6 | id: { 7 | type: 'string', 8 | example: '91739d498840433a8f570029' 9 | }, 10 | body: { 11 | type: 'string', 12 | descriptions: 'Post content.', 13 | example: 'Expedita error est voluptas suscipit sed et inventore minima. Voluptate in minus est tenetur nihil consequuntur.' 14 | }, 15 | owner: { 16 | $ref: '#/components/schemas/Owner', 17 | description: 'Post owner data.' 18 | }, 19 | userIsOwner: { 20 | type: 'boolean', 21 | descriptions: 'Indicates whether the user who run the request is the post owner.' 22 | }, 23 | userHasLiked: { 24 | type: 'boolean', 25 | descriptions: 'Indicates whether the user who run the request has liked the post.' 26 | }, 27 | comments: { 28 | $ref: '#/components/schemas/ExtendedCommentArray', 29 | description: 'Bunch of post comments.' 30 | }, 31 | likes: { 32 | $ref: '#/components/schemas/LikeArray', 33 | description: 'Bunch of post likes.' 34 | }, 35 | createdAt: { 36 | type: 'string', 37 | description: 'Creation timestamp in ISO format.', 38 | example: '2020-11-29T22:29:07.568Z' 39 | }, 40 | updatedAt: { 41 | type: 'string', 42 | description: 'Update timestamp in ISO format.', 43 | example: '2020-11-29T22:29:07.568Z' 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/ExtendedPostArray.ts: -------------------------------------------------------------------------------- 1 | export const extendedPostArrayComponent = { 2 | ExtendedPostArray: { 3 | type: 'array', 4 | items: { 5 | $ref: '#/components/schemas/ExtendedPost' 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/LikeArray.ts: -------------------------------------------------------------------------------- 1 | export const likeArrayComponent = { 2 | LikeArray: { 3 | type: 'array', 4 | items: { 5 | $ref: '#/components/schemas/Owner' 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/NewPost.ts: -------------------------------------------------------------------------------- 1 | export const newPostComponent = { 2 | NewPost: { 3 | type: 'object', 4 | required: ['id', 'body', 'owner'], 5 | properties: { 6 | id: { 7 | type: 'string', 8 | example: '91739d498840433a8f570029' 9 | }, 10 | body: { 11 | type: 'string', 12 | descriptions: 'Post content.', 13 | example: 'Expedita error est voluptas suscipit sed et inventore minima. Voluptate in minus est tenetur nihil consequuntur.' 14 | }, 15 | owner: { 16 | $ref: '#/components/schemas/Owner', 17 | description: 'Post owner data.' 18 | }, 19 | comments: { 20 | $ref: '#/components/schemas/EmptyArray' 21 | }, 22 | likes: { 23 | $ref: '#/components/schemas/EmptyArray' 24 | }, 25 | createdAt: { 26 | type: 'string', 27 | description: 'Creation timestamp in ISO format.', 28 | example: '2020-11-29T22:29:07.568Z' 29 | }, 30 | updatedAt: { 31 | type: 'string', 32 | description: 'Update timestamp in ISO format.', 33 | example: '2020-11-29T22:29:07.568Z' 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/Owner.ts: -------------------------------------------------------------------------------- 1 | export const ownerComponent = { 2 | Owner: { 3 | type: 'object', 4 | required: ['id', 'name', 'surname'], 5 | properties: { 6 | id: { 7 | type: 'string', 8 | example: '91739d498840433a8f570029' 9 | }, 10 | name: { 11 | type: 'string', 12 | example: 'Trenton' 13 | }, 14 | surname: { 15 | type: 'string', 16 | example: 'Kutch' 17 | }, 18 | avatar: { 19 | type: 'string', 20 | description: 'URL where the image is stored.', 21 | example: 'https://cdn.icon-icons.com/icons2/1736/PNG/128/4043253-friday-halloween-jason-movie_113258.png' 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/Post.ts: -------------------------------------------------------------------------------- 1 | export const postComponent = { 2 | Post: { 3 | type: 'object', 4 | required: ['id', 'body', 'owner'], 5 | properties: { 6 | id: { 7 | type: 'string', 8 | example: '91739d498840433a8f570029' 9 | }, 10 | body: { 11 | type: 'string', 12 | descriptions: 'Post content.', 13 | example: 'Expedita error est voluptas suscipit sed et inventore minima. Voluptate in minus est tenetur nihil consequuntur.' 14 | }, 15 | owner: { 16 | $ref: '#/components/schemas/Owner', 17 | description: 'Post owner data.' 18 | }, 19 | comments: { 20 | $ref: '#/components/schemas/CommentArray', 21 | description: 'Bunch of post comments.' 22 | }, 23 | likes: { 24 | $ref: '#/components/schemas/LikeArray', 25 | description: 'Bunch of post likes.' 26 | }, 27 | createdAt: { 28 | type: 'string', 29 | description: 'Creation timestamp in ISO format.', 30 | example: '2020-11-29T22:29:07.568Z' 31 | }, 32 | updatedAt: { 33 | type: 'string', 34 | description: 'Update timestamp in ISO format.', 35 | example: '2020-11-29T22:29:07.568Z' 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/PostArray.ts: -------------------------------------------------------------------------------- 1 | export const postArrayComponent = { 2 | PostArray: { 3 | type: 'array', 4 | items: { 5 | $ref: '#/components/schemas/Post' 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/post/index.ts: -------------------------------------------------------------------------------- 1 | import { postComponent } from './Post' 2 | import { extendedPostComponent } from './ExtendedPost' 3 | import { newPostComponent } from './NewPost' 4 | import { postArrayComponent } from './PostArray' 5 | import { extendedPostArrayComponent } from './ExtendedPostArray' 6 | import { ownerComponent } from './Owner' 7 | import { commentComponent } from './Comment' 8 | import { extendedCommentComponent } from './ExtendedComment' 9 | import { commentArrayComponent } from './CommentArray' 10 | import { extendedCommentArrayComponent } from './ExtendedCommentArray' 11 | import { likeArrayComponent } from './LikeArray' 12 | import { emptyArrayComponent } from './EmptyArray' 13 | 14 | export const posts = { 15 | ...newPostComponent, 16 | ...postComponent, 17 | ...extendedPostComponent, 18 | ...postArrayComponent, 19 | ...extendedPostArrayComponent, 20 | ...ownerComponent, 21 | ...commentComponent, 22 | ...extendedCommentComponent, 23 | ...commentArrayComponent, 24 | ...extendedCommentArrayComponent, 25 | ...likeArrayComponent, 26 | ...emptyArrayComponent 27 | } 28 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/user/NewRegisteredUser.ts: -------------------------------------------------------------------------------- 1 | export const newRegisteredUserComponent = { 2 | NewRegisteredUser: { 3 | type: 'object', 4 | required: ['username', 'fullName'], 5 | properties: { 6 | username: { 7 | type: 'string', 8 | description: 'New registered user identification.', 9 | example: 'mike.mazowski@monsters.com' 10 | }, 11 | fullName: { 12 | type: 'string', 13 | example: 'Mike Wazowski' 14 | }, 15 | avatar: { 16 | type: 'string', 17 | description: 'URL to the avatar location.', 18 | example: 'https://cdn.icon-icons.com/icons2/1736/PNG/128/4043268-alien-avatar-space-ufo_113272.png' 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/user/NewUserInput.ts: -------------------------------------------------------------------------------- 1 | export const newUserInputComponent = { 2 | NewUserInput: { 3 | type: 'object', 4 | required: ['email', 'password'], 5 | properties: { 6 | email: { 7 | type: 'string', 8 | description: 'New user contact email that will be turned into its username.', 9 | example: 'mike.mazowski@monsters.com' 10 | }, 11 | password: { 12 | type: 'string', 13 | example: '123456' 14 | }, 15 | name: { 16 | type: 'string', 17 | example: 'Mike' 18 | }, 19 | surname: { 20 | type: 'string', 21 | example: 'Wazowski' 22 | }, 23 | avatar: { 24 | type: 'string', 25 | description: 'URL to the avatar location.', 26 | example: 'https://cdn.icon-icons.com/icons2/1736/PNG/128/4043268-alien-avatar-space-ufo_113272.png' 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/user/NewUserProfileDataInput.ts: -------------------------------------------------------------------------------- 1 | export const newUserProfileDataInputComponent = { 2 | NewUserProfileDataInput: { 3 | type: 'object', 4 | properties: { 5 | name: { 6 | type: 'string', 7 | description: 'New user\'s name.', 8 | example: 'John' 9 | }, 10 | surname: { 11 | type: 'string', 12 | description: 'New user\'s surname.', 13 | example: 'Doe' 14 | }, 15 | avatar: { 16 | type: 'string', 17 | description: 'New user\'s avatar URL.', 18 | example: 'https://cdn.icon-icons.com/icons2/1736/PNG/128/4043277-avatar-person-pilot-traveller_113245.png' 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/user/UserProfile.ts: -------------------------------------------------------------------------------- 1 | export const userProfileComponent = { 2 | UserProfile: { 3 | type: 'object', 4 | properties: { 5 | username: { 6 | type: 'string', 7 | example: 'trenton.kutch@mail.com' 8 | }, 9 | email: { 10 | type: 'string', 11 | example: 'trenton.kutch@mail.com' 12 | }, 13 | name: { 14 | type: 'string', 15 | example: 'Trenton' 16 | }, 17 | surname: { 18 | type: 'string', 19 | example: 'Kutch' 20 | }, 21 | avatar: { 22 | type: 'string', 23 | example: 'https://cdn.icon-icons.com/icons2/1736/PNG/128/4043253-friday-halloween-jason-movie_113258.png' 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/components/user/index.ts: -------------------------------------------------------------------------------- 1 | import { userProfileComponent } from './UserProfile' 2 | import { newUserInputComponent } from './NewUserInput' 3 | import { newUserProfileDataInputComponent } from './NewUserProfileDataInput' 4 | import { newRegisteredUserComponent } from './NewRegisteredUser' 5 | 6 | export const user = { 7 | ...userProfileComponent, 8 | ...newUserInputComponent, 9 | ...newUserProfileDataInputComponent, 10 | ...newRegisteredUserComponent 11 | } 12 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/index.ts: -------------------------------------------------------------------------------- 1 | import { paths } from './paths' 2 | import { components } from './components' 3 | 4 | export const swaggerDocument = { 5 | openapi: '3.0.1', 6 | info: { 7 | version: '1.0.0-alpha', 8 | title: 'TypeScript Workshop - Backend', 9 | description: 'Workshop aimed to introduce the most basic concepts TypeScript and how to use it in order to create a backend applicaiton.', 10 | termOfService: '', 11 | contact: { 12 | name: 'Dailos Rafael Díaz Lara', 13 | url: 'https://linkedin.com/in/ddialar' 14 | } 15 | }, 16 | paths, 17 | components: { 18 | securitySchemes: { 19 | Bearer: { 20 | type: 'apiKey', 21 | in: 'header', 22 | name: 'Authorization', 23 | description: 'For accessing the API a valid JWT token must be passed in all the queries in the \'Authorization\' header.\n\nA valid JWT token is generated by the API and retourned as answer of a call to the route /login giving a valid user & password.\n\nThe following syntax must be used in the \'Authorization\' header:\n\nbearer xxxxxx.yyyyyyy.zzzzzz' 24 | } 25 | }, 26 | schemas: { ...components } 27 | } 28 | } 29 | 30 | export const swaggerOptions = {} 31 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/authentication/index.ts: -------------------------------------------------------------------------------- 1 | import { postLogin } from './postLogin.path' 2 | import { postLogout } from './postLogout.path' 3 | 4 | export const authentication = { 5 | '/login': { post: postLogin }, 6 | '/logout': { post: postLogout } 7 | } 8 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/authentication/postLogin.path.ts: -------------------------------------------------------------------------------- 1 | export const postLogin = { 2 | tags: ['Authentication'], 3 | descriptions: 'Returns the authorization parameters that identify the user against the API.', 4 | operationId: 'login', 5 | requestBody: { 6 | description: 'User identification parameters', 7 | required: true, 8 | content: { 9 | 'application/json': { 10 | schema: { 11 | $ref: '#/components/schemas/LoginInputParams' 12 | } 13 | } 14 | } 15 | }, 16 | responses: { 17 | 200: { 18 | description: 'Authentication success', 19 | content: { 20 | 'application/json': { 21 | schema: { 22 | $ref: '#/components/schemas/AuthenticatedUser' 23 | } 24 | } 25 | } 26 | }, 27 | 400: { 28 | description: 'Bad request when some of the required fields is not provided or its contend is malformed', 29 | content: { 30 | 'application/json': { 31 | schema: { 32 | $ref: '#/components/schemas/Error400' 33 | } 34 | } 35 | } 36 | }, 37 | 401: { 38 | description: 'Unauthorized user error when the username or password are wrong or they mismatch with the stored information', 39 | content: { 40 | 'application/json': { 41 | schema: { 42 | $ref: '#/components/schemas/Error401' 43 | } 44 | } 45 | } 46 | }, 47 | 500: { 48 | description: 'API Error', 49 | content: { 50 | 'application/json': { 51 | schema: { 52 | $ref: '#/components/schemas/Error500' 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/authentication/postLogout.path.ts: -------------------------------------------------------------------------------- 1 | export const postLogout = { 2 | tags: ['Authentication'], 3 | descriptions: 'Closes the connection between the user and the API.', 4 | operationId: 'logout', 5 | security: [ 6 | { 7 | Bearer: [] 8 | } 9 | ], 10 | responses: { 11 | 200: { 12 | description: 'Logout success' 13 | }, 14 | 400: { 15 | description: `

Bad request error when some of the next situations affect to the sent token:

16 |
    17 |
  • Its content is malformed
  • 18 |
  • It belongs to a non recorded user
  • 19 |
`, 20 | content: { 21 | 'application/json': { 22 | schema: { 23 | $ref: '#/components/schemas/Error400' 24 | } 25 | } 26 | } 27 | }, 28 | 401: { 29 | description: 'Unauthorized user error when the provided token is expired', 30 | content: { 31 | 'application/json': { 32 | schema: { 33 | $ref: '#/components/schemas/Error401' 34 | } 35 | } 36 | } 37 | }, 38 | 403: { 39 | description: `

Forbidden error when some of the next situations happen:

40 |
    41 |
  • The Authorization header is not sent
  • 42 |
  • The token is epmty
  • 43 |
`, 44 | content: { 45 | 'application/json': { 46 | schema: { 47 | $ref: '#/components/schemas/Error403' 48 | } 49 | } 50 | } 51 | }, 52 | 500: { 53 | description: 'API Error', 54 | content: { 55 | 'application/json': { 56 | schema: { 57 | $ref: '#/components/schemas/Error500' 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/index.ts: -------------------------------------------------------------------------------- 1 | import { authentication } from './authentication' 2 | import { users } from './users' 3 | import { posts } from './posts' 4 | 5 | export const paths = { 6 | ...authentication, 7 | ...users, 8 | ...posts 9 | } 10 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/posts/createPost.path.ts: -------------------------------------------------------------------------------- 1 | export const createPost = { 2 | tags: ['Posts'], 3 | descriptions: 'Create a new post based on the provided content.', 4 | operationId: 'createPost', 5 | security: [ 6 | { 7 | Bearer: [] 8 | } 9 | ], 10 | requestBody: { 11 | description: 'New post content.', 12 | required: true, 13 | content: { 14 | 'application/json': { 15 | schema: { 16 | required: ['postBody'], 17 | properties: { 18 | postBody: { 19 | type: 'string', 20 | example: 'New post body.' 21 | } 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | responses: { 28 | 200: { 29 | description: 'New posts created successfully', 30 | content: { 31 | 'application/json': { 32 | schema: { 33 | $ref: '#/components/schemas/NewPost' 34 | } 35 | } 36 | } 37 | }, 38 | 400: { 39 | description: `

Bad request when some of the next situations happend:

40 |
    41 |
  • The token content is malformed
  • 42 |
  • The token belongs to a non recorded user
  • 43 |
  • The post body is empty
  • 44 |
  • The post body is not sent
  • 45 |
`, 46 | content: { 47 | 'application/json': { 48 | schema: { 49 | $ref: '#/components/schemas/Error400' 50 | } 51 | } 52 | } 53 | }, 54 | 401: { 55 | description: 'Unauthorized user error when the provided token is expired', 56 | content: { 57 | 'application/json': { 58 | schema: { 59 | $ref: '#/components/schemas/Error401' 60 | } 61 | } 62 | } 63 | }, 64 | 403: { 65 | description: `

Forbidden error when some of the next situations happen:

66 |
    67 |
  • The Authorization header is not sent
  • 68 |
  • The token is epmty
  • `, 69 | content: { 70 | 'application/json': { 71 | schema: { 72 | $ref: '#/components/schemas/Error403' 73 | } 74 | } 75 | } 76 | }, 77 | 500: { 78 | description: 'API Error', 79 | content: { 80 | 'application/json': { 81 | schema: { 82 | $ref: '#/components/schemas/Error500' 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/posts/createPostLike.path.ts: -------------------------------------------------------------------------------- 1 | export const createPostLike = { 2 | tags: ['Posts'], 3 | descriptions: 'To like a post.', 4 | operationId: 'createPostLike', 5 | security: [ 6 | { 7 | Bearer: [] 8 | } 9 | ], 10 | requestBody: { 11 | description: 'Post to be liked.', 12 | required: true, 13 | content: { 14 | 'application/json': { 15 | schema: { 16 | required: ['postId'], 17 | properties: { 18 | postId: { 19 | type: 'string', 20 | example: '91739d498840433a8f570029' 21 | } 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | responses: { 28 | 200: { 29 | description: 'Selected post liked successfully', 30 | content: { 31 | 'application/json': { 32 | schema: { 33 | $ref: '#/components/schemas/ExtendedPost' 34 | } 35 | } 36 | } 37 | }, 38 | 400: { 39 | description: `

    Bad request when some of the next situations happen:

    40 |
      41 |
    • The token content is malformed
    • 42 |
    • The token belongs to a non recorded user
    • 43 |
    • The post ID is not provided, empty or malformed
    • 44 |
    `, 45 | content: { 46 | 'application/json': { 47 | schema: { 48 | $ref: '#/components/schemas/Error400' 49 | } 50 | } 51 | } 52 | }, 53 | 401: { 54 | description: 'Unauthorized user error when the provided token is expired', 55 | content: { 56 | 'application/json': { 57 | schema: { 58 | $ref: '#/components/schemas/Error401' 59 | } 60 | } 61 | } 62 | }, 63 | 403: { 64 | description: `

    Forbidden error when some of the next situations happen:

    65 |
      66 |
    • The Authorization header is not sent
    • 67 |
    • The token is epmty
    • 68 |
    `, 69 | content: { 70 | 'application/json': { 71 | schema: { 72 | $ref: '#/components/schemas/Error403' 73 | } 74 | } 75 | } 76 | }, 77 | 404: { 78 | description: 'When the selected post is not found', 79 | content: { 80 | 'application/json': { 81 | schema: { 82 | $ref: '#/components/schemas/Error404' 83 | } 84 | } 85 | } 86 | }, 87 | 500: { 88 | description: 'API Error', 89 | content: { 90 | 'application/json': { 91 | schema: { 92 | $ref: '#/components/schemas/Error500' 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/posts/deletePost.path.ts: -------------------------------------------------------------------------------- 1 | export const deletePost = { 2 | tags: ['Posts'], 3 | descriptions: 'Delete a post.', 4 | operationId: 'deletePost', 5 | security: [ 6 | { 7 | Bearer: [] 8 | } 9 | ], 10 | requestBody: { 11 | description: 'The post that we want to delete.', 12 | required: true, 13 | content: { 14 | 'application/json': { 15 | schema: { 16 | required: ['postId'], 17 | properties: { 18 | postId: { 19 | type: 'string', 20 | example: '91739d498840433a8f570029' 21 | } 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | responses: { 28 | 200: { 29 | description: 'The selected post was deleted successfully' 30 | }, 31 | 400: { 32 | description: `

    Bad request when some of the next situations happen:

    33 |
      34 |
    • The token content is malformed
    • 35 |
    • The token belongs to a non recorded user
    • 36 |
    • The post ID is not provided, empty or malformed
    • 37 |
    `, 38 | content: { 39 | 'application/json': { 40 | schema: { 41 | $ref: '#/components/schemas/Error400' 42 | } 43 | } 44 | } 45 | }, 46 | 401: { 47 | description: `

    Unauthorized user error when some of the next situations happen:

    48 |
      49 |
    • The token is expired
    • 50 |
    • The token belongs to a user who is not the post owner
    • 51 |
    `, 52 | content: { 53 | 'application/json': { 54 | schema: { 55 | $ref: '#/components/schemas/Error401' 56 | } 57 | } 58 | } 59 | }, 60 | 403: { 61 | description: `

    Forbidden error when some of the next situations happen:

    62 |
      63 |
    • The Authorization header is not sent
    • 64 |
    • The token is epmty
    • 65 |
    `, 66 | content: { 67 | 'application/json': { 68 | schema: { 69 | $ref: '#/components/schemas/Error403' 70 | } 71 | } 72 | } 73 | }, 74 | 404: { 75 | description: 'When the provided post was not found', 76 | content: { 77 | 'application/json': { 78 | schema: { 79 | $ref: '#/components/schemas/Error404' 80 | } 81 | } 82 | } 83 | }, 84 | 500: { 85 | description: 'API Error', 86 | content: { 87 | 'application/json': { 88 | schema: { 89 | $ref: '#/components/schemas/Error500' 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/posts/deletePostLike.path.ts: -------------------------------------------------------------------------------- 1 | export const deletePostLike = { 2 | tags: ['Posts'], 3 | descriptions: 'Delete a post like.', 4 | operationId: 'deletePostLike', 5 | security: [ 6 | { 7 | Bearer: [] 8 | } 9 | ], 10 | requestBody: { 11 | description: 'The post which like we want to remove.', 12 | required: true, 13 | content: { 14 | 'application/json': { 15 | schema: { 16 | required: ['postId'], 17 | properties: { 18 | postId: { 19 | type: 'string', 20 | example: '91739d498840433a8f570029' 21 | } 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | responses: { 28 | 200: { 29 | description: 'The selected like was removed successfully in the provided post', 30 | content: { 31 | 'application/json': { 32 | schema: { 33 | $ref: '#/components/schemas/Post' 34 | } 35 | } 36 | } 37 | }, 38 | 400: { 39 | description: `

    Bad request when some of the next situations happen:

    40 |
      41 |
    • The token content is malformed
    • 42 |
    • The token belongs to a non recorded user
    • 43 |
    • The post ID is not provided, empty or malformed
    • 44 |
    • The request is performed by an user who has not liked the post previously
    • 45 |
    `, 46 | content: { 47 | 'application/json': { 48 | schema: { 49 | $ref: '#/components/schemas/Error400' 50 | } 51 | } 52 | } 53 | }, 54 | 401: { 55 | description: 'Unauthorized user error when the provided token is expired', 56 | content: { 57 | 'application/json': { 58 | schema: { 59 | $ref: '#/components/schemas/Error401' 60 | } 61 | } 62 | } 63 | }, 64 | 403: { 65 | description: `

    Forbidden error when some of the next situations happen:

    66 |
      67 |
    • The Authorization header is not sent
    • 68 |
    • The token is epmty
    • 69 |
    `, 70 | content: { 71 | 'application/json': { 72 | schema: { 73 | $ref: '#/components/schemas/Error403' 74 | } 75 | } 76 | } 77 | }, 78 | 404: { 79 | description: 'When the provided post was not found', 80 | content: { 81 | 'application/json': { 82 | schema: { 83 | $ref: '#/components/schemas/Error404' 84 | } 85 | } 86 | } 87 | }, 88 | 500: { 89 | description: 'API Error', 90 | content: { 91 | 'application/json': { 92 | schema: { 93 | $ref: '#/components/schemas/Error500' 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/posts/getAllExtendedPosts.path.ts: -------------------------------------------------------------------------------- 1 | export const getAllExtendedPosts = { 2 | tags: ['Posts'], 3 | descriptions: 'Retrieves all posts in their extended version.', 4 | operationId: 'getAllExtendedPosts', 5 | security: [ 6 | { 7 | Bearer: [] 8 | } 9 | ], 10 | responses: { 11 | 200: { 12 | description: 'Extended posts retreived successfully', 13 | content: { 14 | 'application/json': { 15 | schema: { 16 | $ref: '#/components/schemas/ExtendedPostArray' 17 | } 18 | } 19 | } 20 | }, 21 | 400: { 22 | description: `

    Bad request when some of the next situations happen:

    23 |
      24 |
    • The token content is malformed
    • 25 |
    • The token belongs to a non recorded user
    • 26 |
    `, 27 | content: { 28 | 'application/json': { 29 | schema: { 30 | $ref: '#/components/schemas/Error400' 31 | } 32 | } 33 | } 34 | }, 35 | 401: { 36 | description: 'Unauthorized user error when the provided token is expired', 37 | content: { 38 | 'application/json': { 39 | schema: { 40 | $ref: '#/components/schemas/Error401' 41 | } 42 | } 43 | } 44 | }, 45 | 403: { 46 | description: `

    Forbidden error when some of the next situations happen:

    47 |
      48 |
    • The Authorization header is not sent
    • 49 |
    • The token is epmty
    • 50 |
    `, 51 | content: { 52 | 'application/json': { 53 | schema: { 54 | $ref: '#/components/schemas/Error403' 55 | } 56 | } 57 | } 58 | }, 59 | 500: { 60 | description: 'API Error', 61 | content: { 62 | 'application/json': { 63 | schema: { 64 | $ref: '#/components/schemas/Error500' 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/posts/getAllPosts.path.ts: -------------------------------------------------------------------------------- 1 | export const getAllPosts = { 2 | tags: ['Posts'], 3 | descriptions: 'Retrieves the whole persisted posts.', 4 | operationId: 'getAllPosts', 5 | responses: { 6 | 200: { 7 | description: 'Posts retreived successfully or empty array when no posts are found', 8 | content: { 9 | 'application/json': { 10 | schema: { 11 | $ref: '#/components/schemas/PostArray' 12 | } 13 | } 14 | } 15 | }, 16 | 500: { 17 | description: 'API Error', 18 | content: { 19 | 'application/json': { 20 | schema: { 21 | $ref: '#/components/schemas/Error500' 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/posts/getExtendedPostById.path.ts: -------------------------------------------------------------------------------- 1 | export const getExtendedPostById = { 2 | tags: ['Posts'], 3 | descriptions: 'Retrieves post specified by the provided ID, in its extended version.', 4 | operationId: 'getExtendedPostById', 5 | security: [ 6 | { 7 | Bearer: [] 8 | } 9 | ], 10 | parameters: [ 11 | { 12 | in: 'path', 13 | name: 'id', 14 | description: 'Post ID which we want to retrieve.', 15 | required: true, 16 | type: 'string' 17 | } 18 | ], 19 | responses: { 20 | 200: { 21 | description: 'Extended post retreived successfully', 22 | content: { 23 | 'application/json': { 24 | schema: { 25 | $ref: '#/components/schemas/ExtendedPost' 26 | } 27 | } 28 | } 29 | }, 30 | 400: { 31 | description: `

    Bad request when some of the next situations happen:

    32 |
      33 |
    • The token content is malformed
    • 34 |
    • The token belongs to a non recorded user
    • 35 |
    • The post ID is not provided, empty or malformed
    • 36 |
    `, 37 | content: { 38 | 'application/json': { 39 | schema: { 40 | $ref: '#/components/schemas/Error400' 41 | } 42 | } 43 | } 44 | }, 45 | 401: { 46 | description: 'Unauthorized user error when the provided token is expired', 47 | content: { 48 | 'application/json': { 49 | schema: { 50 | $ref: '#/components/schemas/Error401' 51 | } 52 | } 53 | } 54 | }, 55 | 403: { 56 | description: `

    Forbidden error when some of the next situations happen:

    57 |
      58 |
    • The Authorization header is not sent
    • 59 |
    • The token is epmty
    • 60 |
    `, 61 | content: { 62 | 'application/json': { 63 | schema: { 64 | $ref: '#/components/schemas/Error403' 65 | } 66 | } 67 | } 68 | }, 69 | 404: { 70 | description: 'When the provided post was not found', 71 | content: { 72 | 'application/json': { 73 | schema: { 74 | $ref: '#/components/schemas/Error404' 75 | } 76 | } 77 | } 78 | }, 79 | 500: { 80 | description: 'API Error', 81 | content: { 82 | 'application/json': { 83 | schema: { 84 | $ref: '#/components/schemas/Error500' 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/posts/getPostById.path.ts: -------------------------------------------------------------------------------- 1 | export const getPostById = { 2 | tags: ['Posts'], 3 | descriptions: 'Retrieves post specified by the provided ID.', 4 | operationId: 'getPostById', 5 | parameters: [ 6 | { 7 | in: 'path', 8 | name: 'id', 9 | description: 'Post ID which we want to retrieve.', 10 | required: true, 11 | type: 'string' 12 | } 13 | ], 14 | responses: { 15 | 200: { 16 | description: 'Post retreived successfully', 17 | content: { 18 | 'application/json': { 19 | schema: { 20 | $ref: '#/components/schemas/Post' 21 | } 22 | } 23 | } 24 | }, 25 | 400: { 26 | description: 'Bad request when the provided post ID is malformed', 27 | content: { 28 | 'application/json': { 29 | schema: { 30 | $ref: '#/components/schemas/Error400' 31 | } 32 | } 33 | } 34 | }, 35 | 404: { 36 | description: 'When the provided post was not found', 37 | content: { 38 | 'application/json': { 39 | schema: { 40 | $ref: '#/components/schemas/Error404' 41 | } 42 | } 43 | } 44 | }, 45 | 500: { 46 | description: 'API Error', 47 | content: { 48 | 'application/json': { 49 | schema: { 50 | $ref: '#/components/schemas/Error500' 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/posts/index.ts: -------------------------------------------------------------------------------- 1 | import { getAllPosts } from './getAllPosts.path' 2 | import { getAllExtendedPosts } from './getAllExtendedPosts.path' 3 | import { getPostById } from './getPostById.path' 4 | import { getExtendedPostById } from './getExtendedPostById.path' 5 | import { createPost } from './createPost.path' 6 | import { deletePost } from './deletePost.path' 7 | import { createPostComment } from './createPostComment.path' 8 | import { createPostLike } from './createPostLike.path' 9 | import { deletePostComment } from './deletePostComment.path' 10 | import { deletePostLike } from './deletePostLike.path' 11 | 12 | export const posts = { 13 | '/posts': { 14 | get: getAllPosts, 15 | post: createPost, 16 | delete: deletePost 17 | }, 18 | '/posts/{id}': { 19 | get: getPostById 20 | }, 21 | '/posts/ext': { 22 | get: getAllExtendedPosts 23 | }, 24 | '/posts/ext/{id}': { 25 | get: getExtendedPostById 26 | }, 27 | '/posts/comment': { 28 | post: createPostComment, 29 | delete: deletePostComment 30 | }, 31 | '/posts/like': { 32 | post: createPostLike, 33 | delete: deletePostLike 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/users/getProfile.path.ts: -------------------------------------------------------------------------------- 1 | export const getProfile = { 2 | tags: ['Users'], 3 | descriptions: 'Retrieves the user\'s profile.', 4 | operationId: 'getProfile', 5 | security: [ 6 | { 7 | Bearer: [] 8 | } 9 | ], 10 | responses: { 11 | 201: { 12 | description: 'Selected user\'s profile', 13 | content: { 14 | 'application/json': { 15 | schema: { 16 | $ref: '#/components/schemas/UserProfile' 17 | } 18 | } 19 | } 20 | }, 21 | 400: { 22 | description: 'Bad request when the provided token is expired', 23 | content: { 24 | 'application/json': { 25 | schema: { 26 | $ref: '#/components/schemas/Error400' 27 | } 28 | } 29 | } 30 | }, 31 | 401: { 32 | description: 'Unauthorized user error when the provided token is not valid', 33 | content: { 34 | 'application/json': { 35 | schema: { 36 | $ref: '#/components/schemas/Error401' 37 | } 38 | } 39 | } 40 | }, 41 | 403: { 42 | description: 'Bad request when the provided token is empty', 43 | content: { 44 | 'application/json': { 45 | schema: { 46 | $ref: '#/components/schemas/Error403' 47 | } 48 | } 49 | } 50 | }, 51 | 500: { 52 | description: 'API Error', 53 | content: { 54 | 'application/json': { 55 | schema: { 56 | $ref: '#/components/schemas/Error500' 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/users/index.ts: -------------------------------------------------------------------------------- 1 | import { signin } from './signin.path' 2 | import { getProfile } from './getProfile.path' 3 | import { updateProfile } from './updateProfile.path' 4 | 5 | export const users = { 6 | '/signin': { 7 | post: signin 8 | }, 9 | '/profile': { 10 | get: getProfile, 11 | put: updateProfile 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/users/signin.path.ts: -------------------------------------------------------------------------------- 1 | export const signin = { 2 | tags: ['Users'], 3 | descriptions: 'Create a new user.', 4 | operationId: 'signin', 5 | requestBody: { 6 | description: 'New user content.', 7 | required: true, 8 | content: { 9 | 'application/json': { 10 | schema: { 11 | $ref: '#/components/schemas/NewUserInput' 12 | } 13 | } 14 | } 15 | }, 16 | responses: { 17 | 201: { 18 | description: 'New user created successfully', 19 | content: { 20 | 'application/json': { 21 | schema: { 22 | $ref: '#/components/schemas/NewRegisteredUser' 23 | } 24 | } 25 | } 26 | }, 27 | 400: { 28 | description: `

    Bad request when some of the next reasons happen:

    29 |
      30 |
    • The user is alredy recorded in the system.
    • 31 |
    • Some of the required fields is not provided or its content is malformed.
    • 32 |
    `, 33 | content: { 34 | 'application/json': { 35 | schema: { 36 | $ref: '#/components/schemas/Error400' 37 | } 38 | } 39 | } 40 | }, 41 | 500: { 42 | description: 'API Error', 43 | content: { 44 | 'application/json': { 45 | schema: { 46 | $ref: '#/components/schemas/Error500' 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/infrastructure/server/apidoc/paths/users/updateProfile.path.ts: -------------------------------------------------------------------------------- 1 | export const updateProfile = { 2 | tags: ['Users'], 3 | descriptions: 'Update the user\'s profile.', 4 | operationId: 'updateProfile', 5 | security: [ 6 | { 7 | Bearer: [] 8 | } 9 | ], 10 | requestBody: { 11 | description: 'New user\'s profile content.', 12 | required: true, 13 | content: { 14 | 'application/json': { 15 | schema: { 16 | $ref: '#/components/schemas/NewUserProfileDataInput' 17 | } 18 | } 19 | } 20 | }, 21 | responses: { 22 | 200: { 23 | description: 'New user\'s profile data successfully updated', 24 | content: { 25 | 'application/json': { 26 | schema: { 27 | $ref: '#/components/schemas/UserProfile' 28 | } 29 | } 30 | } 31 | }, 32 | 400: { 33 | description: 'Bad request when the provided token is expired', 34 | content: { 35 | 'application/json': { 36 | schema: { 37 | $ref: '#/components/schemas/Error400' 38 | } 39 | } 40 | } 41 | }, 42 | 401: { 43 | description: 'Unauthorized user error when the provided token is not valid', 44 | content: { 45 | 'application/json': { 46 | schema: { 47 | $ref: '#/components/schemas/Error401' 48 | } 49 | } 50 | } 51 | }, 52 | 403: { 53 | description: 'Bad request when the provided token is empty', 54 | content: { 55 | 'application/json': { 56 | schema: { 57 | $ref: '#/components/schemas/Error403' 58 | } 59 | } 60 | } 61 | }, 62 | 500: { 63 | description: 'API Error', 64 | content: { 65 | 'application/json': { 66 | schema: { 67 | $ref: '#/components/schemas/Error500' 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/infrastructure/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './server' 2 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/ensureAuthenticated.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { RequiredTokenNotProvidedError, UserDoesNotExistError, TokenFormatError } from '@errors' 3 | import { checkToken, getUserByToken } from '@domainServices' 4 | import { RequestDto } from '../serverDtos' 5 | import { validateToken } from '@infrastructure/server/validators' 6 | 7 | export const ensureAuthenticated = async (req: RequestDto, res: Response, next: NextFunction) => { 8 | try { 9 | const [, token] = (req.get('authorization') ?? '').split(' ') 10 | if (!token) { 11 | throw new RequiredTokenNotProvidedError() 12 | } 13 | 14 | const { error, value } = validateToken(token) 15 | if (error) { 16 | throw new TokenFormatError(error) 17 | } 18 | const { token: validatedToken } = value 19 | 20 | checkToken(validatedToken) 21 | 22 | const persistedUser = await getUserByToken(validatedToken) 23 | if (!persistedUser) { 24 | throw new UserDoesNotExistError(`Does not exist any user with token '${validatedToken}'.`) 25 | } 26 | 27 | req.user = persistedUser 28 | return next() 29 | } catch (error) { 30 | return next(error) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/handleHttpError.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express' 2 | import { serverLogger } from '@logger' 3 | import { ApiError } from '@errors' 4 | 5 | export const handleHttpError = (error: ApiError, req: Request, res: Response, next: NextFunction): void => { 6 | const { status, message, description } = error 7 | 8 | serverLogger('error', `${message}${description ? ' - ' + description : ''}`) 9 | 10 | res.status(status).send({ error: true, message }) 11 | } 12 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ensureAuthenticated.middleware' 2 | export * from './handleHttpError.middleware' 3 | export * from './validateLogin.middleware' 4 | export * from './validateSignin.middleware' 5 | export * from './validateProfileData.middleware' 6 | export * from './validatePost.middleware' 7 | export * from './validateNewPost.middleware' 8 | export * from './validatePostLike.middleware' 9 | export * from './validatePostComment.middleware' 10 | export * from './validateNewPostComment.middleware' 11 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/validateLogin.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { LoginDataError } from '@errors' 3 | import { RequestDto } from '../serverDtos' 4 | 5 | import { validateLoginParams } from '@infrastructure/server/validators' 6 | 7 | export const validateLogin = async (req: RequestDto, res: Response, next: NextFunction) => { 8 | try { 9 | const { username, password } = req.body 10 | const { error, value } = validateLoginParams({ username, password }) 11 | 12 | if (error) { 13 | throw new LoginDataError(error) 14 | } 15 | 16 | req.loginData = { ...value } 17 | 18 | return next() 19 | } catch (error) { 20 | return next(error) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/validateNewPost.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { NewPostError } from '@errors' 3 | import { RequestDto } from '../serverDtos' 4 | 5 | import { validateNewPostParams } from '@infrastructure/server/validators' 6 | 7 | export const validateNewPost = async (req: RequestDto, res: Response, next: NextFunction) => { 8 | try { 9 | const { postBody } = req.body 10 | 11 | const { error, value } = validateNewPostParams(postBody) 12 | 13 | if (error) { 14 | throw new NewPostError(error) 15 | } 16 | 17 | req.postBody = value.postBody 18 | 19 | return next() 20 | } catch (error) { 21 | return next(error) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/validateNewPostComment.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { NewPostCommentError } from '@errors' 3 | import { RequestDto } from '../serverDtos' 4 | 5 | import { validateNewPostCommentParams } from '@infrastructure/server/validators' 6 | 7 | export const validateNewPostComment = async (req: RequestDto, res: Response, next: NextFunction) => { 8 | try { 9 | const { postId, commentBody } = req.body 10 | 11 | const { error, value } = validateNewPostCommentParams({ postId, commentBody }) 12 | 13 | if (error) { 14 | throw new NewPostCommentError(error) 15 | } 16 | 17 | req.newPostComment = value 18 | 19 | return next() 20 | } catch (error) { 21 | return next(error) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/validatePost.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { PostIdentificationError } from '@errors' 3 | import { RequestDto } from '../serverDtos' 4 | 5 | import { validatePostParams } from '@infrastructure/server/validators' 6 | 7 | export const validatePost = (postIdFrom: 'body' | 'params') => async (req: RequestDto, res: Response, next: NextFunction) => { 8 | try { 9 | const postIdOrigins = { 10 | body: req.body.postId, 11 | params: req.params.id 12 | } 13 | 14 | const postId = postIdOrigins[postIdFrom] 15 | 16 | const { error, value } = validatePostParams(postId) 17 | 18 | if (error) { 19 | throw new PostIdentificationError(error) 20 | } 21 | 22 | req.postId = value.postId 23 | 24 | return next() 25 | } catch (error) { 26 | return next(error) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/validatePostComment.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { PostCommentError } from '@errors' 3 | import { RequestDto } from '../serverDtos' 4 | 5 | import { validatePostCommentParams } from '@infrastructure/server/validators' 6 | 7 | export const validatePostComment = async (req: RequestDto, res: Response, next: NextFunction) => { 8 | try { 9 | const { postId, commentId } = req.body 10 | 11 | const { error, value } = validatePostCommentParams({ postId, commentId }) 12 | 13 | if (error) { 14 | throw new PostCommentError(error) 15 | } 16 | 17 | req.postComment = value 18 | 19 | return next() 20 | } catch (error) { 21 | return next(error) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/validatePostLike.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { PostIdentificationError } from '@errors' 3 | import { RequestDto } from '../serverDtos' 4 | 5 | import { validatePostLikeParams } from '@infrastructure/server/validators' 6 | 7 | export const validatePostLike = async (req: RequestDto, res: Response, next: NextFunction) => { 8 | try { 9 | const { postId } = req.body 10 | 11 | const { error, value } = validatePostLikeParams(postId) 12 | 13 | if (error) { 14 | throw new PostIdentificationError(error) 15 | } 16 | 17 | req.postId = value.postId 18 | 19 | return next() 20 | } catch (error) { 21 | return next(error) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/validateProfileData.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { EmptyProfileDataError, ProfileDataError } from '@errors' 3 | import { RequestDto } from '../serverDtos' 4 | 5 | import { validateProfileParams } from '@infrastructure/server/validators' 6 | 7 | export const validateProfileData = async (req: RequestDto, res: Response, next: NextFunction) => { 8 | try { 9 | const { name, surname, avatar } = req.body 10 | 11 | if (!name && !surname && !avatar) { 12 | throw new EmptyProfileDataError('No one of the allowed fields were provided.') 13 | } 14 | 15 | const { error, value } = validateProfileParams({ name, surname, avatar }) 16 | 17 | if (error) { 18 | throw new ProfileDataError(error) 19 | } 20 | 21 | req.newProfileData = Object 22 | .entries(value) 23 | .reduce((result, [key, value]) => value ? { ...result, [key]: value } : result, {}) 24 | 25 | return next() 26 | } catch (error) { 27 | return next(error) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/infrastructure/server/middlewares/validateSignin.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response, NextFunction } from 'express' 2 | import { SigninDataError } from '@errors' 3 | import { RequestDto } from '../serverDtos' 4 | 5 | import { validateSigninParams } from '@infrastructure/server/validators' 6 | 7 | export const validateSignin = async (req: RequestDto, res: Response, next: NextFunction) => { 8 | try { 9 | const { email, password, name, surname, avatar } = req.body 10 | 11 | const { error, value } = validateSigninParams({ email, password, name, surname, avatar }) 12 | 13 | if (error) { 14 | throw new SigninDataError(error) 15 | } 16 | 17 | req.signinData = { ...value } 18 | 19 | return next() 20 | } catch (error) { 21 | return next(error) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/authentication/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import { loginRoutes } from './login.routes' 4 | import { logoutRoutes } from './logout.routes' 5 | 6 | const authenticationRoutes = express.Router() 7 | 8 | authenticationRoutes.use('', loginRoutes) 9 | authenticationRoutes.use('', logoutRoutes) 10 | 11 | export { authenticationRoutes } 12 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/authentication/login.routes.ts: -------------------------------------------------------------------------------- 1 | import { RequestDto } from '@infrastructure/server/serverDtos' 2 | import { Router } from 'express' 3 | import { login } from '@domainServices' 4 | 5 | import { validateLogin } from '@infrastructure/server/middlewares' 6 | 7 | import { authEndpointsLogger } from '@logger' 8 | 9 | const loginRoutes: Router = Router() 10 | 11 | loginRoutes.post('/login', validateLogin, async (req: RequestDto, res, next) => { 12 | const { username, password } = req.loginData! 13 | 14 | authEndpointsLogger('debug', `Login process started for username: '${username}'.`) 15 | 16 | try { 17 | res.json(await login(username, password)) 18 | } catch (error) { 19 | next(error) 20 | } 21 | }) 22 | 23 | export { loginRoutes } 24 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/authentication/logout.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { logout } from '@domainServices' 3 | import { OK } from '@errors' 4 | 5 | import { RequestDto } from '../../serverDtos' 6 | import { ensureAuthenticated } from '../../middlewares' 7 | 8 | import { authEndpointsLogger } from '@logger' 9 | 10 | const logoutRoutes: Router = Router() 11 | 12 | logoutRoutes.post('/logout', ensureAuthenticated, async (req: RequestDto, res, next) => { 13 | const { id: userId, username } = req.user! 14 | 15 | authEndpointsLogger('debug', `Logout process started for username: '${username}'.`) 16 | 17 | try { 18 | await logout(userId) 19 | res.status(OK).end('User logged out successfully') 20 | } catch (error) { 21 | next(error) 22 | } 23 | }) 24 | 25 | export { logoutRoutes } 26 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication' 2 | export * from './user' 3 | export * from './posts' 4 | export * from './manifest' 5 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/manifest/index.ts: -------------------------------------------------------------------------------- 1 | export * from './manifest.routes' 2 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/manifest/manifest.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import manifest from '@manifest' 3 | 4 | const manifestRoutes = Router() 5 | 6 | manifestRoutes.get('/__/manifest', (req, res) => { 7 | res.json(manifest) 8 | }) 9 | 10 | export { manifestRoutes } 11 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/posts/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import { postGeneralRoutes } from './post.general.routes' 4 | import { postCommentRoutes } from './post.comment.routes' 5 | import { postLikeRoutes } from './post.like.routes' 6 | 7 | const POSTS_PATH = '/posts' 8 | 9 | const postRoutes = express.Router() 10 | 11 | postRoutes.use(POSTS_PATH, postGeneralRoutes) 12 | postRoutes.use(POSTS_PATH, postCommentRoutes) 13 | postRoutes.use(POSTS_PATH, postLikeRoutes) 14 | 15 | export { postRoutes } 16 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/posts/post.comment.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { createPostComment, deletePostComment } from '@domainServices' 3 | import { ensureAuthenticated, validateNewPostComment, validatePostComment } from '../../middlewares' 4 | import { RequestDto } from '@infrastructure/server/serverDtos' 5 | 6 | import { postEndpointsLogger } from '@logger' 7 | 8 | const postCommentRoutes = Router() 9 | 10 | postCommentRoutes.post('/comment', ensureAuthenticated, validateNewPostComment, async (req: RequestDto, res, next) => { 11 | const { id, name, surname, avatar } = req.user! 12 | const { postId, commentBody } = req.newPostComment! 13 | 14 | postEndpointsLogger('debug', `Commenting post '${postId}' by user '${id}'.`) 15 | 16 | try { 17 | res.json(await createPostComment(postId, commentBody, { id, name, surname, avatar })) 18 | } catch (error) { 19 | next(error) 20 | } 21 | }) 22 | 23 | postCommentRoutes.delete('/comment', ensureAuthenticated, validatePostComment, async (req: RequestDto, res, next) => { 24 | const { id: commentOwnerId } = req.user! 25 | const { postId, commentId } = req.postComment! 26 | 27 | postEndpointsLogger('debug', `Removing comment '${commentId}' from post '${postId}' by user '${commentOwnerId}'.`) 28 | 29 | try { 30 | res.json(await deletePostComment(postId as string, commentId as string, commentOwnerId)) 31 | } catch (error) { 32 | next(error) 33 | } 34 | }) 35 | 36 | export { postCommentRoutes } 37 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/posts/post.general.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { getPosts, getPostById, createPost, deletePost, getExtendedPosts, getExtendedPostById } from '@domainServices' 3 | import { ensureAuthenticated } from '../../middlewares' 4 | import { RequestDto } from '@infrastructure/server/serverDtos' 5 | 6 | import { postEndpointsLogger } from '@logger' 7 | import { validateNewPost, validatePost } from '@infrastructure/server/middlewares' 8 | 9 | const postGeneralRoutes = Router() 10 | 11 | postGeneralRoutes.get('/', async (req, res, next) => { 12 | postEndpointsLogger('debug', 'Retrieving all posts') 13 | 14 | try { 15 | res.json(await getPosts()) 16 | } catch (error) { 17 | next(error) 18 | } 19 | }) 20 | 21 | postGeneralRoutes.get('/ext', ensureAuthenticated, async (req: RequestDto, res, next) => { 22 | const { id: userId } = req.user! 23 | 24 | postEndpointsLogger('debug', `Retrieving all posts requested by user '${userId}'`) 25 | 26 | try { 27 | res.json(await getExtendedPosts(userId)) 28 | } catch (error) { 29 | next(error) 30 | } 31 | }) 32 | 33 | postGeneralRoutes.post('/', ensureAuthenticated, validateNewPost, async (req: RequestDto, res, next) => { 34 | const { id, name, surname, avatar } = req.user! 35 | const postBody = req.postBody! 36 | 37 | postEndpointsLogger('debug', `Creating new post by user '${id}'.`) 38 | 39 | try { 40 | res.json(await createPost({ id, name, surname, avatar }, postBody)) 41 | } catch (error) { 42 | next(error) 43 | } 44 | }) 45 | 46 | postGeneralRoutes.delete('/', ensureAuthenticated, validatePost('body'), async (req: RequestDto, res, next) => { 47 | const { id: postOwnerId } = req.user! 48 | const postId = req.postId! 49 | 50 | postEndpointsLogger('debug', `Deleting post '${postId}' by user '${postOwnerId}'.`) 51 | 52 | try { 53 | await deletePost(postId, postOwnerId) 54 | res.end() 55 | } catch (error) { 56 | next(error) 57 | } 58 | }) 59 | 60 | postGeneralRoutes.get('/:id', validatePost('params'), async (req: RequestDto, res, next) => { 61 | const postId = req.postId! 62 | 63 | postEndpointsLogger('debug', `Retrieving post with id '${postId}'.`) 64 | 65 | try { 66 | res.json(await getPostById(postId)) 67 | } catch (error) { 68 | next(error) 69 | } 70 | }) 71 | 72 | postGeneralRoutes.get('/ext/:id', ensureAuthenticated, validatePost('params'), async (req: RequestDto, res, next) => { 73 | const { id: postOwnerId } = req.user! 74 | const postId = req.postId! 75 | 76 | postEndpointsLogger('debug', `Retrieving post with id '${postId}'.`) 77 | 78 | try { 79 | res.json(await getExtendedPostById(postId, postOwnerId)) 80 | } catch (error) { 81 | next(error) 82 | } 83 | }) 84 | 85 | export { postGeneralRoutes } 86 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/posts/post.like.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { likePost, dislikePost } from '@domainServices' 3 | import { ensureAuthenticated, validatePostLike } from '@infrastructure/server/middlewares' 4 | import { RequestDto } from '@infrastructure/server/serverDtos' 5 | 6 | import { postEndpointsLogger } from '@logger' 7 | 8 | const postLikeRoutes = Router() 9 | 10 | postLikeRoutes.post('/like', ensureAuthenticated, validatePostLike, async (req: RequestDto, res, next) => { 11 | const { id, name, surname, avatar } = req.user! 12 | const postId = req.postId! 13 | 14 | postEndpointsLogger('debug', `Liking post '${postId}' by user '${id}'.`) 15 | 16 | try { 17 | res.json(await likePost(postId, { id, name, surname, avatar })) 18 | } catch (error) { 19 | next(error) 20 | } 21 | }) 22 | 23 | postLikeRoutes.delete('/like', ensureAuthenticated, validatePostLike, async (req: RequestDto, res, next) => { 24 | const { id: likeOwnerId } = req.user! 25 | const postId = req.postId! 26 | 27 | postEndpointsLogger('debug', `Disliking post '${postId}' by user '${likeOwnerId}'.`) 28 | 29 | try { 30 | res.json(await dislikePost(postId, likeOwnerId)) 31 | } catch (error) { 32 | next(error) 33 | } 34 | }) 35 | 36 | export { postLikeRoutes } 37 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/user/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import { signinRoutes } from './signin.routes' 4 | import { profileRoutes } from './profile.routes' 5 | 6 | const userRoutes = express.Router() 7 | 8 | userRoutes.use('', signinRoutes) 9 | userRoutes.use('', profileRoutes) 10 | 11 | export { userRoutes } 12 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/user/profile.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | import { getUserProfile, updateUserProfile } from '@domainServices' 4 | 5 | import { ensureAuthenticated, validateProfileData } from '../../middlewares' 6 | import { RequestDto } from '../../serverDtos' 7 | 8 | import { userEndpointsLogger } from '@logger' 9 | 10 | const profileRoutes: Router = Router() 11 | 12 | profileRoutes.get('/profile', ensureAuthenticated, async (req: RequestDto, res, next) => { 13 | const { id } = req.user! 14 | 15 | userEndpointsLogger('debug', `Retrieving profile for user '${id}'.`) 16 | 17 | try { 18 | res.json(await getUserProfile(id)) 19 | } catch (error) { 20 | next(error) 21 | } 22 | }) 23 | 24 | profileRoutes.put('/profile', ensureAuthenticated, validateProfileData, async (req: RequestDto, res, next) => { 25 | const { id } = req.user! 26 | const newProfileData = req.newProfileData! 27 | 28 | userEndpointsLogger('debug', `Updating profile for user '${id}'.`) 29 | 30 | try { 31 | res.json(await updateUserProfile(id, newProfileData)) 32 | } catch (error) { 33 | next(error) 34 | } 35 | }) 36 | 37 | export { profileRoutes } 38 | -------------------------------------------------------------------------------- /src/infrastructure/server/routes/user/signin.routes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { RequestDto } from '@infrastructure/server/serverDtos' 3 | 4 | import { createUser } from '@domainServices' 5 | import { CREATED } from '@errors' 6 | 7 | import { mapNewUserFromDtoToDomainModel } from '@infrastructure/mappers' 8 | import { validateSignin } from '@infrastructure/server/middlewares' 9 | 10 | import { userEndpointsLogger } from '@logger' 11 | 12 | const signinRoutes: Router = Router() 13 | 14 | signinRoutes.post('/signin', validateSignin, async (req: RequestDto, res, next) => { 15 | const newUserData = req.signinData! 16 | 17 | userEndpointsLogger('debug', `Signin request for username '${newUserData.email}'.`) 18 | 19 | try { 20 | const registeredUser = await createUser(mapNewUserFromDtoToDomainModel(newUserData)) 21 | res.status(CREATED).json(registeredUser) 22 | } catch (error) { 23 | next(error) 24 | } 25 | }) 26 | 27 | export { signinRoutes } 28 | -------------------------------------------------------------------------------- /src/infrastructure/server/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http' 2 | import express from 'express' 3 | import helmet from 'helmet' 4 | import { urlencoded, json } from 'body-parser' 5 | 6 | import { serve as swaggerServe, setup as swaggerSetup } from 'swagger-ui-express' 7 | import { swaggerDocument, swaggerOptions } from './apidoc' 8 | 9 | import { authenticationRoutes, userRoutes, postRoutes, manifestRoutes } from './routes' 10 | 11 | import { handleHttpError } from './middlewares' 12 | 13 | import { serverLogger } from '@logger' 14 | 15 | const app = express() 16 | const port = parseInt(process.env.SERVER_PORT ?? '3000', 10) 17 | 18 | app.use(helmet()) 19 | 20 | app.use(urlencoded({ extended: true })) 21 | app.use(json()) 22 | 23 | app.use('/__/apidoc', swaggerServe, swaggerSetup(swaggerDocument, swaggerOptions)) 24 | 25 | app.use(authenticationRoutes) 26 | app.use(userRoutes) 27 | app.use(postRoutes) 28 | app.use(manifestRoutes) 29 | 30 | app.use(handleHttpError) 31 | 32 | const server = createServer(app) 33 | 34 | const runServer = () => server.listen(port, () => serverLogger('info', `Server running in http://localhost:${port}`)) 35 | 36 | const stopServer = () => { 37 | serverLogger('info', 'Clossing server...') 38 | server.close() 39 | } 40 | 41 | export { server, runServer, stopServer } 42 | -------------------------------------------------------------------------------- /src/infrastructure/server/serverDtos/express.dto.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | import { UserDomainModel } from '@domainModels' 3 | import { LoginInputParams } from '@infrastructure/types' 4 | import { PostDto, NewUserInputDto, NewUserProfileDto, PostCommentDto } from '@infrastructure/dtos' 5 | 6 | export interface RequestDto extends Request { 7 | user?: UserDomainModel | null 8 | loginData?: LoginInputParams 9 | signinData?: NewUserInputDto 10 | newProfileData?: NewUserProfileDto 11 | postId?: Required['_id'] 12 | postBody?: PostDto['body'] 13 | postComment?: { 14 | postId: Required['_id'] 15 | commentId: Required['_id'] 16 | } 17 | newPostComment?: { 18 | postId: Required['_id'] 19 | commentBody: PostDto['body'] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/infrastructure/server/serverDtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from './express.dto' 2 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/authentication/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.validator' 2 | export * from './token.validator' 3 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/authentication/login.validator.ts: -------------------------------------------------------------------------------- 1 | import { LoginInputParams } from '@infrastructure/types' 2 | import Joi from 'joi' 3 | 4 | import { username, password } from '../validation.rules' 5 | 6 | const schema = Joi.object({ username, password }) 7 | 8 | interface ValidationResult { 9 | error?: string 10 | value: LoginInputParams 11 | } 12 | 13 | export const validateLoginParams = ({ username, password }: Partial): ValidationResult => { 14 | const { error, value } = schema.validate({ username, password }) 15 | 16 | return { 17 | error: error && error.details[0].message, 18 | value 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/authentication/test/validateToken.test.ts: -------------------------------------------------------------------------------- 1 | import { testingValidJwtTokenForNonPersistedUser, testingMalformedJwtToken } from '@testingFixtures' 2 | 3 | import { validateToken } from '@infrastructure/server/validators' 4 | 5 | describe('[API] - Validation - validateToken', () => { 6 | it('must validate the provided token successfully', () => { 7 | const token = testingValidJwtTokenForNonPersistedUser 8 | 9 | const { error, value } = validateToken(token) 10 | 11 | expect(error).toBeUndefined() 12 | 13 | expect(value).not.toBeUndefined() 14 | expect(value).toHaveProperty('token') 15 | expect(value.token).toBe(token) 16 | }) 17 | 18 | it('must return an error when token is not provided', () => { 19 | const { error, value } = validateToken(undefined) 20 | 21 | const expectedErrorMessage = '"token" is required' 22 | 23 | expect(error).not.toBeUndefined() 24 | expect(error).toBe(expectedErrorMessage) 25 | 26 | expect(value).not.toBeUndefined() 27 | expect(value).toHaveProperty('token') 28 | expect(value.token).toBeUndefined() 29 | }) 30 | 31 | it('must return an error when the provided token has not a valid structure because it includes non allowed characters', () => { 32 | const token = testingMalformedJwtToken + '$' 33 | 34 | const expectedErrorMessage = `"token" with value "${token}" fails to match the required pattern: /^[a-zA-Z0-9]+\\.[a-zA-Z0-9]+\\.[a-zA-Z0-9-_]+$/` 35 | 36 | const { error, value } = validateToken(token) 37 | 38 | expect(error).not.toBeUndefined() 39 | expect(error).toBe(expectedErrorMessage) 40 | 41 | expect(value).not.toBeUndefined() 42 | expect(value).toHaveProperty('token') 43 | expect(value.token).toBe(token) 44 | }) 45 | 46 | it('must return an error when the provided token has not a valid structure because it is incomplete', () => { 47 | const token = testingMalformedJwtToken.split('.').shift() 48 | 49 | const expectedErrorMessage = `"token" with value "${token}" fails to match the required pattern: /^[a-zA-Z0-9]+\\.[a-zA-Z0-9]+\\.[a-zA-Z0-9-_]+$/` 50 | 51 | const { error, value } = validateToken(token) 52 | 53 | expect(error).not.toBeUndefined() 54 | expect(error).toBe(expectedErrorMessage) 55 | 56 | expect(value).not.toBeUndefined() 57 | expect(value).toHaveProperty('token') 58 | expect(value.token).toBe(token) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/authentication/token.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | 3 | import { token } from '../validation.rules' 4 | import { UserDto } from '@infrastructure/dtos' 5 | 6 | const schema = Joi.object({ token }) 7 | 8 | interface ValidationResult { 9 | error?: string 10 | value: { 11 | token: UserDto['token'] 12 | } 13 | } 14 | 15 | export const validateToken = (token?: string): ValidationResult => { 16 | const { error, value } = schema.validate({ token }) 17 | 18 | return { 19 | error: error && error.details[0].message, 20 | value 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication' 2 | export * from './posts' 3 | export * from './user' 4 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/posts/comment.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | 3 | import { postId, commentId, commentBody } from '../validation.rules' 4 | import { PostCommentDto, PostDto } from '@infrastructure/dtos' 5 | 6 | const newCommentSchema = Joi.object({ postId, commentBody }) 7 | const commentSchema = Joi.object({ postId, commentId }) 8 | 9 | interface ValidationResult { 10 | error?: string 11 | value: T 12 | } 13 | 14 | interface NewCommentParams { 15 | postId: Required['_id'] 16 | commentBody: Required['body'] 17 | } 18 | 19 | export const validateNewPostCommentParams = ({ postId, commentBody }: Partial): ValidationResult => { 20 | const { error, value } = newCommentSchema.validate({ postId, commentBody }) 21 | 22 | return { 23 | error: error && error.details[0].message, 24 | value 25 | } 26 | } 27 | 28 | interface CommentParams { 29 | postId: Required['_id'] 30 | commentId: Required['_id'] 31 | } 32 | 33 | export const validatePostCommentParams = ({ postId, commentId }: Partial): ValidationResult => { 34 | const { error, value } = commentSchema.validate({ postId, commentId }) 35 | 36 | return { 37 | error: error && error.details[0].message, 38 | value 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/posts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post.validator' 2 | export * from './comment.validator' 3 | export * from './like.validator' 4 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/posts/like.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | 3 | import { postId } from '../validation.rules' 4 | import { PostDto } from '@infrastructure/dtos' 5 | 6 | const schema = Joi.object({ postId }) 7 | 8 | interface ValidationResult { 9 | error?: string 10 | value: { 11 | postId: Required['_id'] 12 | } 13 | } 14 | 15 | export const validatePostLikeParams = (postId: string | undefined): ValidationResult => { 16 | const { error, value } = schema.validate({ postId }) 17 | 18 | return { 19 | error: error && error.details[0].message, 20 | value 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/posts/post.validator.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | 3 | import { postId, postBody } from '../validation.rules' 4 | import { PostDto } from '@infrastructure/dtos' 5 | 6 | const newPostSchema = Joi.object({ postBody }) 7 | const postSchema = Joi.object({ postId }) 8 | 9 | interface ValidationResult { 10 | error?: string 11 | value: T 12 | } 13 | 14 | interface NewPostParams { 15 | postBody: Required['body'] 16 | } 17 | 18 | export const validateNewPostParams = (postBody: string | undefined): ValidationResult => { 19 | const { error, value } = newPostSchema.validate({ postBody }) 20 | 21 | return { 22 | error: error && error.details[0].message, 23 | value 24 | } 25 | } 26 | 27 | interface PostParams { 28 | postId: Required['_id'] 29 | } 30 | 31 | export const validatePostParams = (postId: string | undefined): ValidationResult => { 32 | const { error, value } = postSchema.validate({ postId }) 33 | 34 | return { 35 | error: error && error.details[0].message, 36 | value 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/posts/test/validateNewPost.test.ts: -------------------------------------------------------------------------------- 1 | import { validateNewPostParams } from '@infrastructure/server/validators' 2 | 3 | const testingPostBody = 'This is a post body' 4 | 5 | describe('[API] - Validation - validateNewPostParams', () => { 6 | it('must validate the provided data successfully', () => { 7 | const postBody = testingPostBody 8 | 9 | const { error, value } = validateNewPostParams(postBody) 10 | 11 | expect(error).toBeUndefined() 12 | 13 | expect(value).not.toBeUndefined() 14 | expect(value).toStrictEqual({ postBody }) 15 | }) 16 | 17 | it('must return an error when postBody is not provided', () => { 18 | const postBody = undefined 19 | const expectedErrorMessage = '"postBody" is required' 20 | 21 | const { error, value } = validateNewPostParams(postBody) 22 | 23 | expect(error).not.toBeUndefined() 24 | expect(error).toBe(expectedErrorMessage) 25 | 26 | expect(value).not.toBeUndefined() 27 | expect(value).toStrictEqual({ postBody }) 28 | }) 29 | 30 | it('must return an error when postBody is empty', () => { 31 | const postBody = '' 32 | const expectedErrorMessage = '"postBody" is not allowed to be empty' 33 | 34 | const { error, value } = validateNewPostParams(postBody) 35 | 36 | expect(error).not.toBeUndefined() 37 | expect(error).toBe(expectedErrorMessage) 38 | 39 | expect(value).not.toBeUndefined() 40 | expect(value).toStrictEqual({ postBody }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './signin.validator' 2 | export * from './profile.validator' 3 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/user/profile.validator.ts: -------------------------------------------------------------------------------- 1 | import { NewUserProfileDto } from '@infrastructure/dtos' 2 | import Joi from 'joi' 3 | 4 | import { optionalName, optionalSurname, optionalAvatar } from '../validation.rules' 5 | 6 | const schema = Joi.object({ name: optionalName, surname: optionalSurname, avatar: optionalAvatar }) 7 | 8 | interface ValidationResult { 9 | error?: string 10 | value: NewUserProfileDto 11 | } 12 | 13 | export const validateProfileParams = (profileParams: NewUserProfileDto): ValidationResult => { 14 | const { name, surname, avatar } = profileParams 15 | const { error, value } = schema.validate({ name, surname, avatar }) 16 | 17 | return { 18 | error: error && error.details[0].message, 19 | value 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/user/signin.validator.ts: -------------------------------------------------------------------------------- 1 | import { NewUserInputDto } from '@infrastructure/dtos' 2 | import Joi from 'joi' 3 | 4 | import { email, password, requiredName, requiredSurname, requiredAvatar } from '../validation.rules' 5 | 6 | const schema = Joi.object({ email, password, name: requiredName, surname: requiredSurname, avatar: requiredAvatar }) 7 | 8 | interface ValidationResult { 9 | error?: string 10 | value: NewUserInputDto 11 | } 12 | 13 | export const validateSigninParams = ({ email, password, name, surname, avatar }: Partial): ValidationResult => { 14 | const { error, value } = schema.validate({ email, password, name, surname, avatar }) 15 | 16 | return { 17 | error: error && error.details[0].message, 18 | value 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/infrastructure/server/validators/validation.rules.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | 3 | export const mongodbId = Joi.string().pattern(/^[a-zA-Z0-9]{24}$/) 4 | export const body = Joi.string() 5 | export const email = Joi.string().email({ minDomainSegments: 2, tlds: { allow: true } }).required() 6 | export const url = Joi.string().uri({ 7 | scheme: ['http', 'https'], 8 | domain: { 9 | minDomainSegments: 2, 10 | tlds: { allow: true } 11 | } 12 | }) 13 | 14 | export const username = email 15 | export const password = Joi.string().pattern(/^[a-zA-Z0-9]{4,}$/).required() 16 | export const optionalName = Joi.string().min(2) 17 | export const optionalSurname = optionalName 18 | export const optionalAvatar = url 19 | export const requiredName = Joi.string().min(2).required() 20 | export const requiredSurname = requiredName 21 | export const requiredAvatar = url.required() 22 | 23 | export const token = Joi.string().pattern(/^[a-zA-Z0-9]+\.[a-zA-Z0-9]+\.[a-zA-Z0-9-_]+$/).required() 24 | 25 | export const postId = mongodbId.required() 26 | export const postBody = body.required() 27 | 28 | export const commentId = mongodbId.required() 29 | export const commentBody = body.required() 30 | -------------------------------------------------------------------------------- /src/infrastructure/types/authentication.types.ts: -------------------------------------------------------------------------------- 1 | export interface LoginInputParams { 2 | username: string 3 | password: string 4 | } 5 | 6 | export interface JwtPayload { 7 | sub: string 8 | username: string 9 | } 10 | 11 | export interface DecodedJwtToken extends JwtPayload { 12 | exp: number 13 | iat: number 14 | } 15 | -------------------------------------------------------------------------------- /src/infrastructure/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication.types' 2 | -------------------------------------------------------------------------------- /src/preset.ts: -------------------------------------------------------------------------------- 1 | export const checkStartup = (requiredEnvVars: string[]) => { 2 | const nonConfiguredVariables = requiredEnvVars.reduce( 3 | (result: string[], variable) => process.env[variable] ? result : [...result, variable], 4 | [] 5 | ) 6 | 7 | if (nonConfiguredVariables.length) { 8 | console.log(`[\x1b[31mERROR\x1b[37m] - [startup] - The next required environment variables are not configured: ${nonConfiguredVariables.join(', ')}`) 9 | process.exit(1) 10 | } else { 11 | console.log('[ \x1b[32mINFO\x1b[37m] - [startup] - The whole required environment variables are successfully declared') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/fixtures/assets/authentication.json: -------------------------------------------------------------------------------- 1 | { 2 | "validHashedPassword": { 3 | "value": "$2b$04$BCFnw0TXw7cGHvk8VuCkMOZw28YjlpInsuSwMaLN83N.ZgnPKHbP2", 4 | "comments": { 5 | "plainPasswd": "123456", 6 | "hashedWith": "bcrypt", 7 | "saltValue": 3 8 | } 9 | }, 10 | "validPlainPassword": { 11 | "value": "123456", 12 | "comments": {} 13 | }, 14 | "wrongPlainPassword": { 15 | "value": "wrongpassword", 16 | "comments": {} 17 | }, 18 | "validJwtTokenForNonPersistedUser": { 19 | "value": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEsInVzZXJuYW1lIjoibm9uLnBlcnNpc3RlZEBtYWlsLmNvbSIsImlhdCI6MTYxMTQwMjEwNSwiZXhwIjo2MzcxMjA1ODk0fQ.O8knUlm8mqR5a9yHJkfDfIhPQO4jrnUt6da8rX15hhsquhZAYVxKzTlyKTBoYdaIOhUAOEp6w-RJBrA6wNhuiw", 20 | "comments": { 21 | "username": "non.persisted@mail.com", 22 | "algorithm": "HS512", 23 | "expiresAt": "2120-10-31T07:43:09.000Z" 24 | } 25 | }, 26 | "expiredJwtToken": { 27 | "value": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjIsInVzZXJuYW1lIjoidGVzdEBtYWlsLmNvbSIsImlhdCI6MTYxMTQwMjEwNSwiZXhwIjoxNjExNDAyMTA2fQ.F-208NpmIa0175rD4kbjUW5YjLpXizaO4iF1ACdWn0lWf3XBCI0t09KIJAYbeFgpWb5gRRLGj6Vgd3g8NtHoDA", 28 | "comments": { 29 | "username": "test@mail.com", 30 | "algorithm": "HS512", 31 | "expiresAt": "2021-01-23T11:41:46.200Z" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/fixtures/authentication.fixtures.ts: -------------------------------------------------------------------------------- 1 | import { 2 | validJwtTokenForNonPersistedUser, 3 | expiredJwtToken, 4 | validHashedPassword, 5 | validPlainPassword, 6 | wrongPlainPassword 7 | } from './assets/authentication.json' 8 | 9 | export const testingValidJwtTokenForNonPersistedUser: string = validJwtTokenForNonPersistedUser.value 10 | export const testingMalformedJwtToken: string = validJwtTokenForNonPersistedUser.value.substring(1) 11 | export const testingExpiredJwtToken: string = expiredJwtToken.value 12 | export const testingValidHashedPassword: string = validHashedPassword.value 13 | export const testingValidPlainPassword: string = validPlainPassword.value 14 | export const testingWrongPlainPassword: string = wrongPlainPassword.value 15 | -------------------------------------------------------------------------------- /src/test/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils.fixtures' 2 | export * from './authentication.fixtures' 3 | export * from './user.fixtures' 4 | export * from './post.fixtures' 5 | export * from './mongodb' 6 | -------------------------------------------------------------------------------- /src/test/fixtures/mockers/runMockers.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { config } from 'dotenv' 4 | // import { createdMokedAuthentication } from './testingAuthenticationMockFactory' 5 | import { createMockedUsers } from './testingUserMockFactory' 6 | import { createMockedPosts } from './testingPostMockFactory' 7 | import { MockedPosts } from '../types' 8 | import { UserDomainModel } from '@domainModels' 9 | 10 | config({ path: path.join(__dirname, '../../../../env/.env.test') }) 11 | 12 | const saveFile = async (fileName: string, data: string) => { 13 | const filePath = path.join(__dirname, '../assets', fileName) 14 | 15 | console.log(`[\x1b[36mDEBUG\x1b[37m] - [mocker] - Creating '${fileName}' file...`) 16 | fs.writeFile(filePath, data, (error) => { 17 | if (error) { console.log(`[\x1b[31mERROR\x1b[37m] - [mocker] - Creating '${fileName}' file. ${error.message}`) } 18 | console.log(`[ \x1b[32mINFO\x1b[37m] - [mocker] - '${fileName}' file succesfully created.`) 19 | }) 20 | } 21 | 22 | // Uncomment this lines when you need to generage new authentication credentials. 23 | // const rawAuthentication: AuthenticationFixture = createdMokedAuthentication() 24 | // saveFile('authentication.json', JSON.stringify(rawAuthentication)) 25 | 26 | const rawUsers: UserDomainModel[] = createMockedUsers(305) 27 | saveFile('users.json', JSON.stringify(rawUsers)) 28 | 29 | const rawPosts: MockedPosts = createMockedPosts(rawUsers) 30 | saveFile('posts.json', JSON.stringify(rawPosts)) 31 | -------------------------------------------------------------------------------- /src/test/fixtures/mockers/testingAuthenticationMockFactory.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { sign, SignOptions, Algorithm } from 'jsonwebtoken' 3 | import bcrypt from 'bcrypt' 4 | import { config } from 'dotenv' 5 | import { AuthenticationFixture, JwtFixtureParams, PrePopulatedJwt } from './../types' 6 | import { testingValidPlainPassword, testingWrongPlainPassword } from '../authentication.fixtures' 7 | 8 | config({ path: path.join(__dirname, '../../../../env/.env.test') }) 9 | 10 | const bcryptSalt = parseInt(process.env.BCRYPT_SALT!, 10) 11 | const plainPassword = testingValidPlainPassword 12 | const wrongPlainPassword = testingWrongPlainPassword 13 | 14 | const passwordMockedData: AuthenticationFixture = { 15 | validHashedPassword: { 16 | value: bcrypt.hashSync(plainPassword, bcryptSalt), 17 | comments: { 18 | plainPasswd: plainPassword, 19 | hashedWith: 'bcrypt', 20 | saltValue: bcryptSalt 21 | } 22 | }, 23 | validPlainPassword: { 24 | value: plainPassword, 25 | comments: {} 26 | }, 27 | wrongPlainPassword: { 28 | value: wrongPlainPassword, 29 | comments: {} 30 | } 31 | } 32 | 33 | const tokensToBeGenerated: PrePopulatedJwt[] = [ 34 | { 35 | tokenName: 'validJwtTokenForNonPersistedUser', 36 | userId: '1', 37 | username: 'non.persisted@mail.com', 38 | algorithm: process.env.JWT_ALGORITHM as Algorithm || 'HS512', 39 | secret: process.env.JWT_KEY!, 40 | expiresAt: Date.parse('2120-10-31T07:43:09.000Z') 41 | }, 42 | { 43 | tokenName: 'expiredJwtToken', 44 | userId: '2', 45 | username: 'test@mail.com', 46 | algorithm: process.env.JWT_ALGORITHM as Algorithm || 'HS512', 47 | secret: process.env.JWT_KEY!, 48 | expiresAt: 1000 49 | } 50 | ] 51 | 52 | const encodeJwt = ({ userId, username, secret, algorithm, expiresIn }: JwtFixtureParams): string => { 53 | const payload = { 54 | sub: userId, 55 | username 56 | } 57 | const options: SignOptions = { 58 | algorithm, 59 | expiresIn 60 | } 61 | 62 | return sign(payload, secret, options) 63 | } 64 | 65 | const generateTokens = (tokensDataCollection: PrePopulatedJwt[]): AuthenticationFixture => tokensDataCollection.reduce((generated, { tokenName, userId, username, algorithm, secret, expiresAt }) => { 66 | const expiresIn = Math.trunc(expiresAt / 1000) 67 | const processedToken = { 68 | [tokenName]: { 69 | value: encodeJwt({ userId, username, algorithm, secret, expiresIn }), 70 | comments: { 71 | username, 72 | algorithm, 73 | expiresAt: (new Date(Date.now() > expiresAt ? Date.now() + expiresAt : expiresAt)).toISOString() 74 | } 75 | } 76 | } 77 | return { ...generated, ...processedToken } 78 | }, {}) 79 | 80 | export const createdMokedAuthentication = (): AuthenticationFixture => ({ 81 | ...passwordMockedData, 82 | ...generateTokens(tokensToBeGenerated) 83 | }) 84 | -------------------------------------------------------------------------------- /src/test/fixtures/mockers/testingUserMockFactory.ts: -------------------------------------------------------------------------------- 1 | import { sign, Secret, SignOptions, Algorithm } from 'jsonwebtoken' 2 | import { name } from 'faker' 3 | import { avatarUrls } from '../assets/avatarUrls.json' 4 | import { validHashedPassword } from '../assets/authentication.json' 5 | import { generateMockedMongoDbId } from './utils' 6 | import { UserDomainModel } from '@domainModels' 7 | import { JwtPayload } from '@infrastructure/types' 8 | 9 | const encodeJwt = (username: string, userId: string) => { 10 | const payload: JwtPayload = { 11 | sub: userId, 12 | username 13 | } 14 | const secret: Secret = process.env.JWT_KEY! 15 | const options: SignOptions = { 16 | algorithm: process.env.JWT_ALGORITHM as Algorithm || 'HS512', 17 | expiresIn: 3153600000 // 100 years 18 | } 19 | 20 | return sign(payload, secret, options) 21 | } 22 | 23 | const testingUserFactory = (usersAmount: number): UserDomainModel[] => { 24 | const avatars = [...avatarUrls] 25 | 26 | return [...Array(usersAmount)].map(() => { 27 | const userId = generateMockedMongoDbId() 28 | const firstName = name.firstName() 29 | const surname = name.lastName() 30 | const username = `${firstName.toLowerCase()}.${surname.toLowerCase()}@mail.com` 31 | const avatarIndex = Math.floor(Math.random() * (avatars.length - 1)) + 1 32 | 33 | return { 34 | id: userId, 35 | name: firstName, 36 | surname, 37 | email: username, 38 | username, 39 | password: validHashedPassword.value, 40 | avatar: avatars[avatarIndex], 41 | token: encodeJwt(username, userId), 42 | enabled: true, 43 | deleted: false, 44 | lastLoginAt: '', 45 | createdAt: (new Date()).toISOString().replace(/\dZ/, '0Z'), 46 | updatedAt: (new Date()).toISOString().replace(/\dZ/, '0Z') 47 | } 48 | }) 49 | } 50 | 51 | export const createMockedUsers = (usersAmount: number) => testingUserFactory(usersAmount) 52 | -------------------------------------------------------------------------------- /src/test/fixtures/mockers/utils.ts: -------------------------------------------------------------------------------- 1 | import { datatype } from 'faker' 2 | 3 | export const generateMockedMongoDbId = () => datatype.uuid().split('-').join('').slice(0, 24) 4 | -------------------------------------------------------------------------------- /src/test/fixtures/mongodb/index.ts: -------------------------------------------------------------------------------- 1 | export * from './users' 2 | export * from './posts' 3 | -------------------------------------------------------------------------------- /src/test/fixtures/mongodb/posts.ts: -------------------------------------------------------------------------------- 1 | import { PostDto } from '@infrastructure/dtos' 2 | import { mongodb } from '@infrastructure/orm' 3 | 4 | const { models: { Post } } = mongodb 5 | 6 | export const cleanPostsCollectionFixture = async () => Post.deleteMany({}) 7 | 8 | export const savePostsFixture = async (postsData: PostDto[]) => Post.insertMany(postsData) 9 | 10 | export const getPostByIdFixture = async (postId: string) => { 11 | const retrievedPost = await Post.findById(postId) 12 | return retrievedPost ? retrievedPost.toJSON() as PostDto : null 13 | } 14 | -------------------------------------------------------------------------------- /src/test/fixtures/mongodb/users.ts: -------------------------------------------------------------------------------- 1 | import { Types } from 'mongoose' 2 | 3 | import { UserDto } from '@infrastructure/dtos' 4 | import { mongodb } from '@infrastructure/orm' 5 | 6 | const { models: { User } } = mongodb 7 | 8 | type OptionalUserData = Partial 9 | type MongoDbAdaptedUserData = Omit & { _id?: string | Types.ObjectId } 10 | 11 | const parseUserDataFixture = (userData: OptionalUserData) => { 12 | const { _id: plainId, ...otherFields } = userData 13 | let parsedUserData: MongoDbAdaptedUserData 14 | 15 | if (plainId) { 16 | parsedUserData = { 17 | _id: Types.ObjectId(plainId), 18 | ...otherFields 19 | } 20 | } else { 21 | parsedUserData = userData 22 | } 23 | 24 | return parsedUserData 25 | } 26 | 27 | export const cleanUsersCollectionFixture = async () => User.deleteMany({}) 28 | 29 | export const saveUserFixture = async (userData: OptionalUserData) => { 30 | const parsedUserData = parseUserDataFixture(userData) 31 | return (await (new User(parsedUserData)).save()).toJSON() as UserDto 32 | } 33 | 34 | export const saveUsersFixture = async (userData: OptionalUserData[]) => User.insertMany(userData) 35 | 36 | export const getUserByUsernameFixture = async (username: string) => await User.findOne({ username }).lean() 37 | -------------------------------------------------------------------------------- /src/test/fixtures/post.fixtures.ts: -------------------------------------------------------------------------------- 1 | import { 2 | basicDtoPostOwners, 3 | basicDtoPostCommentOwners, 4 | basicDtoPostLikeOwners, 5 | basicDtoFreeUsers, 6 | 7 | basicDomainModelPostOwners, 8 | basicDomainModelPostCommentOwners, 9 | basicDomainModelPostLikeOwners, 10 | basicDomainModelFreeUsers, 11 | 12 | basicDtoPersistedPosts, 13 | commentedDtoPersistedPosts, 14 | likedAndCommentedDtoPersistedPosts, 15 | 16 | basicDomainModelPosts, 17 | commentedDomainModelPosts, 18 | likedAndCommentedDomainModelPosts 19 | } from './assets/posts.json' 20 | import { PostDomainModelFixture, PostDtoFixture, UserDomainModelFixture, UserDtoFixture } from './types' 21 | import { generateMockedMongoDbId } from './utils.fixtures' 22 | 23 | export const testingDtoPostOwners: UserDtoFixture[] = basicDtoPostOwners 24 | export const testingDtoPostCommentOwners: UserDtoFixture[] = basicDtoPostCommentOwners 25 | export const testingDtoPostLikeOwners: UserDtoFixture[] = basicDtoPostLikeOwners 26 | export const testingDtoFreeUsers: UserDtoFixture[] = basicDtoFreeUsers 27 | 28 | export const testingDomainModelPostOwners: UserDomainModelFixture[] = basicDomainModelPostOwners 29 | export const testingDomainModelPostCommentOwners: UserDomainModelFixture[] = basicDomainModelPostCommentOwners 30 | export const testingDomainModelPostLikeOwners: UserDomainModelFixture[] = basicDomainModelPostLikeOwners 31 | export const testingDomainModelFreeUsers: UserDomainModelFixture[] = basicDomainModelFreeUsers 32 | 33 | export const testingBasicPersistedDtoPosts: PostDtoFixture[] = basicDtoPersistedPosts 34 | export const testingCommentedPersistedDtoPosts: PostDtoFixture[] = commentedDtoPersistedPosts 35 | export const testingLikedAndCommentedPersistedDtoPosts: PostDtoFixture[] = likedAndCommentedDtoPersistedPosts 36 | 37 | export const testingBasicPersistedDomainModelPosts: PostDomainModelFixture[] = basicDomainModelPosts 38 | export const testingCommentedPersistedDomainModelPosts: PostDomainModelFixture[] = commentedDomainModelPosts 39 | export const testingLikedAndCommentedPersistedDomainModelPosts: PostDomainModelFixture[] = likedAndCommentedDomainModelPosts 40 | 41 | export const testingNonValidPostId = generateMockedMongoDbId() 42 | export const testingNonValidPostOwnerId = generateMockedMongoDbId() 43 | export const testingNonValidPostCommentId = generateMockedMongoDbId() 44 | export const testingNonValidCommentOwnerId = generateMockedMongoDbId() 45 | export const testingNonValidLikeOwnerId = generateMockedMongoDbId() 46 | -------------------------------------------------------------------------------- /src/test/fixtures/types/authentication.fixture.types.ts: -------------------------------------------------------------------------------- 1 | import { Algorithm, Secret } from 'jsonwebtoken' 2 | 3 | export interface JwtFixtureParams { 4 | userId: string 5 | username: string 6 | secret: Secret 7 | algorithm: Algorithm 8 | expiresIn: number 9 | } 10 | 11 | export type PrePopulatedJwt = Omit & { tokenName: string, expiresAt: number } 12 | 13 | export interface AuthenticationFixture { 14 | [key: string]: { 15 | value: string 16 | comments: { 17 | plainPasswd?: string 18 | hashedWith?: string 19 | saltValue?: number 20 | username?: string 21 | algorithm?: string 22 | expiresAt?: string 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/fixtures/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication.fixture.types' 2 | export * from './user.fixture.types' 3 | export * from './post.fixture.types' 4 | -------------------------------------------------------------------------------- /src/test/fixtures/types/post.fixture.types.ts: -------------------------------------------------------------------------------- 1 | import { PostOwnerDomainModel } from '@domainModels' 2 | import { PostOwnerDto } from '@infrastructure/dtos' 3 | import { UserDomainModelFixture, UserDtoFixture } from './user.fixture.types' 4 | interface BasicContentFixture { 5 | body: string 6 | createdAt: string 7 | updatedAt: string 8 | } 9 | 10 | export type OwnerDtoFixture = PostOwnerDto 11 | export type CommentDtoFixture = BasicContentFixture & { _id: string, owner: OwnerDtoFixture } 12 | export type LikeDtoFixture = OwnerDtoFixture 13 | export type PostDtoFixture = BasicContentFixture & { _id: string, owner: OwnerDtoFixture, comments: CommentDtoFixture[], likes: LikeDtoFixture[] } 14 | 15 | type OwnerDomainModelFixture = PostOwnerDomainModel 16 | export type CommentDomainModelFixture = BasicContentFixture & { id: string, owner: OwnerDomainModelFixture } 17 | export type LikeDomainModelFixture = OwnerDomainModelFixture 18 | export type PostDomainModelFixture = BasicContentFixture & { id: string, owner: OwnerDomainModelFixture, comments: CommentDomainModelFixture[], likes: LikeDomainModelFixture[] } 19 | 20 | export interface MockedPosts { 21 | basicDtoPostOwners: UserDtoFixture[] 22 | basicDtoPostCommentOwners: UserDtoFixture[] 23 | basicDtoPostLikeOwners: UserDtoFixture[] 24 | basicDtoFreeUsers: UserDtoFixture[] 25 | 26 | basicDomainModelPostOwners: UserDomainModelFixture[] 27 | basicDomainModelPostCommentOwners: UserDomainModelFixture[] 28 | basicDomainModelPostLikeOwners: UserDomainModelFixture[] 29 | basicDomainModelFreeUsers: UserDomainModelFixture[] 30 | 31 | basicDtoPersistedPosts: PostDtoFixture[] 32 | commentedDtoPersistedPosts: PostDtoFixture[] 33 | likedAndCommentedDtoPersistedPosts: PostDtoFixture[] 34 | 35 | basicDomainModelPosts: PostDomainModelFixture[] 36 | commentedDomainModelPosts: PostDomainModelFixture[] 37 | likedAndCommentedDomainModelPosts: PostDomainModelFixture[] 38 | } 39 | -------------------------------------------------------------------------------- /src/test/fixtures/types/user.fixture.types.ts: -------------------------------------------------------------------------------- 1 | import { UserDomainModel } from '@domainModels' 2 | 3 | export type UserDomainModelFixture = Pick 4 | export type UserDtoFixture = Pick & { _id: string, userId: string } 5 | -------------------------------------------------------------------------------- /src/test/fixtures/user.fixtures.ts: -------------------------------------------------------------------------------- 1 | import mockedUsers from './assets/users.json' 2 | import { avatarUrls } from './assets/avatarUrls.json' 3 | import { UserDomainModel } from '@domainModels' 4 | import { generateMockedMongoDbId } from './utils.fixtures' 5 | 6 | export const testingNonValidUserId = generateMockedMongoDbId() 7 | 8 | export const testingNonPersistedUsername = 'non.persisted@mail.com' 9 | 10 | export const testingUsers: UserDomainModel[] = mockedUsers 11 | export const testingAvatarUrls: string[] = avatarUrls 12 | -------------------------------------------------------------------------------- /src/test/fixtures/utils.fixtures.ts: -------------------------------------------------------------------------------- 1 | export * from './mockers/utils' 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.paths.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noImplicitAny": true, 11 | "outDir": "dist", 12 | "resolveJsonModule": true, 13 | "sourceMap": true 14 | }, 15 | "include": [ 16 | "src/**/*" 17 | ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } -------------------------------------------------------------------------------- /tsconfig.paths.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@common": [ 6 | "src/common" 7 | ], 8 | "@domainModels": [ 9 | "src/domain/models" 10 | ], 11 | "@domainServices": [ 12 | "src/domain/services" 13 | ], 14 | "@errors": [ 15 | "src/common/errors" 16 | ], 17 | "@infrastructure/*": [ 18 | "src/infrastructure/*" 19 | ], 20 | "@logger": [ 21 | "src/common/logger" 22 | ], 23 | "@manifest": [ 24 | "manifest.json" 25 | ], 26 | "@testingFixtures": [ 27 | "src/test/fixtures" 28 | ] 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /webpack/tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "exclude": [ 4 | "../src/**/*.test.ts", 5 | "../src/test/**/*.ts", 6 | "../node_modules" 7 | ] 8 | } -------------------------------------------------------------------------------- /webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin') 3 | 4 | module.exports = { 5 | output: { 6 | filename: 'server.js', 7 | path: path.resolve(__dirname, '../dist') 8 | }, 9 | resolve: { 10 | extensions: ['.ts', '.js'], 11 | plugins: [new TsconfigPathsPlugin({ configFile: path.resolve(__dirname, '../tsconfig.json') })] 12 | }, 13 | target: 'node' 14 | } 15 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const NodemonPlugin = require('nodemon-webpack-plugin') 3 | const { merge } = require('webpack-merge') 4 | const nodeExternals = require('webpack-node-externals') 5 | const path = require('path') 6 | const webpack = require('webpack') 7 | const dotenv = require('dotenv').config({ path: path.join(__dirname, '../env/.env.dev') }) 8 | const common = require('./webpack.common.js') 9 | 10 | module.exports = merge(common, { 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.ts$/, 15 | use: [{ loader: 'ts-loader' }] 16 | } 17 | ] 18 | }, 19 | devtool: 'inline-source-map', 20 | entry: [path.join(__dirname, '../src/app.ts')], 21 | externals: [nodeExternals()], 22 | mode: 'development', 23 | plugins: [ 24 | new webpack.DefinePlugin({ 25 | 'process.env': JSON.stringify(dotenv.parsed) 26 | }), 27 | new NodemonPlugin({ 28 | watch: path.join(__dirname, '../dist'), 29 | verbose: true, 30 | ext: 'ts,js' 31 | }) 32 | ], 33 | watch: true 34 | }) 35 | -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 3 | const { merge } = require('webpack-merge') 4 | const nodeExternals = require('webpack-node-externals') 5 | const path = require('path') 6 | const webpack = require('webpack') 7 | // NOTE Pay attention to have defined the '.env' file bedore running this consigutation. 8 | const dotenv = require('dotenv').config({ path: path.join(__dirname, '../env/.env') }) 9 | const common = require('./webpack.common.js') 10 | 11 | module.exports = merge(common, { 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.ts$/, 16 | use: [ 17 | { 18 | loader: 'ts-loader', 19 | options: { 20 | configFile: path.resolve(__dirname, './tsconfig.webpack.json') 21 | } 22 | } 23 | ] 24 | } 25 | ] 26 | }, 27 | mode: 'production', 28 | entry: [path.join(__dirname, '../src/app.ts')], 29 | node: { 30 | __dirname: false, 31 | __filename: false 32 | }, 33 | optimization: { 34 | usedExports: true 35 | }, 36 | externals: [nodeExternals({})], 37 | plugins: [ 38 | new webpack.DefinePlugin({ 39 | 'process.env': JSON.stringify(dotenv.parsed) 40 | }), 41 | new CleanWebpackPlugin() 42 | ] 43 | }) 44 | --------------------------------------------------------------------------------