[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"env\": {\n    \"browser\": true,\n    \"es2021\": true,\n    \"jest\": true\n  },\n  \"extends\": [\"airbnb-base\", \"prettier\"],\n  \"globals\": {\n    \"NodeJS\": true\n  },\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"ecmaVersion\": 12,\n    \"sourceType\": \"module\"\n  },\n  \"plugins\": [\"@typescript-eslint\"],\n  \"rules\": {\n    \"import/extensions\": [\n      \"error\",\n      \"ignorePackages\",\n      {\n        \"ts\": \"never\"\n      }\n    ],\n    \"no-underscore-dangle\": [\"error\", { \"allow\": [\"_count\"] }],\n    \"no-console\": [\"error\", { \"allow\": [\"info\"] }],\n    \"import/no-extraneous-dependencies\": [\"error\", { \"devDependencies\": [\"tests/prisma-mock.ts\"] }]\n  },\n  \"settings\": {\n    \"import/resolver\": {\n      \"node\": {\n        \"paths\": [\"src\"],\n        \"extensions\": [\".js\", \".ts\", \".d.ts\", \".tsx\"]\n      }\n    }\n  },\n  \"ignorePatterns\": [\"*.d.ts\"]\n}\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: '/'\n    schedule:\n      interval: weekly\n    open-pull-requests-limit: 10\n\n  - package-ecosystem: npm\n    directory: '/'\n    schedule:\n      interval: weekly\n    open-pull-requests-limit: 10\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches:\n      - '**'\n\njobs:\n  run_tests:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [12.x]\n\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions/setup-node@v1\n        with:\n          node-version: '12'\n      - name: Cache node modules\n        uses: actions/cache@v2\n        env:\n          cache-name: cache-node-modules\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-build-${{ env.cache-name }}-\n            ${{ runner.os }}-build-\n            ${{ runner.os }}-\n      - name: Install dependencies\n        run: npm ci --no-audit --prefer-offline --progress=false\n      - name: Check prettier\n        run: npm run prettier:check\n      - name: Check ESLinter\n        run: npm run lint:check\n      - name: Check unit tests\n        run: npm run test --ci --lastCommit --maxWorkers=50%\n        env:\n          CI: true\n"
  },
  {
    "path": ".gitignore",
    "content": "/node_modules\n/dist\n\n# Keep environment variables out of version control\n.env\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n.history/*\n"
  },
  {
    "path": ".husky/.gitignore",
    "content": "_\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n"
  },
  {
    "path": ".prettierignore",
    "content": "/dist\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"printWidth\": 100,\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"bracketSpacing\": true,\n  \"arrowParens\": \"avoid\"\n}\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nhello@thinkster.io.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to RealWorld\n\nWe would love for you to contribute to RealWorld and help make it even better than it is\ntoday! As a contributor, here are the guidelines we would like you to follow:\n\n- [Code of Conduct](#coc)\n- [Question or Problem?](#question)\n- [Issues and Bugs](#issue)\n- [Feature Requests](#feature)\n- [Submission Guidelines](#submit)\n- [Coding Rules](#rules)\n- [Commit Message Guidelines](#commit)\n\n## <a name=\"coc\"></a> Code of Conduct\n\nHelp us keep RealWorld open and inclusive. Please read and follow our [Code of Conduct][coc].\n\n## <a name=\"question\"></a> Got a Question or Problem?\n\nDo not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests.  \nFor open discussions, we encourage you to use the [Github Discussions][github-discussions] channels of the RealWorld repository.\n\n## <a name=\"issue\"></a> Found a Bug?\n\nIf you find a bug in the project, you can help us by\n[submitting an issue][github-issue] to our [GitHub Repository][github]. Even better, you can\n[submit a Pull Request](#submit-pr) with a fix.\n\n## <a name=\"feature\"></a> Missing a Feature?\n\nThis repository follows the RealWorld [specs][github-spec].  \nPlease open feature requests on the RealWorld [repository][github-feature].\n\n## <a name=\"submit\"></a> Submission Guidelines\n\n### <a name=\"submit-issue\"></a> Submitting an Issue\n\nBefore you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.\n\nYou can file new issues by selecting from our [new issue templates][github-choose] and filling out the issue template.\n\n### <a name=\"submit-pr\"></a> Submitting a Pull Request (PR)\n\nBefore you submit your Pull Request (PR) consider the following guidelines:\n\n1. Search [GitHub](https://github.com/gothinkster/node-express-prisma-v1-official-app/pulls) for an open or closed PR\n   that relates to your submission. You don't want to duplicate effort.\n1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add.\n   Discussing the design up front helps to ensure that we're ready to accept your work.\n1. Fork the gothinkster/realworld repo.\n1. Make your changes in a new git branch:\n\n   ```bash\n   git checkout -b my-fix-branch main\n   ```\n\n1. Create your patch.\n\n1. Commit your changes using a descriptive commit message that follows our\n   [commit message conventions](#commit).\n\n1. Push your branch to GitHub:\n\n   ```bash\n   git push origin my-fix-branch\n   ```\n\n1. In GitHub, send a pull request to `node-express-prisma-v1-official-app:main`.\n\n- If we suggest changes then:\n\n  - Make the required updates.\n  - Rebase your branch and force push to your GitHub repository (this will update your Pull Request):\n\n    ```bash\n    git rebase main -i\n    git push -f\n    ```\n\nThat's it! Thank you for your contribution!\n\n#### After your pull request is merged\n\nAfter your pull request is merged, you can safely delete your branch and pull the changes\nfrom the main (upstream) repository:\n\n- Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:\n\n  ```bash\n  git push origin --delete my-fix-branch\n  ```\n\n- Check out the main branch:\n\n  ```bash\n  git checkout main -f\n  ```\n\n- Delete the local branch:\n\n  ```bash\n  git branch -D my-fix-branch\n  ```\n\n- Update your main with the latest upstream version:\n\n  ```bash\n  git pull --ff upstream main\n  ```\n\n## <a name=\"commit\"></a> Commit Message Guidelines\n\n> These guidelines have been added to the project starting from <include date>\n\nWe have very precise rules over how our git commit messages can be formatted. This leads to **more\nreadable messages** that are easy to follow when looking through the **project history**.\n\n### Commit Message Format\n\nEach commit message consists of a **header**, a **body** and a **footer**. The header has a special\nformat that includes a **type**, a **scope** and a **subject**:\n\n```\n<type>(<scope>): <subject>\n<BLANK LINE>\n<body>\n<BLANK LINE>\n<footer>\n```\n\nThe **header** is mandatory and the **scope** of the header is optional.\n\nAny line of the commit message cannot be longer 100 characters! This allows the message to be easier\nto read on GitHub as well as in various git tools.\n\nThe footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.\n\nSamples:\n\n```\ndocs(changelog): update changelog to beta.5\n```\n\n### Type\n\nMust be one of the following:\n\n- **docs**: Documentation only changes\n- **feat**: A new feature\n- **fix**: A bug fix\n\n### Scope\n\nThe scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages).\n\nThe following is the list of supported scopes:\n\n- **specs**\n- **project**\n\n### Subject\n\nThe subject contains a succinct description of the change:\n\n- use the imperative, present tense: \"change\" not \"changed\" nor \"changes\"\n- don't capitalize the first letter\n- no dot (.) at the end\n\n### Body\n\nJust as in the **subject**, use the imperative, present tense: \"change\" not \"changed\" nor \"changes\".\nThe body should include the motivation for the change and contrast this with previous behavior.\n\n### Footer\n\nThe footer should contain any information about **Breaking Changes** and is also the place to\nreference GitHub issues that this commit **Closes**.\n\n**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.\n\nSamples :\n\n```\nClose #394\n```\n\n```\nBREAKING CHANGE:\nchange login route to /users/login\n```\n\n[coc]: https://github.com/gothinkster/node-express-prisma-v1-official-app/blob/main/CODE_OF_CONDUCT.md\n[github]: https://github.com/gothinkster/node-express-prisma-v1-official-app\n[github-issue]: https://github.com/gothinkster/node-express-prisma-v1-official-app/issues/new?assignees=&labels=bug&template=---bug-report.md&title=\n[github-feature]: https://github.com/gothinkster/realworld/issues/new?assignees=&labels=enhancement&template=---feature-request.md&title=\n[github-choose]: https://github.com/gothinkster/node-express-prisma-v1-official-app/issues/new/choose\n[github-discussions]: https://github.com/gothinkster/realworld/discussions\n[github-spec]: https://github.com/gothinkster/realworld/tree/master/spec\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Procfile",
    "content": "web: npm start\n\nrelease: npx prisma migrate deploy\n"
  },
  {
    "path": "README.md",
    "content": "# ![Rails Example App](media/realworld.png)\n\n> Official NodeJS codebase that adheres to the [RealWorld](https://gothinkster.github.io/realworld/docs/specs/backend-specs/introduction) API spec.\n\nThis repo is functionality complete.\n\n# Deploy to Heroku\n\n[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)\n\n# Getting started\n\n### Clone the repository\n\nrun `git clone https://github.com/gothinkster/node-express-prisma-v1-official-app.git`\n\n### Install the dependancies\n\n> [NodeJS](https://nodejs.dev/) is required\n\n```\ncd node-express-prisma-v1-official-app\nnpm install\n```\n\n### Download pgAdmin for PostgreSQL\n\n[PostgreSQL](https://www.postgresql.org/download/) downloads page\n\n### Create a server\n\nrun **pgAdmin**  \ncreate a server (Object/Create/Server)  \nrequired fields:\n\n- name\n- HOST name/address\n\n### Connect the created server\n\ncreate a _.env_ file at the root of the project  \npopulate it with the url of your database\n\n```\nDATABASE_URL=\"postgresql://<username>:<password>@<host_name>:<port>/<database_name>?schema=public\"\n```\n\n### Run the project locally\n\nrun `npm run dev`\n\n## Advanced usage\n\n### Prisma\n\n### Format the Prisma schema\n\n```bash\nnpm run prisma:format\n```\n\n### Migrate the SQL schema\n\n```bash\nprisma migrate dev --name added_job_title\n```\n\n### Update the Prisma Client\n\n```bash\nnpm run prisma:generate\n```\n\n_with watch option_\n\n```bash\nnpm run prisma:generate:watch\n```\n\n### Seed the database\n\n```bash\nnpm run prisma:seed\n```\n\n### Launch Prisma Studio\n\n```bash\nnpm run prisma:studio\n```\n\n### Reset the database\n\n- Drop the database\n- Create a new database\n- Apply migrations\n- Seed the database with data\n\n```bash\nnpm run prisma:reset\n```\n"
  },
  {
    "path": "app.json",
    "content": "{\n  \"name\": \"RealWorld API\",\n  \"description\": \"Node / Express / Prisma API for RealWorld project\",\n  \"keywords\": [\"node\", \"express\", \"prisma\", \"realworld\"],\n  \"website\": \"https://gothinkster.github.io/realworld/\",\n  \"repository\": \"https://github.com/gothinkster/realworld\",\n  \"addons\": [\"heroku-postgresql\"],\n  \"env\": {\n    \"JWT_SECRET\": {\n      \"description\": \"A secret key for verifying authenticated users\",\n      \"generator\": \"secret\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/swagger.json",
    "content": "{\n  \"swagger\": \"2.0\",\n  \"info\": {\n    \"description\": \"Conduit API\",\n    \"version\": \"1.0.0\",\n    \"title\": \"Conduit API\",\n    \"contact\": {\n      \"name\": \"RealWorld\",\n      \"url\": \"https://realworld.io\"\n    },\n    \"license\": {\n      \"name\": \"MIT License\",\n      \"url\": \"https://opensource.org/licenses/MIT\"\n    }\n  },\n  \"basePath\": \"/api\",\n  \"schemes\": [\"https\", \"http\"],\n  \"produces\": [\"application/json\"],\n  \"consumes\": [\"application/json\"],\n  \"securityDefinitions\": {\n    \"Token\": {\n      \"description\": \"For accessing the protected API resources, you must have received a a valid JWT token after registering or logging in. This JWT token must then be used for all protected resources by passing it in via the 'Authorization' header.\\n\\nA JWT token is generated by the API by either registering via /users or logging in via /users/login.\\n\\nThe following format must be in the 'Authorization' header :\\n\\n    Token: xxxxxx.yyyyyyy.zzzzzz\\n    \\n\",\n      \"type\": \"apiKey\",\n      \"name\": \"Authorization\",\n      \"in\": \"header\"\n    }\n  },\n  \"paths\": {\n    \"/users/login\": {\n      \"post\": {\n        \"summary\": \"Existing user login\",\n        \"description\": \"Login for existing user\",\n        \"tags\": [\"User and Authentication\"],\n        \"operationId\": \"Login\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"description\": \"Credentials to use\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/LoginUserRequest\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/UserResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/users\": {\n      \"post\": {\n        \"summary\": \"Register a new user\",\n        \"description\": \"Register a new user\",\n        \"tags\": [\"User and Authentication\"],\n        \"operationId\": \"CreateUser\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"description\": \"Details of the new user to register\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/NewUserRequest\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"201\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/UserResponse\"\n            }\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/user\": {\n      \"get\": {\n        \"summary\": \"Get current user\",\n        \"description\": \"Gets the currently logged-in user\",\n        \"tags\": [\"User and Authentication\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"GetCurrentUser\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/UserResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      },\n      \"put\": {\n        \"summary\": \"Update current user\",\n        \"description\": \"Updated user information for current user\",\n        \"tags\": [\"User and Authentication\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"UpdateCurrentUser\",\n        \"parameters\": [\n          {\n            \"name\": \"body\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"description\": \"User details to update. At least **one** field is required.\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/UpdateUserRequest\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/UserResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/profiles/{username}\": {\n      \"get\": {\n        \"summary\": \"Get a profile\",\n        \"description\": \"Get a profile of a user of the system. Auth is optional\",\n        \"tags\": [\"Profile\"],\n        \"operationId\": \"GetProfileByUsername\",\n        \"parameters\": [\n          {\n            \"name\": \"username\",\n            \"in\": \"path\",\n            \"description\": \"Username of the profile to get\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/ProfileResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/profiles/{username}/follow\": {\n      \"post\": {\n        \"summary\": \"Follow a user\",\n        \"description\": \"Follow a user by username\",\n        \"tags\": [\"Profile\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"FollowUserByUsername\",\n        \"parameters\": [\n          {\n            \"name\": \"username\",\n            \"in\": \"path\",\n            \"description\": \"Username of the profile you want to follow\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/ProfileResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"summary\": \"Unfollow a user\",\n        \"description\": \"Unfollow a user by username\",\n        \"tags\": [\"Profile\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"UnfollowUserByUsername\",\n        \"parameters\": [\n          {\n            \"name\": \"username\",\n            \"in\": \"path\",\n            \"description\": \"Username of the profile you want to unfollow\",\n            \"required\": true,\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/ProfileResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/articles/feed\": {\n      \"get\": {\n        \"summary\": \"Get recent articles from users you follow\",\n        \"description\": \"Get most recent articles from users you follow. Use query parameters to limit. Auth is required\",\n        \"tags\": [\"Articles\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"GetArticlesFeed\",\n        \"parameters\": [\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"Limit number of articles returned (default is 20)\",\n            \"required\": false,\n            \"default\": 20,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"offset\",\n            \"in\": \"query\",\n            \"description\": \"Offset/skip number of articles (default is 0)\",\n            \"required\": false,\n            \"default\": 0,\n            \"type\": \"integer\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/MultipleArticlesResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/articles\": {\n      \"get\": {\n        \"summary\": \"Get recent articles globally\",\n        \"description\": \"Get most recent articles globally. Use query parameters to filter results. Auth is optional\",\n        \"tags\": [\"Articles\"],\n        \"operationId\": \"GetArticles\",\n        \"parameters\": [\n          {\n            \"name\": \"tag\",\n            \"in\": \"query\",\n            \"description\": \"Filter by tag\",\n            \"required\": false,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"author\",\n            \"in\": \"query\",\n            \"description\": \"Filter by author (username)\",\n            \"required\": false,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"favorited\",\n            \"in\": \"query\",\n            \"description\": \"Filter by favorites of a user (username)\",\n            \"required\": false,\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"Limit number of articles returned (default is 20)\",\n            \"required\": false,\n            \"default\": 20,\n            \"type\": \"integer\"\n          },\n          {\n            \"name\": \"offset\",\n            \"in\": \"query\",\n            \"description\": \"Offset/skip number of articles (default is 0)\",\n            \"required\": false,\n            \"default\": 0,\n            \"type\": \"integer\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/MultipleArticlesResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"summary\": \"Create an article\",\n        \"description\": \"Create an article. Auth is required\",\n        \"tags\": [\"Articles\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"CreateArticle\",\n        \"parameters\": [\n          {\n            \"name\": \"article\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"description\": \"Article to create\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/NewArticleRequest\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"201\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/SingleArticleResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/articles/{slug}\": {\n      \"get\": {\n        \"summary\": \"Get an article\",\n        \"description\": \"Get an article. Auth not required\",\n        \"tags\": [\"Articles\"],\n        \"operationId\": \"GetArticle\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"Slug of the article to get\",\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/SingleArticleResponse\"\n            }\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      },\n      \"put\": {\n        \"summary\": \"Update an article\",\n        \"description\": \"Update an article. Auth is required\",\n        \"tags\": [\"Articles\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"UpdateArticle\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"Slug of the article to update\",\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"article\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"description\": \"Article to update\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/UpdateArticleRequest\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/SingleArticleResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"summary\": \"Delete an article\",\n        \"description\": \"Delete an article. Auth is required\",\n        \"tags\": [\"Articles\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"DeleteArticle\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"Slug of the article to delete\",\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/articles/{slug}/comments\": {\n      \"get\": {\n        \"summary\": \"Get comments for an article\",\n        \"description\": \"Get the comments for an article. Auth is optional\",\n        \"tags\": [\"Comments\"],\n        \"operationId\": \"GetArticleComments\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"Slug of the article that you want to get comments for\",\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/MultipleCommentsResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"summary\": \"Create a comment for an article\",\n        \"description\": \"Create a comment for an article. Auth is required\",\n        \"tags\": [\"Comments\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"CreateArticleComment\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"Slug of the article that you want to create a comment for\",\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"comment\",\n            \"in\": \"body\",\n            \"required\": true,\n            \"description\": \"Comment you want to create\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/NewCommentRequest\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/SingleCommentResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/articles/{slug}/comments/{id}\": {\n      \"delete\": {\n        \"summary\": \"Delete a comment for an article\",\n        \"description\": \"Delete a comment for an article. Auth is required\",\n        \"tags\": [\"Comments\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"DeleteArticleComment\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"Slug of the article that you want to delete a comment for\",\n            \"type\": \"string\"\n          },\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"ID of the comment you want to delete\",\n            \"type\": \"integer\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/articles/{slug}/favorite\": {\n      \"post\": {\n        \"summary\": \"Favorite an article\",\n        \"description\": \"Favorite an article. Auth is required\",\n        \"tags\": [\"Favorites\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"CreateArticleFavorite\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"Slug of the article that you want to favorite\",\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/SingleArticleResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"summary\": \"Unfavorite an article\",\n        \"description\": \"Unfavorite an article. Auth is required\",\n        \"tags\": [\"Favorites\"],\n        \"security\": [\n          {\n            \"Token\": []\n          }\n        ],\n        \"operationId\": \"DeleteArticleFavorite\",\n        \"parameters\": [\n          {\n            \"name\": \"slug\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"Slug of the article that you want to unfavorite\",\n            \"type\": \"string\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/SingleArticleResponse\"\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized\"\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    },\n    \"/tags\": {\n      \"get\": {\n        \"summary\": \"Get tags\",\n        \"description\": \"Get tags. Auth not required\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"OK\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/TagsResponse\"\n            }\n          },\n          \"422\": {\n            \"description\": \"Unexpected error\",\n            \"schema\": {\n              \"$ref\": \"#/definitions/GenericErrorModel\"\n            }\n          }\n        }\n      }\n    }\n  },\n  \"definitions\": {\n    \"LoginUser\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"email\": {\n          \"type\": \"string\"\n        },\n        \"password\": {\n          \"type\": \"string\",\n          \"format\": \"password\"\n        }\n      },\n      \"required\": [\"email\", \"password\"]\n    },\n    \"LoginUserRequest\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"user\": {\n          \"$ref\": \"#/definitions/LoginUser\"\n        }\n      },\n      \"required\": [\"user\"]\n    },\n    \"NewUser\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"username\": {\n          \"type\": \"string\"\n        },\n        \"email\": {\n          \"type\": \"string\"\n        },\n        \"password\": {\n          \"type\": \"string\",\n          \"format\": \"password\"\n        }\n      },\n      \"required\": [\"username\", \"email\", \"password\"]\n    },\n    \"NewUserRequest\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"user\": {\n          \"$ref\": \"#/definitions/NewUser\"\n        }\n      },\n      \"required\": [\"user\"]\n    },\n    \"User\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"email\": {\n          \"type\": \"string\"\n        },\n        \"token\": {\n          \"type\": \"string\"\n        },\n        \"username\": {\n          \"type\": \"string\"\n        },\n        \"bio\": {\n          \"type\": \"string\"\n        },\n        \"image\": {\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\"email\", \"token\", \"username\", \"bio\", \"image\"]\n    },\n    \"UserResponse\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"user\": {\n          \"$ref\": \"#/definitions/User\"\n        }\n      },\n      \"required\": [\"user\"]\n    },\n    \"UpdateUser\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"email\": {\n          \"type\": \"string\"\n        },\n        \"token\": {\n          \"type\": \"string\"\n        },\n        \"username\": {\n          \"type\": \"string\"\n        },\n        \"bio\": {\n          \"type\": \"string\"\n        },\n        \"image\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"UpdateUserRequest\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"user\": {\n          \"$ref\": \"#/definitions/UpdateUser\"\n        }\n      },\n      \"required\": [\"user\"]\n    },\n    \"ProfileResponse\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"profile\": {\n          \"$ref\": \"#/definitions/Profile\"\n        }\n      },\n      \"required\": [\"profile\"]\n    },\n    \"Profile\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"username\": {\n          \"type\": \"string\"\n        },\n        \"bio\": {\n          \"type\": \"string\"\n        },\n        \"image\": {\n          \"type\": \"string\"\n        },\n        \"following\": {\n          \"type\": \"boolean\"\n        }\n      },\n      \"required\": [\"username\", \"bio\", \"image\", \"following\"]\n    },\n    \"Article\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"slug\": {\n          \"type\": \"string\"\n        },\n        \"title\": {\n          \"type\": \"string\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"body\": {\n          \"type\": \"string\"\n        },\n        \"tagList\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"updatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"favorited\": {\n          \"type\": \"boolean\"\n        },\n        \"favoritesCount\": {\n          \"type\": \"integer\"\n        },\n        \"author\": {\n          \"$ref\": \"#/definitions/Profile\"\n        }\n      },\n      \"required\": [\n        \"slug\",\n        \"title\",\n        \"description\",\n        \"body\",\n        \"tagList\",\n        \"createdAt\",\n        \"updatedAt\",\n        \"favorited\",\n        \"favoritesCount\",\n        \"author\"\n      ]\n    },\n    \"SingleArticleResponse\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"article\": {\n          \"$ref\": \"#/definitions/Article\"\n        }\n      },\n      \"required\": [\"article\"]\n    },\n    \"MultipleArticlesResponse\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"articles\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Article\"\n          }\n        },\n        \"articlesCount\": {\n          \"type\": \"integer\"\n        }\n      },\n      \"required\": [\"articles\", \"articlesCount\"]\n    },\n    \"NewArticle\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"title\": {\n          \"type\": \"string\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"body\": {\n          \"type\": \"string\"\n        },\n        \"tagList\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"required\": [\"title\", \"description\", \"body\"]\n    },\n    \"NewArticleRequest\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"article\": {\n          \"$ref\": \"#/definitions/NewArticle\"\n        }\n      },\n      \"required\": [\"article\"]\n    },\n    \"UpdateArticle\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"title\": {\n          \"type\": \"string\"\n        },\n        \"description\": {\n          \"type\": \"string\"\n        },\n        \"body\": {\n          \"type\": \"string\"\n        }\n      }\n    },\n    \"UpdateArticleRequest\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"article\": {\n          \"$ref\": \"#/definitions/UpdateArticle\"\n        }\n      },\n      \"required\": [\"article\"]\n    },\n    \"Comment\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"id\": {\n          \"type\": \"integer\"\n        },\n        \"createdAt\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"updatedAt\": {\n          \"type\": \"string\",\n          \"format\": \"date-time\"\n        },\n        \"body\": {\n          \"type\": \"string\"\n        },\n        \"author\": {\n          \"$ref\": \"#/definitions/Profile\"\n        }\n      },\n      \"required\": [\"id\", \"createdAt\", \"updatedAt\", \"body\", \"author\"]\n    },\n    \"SingleCommentResponse\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"comment\": {\n          \"$ref\": \"#/definitions/Comment\"\n        }\n      },\n      \"required\": [\"comment\"]\n    },\n    \"MultipleCommentsResponse\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"comments\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Comment\"\n          }\n        }\n      },\n      \"required\": [\"comments\"]\n    },\n    \"NewComment\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"body\": {\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\"body\"]\n    },\n    \"NewCommentRequest\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"comment\": {\n          \"$ref\": \"#/definitions/NewComment\"\n        }\n      },\n      \"required\": [\"comment\"]\n    },\n    \"TagsResponse\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"tags\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      },\n      \"required\": [\"tags\"]\n    },\n    \"GenericErrorModel\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"errors\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"body\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            }\n          },\n          \"required\": [\"body\"]\n        }\n      },\n      \"required\": [\"errors\"]\n    }\n  }\n}\n"
  },
  {
    "path": "jest.config.js",
    "content": "module.exports = {\n  clearMocks: true,\n  preset: 'ts-jest',\n  testEnvironment: 'node',\n  setupFilesAfterEnv: ['<rootDir>/tests/prisma-mock.ts'],\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"express-prisma-realworld-official-app\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Node.js, Express.js & Prisma RealWorld official app\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"postinstall\": \"tsc\",\n    \"test\": \"jest -i\",\n    \"dev\": \"ts-node-dev --respawn --pretty --transpile-only src/index.ts dev\",\n    \"start\": \"node dist/src/index.js\",\n    \"prisma:migrate\": \"prisma migrate dev --skip-seed\",\n    \"prisma:format\": \"prisma format\",\n    \"prisma:generate\": \"prisma generate\",\n    \"prisma:generate:watch\": \"prisma generate --watch\",\n    \"prisma:seed\": \"prisma db seed --preview-feature\",\n    \"prisma:studio\": \"prisma studio\",\n    \"prisma:reset\": \"prisma migrate reset\",\n    \"prettier:write\": \"npx prettier --write .\",\n    \"prettier:check\": \"npx prettier --check .\",\n    \"lint:check\": \"npx eslint src/**/*.ts\",\n    \"lint:fix\": \"npx eslint --fix src/**/*.ts\",\n    \"prepare\": \"husky install\"\n  },\n  \"keywords\": [\n    \"node\",\n    \"express\",\n    \"prisma\",\n    \"realworld\"\n  ],\n  \"author\": {\n    \"name\": \"Gerome Grignon\",\n    \"email\": \"gerome.grignon.lp2@gmail.com\"\n  },\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@prisma/client\": \"^2.29.1\",\n    \"@types/swagger-ui-express\": \"^4.1.3\",\n    \"bcryptjs\": \"^2.4.3\",\n    \"body-parser\": \"^1.19.0\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.17.1\",\n    \"express-jwt\": \"^6.1.0\",\n    \"jsonwebtoken\": \"^8.5.1\",\n    \"slugify\": \"^1.6.0\",\n    \"swagger-ui-express\": \"^4.1.6\"\n  },\n  \"devDependencies\": {\n    \"@types/bcryptjs\": \"^2.4.2\",\n    \"@types/cors\": \"^2.8.12\",\n    \"@types/cron\": \"^1.7.3\",\n    \"@types/express\": \"^4.17.13\",\n    \"@types/express-rate-limit\": \"^5.1.3\",\n    \"@types/jest\": \"^27.0.1\",\n    \"@types/jsonwebtoken\": \"^8.5.5\",\n    \"@types/node\": \"^15.14.9\",\n    \"@typescript-eslint/eslint-plugin\": \"^4.31.0\",\n    \"@typescript-eslint/parser\": \"^4.31.0\",\n    \"eslint\": \"^7.32.0\",\n    \"eslint-config-airbnb-base\": \"^14.2.1\",\n    \"eslint-config-prettier\": \"^8.3.0\",\n    \"eslint-plugin-import\": \"^2.24.2\",\n    \"husky\": \"^7.0.2\",\n    \"jest\": \"^27.1.0\",\n    \"jest-mock-extended\": \"^2.0.4\",\n    \"lint-staged\": \"^11.1.2\",\n    \"prettier\": \"2.4.0\",\n    \"prisma\": \"^2.29.1\",\n    \"ts-jest\": \"^27.0.5\",\n    \"ts-node-dev\": \"^1.1.8\",\n    \"typescript\": \"^4.4.2\"\n  },\n  \"lint-staged\": {\n    \"*.ts\": [\n      \"npm run prisma:format\",\n      \"npm run lint:fix\",\n      \"npm run prettier:write\",\n      \"git add\"\n    ]\n  }\n}\n"
  },
  {
    "path": "prisma/migrations/20210924222830_initial/migration.sql",
    "content": "-- CreateTable\nCREATE TABLE \"Article\" (\n    \"id\" SERIAL NOT NULL,\n    \"slug\" TEXT NOT NULL,\n    \"title\" TEXT NOT NULL,\n    \"description\" TEXT NOT NULL,\n    \"body\" TEXT NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"authorId\" INTEGER NOT NULL,\n\n    PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"ArticleTags\" (\n    \"articleId\" INTEGER NOT NULL,\n    \"tagId\" INTEGER NOT NULL,\n\n    PRIMARY KEY (\"articleId\",\"tagId\")\n);\n\n-- CreateTable\nCREATE TABLE \"Comment\" (\n    \"id\" SERIAL NOT NULL,\n    \"createdAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"updatedAt\" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,\n    \"body\" TEXT NOT NULL,\n    \"articleId\" INTEGER NOT NULL,\n    \"authorId\" INTEGER NOT NULL,\n\n    PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"Tag\" (\n    \"id\" SERIAL NOT NULL,\n    \"name\" TEXT NOT NULL,\n\n    PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"User\" (\n    \"id\" SERIAL NOT NULL,\n    \"email\" TEXT NOT NULL,\n    \"username\" TEXT NOT NULL,\n    \"password\" TEXT NOT NULL,\n    \"image\" TEXT DEFAULT E'https://realworld-temp-api.herokuapp.com/images/smiley-cyrus.jpeg',\n    \"bio\" TEXT,\n\n    PRIMARY KEY (\"id\")\n);\n\n-- CreateTable\nCREATE TABLE \"_UserFavorites\" (\n    \"A\" INTEGER NOT NULL,\n    \"B\" INTEGER NOT NULL\n);\n\n-- CreateTable\nCREATE TABLE \"_UserFollows\" (\n    \"A\" INTEGER NOT NULL,\n    \"B\" INTEGER NOT NULL\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Article.slug_unique\" ON \"Article\"(\"slug\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"User.email_unique\" ON \"User\"(\"email\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"User.username_unique\" ON \"User\"(\"username\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"_UserFavorites_AB_unique\" ON \"_UserFavorites\"(\"A\", \"B\");\n\n-- CreateIndex\nCREATE INDEX \"_UserFavorites_B_index\" ON \"_UserFavorites\"(\"B\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"_UserFollows_AB_unique\" ON \"_UserFollows\"(\"A\", \"B\");\n\n-- CreateIndex\nCREATE INDEX \"_UserFollows_B_index\" ON \"_UserFollows\"(\"B\");\n\n-- AddForeignKey\nALTER TABLE \"Article\" ADD FOREIGN KEY (\"authorId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ArticleTags\" ADD FOREIGN KEY (\"articleId\") REFERENCES \"Article\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"ArticleTags\" ADD FOREIGN KEY (\"tagId\") REFERENCES \"Tag\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Comment\" ADD FOREIGN KEY (\"articleId\") REFERENCES \"Article\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"Comment\" ADD FOREIGN KEY (\"authorId\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"_UserFavorites\" ADD FOREIGN KEY (\"A\") REFERENCES \"Article\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"_UserFavorites\" ADD FOREIGN KEY (\"B\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"_UserFollows\" ADD FOREIGN KEY (\"A\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"_UserFollows\" ADD FOREIGN KEY (\"B\") REFERENCES \"User\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20211001195651_implicit_articles/migration.sql",
    "content": "/*\n  Warnings:\n\n  - You are about to drop the `ArticleTags` table. If the table is not empty, all the data it contains will be lost.\n  - A unique constraint covering the columns `[name]` on the table `Tag` will be added. If there are existing duplicate values, this will fail.\n\n*/\n-- DropForeignKey\nALTER TABLE \"ArticleTags\" DROP CONSTRAINT \"ArticleTags_articleId_fkey\";\n\n-- DropForeignKey\nALTER TABLE \"ArticleTags\" DROP CONSTRAINT \"ArticleTags_tagId_fkey\";\n\n-- DropTable\nDROP TABLE \"ArticleTags\";\n\n-- CreateTable\nCREATE TABLE \"_ArticleToTag\" (\n    \"A\" INTEGER NOT NULL,\n    \"B\" INTEGER NOT NULL\n);\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"_ArticleToTag_AB_unique\" ON \"_ArticleToTag\"(\"A\", \"B\");\n\n-- CreateIndex\nCREATE INDEX \"_ArticleToTag_B_index\" ON \"_ArticleToTag\"(\"B\");\n\n-- CreateIndex\nCREATE UNIQUE INDEX \"Tag.name_unique\" ON \"Tag\"(\"name\");\n\n-- AddForeignKey\nALTER TABLE \"_ArticleToTag\" ADD FOREIGN KEY (\"A\") REFERENCES \"Article\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n-- AddForeignKey\nALTER TABLE \"_ArticleToTag\" ADD FOREIGN KEY (\"B\") REFERENCES \"Tag\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE;\n"
  },
  {
    "path": "prisma/migrations/20211105082430_api_url/migration.sql",
    "content": "-- AlterTable\nALTER TABLE \"User\" ALTER COLUMN \"image\" SET DEFAULT E'https://api.realworld.io/images/smiley-cyrus.jpeg';\n"
  },
  {
    "path": "prisma/migrations/migration_lock.toml",
    "content": "# Please do not edit this file manually\n# It should be added in your version-control system (i.e. Git)\nprovider = \"postgresql\""
  },
  {
    "path": "prisma/prisma-client.ts",
    "content": "import { PrismaClient } from '@prisma/client';\n\n// add prisma to the NodeJS global type\n// TODO : downgraded @types/node to 15.14.1 to avoid error on NodeJS.Global\ninterface CustomNodeJsGlobal extends NodeJS.Global {\n  prisma: PrismaClient;\n}\n\n// Prevent multiple instances of Prisma Client in development\ndeclare const global: CustomNodeJsGlobal;\n\nconst prisma = global.prisma || new PrismaClient();\n\nif (process.env.NODE_ENV === 'development') {\n  global.prisma = prisma;\n}\n\nexport default prisma;\n"
  },
  {
    "path": "prisma/schema.prisma",
    "content": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ndatasource db {\n  provider = \"postgresql\"\n  url      = env(\"DATABASE_URL\")\n}\n\ngenerator client {\n  provider        = \"prisma-client-js\"\n  previewFeatures = [\"orderByAggregateGroup\", \"selectRelationCount\", \"referentialActions\"]\n}\n\nmodel Article {\n  id          Int       @id @default(autoincrement())\n  slug        String    @unique\n  title       String\n  description String\n  body        String\n  createdAt   DateTime  @default(now())\n  updatedAt   DateTime  @default(now())\n  tagList     Tag[]\n  author      User      @relation(\"UserArticles\", fields: [authorId], references: [id], onDelete: Cascade)\n  authorId    Int\n  favoritedBy User[]    @relation(\"UserFavorites\", references: [id])\n  comments    Comment[]\n}\n\nmodel Comment {\n  id        Int      @id @default(autoincrement())\n  createdAt DateTime @default(now())\n  updatedAt DateTime @default(now())\n  body      String\n  article   Article  @relation(fields: [articleId], references: [id], onDelete: Cascade)\n  articleId Int\n  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)\n  authorId  Int\n}\n\nmodel Tag {\n  id       Int       @id @default(autoincrement())\n  name     String    @unique\n  articles Article[]\n}\n\nmodel User {\n  id         Int       @id @default(autoincrement())\n  email      String    @unique\n  username   String    @unique\n  password   String\n  image      String?   @default(\"https://api.realworld.io/images/smiley-cyrus.jpeg\")\n  bio        String?\n  articles   Article[] @relation(\"UserArticles\")\n  favorites  Article[] @relation(\"UserFavorites\", references: [id])\n  followedBy User[]    @relation(\"UserFollows\", references: [id])\n  following  User[]    @relation(\"UserFollows\", references: [id])\n  comments   Comment[]\n}\n"
  },
  {
    "path": "src/controllers/article.controller.ts",
    "content": "import { NextFunction, Request, Response, Router } from 'express';\nimport auth from '../utils/auth';\nimport {\n  addComment,\n  createArticle,\n  deleteArticle,\n  deleteComment,\n  favoriteArticle,\n  getArticle,\n  getArticles,\n  getCommentsByArticle,\n  getFeed,\n  unfavoriteArticle,\n  updateArticle,\n} from '../services/article.service';\n\nconst router = Router();\n\n/**\n * Get paginated articles\n * @auth optional\n * @route {GET} /articles\n * @queryparam offset number of articles dismissed from the first one\n * @queryparam limit number of articles returned\n * @queryparam tag\n * @queryparam author\n * @queryparam favorited\n * @returns articles: list of articles\n */\nrouter.get('/articles', auth.optional, async (req: Request, res: Response, next: NextFunction) => {\n  try {\n    const result = await getArticles(req.query, req.user?.username);\n    res.json(result);\n  } catch (error) {\n    next(error);\n  }\n});\n\n/**\n * Get paginated feed articles\n * @auth required\n * @route {GET} /articles/feed\n * @returns articles list of articles\n */\nrouter.get(\n  '/articles/feed',\n  auth.required,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const result = await getFeed(\n        Number(req.query.offset),\n        Number(req.query.limit),\n        req.user?.username as string,\n      );\n      res.json(result);\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Create article\n * @route {POST} /articles\n * @bodyparam  title\n * @bodyparam  description\n * @bodyparam  body\n * @bodyparam  tagList list of tags\n * @returns article created article\n */\nrouter.post('/articles', auth.required, async (req: Request, res: Response, next: NextFunction) => {\n  try {\n    const article = await createArticle(req.body.article, req.user?.username as string);\n    res.json({ article });\n  } catch (error) {\n    next(error);\n  }\n});\n\n/**\n * Get unique article\n * @auth optional\n * @route {GET} /article/:slug\n * @param slug slug of the article (based on the title)\n * @returns article\n */\nrouter.get(\n  '/articles/:slug',\n  auth.optional,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const article = await getArticle(req.params.slug, req.user?.username as string);\n      res.json({ article });\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Update article\n * @auth required\n * @route {PUT} /articles/:slug\n * @param slug slug of the article (based on the title)\n * @bodyparam title new title\n * @bodyparam description new description\n * @bodyparam body new content\n * @returns article updated article\n */\nrouter.put(\n  '/articles/:slug',\n  auth.required,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const article = await updateArticle(\n        req.body.article,\n        req.params.slug,\n        req.user?.username as string,\n      );\n      res.json({ article });\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Delete article\n * @auth required\n * @route {DELETE} /article/:id\n * @param slug slug of the article\n */\nrouter.delete(\n  '/articles/:slug',\n  auth.required,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      await deleteArticle(req.params.slug);\n      res.sendStatus(204);\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Get comments from an article\n * @auth optional\n * @route {GET} /articles/:slug/comments\n * @param slug slug of the article (based on the title)\n * @returns comments list of comments\n */\nrouter.get(\n  '/articles/:slug/comments',\n  auth.optional,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const comments = await getCommentsByArticle(req.params.slug, req.user?.username);\n      res.json({ comments });\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Add comment to article\n * @auth required\n * @route {POST} /articles/:slug/comments\n * @param slug slug of the article (based on the title)\n * @bodyparam body content of the comment\n * @returns comment created comment\n */\nrouter.post(\n  '/articles/:slug/comments',\n  auth.required,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const comment = await addComment(\n        req.body.comment.body,\n        req.params.slug,\n        req.user?.username as string,\n      );\n      res.json({ comment });\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Delete comment\n * @auth required\n * @route {DELETE} /articles/:slug/comments/:id\n * @param slug slug of the article (based on the title)\n * @param id id of the comment\n */\nrouter.delete(\n  '/articles/:slug/comments/:id',\n  auth.required,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      await deleteComment(Number(req.params.id), req.user?.username as string);\n      res.sendStatus(204);\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Favorite article\n * @auth required\n * @route {POST} /articles/:slug/favorite\n * @param slug slug of the article (based on the title)\n * @returns article favorited article\n */\nrouter.post(\n  '/articles/:slug/favorite',\n  auth.required,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const article = await favoriteArticle(req.params.slug, req.user?.username as string);\n      res.json({ article });\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Unfavorite article\n * @auth required\n * @route {DELETE} /articles/:slug/favorite\n * @param slug slug of the article (based on the title)\n * @returns article unfavorited article\n */\nrouter.delete(\n  '/articles/:slug/favorite',\n  auth.required,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const article = await unfavoriteArticle(req.params.slug, req.user?.username as string);\n      res.json({ article });\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\nexport default router;\n"
  },
  {
    "path": "src/controllers/auth.controller.ts",
    "content": "import { NextFunction, Request, Response, Router } from 'express';\nimport auth from '../utils/auth';\nimport { createUser, getCurrentUser, login, updateUser } from '../services/auth.service';\n\nconst router = Router();\n\n/**\n * Create an user\n * @auth none\n * @route {POST} /users\n * @bodyparam user User\n * @returns user User\n */\nrouter.post('/users', async (req: Request, res: Response, next: NextFunction) => {\n  try {\n    const user = await createUser(req.body.user);\n    res.json({ user });\n  } catch (error) {\n    next(error);\n  }\n});\n\n/**\n * Login\n * @auth none\n * @route {POST} /users/login\n * @bodyparam user User\n * @returns user User\n */\nrouter.post('/users/login', async (req: Request, res: Response, next: NextFunction) => {\n  try {\n    const user = await login(req.body.user);\n    res.json({ user });\n  } catch (error) {\n    next(error);\n  }\n});\n\n/**\n * Get current user\n * @auth required\n * @route {GET} /user\n * @returns user User\n */\nrouter.get('/user', auth.required, async (req: Request, res: Response, next: NextFunction) => {\n  try {\n    const user = await getCurrentUser(req.user?.username as string);\n    res.json({ user });\n  } catch (error) {\n    next(error);\n  }\n});\n\n/**\n * Update user\n * @auth required\n * @route {PUT} /user\n * @bodyparam user User\n * @returns user User\n */\nrouter.put('/user', auth.required, async (req: Request, res: Response, next: NextFunction) => {\n  try {\n    const user = await updateUser(req.body.user, req.user?.username as string);\n    res.json({ user });\n  } catch (error) {\n    next(error);\n  }\n});\n\nexport default router;\n"
  },
  {
    "path": "src/controllers/profile.controller.ts",
    "content": "import { NextFunction, Request, Response, Router } from 'express';\nimport auth from '../utils/auth';\nimport { followUser, getProfile, unfollowUser } from '../services/profile.service';\n\nconst router = Router();\n\n/**\n * Get profile\n * @auth optional\n * @route {GET} /profiles/:username\n * @param username string\n * @returns profile\n */\nrouter.get(\n  '/profiles/:username',\n  auth.optional,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const profile = await getProfile(req.params?.username, req.user?.username as string);\n      res.json({ profile });\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Follow user\n * @auth required\n * @route {POST} /profiles/:username/follow\n * @param username string\n * @returns profile\n */\nrouter.post(\n  '/profiles/:username/follow',\n  auth.required,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const profile = await followUser(req.params?.username, req.user?.username as string);\n      res.json({ profile });\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\n/**\n * Unfollow user\n * @auth required\n * @route {DELETE} /profiles/:username/follow\n * @param username string\n * @returns profiles\n */\nrouter.delete(\n  '/profiles/:username/follow',\n  auth.required,\n  async (req: Request, res: Response, next: NextFunction) => {\n    try {\n      const profile = await unfollowUser(req.params.username, req.user?.username as string);\n      res.json({ profile });\n    } catch (error) {\n      next(error);\n    }\n  },\n);\n\nexport default router;\n"
  },
  {
    "path": "src/controllers/tag.controller.ts",
    "content": "import { NextFunction, Request, Response, Router } from 'express';\nimport auth from '../utils/auth';\nimport getTags from '../services/tag.service';\n\nconst router = Router();\n\n/**\n * Get top 10 popular tags\n * @auth optional\n * @route {GET} /api/tags\n * @returns tags list of tag names\n */\nrouter.get('/tags', auth.optional, async (req: Request, res: Response, next: NextFunction) => {\n  try {\n    const tags = await getTags(req.user?.username);\n    res.json({ tags });\n  } catch (error) {\n    next(error);\n  }\n});\n\nexport default router;\n"
  },
  {
    "path": "src/index.ts",
    "content": "import express, { NextFunction, Request, Response } from 'express';\nimport cors from 'cors';\nimport swaggerUi from 'swagger-ui-express';\nimport bodyParser from 'body-parser';\nimport routes from './routes/routes';\nimport HttpException from './models/http-exception.model';\nimport swaggerDocument from '../docs/swagger.json';\n\nconst app = express();\n\n/**\n * App Configuration\n */\n\napp.use(cors());\napp.use(bodyParser.json());\napp.use(bodyParser.urlencoded({ extended: true }));\napp.use(routes);\n\n// Serves images\napp.use(express.static('public'));\n\napp.get('/', (req: Request, res: Response) => {\n  res.json({ status: 'API is running on /api' });\n});\n\napp.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));\n\napp.get('/api-docs', (req: Request, res: Response) => {\n  res.json({\n    swagger:\n      'the API documentation  is now available on https://realworld-temp-api.herokuapp.com/api',\n  });\n});\n\n/* eslint-disable */\napp.use((err: Error | HttpException, req: Request, res: Response, next: NextFunction) => {\n  // @ts-ignore\n  if (err && err.name === 'UnauthorizedError') {\n    return res.status(401).json({\n      status: 'error',\n      message: 'missing authorization credentials',\n    });\n    // @ts-ignore\n  } else if (err && err.errorCode) {\n    // @ts-ignore\n    res.status(err.errorCode).json(err.message);\n  } else if (err) {\n    res.status(500).json(err.message);\n  }\n});\n\n/**\n * Server activation\n */\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n  console.info(`server up on port ${PORT}`);\n});\n"
  },
  {
    "path": "src/models/article.model.ts",
    "content": "import { Comment } from './comment.model';\n\nexport interface Article {\n  id: number;\n  title: string;\n  slug: string;\n  description: string;\n  comments: Comment[];\n  favorited: boolean;\n}\n"
  },
  {
    "path": "src/models/comment.model.ts",
    "content": "import { Article } from './article.model';\n\nexport interface Comment {\n  id: number;\n  createdAt: Date;\n  updatedAt: Date;\n  body: string;\n  article?: Article;\n}\n"
  },
  {
    "path": "src/models/http-exception.model.ts",
    "content": "class HttpException extends Error {\n  errorCode: number;\n\n  constructor(errorCode: number, public readonly message: string | any) {\n    super(message);\n    this.errorCode = errorCode;\n  }\n}\n\nexport default HttpException;\n"
  },
  {
    "path": "src/models/profile.model.ts",
    "content": "export interface Profile {\n  username: string;\n  bio: string;\n  image: string;\n  following: boolean;\n}\n"
  },
  {
    "path": "src/models/register-input.model.ts",
    "content": "export interface RegisterInput {\n  email: string;\n  username: string;\n  password: string;\n  image?: string;\n  bio?: string;\n}\n"
  },
  {
    "path": "src/models/registered-user.model.ts",
    "content": "export interface RegisteredUser {\n  email: string;\n  username: string;\n  bio: string | null;\n  image: string | null;\n  token: string;\n}\n"
  },
  {
    "path": "src/models/tag.model.ts",
    "content": "export interface Tag {\n  name: string;\n}\n"
  },
  {
    "path": "src/models/user.model.ts",
    "content": "import { Article } from './article.model';\nimport { Comment } from './comment.model';\n\nexport interface User {\n  id: number;\n  username: string;\n  email: string;\n  password: string;\n  bio: string | null;\n  image: any | null;\n  articles: Article[];\n  favorites: Article[];\n  followedBy: User[];\n  following: User[];\n  comments: Comment[];\n}\n"
  },
  {
    "path": "src/routes/routes.ts",
    "content": "import { Router } from 'express';\nimport tagsController from '../controllers/tag.controller';\nimport articlesController from '../controllers/article.controller';\nimport authController from '../controllers/auth.controller';\nimport profileController from '../controllers/profile.controller';\n\nconst api = Router()\n  .use(tagsController)\n  .use(articlesController)\n  .use(profileController)\n  .use(authController);\n\nexport default Router().use('/api', api);\n"
  },
  {
    "path": "src/services/article.service.ts",
    "content": "import slugify from 'slugify';\nimport prisma from '../../prisma/prisma-client';\nimport HttpException from '../models/http-exception.model';\nimport { findUserIdByUsername } from './auth.service';\nimport profileMapper from '../utils/profile.utils';\n\nconst buildFindAllQuery = (query: any, username: string | undefined) => {\n  const queries: any = [];\n  const orAuthorQuery = [];\n  const andAuthorQuery = [];\n\n  if (username) {\n    orAuthorQuery.push({\n      username: {\n        equals: username,\n      },\n    });\n  }\n\n  if ('author' in query) {\n    andAuthorQuery.push({\n      username: {\n        equals: query.author,\n      },\n    });\n  }\n\n  const authorQuery = {\n    author: {\n      OR: orAuthorQuery,\n      AND: andAuthorQuery,\n    },\n  };\n\n  queries.push(authorQuery);\n\n  if ('tag' in query) {\n    queries.push({\n      tagList: {\n        some: {\n          name: query.tag,\n        },\n      },\n    });\n  }\n\n  if ('favorited' in query) {\n    queries.push({\n      favoritedBy: {\n        some: {\n          username: {\n            equals: query.favorited,\n          },\n        },\n      },\n    });\n  }\n\n  return queries;\n};\n\nexport const getArticles = async (query: any, username?: string) => {\n  const andQueries = buildFindAllQuery(query, username);\n  const articlesCount = await prisma.article.count({\n    where: {\n      AND: andQueries,\n    },\n  });\n\n  const articles = await prisma.article.findMany({\n    where: { AND: andQueries },\n    orderBy: {\n      createdAt: 'desc',\n    },\n    skip: Number(query.offset) || 0,\n    take: Number(query.limit) || 10,\n    include: {\n      tagList: {\n        select: {\n          name: true,\n        },\n      },\n      author: {\n        select: {\n          username: true,\n          bio: true,\n          image: true,\n          followedBy: true,\n        },\n      },\n      favoritedBy: true,\n      _count: {\n        select: {\n          favoritedBy: true,\n        },\n      },\n    },\n  });\n\n  return {\n    articles: articles.map(({ authorId, id, _count, favoritedBy, ...article }) => ({\n      ...article,\n      author: profileMapper(article.author, username),\n      tagList: article.tagList.map(tag => tag.name),\n      favoritesCount: _count?.favoritedBy,\n      favorited: favoritedBy.some(item => item.username === username),\n    })),\n    articlesCount,\n  };\n};\n\nexport const getFeed = async (offset: number, limit: number, username: string) => {\n  const user = await findUserIdByUsername(username);\n\n  const articlesCount = await prisma.article.count({\n    where: {\n      author: {\n        followedBy: { some: { id: user?.id } },\n      },\n    },\n  });\n\n  const articles = await prisma.article.findMany({\n    where: {\n      author: {\n        followedBy: { some: { id: user?.id } },\n      },\n    },\n    orderBy: {\n      createdAt: 'desc',\n    },\n    skip: offset || 0,\n    take: limit || 10,\n    include: {\n      tagList: {\n        select: {\n          name: true,\n        },\n      },\n      author: {\n        select: {\n          username: true,\n          bio: true,\n          image: true,\n          followedBy: true,\n        },\n      },\n      favoritedBy: true,\n      _count: {\n        select: {\n          favoritedBy: true,\n        },\n      },\n    },\n  });\n\n  return {\n    articles: articles.map(({ authorId, id, _count, favoritedBy, ...article }) => ({\n      ...article,\n      author: profileMapper(article.author, username),\n      tagList: article.tagList.map(tag => tag.name),\n      favoritesCount: _count?.favoritedBy,\n      favorited: favoritedBy.some(item => item.username === username),\n    })),\n    articlesCount,\n  };\n};\n\nexport const createArticle = async (article: any, username: string) => {\n  const { title, description, body, tagList } = article;\n\n  if (!title) {\n    throw new HttpException(422, { errors: { title: [\"can't be blank\"] } });\n  }\n\n  if (!description) {\n    throw new HttpException(422, { errors: { description: [\"can't be blank\"] } });\n  }\n\n  if (!body) {\n    throw new HttpException(422, { errors: { body: [\"can't be blank\"] } });\n  }\n\n  const user = await findUserIdByUsername(username);\n\n  const slug = `${slugify(title)}-${user?.id}`;\n\n  const existingTitle = await prisma.article.findUnique({\n    where: {\n      slug,\n    },\n    select: {\n      slug: true,\n    },\n  });\n\n  if (existingTitle) {\n    throw new HttpException(422, { errors: { title: ['must be unique'] } });\n  }\n\n  const { authorId, id, ...createdArticle } = await prisma.article.create({\n    data: {\n      title,\n      description,\n      body,\n      slug,\n      tagList: {\n        connectOrCreate: tagList.map((tag: string) => ({\n          create: { name: tag },\n          where: { name: tag },\n        })),\n      },\n      author: {\n        connect: {\n          id: user?.id,\n        },\n      },\n    },\n    include: {\n      tagList: {\n        select: {\n          name: true,\n        },\n      },\n      author: {\n        select: {\n          username: true,\n          bio: true,\n          image: true,\n        },\n      },\n      favoritedBy: true,\n      _count: {\n        select: {\n          favoritedBy: true,\n        },\n      },\n    },\n  });\n\n  return {\n    ...createdArticle,\n    tagList: createdArticle.tagList.map(tag => tag.name),\n    favoritesCount: createdArticle._count?.favoritedBy,\n    favorited: createdArticle.favoritedBy.some(item => item.username === username),\n  };\n};\n\nexport const getArticle = async (slug: string, username?: string) => {\n  const article = await prisma.article.findUnique({\n    where: {\n      slug,\n    },\n    include: {\n      tagList: {\n        select: {\n          name: true,\n        },\n      },\n      author: {\n        select: {\n          username: true,\n          bio: true,\n          image: true,\n          followedBy: true,\n        },\n      },\n      favoritedBy: true,\n      _count: {\n        select: {\n          favoritedBy: true,\n        },\n      },\n    },\n  });\n\n  return {\n    title: article?.title,\n    slug: article?.slug,\n    body: article?.body,\n    description: article?.description,\n    createdAt: article?.createdAt,\n    updatedAt: article?.updatedAt,\n    tagList: article?.tagList.map(tag => tag.name),\n    favoritesCount: article?._count?.favoritedBy,\n    favorited: article?.favoritedBy.some(item => item.username === username),\n    author: {\n      ...article?.author,\n      following: article?.author.followedBy.some(follow => follow.username === username),\n    },\n  };\n};\n\nconst disconnectArticlesTags = async (slug: string) => {\n  await prisma.article.update({\n    where: {\n      slug,\n    },\n    data: {\n      tagList: {\n        set: [],\n      },\n    },\n  });\n};\n\nexport const updateArticle = async (article: any, slug: string, username: string) => {\n  let newSlug = null;\n  const user = await findUserIdByUsername(username);\n\n  if (article.title) {\n    newSlug = `${slugify(article.title)}-${user?.id}`;\n\n    if (newSlug !== slug) {\n      const existingTitle = await prisma.article.findFirst({\n        where: {\n          slug: newSlug,\n        },\n        select: {\n          slug: true,\n        },\n      });\n\n      if (existingTitle) {\n        throw new HttpException(422, { errors: { title: ['must be unique'] } });\n      }\n    }\n  }\n\n  const tagList = article.tagList?.length\n    ? article.tagList.map((tag: string) => ({\n        create: { name: tag },\n        where: { name: tag },\n      }))\n    : [];\n\n  await disconnectArticlesTags(slug);\n\n  const updatedArticle = await prisma.article.update({\n    where: {\n      slug,\n    },\n    data: {\n      ...(article.title ? { title: article.title } : {}),\n      ...(article.body ? { body: article.body } : {}),\n      ...(article.description ? { description: article.description } : {}),\n      ...(newSlug ? { slug: newSlug } : {}),\n      updatedAt: new Date(),\n      tagList: {\n        connectOrCreate: tagList,\n      },\n    },\n    include: {\n      tagList: {\n        select: {\n          name: true,\n        },\n      },\n      author: {\n        select: {\n          username: true,\n          bio: true,\n          image: true,\n        },\n      },\n      favoritedBy: true,\n      _count: {\n        select: {\n          favoritedBy: true,\n        },\n      },\n    },\n  });\n\n  return {\n    title: updatedArticle?.title,\n    slug: updatedArticle?.slug,\n    body: updatedArticle?.body,\n    description: updatedArticle?.description,\n    createdAt: updatedArticle?.createdAt,\n    updatedAt: updatedArticle?.updatedAt,\n    tagList: updatedArticle?.tagList.map(tag => tag.name),\n    favoritesCount: updatedArticle?._count?.favoritedBy,\n    favorited: updatedArticle?.favoritedBy.some(item => item.username === username),\n    author: updatedArticle?.author,\n  };\n};\n\nexport const deleteArticle = async (slug: string) => {\n  await prisma.article.delete({\n    where: {\n      slug,\n    },\n  });\n};\n\nexport const getCommentsByArticle = async (slug: string, username?: string) => {\n  const queries = [];\n\n  if (username) {\n    queries.push({\n      author: {\n        username,\n      },\n    });\n  }\n\n  const comments = await prisma.article.findUnique({\n    where: {\n      slug,\n    },\n    include: {\n      comments: {\n        where: {\n          OR: queries,\n        },\n        select: {\n          id: true,\n          createdAt: true,\n          updatedAt: true,\n          body: true,\n          author: {\n            select: {\n              username: true,\n              bio: true,\n              image: true,\n              followedBy: true,\n            },\n          },\n        },\n      },\n    },\n  });\n\n  const result = comments?.comments.map(comment => ({\n    ...comment,\n    author: {\n      username: comment.author.username,\n      bio: comment.author.bio,\n      image: comment.author.image,\n      following: comment.author.followedBy.some(follow => follow.username === username),\n    },\n  }));\n\n  return result;\n};\n\nexport const addComment = async (body: string, slug: string, username: string) => {\n  if (!body) {\n    throw new HttpException(422, { errors: { body: [\"can't be blank\"] } });\n  }\n\n  const user = await findUserIdByUsername(username);\n\n  const article = await prisma.article.findUnique({\n    where: {\n      slug,\n    },\n    select: {\n      id: true,\n    },\n  });\n\n  const comment = await prisma.comment.create({\n    data: {\n      body,\n      article: {\n        connect: {\n          id: article?.id,\n        },\n      },\n      author: {\n        connect: {\n          id: user?.id,\n        },\n      },\n    },\n    include: {\n      author: {\n        select: {\n          username: true,\n          bio: true,\n          image: true,\n          followedBy: true,\n        },\n      },\n    },\n  });\n\n  return {\n    id: comment.id,\n    createdAt: comment.createdAt,\n    updatedAt: comment.updatedAt,\n    body: comment.body,\n    author: {\n      username: comment.author.username,\n      bio: comment.author.bio,\n      image: comment.author.image,\n      following: comment.author.followedBy.some(follow => follow.id === user?.id),\n    },\n  };\n};\n\nexport const deleteComment = async (id: number, username: string) => {\n  const comment = await prisma.comment.findFirst({\n    where: {\n      id,\n      author: {\n        username,\n      },\n    },\n  });\n\n  if (!comment) {\n    throw new HttpException(201, {});\n  }\n\n  await prisma.comment.delete({\n    where: {\n      id,\n    },\n  });\n};\n\nexport const favoriteArticle = async (slugPayload: string, usernameAuth: string) => {\n  const user = await findUserIdByUsername(usernameAuth);\n\n  const { _count, ...article } = await prisma.article.update({\n    where: {\n      slug: slugPayload,\n    },\n    data: {\n      favoritedBy: {\n        connect: {\n          id: user?.id,\n        },\n      },\n    },\n    include: {\n      tagList: {\n        select: {\n          name: true,\n        },\n      },\n      author: {\n        select: {\n          username: true,\n          bio: true,\n          image: true,\n          followedBy: true,\n        },\n      },\n      favoritedBy: true,\n      _count: {\n        select: {\n          favoritedBy: true,\n        },\n      },\n    },\n  });\n\n  const result = {\n    ...article,\n    author: profileMapper(article.author, usernameAuth),\n    tagList: article?.tagList.map(tag => tag.name),\n    favorited: article.favoritedBy.some(favorited => favorited.id === user?.id),\n    favoritesCount: _count?.favoritedBy,\n  };\n\n  return result;\n};\n\nexport const unfavoriteArticle = async (slugPayload: string, usernameAuth: string) => {\n  const user = await findUserIdByUsername(usernameAuth);\n\n  const { _count, ...article } = await prisma.article.update({\n    where: {\n      slug: slugPayload,\n    },\n    data: {\n      favoritedBy: {\n        disconnect: {\n          id: user?.id,\n        },\n      },\n    },\n    include: {\n      tagList: {\n        select: {\n          name: true,\n        },\n      },\n      author: {\n        select: {\n          username: true,\n          bio: true,\n          image: true,\n          followedBy: true,\n        },\n      },\n      favoritedBy: true,\n      _count: {\n        select: {\n          favoritedBy: true,\n        },\n      },\n    },\n  });\n\n  const result = {\n    ...article,\n    author: profileMapper(article.author, usernameAuth),\n    tagList: article?.tagList.map(tag => tag.name),\n    favorited: article.favoritedBy.some(favorited => favorited.id === user?.id),\n    favoritesCount: _count?.favoritedBy,\n  };\n\n  return result;\n};\n"
  },
  {
    "path": "src/services/auth.service.ts",
    "content": "import bcrypt from 'bcryptjs';\nimport { RegisterInput } from '../models/register-input.model';\nimport prisma from '../../prisma/prisma-client';\nimport HttpException from '../models/http-exception.model';\nimport { RegisteredUser } from '../models/registered-user.model';\nimport generateToken from '../utils/token.utils';\nimport { User } from '../models/user.model';\n\nconst checkUserUniqueness = async (email: string, username: string) => {\n  const existingUserByEmail = await prisma.user.findUnique({\n    where: {\n      email,\n    },\n    select: {\n      id: true,\n    },\n  });\n\n  const existingUserByUsername = await prisma.user.findUnique({\n    where: {\n      username,\n    },\n    select: {\n      id: true,\n    },\n  });\n\n  if (existingUserByEmail || existingUserByUsername) {\n    throw new HttpException(422, {\n      errors: {\n        ...(existingUserByEmail ? { email: ['has already been taken'] } : {}),\n        ...(existingUserByUsername ? { username: ['has already been taken'] } : {}),\n      },\n    });\n  }\n};\n\nexport const createUser = async (input: RegisterInput): Promise<RegisteredUser> => {\n  const email = input.email?.trim();\n  const username = input.username?.trim();\n  const password = input.password?.trim();\n  const { image, bio } = input;\n\n  if (!email) {\n    throw new HttpException(422, { errors: { email: [\"can't be blank\"] } });\n  }\n\n  if (!username) {\n    throw new HttpException(422, { errors: { username: [\"can't be blank\"] } });\n  }\n\n  if (!password) {\n    throw new HttpException(422, { errors: { password: [\"can't be blank\"] } });\n  }\n\n  await checkUserUniqueness(email, username);\n\n  const hashedPassword = await bcrypt.hash(password, 10);\n\n  const user = await prisma.user.create({\n    data: {\n      username,\n      email,\n      password: hashedPassword,\n      ...(image ? { image } : {}),\n      ...(bio ? { bio } : {}),\n    },\n    select: {\n      email: true,\n      username: true,\n      bio: true,\n      image: true,\n    },\n  });\n\n  return {\n    ...user,\n    token: generateToken(user),\n  };\n};\n\nexport const login = async (userPayload: any) => {\n  const email = userPayload.email?.trim();\n  const password = userPayload.password?.trim();\n\n  if (!email) {\n    throw new HttpException(422, { errors: { email: [\"can't be blank\"] } });\n  }\n\n  if (!password) {\n    throw new HttpException(422, { errors: { password: [\"can't be blank\"] } });\n  }\n\n  const user = await prisma.user.findUnique({\n    where: {\n      email,\n    },\n    select: {\n      email: true,\n      username: true,\n      password: true,\n      bio: true,\n      image: true,\n    },\n  });\n\n  if (user) {\n    const match = await bcrypt.compare(password, user.password);\n\n    if (match) {\n      return {\n        email: user.email,\n        username: user.username,\n        bio: user.bio,\n        image: user.image,\n        token: generateToken(user),\n      };\n    }\n  }\n\n  throw new HttpException(403, {\n    errors: {\n      'email or password': ['is invalid'],\n    },\n  });\n};\n\nexport const getCurrentUser = async (username: string) => {\n  const user = (await prisma.user.findUnique({\n    where: {\n      username,\n    },\n    select: {\n      email: true,\n      username: true,\n      bio: true,\n      image: true,\n    },\n  })) as User;\n\n  return {\n    ...user,\n    token: generateToken(user),\n  };\n};\n\nexport const updateUser = async (userPayload: any, loggedInUsername: string) => {\n  const { email, username, password, image, bio } = userPayload;\n\n  const hashedPassword = await bcrypt.hash(password, 10);\n\n  const user = await prisma.user.update({\n    where: {\n      username: loggedInUsername,\n    },\n    data: {\n      ...(email ? { email } : {}),\n      ...(username ? { username } : {}),\n      ...(password ? { password: hashedPassword } : {}),\n      ...(image ? { image } : {}),\n      ...(bio ? { bio } : {}),\n    },\n    select: {\n      email: true,\n      username: true,\n      bio: true,\n      image: true,\n    },\n  });\n\n  return {\n    ...user,\n    token: generateToken(user),\n  };\n};\n\nexport const findUserIdByUsername = async (username: string) => {\n  const user = await prisma.user.findUnique({\n    where: {\n      username,\n    },\n    select: {\n      id: true,\n    },\n  });\n\n  if (!user) {\n    throw new HttpException(404, {});\n  }\n\n  return user;\n};\n"
  },
  {
    "path": "src/services/profile.service.ts",
    "content": "import prisma from '../../prisma/prisma-client';\nimport profileMapper from '../utils/profile.utils';\nimport HttpException from '../models/http-exception.model';\nimport { findUserIdByUsername } from './auth.service';\n\nexport const getProfile = async (usernamePayload: string, usernameAuth: string) => {\n  const profile = await prisma.user.findUnique({\n    where: {\n      username: usernamePayload,\n    },\n    include: {\n      followedBy: true,\n    },\n  });\n\n  if (!profile) {\n    throw new HttpException(404, {});\n  }\n\n  return profileMapper(profile, usernameAuth);\n};\n\nexport const followUser = async (usernamePayload: string, usernameAuth: string) => {\n  const { id } = await findUserIdByUsername(usernameAuth);\n\n  const profile = await prisma.user.update({\n    where: {\n      username: usernamePayload,\n    },\n    data: {\n      followedBy: {\n        connect: {\n          id,\n        },\n      },\n    },\n    include: {\n      followedBy: true,\n    },\n  });\n\n  return profileMapper(profile, usernameAuth);\n};\n\nexport const unfollowUser = async (usernamePayload: string, usernameAuth: string) => {\n  const { id } = await findUserIdByUsername(usernameAuth);\n\n  const profile = await prisma.user.update({\n    where: {\n      username: usernamePayload,\n    },\n    data: {\n      followedBy: {\n        disconnect: {\n          id,\n        },\n      },\n    },\n    include: {\n      followedBy: true,\n    },\n  });\n\n  return profileMapper(profile, usernameAuth);\n};\n"
  },
  {
    "path": "src/services/tag.service.ts",
    "content": "import prisma from '../../prisma/prisma-client';\n\nconst getTags = async (username?: string): Promise<string[]> => {\n  const queries = [];\n\n  if (username) {\n    queries.push({\n      username: {\n        equals: username,\n      },\n    });\n  }\n\n  const tags = await prisma.tag.groupBy({\n    where: {\n      articles: {\n        some: {\n          author: {\n            OR: queries,\n          },\n        },\n      },\n    },\n    by: ['name'],\n    orderBy: {\n      _count: {\n        name: 'desc',\n      },\n    },\n    take: 10,\n  });\n\n  return tags.map(tag => tag.name);\n};\n\nexport default getTags;\n"
  },
  {
    "path": "src/utils/auth.ts",
    "content": "const jwt = require('express-jwt');\n\nconst getTokenFromHeaders = (req: { headers: { authorization: string } }): string | null => {\n  if (\n    (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Token') ||\n    (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer')\n  ) {\n    return req.headers.authorization.split(' ')[1];\n  }\n  return null;\n};\n\nconst auth = {\n  required: jwt({\n    secret: process.env.JWT_SECRET || 'superSecret',\n    getToken: getTokenFromHeaders,\n    algorithms: ['HS256'],\n  }),\n  optional: jwt({\n    secret: process.env.JWT_SECRET || 'superSecret',\n    credentialsRequired: false,\n    getToken: getTokenFromHeaders,\n    algorithms: ['HS256'],\n  }),\n};\n\nexport default auth;\n"
  },
  {
    "path": "src/utils/profile.utils.ts",
    "content": "import { User } from '../models/user.model';\nimport { Profile } from '../models/profile.model';\n\nconst profileMapper = (user: any, username: string | undefined): Profile => ({\n  username: user.username,\n  bio: user.bio,\n  image: user.image,\n  following: username\n    ? user?.followedBy.some((followingUser: Partial<User>) => followingUser.username === username)\n    : false,\n});\n\nexport default profileMapper;\n"
  },
  {
    "path": "src/utils/token.utils.ts",
    "content": "import jwt from 'jsonwebtoken';\nimport { User } from '../models/user.model';\n\nconst generateToken = (user: Partial<User>): string =>\n  jwt.sign(user, process.env.JWT_SECRET || 'superSecret', { expiresIn: '60d' });\n\nexport default generateToken;\n"
  },
  {
    "path": "src/utils/user-request.d.ts",
    "content": "declare namespace Express {\n  export interface Request {\n    user?: {\n      username?: string;\n    };\n  }\n}\n"
  },
  {
    "path": "tests/prisma-mock.ts",
    "content": "import { PrismaClient } from '@prisma/client';\nimport { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended';\n\nimport prisma from '../prisma/prisma-client';\n\njest.mock('../prisma/prisma-client', () => ({\n  __esModule: true,\n  default: mockDeep<PrismaClient>(),\n}));\n\nconst prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>;\n\nbeforeEach(() => {\n  mockReset(prismaMock);\n});\n\nexport default prismaMock;\n"
  },
  {
    "path": "tests/services/article.service.test.ts",
    "content": "import prismaMock from '../prisma-mock';\nimport {\n  deleteComment,\n  favoriteArticle,\n  unfavoriteArticle,\n} from '../../src/services/article.service';\n\ndescribe('ArticleService', () => {\n  describe('deleteComment', () => {\n    test('should throw an error ', () => {\n      // Given\n      const id = 123;\n      const username = 'RealWorld';\n\n      // When\n      prismaMock.comment.findFirst.mockResolvedValue(null);\n\n      // Then\n      expect(deleteComment(id, username)).rejects.toThrowError();\n    });\n  });\n\n  describe('favoriteArticle', () => {\n    test('should return the favorited article', async () => {\n      // Given\n      const slug = 'How-to-train-your-dragon';\n      const username = 'RealWorld';\n\n      const mockedUserResponse = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n      };\n\n      const mockedArticleResponse = {\n        id: 123,\n        slug: 'How-to-train-your-dragon',\n        title: 'How to train your dragon',\n        description: '',\n        body: '',\n        createdAt: new Date(),\n        updatedAt: new Date(),\n        authorId: 456,\n        tagList: [],\n        favoritedBy: [],\n        author: {\n          username: 'RealWorld',\n          bio: null,\n          image: null,\n          followedBy: [],\n        },\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(mockedUserResponse);\n      prismaMock.article.update.mockResolvedValue(mockedArticleResponse);\n\n      // Then\n      await expect(favoriteArticle(slug, username)).resolves.toHaveProperty('favoritesCount');\n    });\n\n    test('should throw an error if no user is found', async () => {\n      // Given\n      const slug = 'how-to-train-your-dragon';\n      const username = 'RealWorld';\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(null);\n\n      // Then\n      await expect(favoriteArticle(slug, username)).rejects.toThrowError();\n    });\n  });\n  describe('unfavoriteArticle', () => {\n    test('should return the unfavorited article', async () => {\n      // Given\n      const slug = 'How-to-train-your-dragon';\n      const username = 'RealWorld';\n\n      const mockedUserResponse = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n      };\n\n      const mockedArticleResponse = {\n        id: 123,\n        slug: 'How-to-train-your-dragon',\n        title: 'How to train your dragon',\n        description: '',\n        body: '',\n        createdAt: new Date(),\n        updatedAt: new Date(),\n        authorId: 456,\n        tagList: [],\n        favoritedBy: [],\n        author: {\n          username: 'RealWorld',\n          bio: null,\n          image: null,\n          followedBy: [],\n        },\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(mockedUserResponse);\n      prismaMock.article.update.mockResolvedValue(mockedArticleResponse);\n\n      // Then\n      await expect(unfavoriteArticle(slug, username)).resolves.toHaveProperty('favoritesCount');\n    });\n\n    test('should throw an error if no user is found', async () => {\n      // Given\n      const slug = 'how-to-train-your-dragon';\n      const username = 'RealWorld';\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(null);\n\n      // Then\n      await expect(unfavoriteArticle(slug, username)).rejects.toThrowError();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/services/auth.service.test.ts",
    "content": "import bcrypt from 'bcryptjs';\nimport { createUser, getCurrentUser, login, updateUser } from '../../src/services/auth.service';\nimport prismaMock from '../prisma-mock';\n\ndescribe('AuthService', () => {\n  describe('createUser', () => {\n    test('should create new user ', async () => {\n      // Given\n      const user = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n      };\n\n      const mockedResponse = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n      };\n\n      // When\n      prismaMock.user.create.mockResolvedValue(mockedResponse);\n\n      // Then\n      await expect(createUser(user)).resolves.toHaveProperty('token');\n    });\n\n    test('should throw an error when creating new user with empty username ', async () => {\n      // Given\n      const user = {\n        id: 123,\n        username: ' ',\n        email: 'realworld@me',\n        password: '1234',\n      };\n\n      // Then\n      const error = String({ errors: { username: [\"can't be blank\"] } });\n      await expect(createUser(user)).rejects.toThrow(error);\n    });\n\n    test('should throw an error when creating new user with empty email ', async () => {\n      // Given\n      const user = {\n        id: 123,\n        username: 'RealWorld',\n        email: '  ',\n        password: '1234',\n      };\n\n      // Then\n      const error = String({ errors: { email: [\"can't be blank\"] } });\n      await expect(createUser(user)).rejects.toThrow(error);\n    });\n\n    test('should throw an error when creating new user with empty password ', async () => {\n      // Given\n      const user = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: ' ',\n      };\n\n      // Then\n      const error = String({ errors: { password: [\"can't be blank\"] } });\n      await expect(createUser(user)).rejects.toThrow(error);\n    });\n\n    test('should throw an exception when creating a new user with already existing user on same username ', async () => {\n      // Given\n      const user = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n      };\n\n      const mockedExistingUser = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(mockedExistingUser);\n\n      // Then\n      const error = { email: ['has already been taken'] }.toString();\n      await expect(createUser(user)).rejects.toThrow(error);\n    });\n  });\n\n  describe('login', () => {\n    test('should return a token', async () => {\n      // Given\n      const user = {\n        email: 'realworld@me',\n        password: '1234',\n      };\n\n      const hashedPassword = await bcrypt.hash(user.password, 10);\n\n      const mockedResponse = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: hashedPassword,\n        bio: null,\n        image: null,\n        token: '',\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(mockedResponse);\n\n      // Then\n      await expect(login(user)).resolves.toHaveProperty('token');\n    });\n\n    test('should throw an error when the email is empty', async () => {\n      // Given\n      const user = {\n        email: ' ',\n        password: '1234',\n      };\n\n      // Then\n      const error = String({ errors: { email: [\"can't be blank\"] } });\n      await expect(login(user)).rejects.toThrow(error);\n    });\n\n    test('should throw an error when the password is empty', async () => {\n      // Given\n      const user = {\n        email: 'realworld@me',\n        password: ' ',\n      };\n\n      // Then\n      const error = String({ errors: { password: [\"can't be blank\"] } });\n      await expect(login(user)).rejects.toThrow(error);\n    });\n\n    test('should throw an error when no user is found', async () => {\n      // Given\n      const user = {\n        email: 'realworld@me',\n        password: '1234',\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(null);\n\n      // Then\n      const error = String({ errors: { 'email or password': ['is invalid'] } });\n      await expect(login(user)).rejects.toThrow(error);\n    });\n\n    test('should throw an error if the password is wrong', async () => {\n      // Given\n      const user = {\n        email: 'realworld@me',\n        password: '1234',\n      };\n\n      const hashedPassword = await bcrypt.hash('4321', 10);\n\n      const mockedResponse = {\n        id: 123,\n        username: 'Gerome',\n        email: 'realworld@me',\n        password: hashedPassword,\n        bio: null,\n        image: null,\n        token: '',\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(mockedResponse);\n\n      // Then\n      const error = String({ errors: { 'email or password': ['is invalid'] } });\n      await expect(login(user)).rejects.toThrow(error);\n    });\n  });\n\n  describe('getCurrentUser', () => {\n    test('should return a token', async () => {\n      // Given\n      const username = 'RealWorld';\n\n      const mockedResponse = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(mockedResponse);\n\n      // Then\n      await expect(getCurrentUser(username)).resolves.toHaveProperty('token');\n    });\n  });\n\n  describe('updateUser', () => {\n    test('should return a token', async () => {\n      // Given\n      const user = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n      };\n\n      const mockedResponse = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n      };\n\n      // When\n      prismaMock.user.update.mockResolvedValue(mockedResponse);\n\n      // Then\n      await expect(updateUser(user, user.username)).resolves.toHaveProperty('token');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/services/profile.service.test.ts",
    "content": "import prismaMock from '../prisma-mock';\nimport { followUser, getProfile, unfollowUser } from '../../src/services/profile.service';\n\ndescribe('ProfileService', () => {\n  describe('getProfile', () => {\n    test('should return a following property', async () => {\n      // Given\n      const username = 'RealWorld';\n      const usernameAuth = 'Gerome';\n\n      const mockedResponse = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n        followedBy: [],\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(mockedResponse);\n\n      // Then\n      await expect(getProfile(username, usernameAuth)).resolves.toHaveProperty('following');\n    });\n\n    test('should throw an error if no user is found', async () => {\n      // Given\n      const username = 'RealWorld';\n      const usernameAuth = 'Gerome';\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(null);\n\n      // Then\n      await expect(getProfile(username, usernameAuth)).rejects.toThrowError();\n    });\n  });\n\n  describe('followUser', () => {\n    test('shoud return a following property', async () => {\n      // Given\n      const usernamePayload = 'AnotherUser';\n      const usernameAuth = 'RealWorld';\n\n      const mockedAuthUser = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n        followedBy: [],\n      };\n\n      const mockedResponse = {\n        id: 123,\n        username: 'AnotherUser',\n        email: 'another@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n        followedBy: [],\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(mockedAuthUser);\n      prismaMock.user.update.mockResolvedValue(mockedResponse);\n\n      // Then\n      await expect(followUser(usernamePayload, usernameAuth)).resolves.toHaveProperty('following');\n    });\n\n    test('shoud throw an error if no user is found', async () => {\n      // Given\n      const usernamePayload = 'AnotherUser';\n      const usernameAuth = 'RealWorld';\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(null);\n\n      // Then\n      await expect(followUser(usernamePayload, usernameAuth)).rejects.toThrowError();\n    });\n  });\n\n  describe('unfollowUser', () => {\n    test('shoud return a following property', async () => {\n      // Given\n      const usernamePayload = 'AnotherUser';\n      const usernameAuth = 'RealWorld';\n\n      const mockedAuthUser = {\n        id: 123,\n        username: 'RealWorld',\n        email: 'realworld@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n        followedBy: [],\n      };\n\n      const mockedResponse = {\n        id: 123,\n        username: 'AnotherUser',\n        email: 'another@me',\n        password: '1234',\n        bio: null,\n        image: null,\n        token: '',\n        followedBy: [],\n      };\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(mockedAuthUser);\n      prismaMock.user.update.mockResolvedValue(mockedResponse);\n\n      // Then\n      await expect(unfollowUser(usernamePayload, usernameAuth)).resolves.toHaveProperty(\n        'following',\n      );\n    });\n\n    test('shoud throw an error if no user is found', async () => {\n      // Given\n      const usernamePayload = 'AnotherUser';\n      const usernameAuth = 'RealWorld';\n\n      // When\n      prismaMock.user.findUnique.mockResolvedValue(null);\n\n      // Then\n      await expect(unfollowUser(usernamePayload, usernameAuth)).rejects.toThrowError();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/services/tag.service.test.ts",
    "content": "describe('TagService', () => {\n  describe('getTags', () => {\n    // TODO : prismaMock.tag.groupBy.mockResolvedValue(mockedResponse) doesn't work\n    test.todo('should return a list of strings');\n  });\n});\n"
  },
  {
    "path": "tests/utils/profile.utils.test.ts",
    "content": "import profileMapper from '../../src/utils/profile.utils';\n\ndescribe('ProfileUtils', () => {\n  describe('profileMapper', () => {\n    test('should return a profile', () => {\n      // Given\n      const user = {\n        username: 'RealWorld',\n        bio: 'My happy life',\n        image: null,\n        followedBy: [],\n      };\n      const username = 'RealWorld';\n\n      // When\n      const expected = {\n        username: 'RealWorld',\n        bio: 'My happy life',\n        image: null,\n        following: false,\n      };\n\n      // Then\n      expect(profileMapper(user, username)).toEqual(expected);\n    });\n\n    test('should return a profile followed by the user', () => {\n      // Given\n      const user = {\n        username: 'RealWorld',\n        bio: 'My happy life',\n        image: null,\n        followedBy: [\n          {\n            username: 'RealWorld',\n          },\n        ],\n      };\n      const username = 'RealWorld';\n\n      // When\n      const expected = {\n        username: 'RealWorld',\n        bio: 'My happy life',\n        image: null,\n        following: true,\n      };\n\n      // Then\n      expect(profileMapper(user, username)).toEqual(expected);\n    });\n\n    test('should return a profile not followed by the user', () => {\n      // Given\n      const user = {\n        username: 'RealWorld',\n        bio: 'My happy life',\n        image: null,\n        followedBy: [\n          {\n            username: 'NotRealWorld',\n          },\n        ],\n      };\n      const username = 'RealWorld';\n\n      // When\n      const expected = {\n        username: 'RealWorld',\n        bio: 'My happy life',\n        image: null,\n        following: false,\n      };\n\n      // Then\n      expect(profileMapper(user, username)).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    /* Visit https://aka.ms/tsconfig.json to read more about this file */\n\n    /* Basic Options */\n    // \"incremental\": true,                   /* Enable incremental compilation */\n    \"target\": \"es5\" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,\n    \"module\": \"commonjs\" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,\n    \"lib\": [\"es2015\"] /* Specify library files to be included in the compilation. */,\n    // \"allowJs\": true,                       /* Allow javascript files to be compiled. */\n    // \"checkJs\": true,                       /* Report errors in .js files. */\n    // \"jsx\": \"preserve\",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */\n    // \"declaration\": true,                   /* Generates corresponding '.d.ts' file. */\n    // \"declarationMap\": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */\n    // \"sourceMap\": true,                     /* Generates corresponding '.map' file. */\n    // \"outFile\": \"./\",                       /* Concatenate and emit output to single file. */\n    \"outDir\": \"dist\" /* Redirect output structure to the directory. */,\n    // \"rootDir\": \"./\",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */\n    // \"composite\": true,                     /* Enable project compilation */\n    // \"tsBuildInfoFile\": \"./\",               /* Specify file to store incremental compilation information */\n    // \"removeComments\": true,                /* Do not emit comments to output. */\n    // \"noEmit\": true,                        /* Do not emit outputs. */\n    // \"importHelpers\": true,                 /* Import emit helpers from 'tslib'. */\n    // \"downlevelIteration\": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */\n    // \"isolatedModules\": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */\n\n    /* Strict Type-Checking Options */\n    \"strict\": true /* Enable all strict type-checking options. */,\n    // \"noImplicitAny\": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */\n    // \"strictNullChecks\": true,              /* Enable strict null checks. */\n    // \"strictFunctionTypes\": true,           /* Enable strict checking of function types. */\n    // \"strictBindCallApply\": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */\n    // \"strictPropertyInitialization\": true,  /* Enable strict checking of property initialization in classes. */\n    // \"noImplicitThis\": true,                /* Raise error on 'this' expressions with an implied 'any' type. */\n    // \"alwaysStrict\": true,                  /* Parse in strict mode and emit \"use strict\" for each source file. */\n\n    /* Additional Checks */\n    // \"noUnusedLocals\": true,                /* Report errors on unused locals. */\n    // \"noUnusedParameters\": true,            /* Report errors on unused parameters. */\n    // \"noImplicitReturns\": true,             /* Report error when not all code paths in function return a value. */\n    // \"noFallthroughCasesInSwitch\": true,    /* Report errors for fallthrough cases in switch statement. */\n\n    /* Module Resolution Options */\n    // \"moduleResolution\": \"node\",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */\n    // \"baseUrl\": \"./\",                       /* Base directory to resolve non-absolute module names. */\n    // \"paths\": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */\n    // \"rootDirs\": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */\n    // \"typeRoots\": [],                       /* List of folders to include type definitions from. */\n    // \"types\": [],                           /* Type declaration files to be included in compilation. */\n    // \"allowSyntheticDefaultImports\": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */\n    \"esModuleInterop\": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,\n    // \"preserveSymlinks\": true,              /* Do not resolve the real path of symlinks. */\n    // \"allowUmdGlobalAccess\": true,          /* Allow accessing UMD globals from modules. */\n\n    /* Source Map Options */\n    // \"sourceRoot\": \"\",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */\n    // \"mapRoot\": \"\",                         /* Specify the location where debugger should locate map files instead of generated locations. */\n    // \"inlineSourceMap\": true,               /* Emit a single file with source maps instead of having a separate file. */\n    // \"inlineSources\": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */\n\n    /* Experimental Options */\n    // \"experimentalDecorators\": true,        /* Enables experimental support for ES7 decorators. */\n    // \"emitDecoratorMetadata\": true,         /* Enables experimental support for emitting type metadata for decorators. */\n\n    /* Advanced Options */\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true /* Skip type checking of declaration files. */,\n    \"forceConsistentCasingInFileNames\": true /* Disallow inconsistently-cased references to the same file. */\n  },\n  \"exclude\": [\"tests\"]\n}\n"
  }
]