├── .deepsource.toml ├── .eslintrc.js ├── .github └── workflows │ ├── codeql-analysis.yml │ └── nodejs.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE.md ├── README.md ├── build └── postInstall.js ├── docs ├── classes │ ├── LoginRequestBody.html │ ├── LoginResponseBody.html │ ├── Mention.html │ ├── MockPostsRepository.html │ ├── MockUsersRepository.html │ ├── MooBaseEntity.html │ ├── PasswordEntity.html │ ├── PostCreateRequestBody.html │ ├── PostDetailsQueryParams.html │ ├── PostEntity.html │ ├── PostsRepository.html │ ├── SessionsEntity.html │ ├── TokenAuthorizer.html │ ├── UserCreateRequestBody.html │ ├── UserEntity.html │ ├── UserFollowingEntity.html │ ├── UserUpdateRequestBody.html │ └── UsersRepository.html ├── controllers │ ├── AppController.html │ ├── AuthController.html │ ├── HashtagsController.html │ ├── PostsController.html │ └── UsersController.html ├── coverage.html ├── dependencies.html ├── fonts │ ├── ionicons.eot │ ├── ionicons.svg │ ├── ionicons.ttf │ ├── ionicons.woff │ ├── ionicons.woff2 │ ├── roboto-v15-latin-300.eot │ ├── roboto-v15-latin-300.svg │ ├── roboto-v15-latin-300.ttf │ ├── roboto-v15-latin-300.woff │ ├── roboto-v15-latin-300.woff2 │ ├── roboto-v15-latin-700.eot │ ├── roboto-v15-latin-700.svg │ ├── roboto-v15-latin-700.ttf │ ├── roboto-v15-latin-700.woff │ ├── roboto-v15-latin-700.woff2 │ ├── roboto-v15-latin-italic.eot │ ├── roboto-v15-latin-italic.svg │ ├── roboto-v15-latin-italic.ttf │ ├── roboto-v15-latin-italic.woff │ ├── roboto-v15-latin-italic.woff2 │ ├── roboto-v15-latin-regular.eot │ ├── roboto-v15-latin-regular.svg │ ├── roboto-v15-latin-regular.ttf │ ├── roboto-v15-latin-regular.woff │ └── roboto-v15-latin-regular.woff2 ├── graph │ └── dependencies.svg ├── guards │ ├── OptionalAuthGuard.html │ └── RequiredAuthGuard.html ├── images │ ├── compodoc-vectorise-inverted.png │ ├── compodoc-vectorise-inverted.svg │ ├── compodoc-vectorise.png │ ├── compodoc-vectorise.svg │ ├── coverage-badge-documentation.svg │ └── favicon.ico ├── index.html ├── injectables │ ├── AppService.html │ ├── AuthService.html │ ├── PostsService.html │ └── UsersService.html ├── js │ ├── compodoc.js │ ├── lazy-load-graphs.js │ ├── libs │ │ ├── EventDispatcher.js │ │ ├── bootstrap-native.js │ │ ├── clipboard.min.js │ │ ├── custom-elements-es5-adapter.js │ │ ├── custom-elements.min.js │ │ ├── d3.v3.min.js │ │ ├── deep-iterator.js │ │ ├── es6-shim.min.js │ │ ├── htmlparser.js │ │ ├── innersvg.js │ │ ├── lit-html.js │ │ ├── prism.js │ │ ├── promise.min.js │ │ ├── svg-pan-zoom.min.js │ │ ├── tablesort.min.js │ │ ├── tablesort.number.min.js │ │ ├── vis.min.js │ │ └── zepto.min.js │ ├── menu-wc.js │ ├── menu-wc_es5.js │ ├── menu.js │ ├── routes.js │ ├── search │ │ ├── lunr.min.js │ │ ├── search-lunr.js │ │ ├── search.js │ │ └── search_index.js │ ├── sourceCode.js │ ├── svg-pan-zoom.controls.js │ ├── tabs.js │ └── tree.js ├── license.html ├── miscellaneous │ ├── functions.html │ └── variables.html ├── modules.html ├── modules │ ├── ApiModule.html │ ├── ApiModule │ │ └── dependencies.svg │ ├── AppModule.html │ ├── AppModule │ │ └── dependencies.svg │ ├── AuthModule.html │ ├── AuthModule │ │ └── dependencies.svg │ ├── HashtagsModule.html │ ├── MockPostsModule.html │ ├── MockPostsModule │ │ └── dependencies.svg │ ├── PostsModule.html │ ├── PostsModule │ │ └── dependencies.svg │ ├── ProdDbModule.html │ ├── TestDbModule.html │ ├── UsersModule.html │ └── UsersModule │ │ └── dependencies.svg ├── overview.html └── styles │ ├── bootstrap-card.css │ ├── bootstrap.min.css │ ├── compodoc.css │ ├── dark.css │ ├── ionicons.min.css │ ├── laravel.css │ ├── material.css │ ├── original.css │ ├── postmark.css │ ├── prism.css │ ├── readthedocs.css │ ├── reset.css │ ├── stripe.css │ ├── style.css │ ├── tablesort.css │ └── vagrant.css ├── nest-cli.json ├── package-lock.json ├── package.json ├── src ├── api.module.ts ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── auth │ ├── auth.controller.spec.ts │ ├── auth.controller.ts │ ├── auth.decorator.ts │ ├── auth.guard.spec.ts │ ├── auth.guard.ts │ ├── auth.module.ts │ ├── auth.service.spec.ts │ ├── auth.service.ts │ ├── passwords.entity.ts │ └── sessions.entity.ts ├── commons │ ├── base.entity.ts │ ├── db.module.ts │ └── mocks │ │ ├── likes.repository.mock.ts │ │ ├── mock.providers.ts │ │ ├── posts.repository.mock.ts │ │ └── users.repository.mock.ts ├── hashtags │ ├── hashtags.controller.spec.ts │ ├── hashtags.controller.ts │ └── hashtags.module.ts ├── likes │ ├── likes.controller.spec.ts │ ├── likes.controller.ts │ ├── likes.entity.ts │ ├── likes.module.mock.ts │ ├── likes.module.ts │ ├── likes.repository.ts │ ├── likes.service.spec.ts │ └── likes.service.ts ├── main.ts ├── posts │ ├── posts.controller.spec.ts │ ├── posts.controller.ts │ ├── posts.entity.ts │ ├── posts.module.mock.ts │ ├── posts.module.ts │ ├── posts.repository.ts │ ├── posts.service.spec.ts │ └── posts.service.ts └── users │ ├── user-followings.entity.ts │ ├── users.controller.spec.ts │ ├── users.controller.ts │ ├── users.entity.ts │ ├── users.module.ts │ ├── users.repository.ts │ ├── users.service.spec.ts │ └── users.service.ts ├── test ├── app.e2e-spec.ts ├── jest-e2e.json └── users │ └── users.e2e-spec.ts ├── tsconfig.build.json └── tsconfig.json /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | 4 | exclude_patterns = [ 5 | "dist/**", 6 | "docs/**" 7 | ] 8 | 9 | [[analyzers]] 10 | name = "javascript" 11 | enabled = true 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js', 'docs/**/*'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | # schedule: 21 | # - cron: '42 5 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'typescript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | postgres: 16 | image: postgres:latest 17 | env: 18 | POSTGRES_DB: moodb_test 19 | POSTGRES_PASSWORD: moopass 20 | POSTGRES_USER: mooadmin 21 | ports: 22 | - 5432:5432 23 | # Set health checks to wait until postgres has started 24 | options: >- 25 | --health-cmd pg_isready 26 | --health-interval 10s 27 | --health-timeout 5s 28 | --health-retries 5 29 | strategy: 30 | matrix: 31 | node-version: [14.x, 16.x] 32 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 33 | 34 | steps: 35 | - name: Checkout Code 36 | uses: actions/checkout@v2 37 | 38 | - name: Cache node modules 39 | uses: actions/cache@v2 40 | env: 41 | cache-name: cache-node-modules 42 | with: 43 | path: ~/.npm 44 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 45 | restore-keys: | 46 | ${{ runner.os }}-build-${{ env.cache-name }}- 47 | ${{ runner.os }}-build- 48 | ${{ runner.os }}- 49 | 50 | - name: Use Node.js ${{ matrix.node-version }} 51 | uses: actions/setup-node@v2 52 | with: 53 | node-version: ${{ matrix.node-version }} 54 | 55 | - name: Run CI 56 | run: npm ci 57 | 58 | - name: Build Application 59 | run: npm run build 60 | 61 | - name: Run Unit Tests (with Coverage) 62 | run: npm run test:cov 63 | 64 | - name: Upload unit test coverage to Codecov 65 | uses: codecov/codecov-action@v2 66 | with: 67 | token: ${{ secrets.CODECOV_TOKEN }} 68 | directory: ./coverage 69 | fail_ci_if_error: true 70 | flags: unittests 71 | verbose: true 72 | 73 | - name: Run E2E Tests (with Coverage) 74 | run: npm run test:e2e:cov 75 | 76 | - name: Upload E2E test coverage to Codecov 77 | uses: codecov/codecov-action@v2 78 | with: 79 | token: ${{ secrets.CODECOV_TOKEN }} 80 | directory: ./coverage-e2e 81 | fail_ci_if_error: true 82 | flags: e2etests 83 | verbose: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /coverage-e2e 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moo - A Twitter Clone 2 | 3 | Backend API for Moo using Nest framework (NodeJS + TypeScript + PostgreSQL) 4 | 5 | ![NodeJS Workflow](https://github.com/scaleracademy/twitter-backend-node/actions/workflows/nodejs.yml/badge.svg) 6 | [![DeepSource](https://deepsource.io/gh/scaleracademy/twitter-backend-node.svg/?label=active+issues&show_trend=true&token=ZAK6LYPxCowffXKqHhnZHsMI)](https://deepsource.io/gh/scaleracademy/twitter-backend-node/?ref=repository-badge) 7 | [![codecov](https://codecov.io/gh/scaleracademy/twitter-backend-node/branch/main/graph/badge.svg?token=WrC1l2AeDE)](https://codecov.io/gh/scaleracademy/twitter-backend-node) 8 | 9 | ## About 10 | 11 | ### Idea 12 | 13 | Moo is a parody of Twitter. Further information about features and DB schema requirements can be found on this discussion board : 14 | https://github.com/scaleracademy/open-source-projects/discussions/81 15 | 16 | ### UI Design 17 | 18 | The UI is being designed on Figma if you'd like to view 19 | 20 | - [Figma UI Prototype](https://www.figma.com/file/i7IjqvJVL6c5h2Tdzuul3c/Moo-Twitter-Design-File) 21 | - [Figma Discussion Jam Board](https://www.figma.com/file/onuHbJL39i2be0OosK4vYX/Moo-Twitter-Discussion-Board?node-id=0%3A1) 22 | 23 | ### Tutorials 24 | 25 | If you'd like to see how the initial project was built please watch the following YouTube video 26 | 27 | 28 |
29 | ▶️ Building Twitter Clone from Scratch | End-to-End Coding Project 30 |
31 | 32 | ## Installation 33 | 34 | ```bash 35 | $ npm install 36 | ``` 37 | 38 | ## Running the app 39 | 40 | ```bash 41 | # development 42 | $ npm run start 43 | 44 | # watch mode 45 | $ npm run start:dev 46 | 47 | # production mode 48 | $ npm run start:prod 49 | ``` 50 | 51 | ## Test 52 | 53 | ```bash 54 | # unit tests 55 | $ npm run test 56 | 57 | # e2e tests 58 | $ npm run test:e2e 59 | 60 | # test coverage 61 | $ npm run test:cov 62 | ``` 63 | 64 | ## Setup Database 65 | 66 | ```psql 67 | create database moodb; 68 | create user mooadmin with password 'moopass'; 69 | grant all privileges on database moodb to mooadmin; 70 | ``` 71 | 72 | ## Progress 73 | 74 | - `auth` 75 | 76 | - [x] `POST /auth/login` 77 | 78 | - `users` 79 | 80 | - [ ] `GET /users` 📃 81 | - [x] `GET /users/@{username}` 82 | - [x] `GET /users/{userid}` 83 | - [x] `POST /users` 84 | - [x] `PATCH /users/{userid}` 🔒 85 | - [x] `PUT /users/{userid}/follow` 🔒 86 | - [x] `DELETE /users/{userid}/follow` 🔒 87 | - [ ] `GET /users/{userid}/followers` 📃 88 | - [ ] `GET /users/{userid}/followees` 📃 89 | 90 | - `posts` 91 | 92 | - [ ] `GET /posts` 📃 93 | - [x] filter by author 94 | - [ ] filter by replyTo 95 | - [ ] filter by origPosts 96 | - [ ] full-text-search on post content 97 | - [x] `GET /posts/{postid}` 98 | - [ ] `POST /posts` 🔒 99 | - [x] simple posts 100 | - [x] reply to a post 101 | - [x] repost / quote post 102 | - [ ] \#hashtags 103 | - [ ] \@mentions 104 | - [x] `DELETE /posts/{postid}` 🔒 105 | - [x] `PUT /posts/{postid}/like` 🔒 106 | - [x] `DELETE /posts/{postid}/like` 🔒 107 | 108 | - `hashtags` 109 | - [ ] `GET /hashtags` 📃 110 | - [ ] `GET /hashtags/{tag}/posts` 📃 111 | 112 | ## License 113 | 114 | This project is under the GNU AGPL v3.0 license 115 | -------------------------------------------------------------------------------- /build/postInstall.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require('os'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | 6 | /** 7 | * Fix compilation issues in jsdom files. 8 | */ 9 | function updateJSDomTypeDefinition() { 10 | var relativePath = path.join('node_modules', '@types', 'jsdom', 'base.d.ts'); 11 | var filePath = relativePath; 12 | if (!fs.existsSync(filePath)) { 13 | console.warn("JSdom base.d.ts not found '" + filePath + "' (Jupyter Extension post install script)"); 14 | return; 15 | } 16 | var fileContents = fs.readFileSync(filePath, { encoding: 'utf8' }); 17 | var replacedContents = fileContents.replace( 18 | /\s*globalThis: DOMWindow;\s*readonly \["Infinity"]: number;\s*readonly \["NaN"]: number;/g, 19 | [ 20 | 'globalThis: DOMWindow;', 21 | '// @ts-ignore', 22 | 'readonly ["Infinity"]: number;', 23 | '// @ts-ignore', 24 | 'readonly ["NaN"]: number;' 25 | ].join(`${EOL} `) 26 | ); 27 | if (replacedContents === fileContents) { 28 | console.warn('JSdom base.d.ts not updated'); 29 | return; 30 | } 31 | fs.writeFileSync(filePath, replacedContents); 32 | } 33 | 34 | 35 | updateJSDomTypeDefinition(); 36 | -------------------------------------------------------------------------------- /docs/classes/MockPostsRepository.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twitter-backend-node documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 |
26 |
27 | 30 | 31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 57 | 58 |
59 |
60 |

61 |

File

62 |

63 |

64 | src/commons/mocks/posts.repository.mock.ts 65 |

66 | 67 | 68 | 69 |

70 |

Extends

71 |

72 |

73 | Repository 74 |

75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
88 | 89 | 90 |
91 |
import { PostEntity } from 'src/posts/posts.entity';
 92 | import { Repository } from 'typeorm';
 93 | 
 94 | export class MockPostsRepository extends Repository<PostEntity> {}
 95 | 
96 |
97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 |
108 |

results matching ""

109 |
    110 |
    111 |
    112 |

    No results matching ""

    113 |
    114 |
    115 |
    116 | 117 |
    118 |
    119 | 120 | 128 | 129 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /docs/classes/PostsRepository.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twitter-backend-node documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 |
    26 |
    27 | 30 | 31 |
    32 |
    33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 57 | 58 |
    59 |
    60 |

    61 |

    File

    62 |

    63 |

    64 | src/posts/posts.repository.ts 65 |

    66 | 67 | 68 | 69 |

    70 |

    Extends

    71 |

    72 |

    73 | Repository 74 |

    75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
    88 | 89 | 90 |
    91 |
    import { EntityRepository, Repository } from 'typeorm';
     92 | import { PostEntity } from './posts.entity';
     93 | 
     94 | @EntityRepository(PostEntity)
     95 | export class PostsRepository extends Repository<PostEntity> {}
     96 | 
    97 |
    98 |
    99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
    108 |
    109 |

    results matching ""

    110 |
      111 |
      112 |
      113 |

      No results matching ""

      114 |
      115 |
      116 |
      117 | 118 |
      119 |
      120 | 121 | 129 | 130 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /docs/classes/UsersRepository.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twitter-backend-node documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 |
      26 |
      27 | 30 | 31 |
      32 |
      33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 57 | 58 |
      59 |
      60 |

      61 |

      File

      62 |

      63 |

      64 | src/users/users.repository.ts 65 |

      66 | 67 | 68 | 69 |

      70 |

      Extends

      71 |

      72 |

      73 | Repository 74 |

      75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
      88 | 89 | 90 |
      91 |
      import { EntityRepository, Repository } from 'typeorm';
       92 | import { UserEntity } from './users.entity';
       93 | 
       94 | @EntityRepository(UserEntity)
       95 | export class UsersRepository extends Repository<UserEntity> {}
       96 | 
      97 |
      98 |
      99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
      108 |
      109 |

      results matching ""

      110 |
        111 |
        112 |
        113 |

        No results matching ""

        114 |
        115 |
        116 |
        117 | 118 |
        119 |
        120 | 121 | 129 | 130 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /docs/dependencies.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twitter-backend-node documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 |
        26 |
        27 | 30 | 31 |
        32 |
        33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 |
          51 |
        • 52 | @nestjs/common : ^8.2.3
        • 53 |
        • 54 | @nestjs/core : ^8.2.3
        • 55 |
        • 56 | @nestjs/platform-fastify : ^8.2.3
        • 57 |
        • 58 | @nestjs/swagger : ^5.1.5
        • 59 |
        • 60 | @nestjs/typeorm : ^8.0.2
        • 61 |
        • 62 | bcrypt : ^5.0.1
        • 63 |
        • 64 | fastify-compress : ^3.6.0
        • 65 |
        • 66 | fastify-swagger : ^4.8.0
        • 67 |
        • 68 | pg : ^8.6.0
        • 69 |
        • 70 | pg-hstore : ^2.3.4
        • 71 |
        • 72 | reflect-metadata : ^0.1.13
        • 73 |
        • 74 | rimraf : ^3.0.2
        • 75 |
        • 76 | rxjs : ^7.4.0
        • 77 |
        • 78 | typeorm : ^0.2.34
        • 79 |
        80 | 81 | 82 | 83 | 84 | 85 | 86 |
        87 |
        88 |

        results matching ""

        89 |
          90 |
          91 |
          92 |

          No results matching ""

          93 |
          94 |
          95 |
          96 | 97 |
          98 |
          99 | 100 | 108 | 109 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /docs/fonts/ionicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/ionicons.eot -------------------------------------------------------------------------------- /docs/fonts/ionicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/ionicons.ttf -------------------------------------------------------------------------------- /docs/fonts/ionicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/ionicons.woff -------------------------------------------------------------------------------- /docs/fonts/ionicons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/ionicons.woff2 -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-300.eot -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-300.ttf -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-300.woff -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-300.woff2 -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-700.eot -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-700.ttf -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-700.woff -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-700.woff2 -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-italic.eot -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-italic.ttf -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-italic.woff -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-italic.woff2 -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-regular.eot -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-regular.ttf -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-regular.woff -------------------------------------------------------------------------------- /docs/fonts/roboto-v15-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/fonts/roboto-v15-latin-regular.woff2 -------------------------------------------------------------------------------- /docs/images/compodoc-vectorise-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/images/compodoc-vectorise-inverted.png -------------------------------------------------------------------------------- /docs/images/compodoc-vectorise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/images/compodoc-vectorise.png -------------------------------------------------------------------------------- /docs/images/coverage-badge-documentation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | documentation 7 | 1% 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleracademy/twitter-backend-node/7052fb9f1e1da61e35dd40556c2d0de0d53a5ec1/docs/images/favicon.ico -------------------------------------------------------------------------------- /docs/injectables/AppService.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twitter-backend-node documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 |
          26 |
          27 | 30 | 31 |
          32 |
          33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 53 | 54 |
          55 |
          56 |

          57 |

          File

          58 |

          59 |

          60 | src/app.service.ts 61 |

          62 | 63 | 64 | 65 | 66 | 67 |
          68 |

          Index

          69 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
          74 |
          Methods
          75 |
          79 | 84 |
          93 |
          94 | 95 | 96 |
          97 | 98 |

          99 | Methods 100 |

          101 | 102 | 103 | 104 | 111 | 112 | 113 | 116 | 117 | 118 | 119 | 120 | 124 | 125 | 126 | 127 | 128 | 135 | 136 | 137 |
          105 | 106 | 107 | getHello 108 | 109 | 110 |
          114 | getHello() 115 |
          121 | 123 |
          129 | 130 |
          131 | Returns : string 132 | 133 |
          134 |
          138 |
          139 | 140 |
          141 | 142 | 143 |
          144 |
          import { Injectable } from '@nestjs/common';
          145 | 
          146 | @Injectable()
          147 | export class AppService {
          148 |   getHello(): string {
          149 |     return 'Hello World!';
          150 |   }
          151 | }
          152 | 
          153 |
          154 | 155 |
          156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 |
          169 |
          170 |

          results matching ""

          171 |
            172 |
            173 |
            174 |

            No results matching ""

            175 |
            176 |
            177 |
            178 | 179 |
            180 |
            181 | 182 | 190 | 191 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /docs/js/compodoc.js: -------------------------------------------------------------------------------- 1 | var compodoc = { 2 | EVENTS: { 3 | READY: 'compodoc.ready', 4 | SEARCH_READY: 'compodoc.search.ready' 5 | } 6 | }; 7 | 8 | Object.assign( compodoc, EventDispatcher.prototype ); 9 | 10 | document.addEventListener('DOMContentLoaded', function() { 11 | compodoc.dispatchEvent({ 12 | type: compodoc.EVENTS.READY 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /docs/js/lazy-load-graphs.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | var lazyGraphs = [].slice.call(document.querySelectorAll('[lazy]')); 3 | var active = false; 4 | 5 | var lazyLoad = function() { 6 | if (active === false) { 7 | active = true; 8 | 9 | setTimeout(function() { 10 | lazyGraphs.forEach(function(lazyGraph) { 11 | if ( 12 | lazyGraph.getBoundingClientRect().top <= window.innerHeight && 13 | lazyGraph.getBoundingClientRect().bottom >= 0 && 14 | getComputedStyle(lazyGraph).display !== 'none' 15 | ) { 16 | lazyGraph.data = lazyGraph.getAttribute('lazy'); 17 | lazyGraph.removeAttribute('lazy'); 18 | 19 | lazyGraphs = lazyGraphs.filter(function(image) { return image !== lazyGraph}); 20 | 21 | if (lazyGraphs.length === 0) { 22 | document.removeEventListener('scroll', lazyLoad); 23 | window.removeEventListener('resize', lazyLoad); 24 | window.removeEventListener('orientationchange', lazyLoad); 25 | } 26 | } 27 | }); 28 | 29 | active = false; 30 | }, 200); 31 | } 32 | }; 33 | 34 | // initial load 35 | lazyLoad(); 36 | 37 | var container = document.querySelector('.container-fluid.modules'); 38 | if (container) { 39 | container.addEventListener('scroll', lazyLoad); 40 | window.addEventListener('resize', lazyLoad); 41 | window.addEventListener('orientationchange', lazyLoad); 42 | } 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /docs/js/libs/EventDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | var EventDispatcher=function(){};Object.assign(EventDispatcher.prototype,{addEventListener:function(i,t){void 0===this._listeners&&(this._listeners={});var e=this._listeners;void 0===e[i]&&(e[i]=[]),-1===e[i].indexOf(t)&&e[i].push(t)},hasEventListener:function(i,t){if(void 0===this._listeners)return!1;var e=this._listeners;return void 0!==e[i]&&-1!==e[i].indexOf(t)},removeEventListener:function(i,t){if(void 0!==this._listeners){var e=this._listeners[i];if(void 0!==e){var s=e.indexOf(t);-1!==s&&e.splice(s,1)}}},dispatchEvent:function(i){if(void 0!==this._listeners){var t=this._listeners[i.type];if(void 0!==t){i.target=this;var e=[],s=0,n=t.length;for(s=0;s1?t[t.length-1]:void 0:t[0]},this.getActiveContent=function(){var t=this.getActiveTab().getElementsByTagName("A")[0].getAttribute("href").replace("#","");return t&&document.getElementById("c-"+t)},this.tab.addEventListener("click",this.handle,!1)},d=document.querySelectorAll("[data-toggle='tab'], [data-toggle='pill']"),u=0,h=d.length;u",">"));else if(1==i){if(r.push("<",e.tagName),e.hasAttributes())for(var n=e.attributes,s=0,o=n.length;s");for(var h=e.childNodes,s=0,o=h.length;s")}else r.push("/>")}else{if(8!=i)throw"Error serializing XML. Unhandled node of type: "+i;r.push("\x3c!--",e.nodeValue,"--\x3e")}};Object.defineProperty(e.prototype,"innerHTML",{get:function(){for(var e=[],r=this.firstChild;r;)t(r,e),r=r.nextSibling;return e.join("")},set:function(e){for(;this.firstChild;)this.removeChild(this.firstChild);try{var t=new DOMParser;t.async=!1,sXML=""+e+"";for(var r=t.parseFromString(sXML,"text/xml").documentElement.firstChild;r;)this.appendChild(this.ownerDocument.importNode(r,!0)),r=r.nextSibling}catch(e){throw new Error("Error parsing XML string")}}})}}((0,eval)("this").SVGElement); -------------------------------------------------------------------------------- /docs/js/libs/promise.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2013 (c) Pierre Duquesne 3 | * Licensed under the New BSD License. 4 | * https://github.com/stackp/promisejs 5 | */ 6 | (function(a){function b(){this._callbacks=[];}b.prototype.then=function(a,c){var d;if(this._isdone)d=a.apply(c,this.result);else{d=new b();this._callbacks.push(function(){var b=a.apply(c,arguments);if(b&&typeof b.then==='function')b.then(d.done,d);});}return d;};b.prototype.done=function(){this.result=arguments;this._isdone=true;for(var a=0;a=300)&&j.status!==304);h.done(a,j.responseText,j);}};j.send(k);return h;}function h(a){return function(b,c,d){return g(a,b,c,d);};}var i={Promise:b,join:c,chain:d,ajax:g,get:h('GET'),post:h('POST'),put:h('PUT'),del:h('DELETE'),ENOXHR:1,ETIMEOUT:2,ajaxTimeout:0};if(typeof define==='function'&&define.amd)define(function(){return i;});else a.promise=i;})(this); -------------------------------------------------------------------------------- /docs/js/libs/tablesort.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * tablesort v5.1.0 (2018-09-14) 3 | * http://tristen.ca/tablesort/demo/ 4 | * Copyright (c) 2018 ; Licensed MIT 5 | */ 6 | !function(){function a(b,c){if(!(this instanceof a))return new a(b,c);if(!b||"TABLE"!==b.tagName)throw new Error("Element must be a table");this.init(b,c||{})}var b=[],c=function(a){var b;return window.CustomEvent&&"function"==typeof window.CustomEvent?b=new CustomEvent(a):(b=document.createEvent("CustomEvent"),b.initCustomEvent(a,!1,!1,void 0)),b},d=function(a){return a.getAttribute("data-sort")||a.textContent||a.innerText||""},e=function(a,b){return a=a.trim().toLowerCase(),b=b.trim().toLowerCase(),a===b?0:a0)if(a.tHead&&a.tHead.rows.length>0){for(e=0;e0&&l.push(k),m++;if(!l)return}for(m=0;m 0) { 9 | tabs = tabs[0].querySelectorAll('li'); 10 | for (var i = 0; i < tabs.length; i++) { 11 | tabs[i].addEventListener('click', updateAddress); 12 | var linkTag = tabs[i].querySelector('a'); 13 | if (location.hash !== '') { 14 | var currentHash = location.hash.substr(1); 15 | if (currentHash === linkTag.dataset.link) { 16 | linkTag.click(); 17 | } 18 | } 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /docs/js/tree.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | var tabs = document.getElementsByClassName('nav-tabs')[0], 3 | tabsCollection = tabs.getElementsByTagName('A'), 4 | treeTab; 5 | var len = tabsCollection.length; 6 | for(var i = 0; i < len; i++) { 7 | if (tabsCollection[i].getAttribute('id') === 'tree-tab') { 8 | treeTab = tabsCollection[i]; 9 | } 10 | } 11 | 12 | // short-circuit if no tree tab 13 | if (!treeTab) return; 14 | 15 | var handler = new Tautologistics.NodeHtmlParser.HtmlBuilder(function(error, dom) { 16 | if (error) { 17 | console.log('handler ko'); 18 | } 19 | }), 20 | parser = new Tautologistics.NodeHtmlParser.Parser(handler), 21 | currentLocation = window.location; 22 | parser.parseComplete(COMPONENT_TEMPLATE); 23 | 24 | var newNodes = [], 25 | newEdges = [], 26 | parsedHtml = handler.dom[0], 27 | nodeCount = 0, 28 | nodeLevel = 0; 29 | 30 | newNodes.push({ 31 | _id: 0, 32 | label: parsedHtml.name, 33 | type: parsedHtml.type 34 | }) 35 | //Add id for nodes 36 | var traverseIds = function(o) { 37 | for (i in o) { 38 | if (!!o[i] && typeof(o[i]) == "object") { 39 | if (!o[i].length && o[i].type === 'tag') { 40 | nodeCount += 1; 41 | o[i]._id = nodeCount; 42 | } 43 | traverseIds(o[i]); 44 | } 45 | } 46 | } 47 | parsedHtml._id = 0; 48 | traverseIds(parsedHtml); 49 | 50 | 51 | var DeepIterator = deepIterator.default, 52 | it = DeepIterator(parsedHtml); 53 | for (let { 54 | value, 55 | parent, 56 | parentNode, 57 | key, 58 | type 59 | } of it) { 60 | if (type === 'NonIterableObject' && typeof key !== 'undefined' && value.type === 'tag') { 61 | var newNode = { 62 | id: value._id, 63 | label: value.name, 64 | type: value.type 65 | }; 66 | for(var i = 0; i < COMPONENTS.length; i++) { 67 | if (COMPONENTS[i].selector === value.name) { 68 | newNode.font = { 69 | multi: 'html' 70 | }; 71 | newNode.label = '' + newNode.label + ''; 72 | newNode.color = '#FB7E81'; 73 | newNode.name = COMPONENTS[i].name; 74 | } 75 | } 76 | for(var i = 0; i < DIRECTIVES.length; i++) { 77 | if (value.attributes) { 78 | for(attr in value.attributes) { 79 | if (DIRECTIVES[i].selector.indexOf(attr) !== -1) { 80 | newNode.font = { 81 | multi: 'html' 82 | }; 83 | newNode.label = '' + newNode.label + ''; 84 | newNode.color = '#FF9800'; 85 | newNode.name = DIRECTIVES[i].name; 86 | } 87 | } 88 | } 89 | } 90 | newNodes.push(newNode); 91 | newEdges.push({ 92 | from: parentNode._parent._id, 93 | to: value._id, 94 | arrows: 'to' 95 | }); 96 | } 97 | } 98 | 99 | newNodes.shift(); 100 | 101 | var container = document.getElementById('tree-container'), 102 | data = { 103 | nodes: newNodes, 104 | edges: newEdges 105 | }, 106 | options = { 107 | layout: { 108 | hierarchical: { 109 | sortMethod: 'directed', 110 | enabled: true 111 | } 112 | }, 113 | nodes: { 114 | shape: 'ellipse', 115 | fixed: true 116 | } 117 | }, 118 | 119 | handleClickNode = function(params) { 120 | var clickeNodeId; 121 | if (params.nodes.length > 0) { 122 | clickeNodeId = params.nodes[0]; 123 | for(var i = 0; i < newNodes.length; i++) { 124 | if (newNodes[i].id === clickeNodeId) { 125 | for(var j = 0; j < COMPONENTS.length; j++) { 126 | if (COMPONENTS[j].name === newNodes[i].name) { 127 | document.location.href = currentLocation.origin + currentLocation.pathname.replace(ACTUAL_COMPONENT.name, newNodes[i].name); 128 | } 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | 135 | loadTree = function () { 136 | setTimeout(function() { 137 | container.style.height = document.getElementsByClassName('content')[0].offsetHeight - 140 + 'px'; 138 | var network = new vis.Network(container, data, options); 139 | network.on('click', handleClickNode); 140 | }, 200); // Fade is 0.150 141 | }; 142 | 143 | loadTree(); 144 | treeTab.addEventListener('click', function() { 145 | loadTree(); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /docs/miscellaneous/functions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twitter-backend-node documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 |
            26 |
            27 | 30 | 31 |
            32 |
            33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 |
            55 |

            Index

            56 | 57 | 58 | 59 | 66 | 67 | 68 |
            60 | 65 |
            69 |
            70 | 71 |

            src/main.ts

            72 |
            73 |

            74 | 75 | 76 | 83 | 84 | 85 | 88 | 89 | 90 | 91 | 92 | 93 | 94 |
            77 | 78 | 79 | bootstrap 80 | 81 | 82 |
            86 | bootstrap() 87 |
            95 |
            96 | 97 | 98 |
            99 |
            100 |

            results matching ""

            101 |
              102 |
              103 |
              104 |

              No results matching ""

              105 |
              106 |
              107 |
              108 | 109 |
              110 |
              111 | 112 | 120 | 121 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /docs/modules/ApiModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_ApiModule 15 | 16 | 17 | 18 | cluster_ApiModule_imports 19 | 20 | 21 | 22 | 23 | AuthModule 24 | 25 | AuthModule 26 | 27 | 28 | 29 | ApiModule 30 | 31 | ApiModule 32 | 33 | 34 | 35 | AuthModule->ApiModule 36 | 37 | 38 | 39 | 40 | 41 | HashtagsModule 42 | 43 | HashtagsModule 44 | 45 | 46 | 47 | HashtagsModule->ApiModule 48 | 49 | 50 | 51 | 52 | 53 | PostsModule 54 | 55 | PostsModule 56 | 57 | 58 | 59 | PostsModule->ApiModule 60 | 61 | 62 | 63 | 64 | 65 | UsersModule 66 | 67 | UsersModule 68 | 69 | 70 | 71 | UsersModule->ApiModule 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/modules/AppModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_AppModule 15 | 16 | 17 | 18 | cluster_AppModule_providers 19 | 20 | 21 | 22 | cluster_AppModule_imports 23 | 24 | 25 | 26 | 27 | ApiModule 28 | 29 | ApiModule 30 | 31 | 32 | 33 | AppModule 34 | 35 | AppModule 36 | 37 | 38 | 39 | ApiModule->AppModule 40 | 41 | 42 | 43 | 44 | 45 | ProdDbModule 46 | 47 | ProdDbModule 48 | 49 | 50 | 51 | ProdDbModule->AppModule 52 | 53 | 54 | 55 | 56 | 57 | AppService 58 | 59 | AppService 60 | 61 | 62 | 63 | AppService->AppModule 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/modules/AuthModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_AuthModule 15 | 16 | 17 | 18 | cluster_AuthModule_exports 19 | 20 | 21 | 22 | cluster_AuthModule_providers 23 | 24 | 25 | 26 | 27 | AuthService 28 | 29 | AuthService 30 | 31 | 32 | 33 | AuthModule 34 | 35 | AuthModule 36 | 37 | 38 | 39 | AuthModule->AuthService 40 | 41 | 42 | 43 | 44 | 45 | AuthService 46 | 47 | AuthService 48 | 49 | 50 | 51 | AuthService->AuthModule 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /docs/modules/HashtagsModule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twitter-backend-node documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 |
              26 |
              27 | 30 | 31 |
              32 |
              33 | 34 | 35 | 36 | 40 | 41 | 42 | 50 | 51 |
              52 |
              53 | 54 |

              55 |

              File

              56 |

              57 |

              58 | src/hashtags/hashtags.module.ts 59 |

              60 | 61 | 62 | 63 | 64 | 65 |
              66 |
              67 |
              68 |

              Controllers

              70 | 75 |
              76 |
              77 |
              78 | 79 | 80 |
              81 | 82 | 83 |
              84 |
              import { Module } from '@nestjs/common';
               85 | import { HashtagsController } from './hashtags.controller';
               86 | 
               87 | @Module({
               88 |   controllers: [HashtagsController],
               89 | })
               90 | export class HashtagsModule {}
               91 | 
              92 |
              93 |
              94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
              110 |
              111 |

              results matching ""

              112 |
                113 |
                114 |
                115 |

                No results matching ""

                116 |
                117 |
                118 |
                119 | 120 |
                121 |
                122 | 123 | 131 | 132 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /docs/modules/MockPostsModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_MockPostsModule 15 | 16 | 17 | 18 | cluster_MockPostsModule_providers 19 | 20 | 21 | 22 | cluster_MockPostsModule_exports 23 | 24 | 25 | 26 | 27 | AuthService 28 | 29 | AuthService 30 | 31 | 32 | 33 | RequiredAuthGuard 34 | 35 | RequiredAuthGuard 36 | 37 | 38 | 39 | MockPostsModule 40 | 41 | MockPostsModule 42 | 43 | 44 | 45 | MockPostsModule->AuthService 46 | 47 | 48 | 49 | 50 | 51 | MockPostsModule->RequiredAuthGuard 52 | 53 | 54 | 55 | 56 | 57 | AuthService 58 | 59 | AuthService 60 | 61 | 62 | 63 | AuthService->MockPostsModule 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/modules/PostsModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_PostsModule 15 | 16 | 17 | 18 | cluster_PostsModule_providers 19 | 20 | 21 | 22 | 23 | PostsService 24 | 25 | PostsService 26 | 27 | 28 | 29 | PostsModule 30 | 31 | PostsModule 32 | 33 | 34 | 35 | PostsService->PostsModule 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/modules/ProdDbModule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twitter-backend-node documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 |
                26 |
                27 | 30 | 31 |
                32 |
                33 | 34 | 35 | 36 | 40 | 41 | 42 | 50 | 51 |
                52 |
                53 | 54 |

                55 |

                File

                56 |

                57 |

                58 | src/commons/db.module.ts 59 |

                60 | 61 | 62 | 63 |

                64 |

                Description

                65 |

                66 |

                67 |

                Database module for production

                68 | 69 |

                70 | 71 | 72 |
                73 |
                74 |
                75 |
                76 | 77 | 78 |
                79 | 80 | 81 |
                82 |
                import { Global, Module } from '@nestjs/common';
                 83 | import { TypeOrmModule } from '@nestjs/typeorm';
                 84 | import { PasswordEntity } from 'src/auth/passwords.entity';
                 85 | import { SessionsEntity } from 'src/auth/sessions.entity';
                 86 | import { PostEntity } from 'src/posts/posts.entity';
                 87 | import { UserFollowingEntity } from 'src/users/user-followings.entity';
                 88 | import { UserEntity } from 'src/users/users.entity';
                 89 | 
                 90 | /**
                 91 |  * Database module for production
                 92 |  */
                 93 | @Global()
                 94 | @Module({
                 95 |   imports: [
                 96 |     TypeOrmModule.forRoot({
                 97 |       type: 'postgres',
                 98 |       username: 'mooadmin',
                 99 |       password: 'moopass',
                100 |       database: 'moodb',
                101 |       synchronize: true,
                102 |       logger: 'advanced-console',
                103 |       logging: 'all',
                104 |       entities: [
                105 |         UserEntity,
                106 |         PostEntity,
                107 |         PasswordEntity,
                108 |         SessionsEntity,
                109 |         UserFollowingEntity,
                110 |       ],
                111 |     }),
                112 |   ],
                113 | })
                114 | export class ProdDbModule {}
                115 | 
                116 | /**
                117 |  * Database module for testing purposes
                118 |  */
                119 | @Global()
                120 | @Module({
                121 |   imports: [
                122 |     TypeOrmModule.forRoot({
                123 |       type: 'postgres',
                124 |       username: 'mooadmin',
                125 |       password: 'moopass',
                126 |       database: 'moodb_test',
                127 |       synchronize: true,
                128 |       dropSchema: true,
                129 |       logger: 'advanced-console',
                130 |       logging: 'all',
                131 |       entities: [
                132 |         UserEntity,
                133 |         PostEntity,
                134 |         PasswordEntity,
                135 |         SessionsEntity,
                136 |         UserFollowingEntity,
                137 |       ],
                138 |     }),
                139 |   ],
                140 | })
                141 | export class TestDbModule {}
                142 | 
                143 |
                144 |
                145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
                161 |
                162 |

                results matching ""

                163 |
                  164 |
                  165 |
                  166 |

                  No results matching ""

                  167 |
                  168 |
                  169 |
                  170 | 171 |
                  172 |
                  173 | 174 | 182 | 183 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /docs/modules/TestDbModule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | twitter-backend-node documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 24 | 25 |
                  26 |
                  27 | 30 | 31 |
                  32 |
                  33 | 34 | 35 | 36 | 40 | 41 | 42 | 50 | 51 |
                  52 |
                  53 | 54 |

                  55 |

                  File

                  56 |

                  57 |

                  58 | src/commons/db.module.ts 59 |

                  60 | 61 | 62 | 63 |

                  64 |

                  Description

                  65 |

                  66 |

                  67 |

                  Database module for testing purposes

                  68 | 69 |

                  70 | 71 | 72 |
                  73 |
                  74 |
                  75 |
                  76 | 77 | 78 |
                  79 | 80 | 81 |
                  82 |
                  import { Global, Module } from '@nestjs/common';
                   83 | import { TypeOrmModule } from '@nestjs/typeorm';
                   84 | import { PasswordEntity } from 'src/auth/passwords.entity';
                   85 | import { SessionsEntity } from 'src/auth/sessions.entity';
                   86 | import { PostEntity } from 'src/posts/posts.entity';
                   87 | import { UserFollowingEntity } from 'src/users/user-followings.entity';
                   88 | import { UserEntity } from 'src/users/users.entity';
                   89 | 
                   90 | /**
                   91 |  * Database module for production
                   92 |  */
                   93 | @Global()
                   94 | @Module({
                   95 |   imports: [
                   96 |     TypeOrmModule.forRoot({
                   97 |       type: 'postgres',
                   98 |       username: 'mooadmin',
                   99 |       password: 'moopass',
                  100 |       database: 'moodb',
                  101 |       synchronize: true,
                  102 |       logger: 'advanced-console',
                  103 |       logging: 'all',
                  104 |       entities: [
                  105 |         UserEntity,
                  106 |         PostEntity,
                  107 |         PasswordEntity,
                  108 |         SessionsEntity,
                  109 |         UserFollowingEntity,
                  110 |       ],
                  111 |     }),
                  112 |   ],
                  113 | })
                  114 | export class ProdDbModule {}
                  115 | 
                  116 | /**
                  117 |  * Database module for testing purposes
                  118 |  */
                  119 | @Global()
                  120 | @Module({
                  121 |   imports: [
                  122 |     TypeOrmModule.forRoot({
                  123 |       type: 'postgres',
                  124 |       username: 'mooadmin',
                  125 |       password: 'moopass',
                  126 |       database: 'moodb_test',
                  127 |       synchronize: true,
                  128 |       dropSchema: true,
                  129 |       logger: 'advanced-console',
                  130 |       logging: 'all',
                  131 |       entities: [
                  132 |         UserEntity,
                  133 |         PostEntity,
                  134 |         PasswordEntity,
                  135 |         SessionsEntity,
                  136 |         UserFollowingEntity,
                  137 |       ],
                  138 |     }),
                  139 |   ],
                  140 | })
                  141 | export class TestDbModule {}
                  142 | 
                  143 |
                  144 |
                  145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
                  161 |
                  162 |

                  results matching ""

                  163 |
                    164 |
                    165 |
                    166 |

                    No results matching ""

                    167 |
                    168 |
                    169 |
                    170 | 171 |
                    172 |
                    173 | 174 | 182 | 183 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /docs/modules/UsersModule/dependencies.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | dependencies 11 | 12 | dependencies 13 | 14 | cluster_UsersModule 15 | 16 | 17 | 18 | cluster_UsersModule_providers 19 | 20 | 21 | 22 | 23 | UsersService 24 | 25 | UsersService 26 | 27 | 28 | 29 | UsersModule 30 | 31 | UsersModule 32 | 33 | 34 | 35 | UsersService->UsersModule 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/styles/bootstrap-card.css: -------------------------------------------------------------------------------- 1 | .card { 2 | position: relative; 3 | display: block; 4 | margin-bottom: 20px; 5 | background-color: #fff; 6 | border: 1px solid #ddd; 7 | border-radius: 4px; 8 | } 9 | 10 | .card-block { 11 | padding: 15px; 12 | } 13 | .card-block:before, .card-block:after { 14 | content: " "; 15 | display: table; 16 | } 17 | .card-block:after { 18 | clear: both; 19 | } 20 | 21 | .card-title { 22 | margin: 5px; 23 | margin-bottom: 2px; 24 | text-align: center; 25 | } 26 | 27 | .card-subtitle { 28 | margin-top: -10px; 29 | margin-bottom: 0; 30 | } 31 | 32 | .card-text:last-child { 33 | margin-bottom: 0; 34 | margin-top: 10px; 35 | } 36 | 37 | .card-link:hover { 38 | text-decoration: none; 39 | } 40 | .card-link + .card-link { 41 | margin-left: 15px; 42 | } 43 | 44 | .card > .list-group:first-child .list-group-item:first-child { 45 | border-top-right-radius: 4px; 46 | border-top-left-radius: 4px; 47 | } 48 | .card > .list-group:last-child .list-group-item:last-child { 49 | border-bottom-right-radius: 4px; 50 | border-bottom-left-radius: 4px; 51 | } 52 | 53 | .card-header { 54 | padding: 10px 15px; 55 | background-color: #f5f5f5; 56 | border-bottom: 1px solid #ddd; 57 | } 58 | .card-header:before, .card-header:after { 59 | content: " "; 60 | display: table; 61 | } 62 | .card-header:after { 63 | clear: both; 64 | } 65 | .card-header:first-child { 66 | border-radius: 4px 4px 0 0; 67 | } 68 | 69 | .card-footer { 70 | padding: 10px 15px; 71 | background-color: #f5f5f5; 72 | border-top: 1px solid #ddd; 73 | } 74 | .card-footer:before, .card-footer:after { 75 | content: " "; 76 | display: table; 77 | } 78 | .card-footer:after { 79 | clear: both; 80 | } 81 | .card-footer:last-child { 82 | border-radius: 0 0 4px 4px; 83 | } 84 | 85 | .card-header-tabs { 86 | margin-right: -5px; 87 | margin-bottom: -10px; 88 | margin-left: -5px; 89 | border-bottom: 0; 90 | } 91 | 92 | .card-header-pills { 93 | margin-right: -5px; 94 | margin-left: -5px; 95 | } 96 | 97 | .card-primary { 98 | background-color: #337ab7; 99 | border-color: #337ab7; 100 | } 101 | .card-primary .card-header, 102 | .card-primary .card-footer { 103 | background-color: transparent; 104 | } 105 | 106 | .card-success { 107 | background-color: #5cb85c; 108 | border-color: #5cb85c; 109 | } 110 | .card-success .card-header, 111 | .card-success .card-footer { 112 | background-color: transparent; 113 | } 114 | 115 | .card-info { 116 | background-color: #5bc0de; 117 | border-color: #5bc0de; 118 | } 119 | .card-info .card-header, 120 | .card-info .card-footer { 121 | background-color: transparent; 122 | } 123 | 124 | .card-warning { 125 | background-color: #f0ad4e; 126 | border-color: #f0ad4e; 127 | } 128 | .card-warning .card-header, 129 | .card-warning .card-footer { 130 | background-color: transparent; 131 | } 132 | 133 | .card-danger { 134 | background-color: #d9534f; 135 | border-color: #d9534f; 136 | } 137 | .card-danger .card-header, 138 | .card-danger .card-footer { 139 | background-color: transparent; 140 | } 141 | 142 | .card-outline-primary { 143 | background-color: transparent; 144 | border-color: #337ab7; 145 | } 146 | 147 | .card-outline-secondary { 148 | background-color: transparent; 149 | border-color: #ccc; 150 | } 151 | 152 | .card-outline-info { 153 | background-color: transparent; 154 | border-color: #5bc0de; 155 | } 156 | 157 | .card-outline-success { 158 | background-color: transparent; 159 | border-color: #5cb85c; 160 | } 161 | 162 | .card-outline-warning { 163 | background-color: transparent; 164 | border-color: #f0ad4e; 165 | } 166 | 167 | .card-outline-danger { 168 | background-color: transparent; 169 | border-color: #d9534f; 170 | } 171 | 172 | .card-inverse .card-header, 173 | .card-inverse .card-footer { 174 | border-color: rgba(255, 255, 255, 0.2); 175 | } 176 | .card-inverse .card-header, 177 | .card-inverse .card-footer, 178 | .card-inverse .card-title, 179 | .card-inverse .card-blockquote { 180 | color: #fff; 181 | } 182 | .card-inverse .card-link, 183 | .card-inverse .card-text, 184 | .card-inverse .card-subtitle, 185 | .card-inverse .card-blockquote .blockquote-footer { 186 | color: rgba(255, 255, 255, 0.65); 187 | } 188 | .card-inverse .card-link:hover, .card-inverse .card-link:focus { 189 | color: #fff; 190 | } 191 | 192 | .card-blockquote { 193 | padding: 0; 194 | margin-bottom: 0; 195 | border-left: 0; 196 | } 197 | 198 | .card-img { 199 | border-radius: .25em; 200 | } 201 | 202 | .card-img-overlay { 203 | position: absolute; 204 | top: 0; 205 | right: 0; 206 | bottom: 0; 207 | left: 0; 208 | padding: 15px; 209 | } 210 | 211 | .card-img-top { 212 | border-top-right-radius: 4px; 213 | border-top-left-radius: 4px; 214 | } 215 | 216 | .card-img-bottom { 217 | border-bottom-right-radius: 4px; 218 | border-bottom-left-radius: 4px; 219 | } 220 | -------------------------------------------------------------------------------- /docs/styles/dark.css: -------------------------------------------------------------------------------- 1 | body.dark { 2 | background: #212121; 3 | color: #fafafa; 4 | } 5 | 6 | .dark code { 7 | color: #e09393; 8 | } 9 | 10 | .dark a, 11 | .dark .menu ul.list li a.active { 12 | color: #7fc9ff; 13 | } 14 | 15 | .dark .menu { 16 | background: #212121; 17 | border-right: 1px solid #444; 18 | } 19 | 20 | .dark .menu ul.list li a { 21 | color: #fafafa; 22 | } 23 | 24 | .dark .menu ul.list li.divider { 25 | background: #444; 26 | } 27 | 28 | .dark .xs-menu ul.list li:nth-child(2) { 29 | margin: 0; 30 | background: none; 31 | } 32 | 33 | .dark .menu ul.list li:nth-child(2) { 34 | margin: 0; 35 | background: none; 36 | } 37 | 38 | .dark #book-search-input { 39 | background: #212121; 40 | border-top: 1px solid #444; 41 | border-bottom: 1px solid #444; 42 | color: #fafafa; 43 | } 44 | 45 | .dark .table.metadata > tbody > tr:hover { 46 | color: #555; 47 | } 48 | 49 | .dark .table-bordered { 50 | border: 1px solid #444; 51 | } 52 | 53 | .dark .table-bordered > tbody > tr > td, 54 | .dark .table-bordered > tbody > tr > th, 55 | .dark .table-bordered > tfoot > tr > td, 56 | .dark .table-bordered > tfoot > tr > th, 57 | .dark .table-bordered > thead > tr > td, 58 | .dark .table-bordered > thead > tr > th { 59 | border: 1px solid #444; 60 | } 61 | 62 | .dark .coverage a, 63 | .dark .coverage-count { 64 | color: #fafafa; 65 | } 66 | 67 | .dark .coverage-header { 68 | color: black; 69 | } 70 | 71 | .dark .routes svg text, 72 | .dark .routes svg a { 73 | fill: white; 74 | } 75 | .dark .routes svg rect { 76 | fill: #212121 !important; 77 | } 78 | 79 | .dark .navbar-default, 80 | .dark .btn-default { 81 | background-color: black; 82 | border-color: #444; 83 | color: #fafafa; 84 | } 85 | 86 | .dark .navbar-default .navbar-brand { 87 | color: #fafafa; 88 | } 89 | 90 | .dark .overview .card, 91 | .dark .modules .card { 92 | background: #171717; 93 | color: #fafafa; 94 | border: 1px solid #444; 95 | } 96 | .dark .overview .card a { 97 | color: #fafafa; 98 | } 99 | 100 | .dark .modules .card-header { 101 | background: none; 102 | border-bottom: 1px solid #444; 103 | } 104 | 105 | .dark .module .list-group-item { 106 | background: none; 107 | border: 1px solid #444; 108 | } 109 | 110 | .dark .container-fluid.module h3 a { 111 | color: #337ab7; 112 | } 113 | 114 | .dark table.params thead { 115 | background: #484848; 116 | color: #fafafa; 117 | } 118 | -------------------------------------------------------------------------------- /docs/styles/laravel.css: -------------------------------------------------------------------------------- 1 | .nav-tabs > li > a { 2 | text-decoration: none; 3 | } 4 | 5 | .navbar-default .navbar-brand { 6 | color: #f4645f; 7 | text-decoration: none; 8 | font-size: 16px; 9 | } 10 | 11 | .menu ul.list li a[data-type='chapter-link'], 12 | .menu ul.list li.chapter .simple { 13 | color: #525252; 14 | border-bottom: 1px dashed rgba(0, 0, 0, 0.1); 15 | } 16 | 17 | .content h1, 18 | .content h2, 19 | .content h3, 20 | .content h4, 21 | .content h5 { 22 | color: #292e31; 23 | font-weight: normal; 24 | } 25 | 26 | .content { 27 | color: #4c555a; 28 | } 29 | 30 | a { 31 | color: #f4645f; 32 | text-decoration: underline; 33 | } 34 | a:hover { 35 | color: #f1362f; 36 | } 37 | 38 | .menu ul.list li:nth-child(2) { 39 | margin-top: 0; 40 | } 41 | 42 | .menu ul.list li.title a { 43 | color: #f4645f; 44 | text-decoration: none; 45 | font-size: 16px; 46 | } 47 | 48 | .menu ul.list li a { 49 | color: #f4645f; 50 | text-decoration: none; 51 | } 52 | .menu ul.list li a.active { 53 | color: #f4645f; 54 | font-weight: bold; 55 | } 56 | 57 | code { 58 | box-sizing: border-box; 59 | display: inline-block; 60 | padding: 0 5px; 61 | background: #f0f2f1; 62 | border-radius: 3px; 63 | color: #b93d6a; 64 | font-size: 13px; 65 | line-height: 20px; 66 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.125); 67 | } 68 | 69 | pre { 70 | margin: 0; 71 | padding: 12px 12px; 72 | background: rgba(238, 238, 238, 0.35); 73 | border-radius: 3px; 74 | font-size: 13px; 75 | line-height: 1.5em; 76 | font-weight: 500; 77 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.125); 78 | } 79 | 80 | .dark body { 81 | color: #fafafa; 82 | } 83 | .dark .content h1, 84 | .dark .content h2, 85 | .dark .content h3, 86 | .dark .content h4, 87 | .dark .content h5 { 88 | color: #fafafa; 89 | } 90 | 91 | .dark code { 92 | background: none; 93 | } 94 | 95 | .dark .content { 96 | color: #fafafa; 97 | } 98 | 99 | .dark .menu ul.list li a[data-type='chapter-link'], 100 | .dark .menu ul.list li.chapter .simple { 101 | color: #fafafa; 102 | } 103 | 104 | .dark .menu ul.list li.title a { 105 | color: #fafafa; 106 | } 107 | 108 | .dark .menu ul.list li a { 109 | color: #fafafa; 110 | } 111 | .dark .menu ul.list li a.active { 112 | color: #7fc9ff; 113 | } 114 | -------------------------------------------------------------------------------- /docs/styles/material.css: -------------------------------------------------------------------------------- 1 | .menu { 2 | background: none; 3 | } 4 | 5 | a:hover { 6 | text-decoration: none; 7 | } 8 | 9 | /** LINK **/ 10 | 11 | .menu ul.list li a { 12 | text-decoration: none; 13 | } 14 | 15 | .menu ul.list li a:hover, 16 | .menu ul.list li.chapter .simple:hover { 17 | background-color: #f8f9fa; 18 | text-decoration: none; 19 | } 20 | 21 | #book-search-input { 22 | margin-bottom: 0; 23 | } 24 | 25 | .menu ul.list li.divider { 26 | margin-top: 0; 27 | background: #e9ecef; 28 | } 29 | 30 | .menu .title:hover { 31 | background-color: #f8f9fa; 32 | } 33 | 34 | /** CARD **/ 35 | 36 | .card { 37 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 38 | 0 1px 5px 0 rgba(0, 0, 0, 0.12); 39 | border-radius: 0.125rem; 40 | border: 0; 41 | margin-top: 1px; 42 | } 43 | 44 | .card-header { 45 | background: none; 46 | } 47 | 48 | /** BUTTON **/ 49 | 50 | .btn { 51 | border-radius: 0.125rem; 52 | } 53 | 54 | /** NAV BAR **/ 55 | 56 | .nav { 57 | border: 0; 58 | } 59 | .nav-tabs > li > a { 60 | border: 0; 61 | border-bottom: 0.214rem solid transparent; 62 | color: rgba(0, 0, 0, 0.54); 63 | margin-right: 0; 64 | } 65 | .nav-tabs > li.active > a, 66 | .nav-tabs > li.active > a:focus, 67 | .nav-tabs > li.active > a:hover { 68 | color: rgba(0, 0, 0, 0.87); 69 | border-top: 0; 70 | border-left: 0; 71 | border-right: 0; 72 | border-bottom: 0.214rem solid transparent; 73 | border-color: #008cff; 74 | font-weight: bold; 75 | } 76 | .nav > li > a:focus, 77 | .nav > li > a:hover { 78 | background: none; 79 | } 80 | 81 | /** LIST **/ 82 | 83 | .list-group-item:first-child { 84 | border-top-left-radius: 0.125rem; 85 | border-top-right-radius: 0.125rem; 86 | } 87 | .list-group-item:last-child { 88 | border-bottom-left-radius: 0.125rem; 89 | border-bottom-right-radius: 0.125rem; 90 | } 91 | 92 | /** MISC **/ 93 | 94 | .modifier { 95 | border-radius: 0.125rem; 96 | } 97 | 98 | pre[class*='language-'] { 99 | border-radius: 0.125rem; 100 | } 101 | 102 | /** TABLE **/ 103 | 104 | .table-hover > tbody > tr:hover { 105 | background: rgba(0, 0, 0, 0.075); 106 | } 107 | 108 | table.params thead { 109 | background: none; 110 | } 111 | table.params thead td { 112 | color: rgba(0, 0, 0, 0.54); 113 | font-weight: bold; 114 | } 115 | 116 | .dark .menu .title:hover { 117 | background-color: #2d2d2d; 118 | } 119 | .dark .menu ul.list li a:hover, 120 | .dark .menu ul.list li.chapter .simple:hover { 121 | background-color: #2d2d2d; 122 | } 123 | .dark .nav-tabs > li:not(.active) > a { 124 | color: #fafafa; 125 | } 126 | .dark table.params thead { 127 | background: #484848; 128 | } 129 | .dark table.params thead td { 130 | color: #fafafa; 131 | } 132 | -------------------------------------------------------------------------------- /docs/styles/original.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand, 2 | .menu ul.list li.title { 3 | font-weight: bold; 4 | color: #3c3c3c; 5 | padding-bottom: 5px; 6 | } 7 | 8 | .menu ul.list li a[data-type='chapter-link'], 9 | .menu ul.list li.chapter .simple { 10 | font-weight: bold; 11 | font-size: 14px; 12 | } 13 | 14 | .menu ul.list li a[href='./routes.html'] { 15 | border-bottom: none; 16 | } 17 | 18 | .menu ul.list > li:nth-child(2) { 19 | display: none; 20 | } 21 | 22 | .menu ul.list li.chapter ul.links { 23 | background: #fff; 24 | padding-left: 0; 25 | } 26 | 27 | .menu ul.list li.chapter ul.links li { 28 | border-bottom: 1px solid #ddd; 29 | padding-left: 20px; 30 | } 31 | 32 | .menu ul.list li.chapter ul.links li:last-child { 33 | border-bottom: none; 34 | } 35 | 36 | .menu ul.list li a.active { 37 | color: #337ab7; 38 | font-weight: bold; 39 | } 40 | 41 | #book-search-input { 42 | margin-bottom: 0; 43 | border-bottom: none; 44 | } 45 | .menu ul.list li.divider { 46 | margin: 0; 47 | } 48 | 49 | .dark .menu ul.list li.chapter ul.links { 50 | background: none; 51 | } 52 | -------------------------------------------------------------------------------- /docs/styles/postmark.css: -------------------------------------------------------------------------------- 1 | .navbar-default { 2 | background: #ffde00; 3 | border: none; 4 | } 5 | 6 | .navbar-default .navbar-brand { 7 | color: #333; 8 | font-weight: bold; 9 | } 10 | 11 | .menu { 12 | background: #333; 13 | color: #fcfcfc; 14 | } 15 | 16 | .menu ul.list li a { 17 | color: #333; 18 | } 19 | 20 | .menu ul.list li.title { 21 | background: #ffde00; 22 | color: #333; 23 | padding-bottom: 5px; 24 | } 25 | 26 | .menu ul.list li:nth-child(2) { 27 | margin-top: 0; 28 | } 29 | 30 | .menu ul.list li.chapter a, 31 | .menu ul.list li.chapter .simple { 32 | color: white; 33 | text-decoration: none; 34 | } 35 | 36 | .menu ul.list li.chapter ul.links a { 37 | color: #949494; 38 | text-transform: none; 39 | padding-left: 35px; 40 | } 41 | 42 | .menu ul.list li.chapter ul.links a:hover, 43 | .menu ul.list li.chapter ul.links a.active { 44 | color: #ffde00; 45 | } 46 | 47 | .menu ul.list li.chapter ul.links { 48 | padding-left: 0; 49 | } 50 | 51 | .menu ul.list li.divider { 52 | background: rgba(255, 255, 255, 0.07); 53 | } 54 | 55 | #book-search-input input, 56 | #book-search-input input:focus, 57 | #book-search-input input:hover { 58 | color: #949494; 59 | } 60 | 61 | .copyright { 62 | color: #b3b3b3; 63 | background: #272525; 64 | } 65 | 66 | .content { 67 | background: #fcfcfc; 68 | } 69 | 70 | .content a { 71 | color: #007dcc; 72 | } 73 | 74 | .content a:visited { 75 | color: #0165a5; 76 | } 77 | 78 | .menu ul.list li:nth-last-child(2) { 79 | background: none; 80 | } 81 | 82 | .list-group-item:first-child, 83 | .list-group-item:last-child { 84 | border-radius: 0; 85 | } 86 | 87 | .menu ul.list li.title a { 88 | text-decoration: none; 89 | font-weight: bold; 90 | } 91 | 92 | .menu ul.list li.title a:hover { 93 | background: rgba(255, 255, 255, 0.1); 94 | } 95 | 96 | .breadcrumb > li + li:before { 97 | content: '»\00a0'; 98 | } 99 | 100 | .breadcrumb { 101 | padding-bottom: 15px; 102 | border-bottom: 1px solid #e1e4e5; 103 | } 104 | 105 | code { 106 | white-space: nowrap; 107 | max-width: 100%; 108 | background: #f5f5f5; 109 | padding: 2px 5px; 110 | color: #666666; 111 | overflow-x: auto; 112 | border-radius: 0; 113 | } 114 | 115 | pre { 116 | white-space: pre; 117 | margin: 0; 118 | padding: 12px 12px; 119 | font-size: 12px; 120 | line-height: 1.5; 121 | display: block; 122 | overflow: auto; 123 | color: #404040; 124 | background: #f3f3f3; 125 | } 126 | 127 | pre code.hljs { 128 | border: none; 129 | background: inherit; 130 | } 131 | 132 | /* 133 | Atom One Light by Daniel Gamage 134 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax 135 | base: #fafafa 136 | mono-1: #383a42 137 | mono-2: #686b77 138 | mono-3: #a0a1a7 139 | hue-1: #0184bb 140 | hue-2: #4078f2 141 | hue-3: #a626a4 142 | hue-4: #50a14f 143 | hue-5: #e45649 144 | hue-5-2: #c91243 145 | hue-6: #986801 146 | hue-6-2: #c18401 147 | */ 148 | 149 | .hljs { 150 | display: block; 151 | overflow-x: auto; 152 | padding: 0.5em; 153 | color: #383a42; 154 | background: #fafafa; 155 | } 156 | 157 | .hljs-comment, 158 | .hljs-quote { 159 | color: #a0a1a7; 160 | font-style: italic; 161 | } 162 | 163 | .hljs-doctag, 164 | .hljs-keyword, 165 | .hljs-formula { 166 | color: #a626a4; 167 | } 168 | 169 | .hljs-section, 170 | .hljs-name, 171 | .hljs-selector-tag, 172 | .hljs-deletion, 173 | .hljs-subst { 174 | color: #e45649; 175 | } 176 | 177 | .hljs-literal { 178 | color: #0184bb; 179 | } 180 | 181 | .hljs-string, 182 | .hljs-regexp, 183 | .hljs-addition, 184 | .hljs-attribute, 185 | .hljs-meta-string { 186 | color: #50a14f; 187 | } 188 | 189 | .hljs-built_in, 190 | .hljs-class .hljs-title { 191 | color: #c18401; 192 | } 193 | 194 | .hljs-attr, 195 | .hljs-variable, 196 | .hljs-template-variable, 197 | .hljs-type, 198 | .hljs-selector-class, 199 | .hljs-selector-attr, 200 | .hljs-selector-pseudo, 201 | .hljs-number { 202 | color: #986801; 203 | } 204 | 205 | .hljs-symbol, 206 | .hljs-bullet, 207 | .hljs-link, 208 | .hljs-meta, 209 | .hljs-selector-id, 210 | .hljs-title { 211 | color: #4078f2; 212 | } 213 | 214 | .hljs-emphasis { 215 | font-style: italic; 216 | } 217 | 218 | .hljs-strong { 219 | font-weight: bold; 220 | } 221 | 222 | .hljs-link { 223 | text-decoration: underline; 224 | } 225 | 226 | .dark .content { 227 | background: none; 228 | } 229 | .dark code { 230 | background: none; 231 | color: #e09393; 232 | } 233 | .dark .menu ul.list li.chapter a.active { 234 | color: #ffde00; 235 | } 236 | .dark .menu { 237 | background: #272525; 238 | } 239 | -------------------------------------------------------------------------------- /docs/styles/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.24.0 2 | https://prismjs.com/download.html?#themes=prism-okaidia&languages=markup+css+clike+javascript+apacheconf+aspnet+bash+c+csharp+cpp+coffeescript+dart+docker+elm+git+go+graphql+handlebars+haskell+http+ignore+java+json+kotlin+less+markdown+markup-templating+nginx+php+powershell+ruby+rust+sass+scss+sql+swift+typescript+wasm+yaml&plugins=line-highlight+line-numbers+toolbar+copy-to-clipboard */ 3 | /** 4 | * okaidia theme for JavaScript, CSS and HTML 5 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/ 6 | * @author ocodia 7 | */ 8 | 9 | code[class*='language-'], 10 | pre[class*='language-'] { 11 | color: #f8f8f2; 12 | background: none; 13 | text-shadow: 0 1px rgba(0, 0, 0, 0.3); 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | font-size: 1em; 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | /* Code blocks */ 34 | pre[class*='language-'] { 35 | padding: 1em; 36 | margin: 0.5em 0; 37 | overflow: auto; 38 | border-radius: 0.3em; 39 | } 40 | 41 | :not(pre) > code[class*='language-'], 42 | pre[class*='language-'] { 43 | background: #272822; 44 | } 45 | 46 | /* Inline code */ 47 | :not(pre) > code[class*='language-'] { 48 | padding: 0.1em; 49 | border-radius: 0.3em; 50 | white-space: normal; 51 | } 52 | 53 | .token.comment, 54 | .token.prolog, 55 | .token.doctype, 56 | .token.cdata { 57 | color: #8292a2; 58 | } 59 | 60 | .token.punctuation { 61 | color: #f8f8f2; 62 | } 63 | 64 | .token.namespace { 65 | opacity: 0.7; 66 | } 67 | 68 | .token.property, 69 | .token.tag, 70 | .token.constant, 71 | .token.symbol, 72 | .token.deleted { 73 | color: #f92672; 74 | } 75 | 76 | .token.boolean, 77 | .token.number { 78 | color: #ae81ff; 79 | } 80 | 81 | .token.selector, 82 | .token.attr-name, 83 | .token.string, 84 | .token.char, 85 | .token.builtin, 86 | .token.inserted { 87 | color: #a6e22e; 88 | } 89 | 90 | .token.operator, 91 | .token.entity, 92 | .token.url, 93 | .language-css .token.string, 94 | .style .token.string, 95 | .token.variable { 96 | color: #f8f8f2; 97 | } 98 | 99 | .token.atrule, 100 | .token.attr-value, 101 | .token.function, 102 | .token.class-name { 103 | color: #e6db74; 104 | } 105 | 106 | .token.keyword { 107 | color: #66d9ef; 108 | } 109 | 110 | .token.regex, 111 | .token.important { 112 | color: #fd971f; 113 | } 114 | 115 | .token.important, 116 | .token.bold { 117 | font-weight: bold; 118 | } 119 | .token.italic { 120 | font-style: italic; 121 | } 122 | 123 | .token.entity { 124 | cursor: help; 125 | } 126 | 127 | pre[data-line] { 128 | position: relative; 129 | padding: 1em 0 1em 3em; 130 | } 131 | 132 | .line-highlight { 133 | position: absolute; 134 | left: 0; 135 | right: 0; 136 | padding: inherit 0; 137 | margin-top: 1em; /* Same as .prism’s padding-top */ 138 | 139 | background: hsla(24, 20%, 50%, 0.08); 140 | background: linear-gradient(to right, hsla(24, 20%, 50%, 0.1) 70%, hsla(24, 20%, 50%, 0)); 141 | 142 | pointer-events: none; 143 | 144 | line-height: inherit; 145 | white-space: pre; 146 | } 147 | 148 | @media print { 149 | .line-highlight { 150 | /* 151 | * This will prevent browsers from replacing the background color with white. 152 | * It's necessary because the element is layered on top of the displayed code. 153 | */ 154 | -webkit-print-color-adjust: exact; 155 | color-adjust: exact; 156 | } 157 | } 158 | 159 | .line-highlight:before, 160 | .line-highlight[data-end]:after { 161 | content: attr(data-start); 162 | position: absolute; 163 | top: 0.4em; 164 | left: 0.6em; 165 | min-width: 1em; 166 | padding: 0 0.5em; 167 | background-color: hsla(24, 20%, 50%, 0.4); 168 | color: hsl(24, 20%, 95%); 169 | font: bold 65%/1.5 sans-serif; 170 | text-align: center; 171 | vertical-align: 0.3em; 172 | border-radius: 999px; 173 | text-shadow: none; 174 | box-shadow: 0 1px white; 175 | } 176 | 177 | .line-highlight[data-end]:after { 178 | content: attr(data-end); 179 | top: auto; 180 | bottom: 0.4em; 181 | } 182 | 183 | .line-numbers .line-highlight:before, 184 | .line-numbers .line-highlight:after { 185 | content: none; 186 | } 187 | 188 | pre[id].linkable-line-numbers span.line-numbers-rows { 189 | pointer-events: all; 190 | } 191 | pre[id].linkable-line-numbers span.line-numbers-rows > span:before { 192 | cursor: pointer; 193 | } 194 | pre[id].linkable-line-numbers span.line-numbers-rows > span:hover:before { 195 | background-color: rgba(128, 128, 128, 0.2); 196 | } 197 | 198 | pre[class*='language-'].line-numbers { 199 | position: relative; 200 | padding-left: 3.8em; 201 | counter-reset: linenumber; 202 | } 203 | 204 | pre[class*='language-'].line-numbers > code { 205 | position: relative; 206 | white-space: inherit; 207 | } 208 | 209 | .line-numbers .line-numbers-rows { 210 | position: absolute; 211 | pointer-events: none; 212 | top: 0; 213 | font-size: 100%; 214 | left: -3.8em; 215 | width: 3em; /* works for line-numbers below 1000 lines */ 216 | letter-spacing: -1px; 217 | border-right: 1px solid #999; 218 | 219 | -webkit-user-select: none; 220 | -moz-user-select: none; 221 | -ms-user-select: none; 222 | user-select: none; 223 | } 224 | 225 | .line-numbers-rows > span { 226 | display: block; 227 | counter-increment: linenumber; 228 | } 229 | 230 | .line-numbers-rows > span:before { 231 | content: counter(linenumber); 232 | color: #999; 233 | display: block; 234 | padding-right: 0.8em; 235 | text-align: right; 236 | } 237 | 238 | div.code-toolbar { 239 | position: relative; 240 | } 241 | 242 | div.code-toolbar > .toolbar { 243 | position: absolute; 244 | top: 0.3em; 245 | right: 0.2em; 246 | transition: opacity 0.3s ease-in-out; 247 | opacity: 0; 248 | } 249 | 250 | div.code-toolbar:hover > .toolbar { 251 | opacity: 1; 252 | } 253 | 254 | /* Separate line b/c rules are thrown out if selector is invalid. 255 | IE11 and old Edge versions don't support :focus-within. */ 256 | div.code-toolbar:focus-within > .toolbar { 257 | opacity: 1; 258 | } 259 | 260 | div.code-toolbar > .toolbar .toolbar-item { 261 | display: inline-block; 262 | } 263 | 264 | div.code-toolbar > .toolbar a { 265 | cursor: pointer; 266 | } 267 | 268 | div.code-toolbar > .toolbar button { 269 | background: none; 270 | border: 0; 271 | color: inherit; 272 | font: inherit; 273 | line-height: normal; 274 | overflow: visible; 275 | padding: 0; 276 | -webkit-user-select: none; /* for button */ 277 | -moz-user-select: none; 278 | -ms-user-select: none; 279 | } 280 | 281 | div.code-toolbar > .toolbar a, 282 | div.code-toolbar > .toolbar button, 283 | div.code-toolbar > .toolbar span { 284 | color: #bbb; 285 | font-size: 0.8em; 286 | padding: 0 0.5em; 287 | background: #f5f2f0; 288 | background: rgba(224, 224, 224, 0.2); 289 | box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); 290 | border-radius: 0.5em; 291 | } 292 | 293 | div.code-toolbar > .toolbar a:hover, 294 | div.code-toolbar > .toolbar a:focus, 295 | div.code-toolbar > .toolbar button:hover, 296 | div.code-toolbar > .toolbar button:focus, 297 | div.code-toolbar > .toolbar span:hover, 298 | div.code-toolbar > .toolbar span:focus { 299 | color: inherit; 300 | text-decoration: none; 301 | } 302 | -------------------------------------------------------------------------------- /docs/styles/readthedocs.css: -------------------------------------------------------------------------------- 1 | .navbar-default { 2 | background: #2980b9; 3 | border: none; 4 | } 5 | 6 | .navbar-default .navbar-brand { 7 | color: #fcfcfc; 8 | } 9 | 10 | .menu { 11 | background: #343131; 12 | color: #fcfcfc; 13 | } 14 | 15 | .menu ul.list li a { 16 | color: #fcfcfc; 17 | } 18 | 19 | .menu ul.list li.title { 20 | background: #2980b9; 21 | padding-bottom: 5px; 22 | } 23 | 24 | .menu ul.list li:nth-child(2) { 25 | margin-top: 0; 26 | } 27 | 28 | .menu ul.list li.chapter a, 29 | .menu ul.list li.chapter .simple { 30 | color: #555; 31 | text-transform: uppercase; 32 | text-decoration: none; 33 | } 34 | 35 | .menu ul.list li.chapter ul.links a { 36 | color: #b3b3b3; 37 | text-transform: none; 38 | padding-left: 35px; 39 | } 40 | 41 | .menu ul.list li.chapter ul.links a:hover { 42 | background: #4e4a4a; 43 | } 44 | 45 | .menu ul.list li.chapter a.active, 46 | .menu ul.list li.chapter ul.links a.active { 47 | color: #0099e5; 48 | } 49 | 50 | .menu ul.list li.chapter ul.links { 51 | padding-left: 0; 52 | } 53 | 54 | .menu ul.list li.divider { 55 | background: rgba(255, 255, 255, 0.07); 56 | } 57 | 58 | #book-search-input input, 59 | #book-search-input input:focus, 60 | #book-search-input input:hover { 61 | color: #949494; 62 | } 63 | 64 | .copyright { 65 | color: #b3b3b3; 66 | background: #272525; 67 | } 68 | 69 | .content { 70 | background: #fcfcfc; 71 | } 72 | 73 | .content a { 74 | color: #2980b9; 75 | } 76 | 77 | .content a:hover { 78 | color: #3091d1; 79 | } 80 | 81 | .content a:visited { 82 | color: #9b59b6; 83 | } 84 | 85 | .menu ul.list li:nth-last-child(2) { 86 | background: none; 87 | } 88 | 89 | code { 90 | white-space: nowrap; 91 | max-width: 100%; 92 | background: #fff; 93 | padding: 2px 5px; 94 | color: #e74c3c; 95 | overflow-x: auto; 96 | border-radius: 0; 97 | } 98 | 99 | pre { 100 | white-space: pre; 101 | margin: 0; 102 | padding: 12px 12px; 103 | font-size: 12px; 104 | line-height: 1.5; 105 | display: block; 106 | overflow: auto; 107 | color: #404040; 108 | background: rgba(238, 238, 238, 0.35); 109 | } 110 | 111 | .dark .content { 112 | background: none; 113 | } 114 | .dark code { 115 | background: none; 116 | color: #e09393; 117 | } 118 | -------------------------------------------------------------------------------- /docs/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font: inherit; 91 | font-size: 100%; 92 | vertical-align: baseline; 93 | } 94 | /* HTML5 display-role reset for older browsers */ 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | body { 109 | line-height: 1; 110 | } 111 | ol, 112 | ul { 113 | list-style: none; 114 | } 115 | blockquote, 116 | q { 117 | quotes: none; 118 | } 119 | blockquote:before, 120 | blockquote:after, 121 | q:before, 122 | q:after { 123 | content: ''; 124 | content: none; 125 | } 126 | table { 127 | border-collapse: collapse; 128 | border-spacing: 0; 129 | } 130 | -------------------------------------------------------------------------------- /docs/styles/stripe.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand { 2 | color: #0099e5; 3 | } 4 | 5 | .menu ul.list li a[data-type='chapter-link'], 6 | .menu ul.list li.chapter .simple { 7 | color: #939da3; 8 | text-transform: uppercase; 9 | } 10 | 11 | .content h1, 12 | .content h2, 13 | .content h3, 14 | .content h4, 15 | .content h5 { 16 | color: #292e31; 17 | font-weight: normal; 18 | } 19 | 20 | .content { 21 | color: #4c555a; 22 | } 23 | 24 | .menu ul.list li.title { 25 | padding: 5px 0; 26 | } 27 | 28 | a { 29 | color: #0099e5; 30 | text-decoration: none; 31 | } 32 | a:hover { 33 | color: #292e31; 34 | text-decoration: none; 35 | } 36 | 37 | .menu ul.list li:nth-child(2) { 38 | margin-top: 0; 39 | } 40 | 41 | .menu ul.list li.title a, 42 | .navbar a { 43 | color: #0099e5; 44 | text-decoration: none; 45 | font-size: 16px; 46 | } 47 | 48 | .menu ul.list li a.active { 49 | color: #0099e5; 50 | } 51 | 52 | code { 53 | box-sizing: border-box; 54 | display: inline-block; 55 | padding: 0 5px; 56 | background: #fafcfc; 57 | border-radius: 4px; 58 | color: #b93d6a; 59 | font-size: 13px; 60 | line-height: 20px; 61 | } 62 | 63 | pre { 64 | margin: 0; 65 | padding: 12px 12px; 66 | background: #272b2d; 67 | border-radius: 5px; 68 | font-size: 13px; 69 | line-height: 1.5em; 70 | font-weight: 500; 71 | } 72 | 73 | .dark body { 74 | color: #fafafa; 75 | } 76 | .dark .content h1, 77 | .dark .content h2, 78 | .dark .content h3, 79 | .dark .content h4, 80 | .dark .content h5 { 81 | color: #fafafa; 82 | } 83 | 84 | .dark code { 85 | background: none; 86 | } 87 | 88 | .dark .content { 89 | color: #fafafa; 90 | } 91 | 92 | .dark .menu ul.list li a[data-type='chapter-link'], 93 | .dark .menu ul.list li.chapter .simple { 94 | color: #fafafa; 95 | } 96 | 97 | .dark .menu ul.list li.title a { 98 | color: #fafafa; 99 | } 100 | 101 | .dark .menu ul.list li a { 102 | color: #fafafa; 103 | } 104 | .dark .menu ul.list li a.active { 105 | color: #7fc9ff; 106 | } 107 | -------------------------------------------------------------------------------- /docs/styles/style.css: -------------------------------------------------------------------------------- 1 | @import "./reset.css"; 2 | @import "./bootstrap.min.css"; 3 | @import "./bootstrap-card.css"; 4 | @import "./prism.css"; 5 | @import "./ionicons.min.css"; 6 | @import "./compodoc.css"; 7 | @import "./tablesort.css"; 8 | -------------------------------------------------------------------------------- /docs/styles/tablesort.css: -------------------------------------------------------------------------------- 1 | th[role=columnheader]:not(.no-sort) { 2 | cursor: pointer; 3 | } 4 | 5 | th[role=columnheader]:not(.no-sort):after { 6 | content: ''; 7 | float: right; 8 | margin-top: 7px; 9 | border-width: 0 4px 4px; 10 | border-style: solid; 11 | border-color: #404040 transparent; 12 | visibility: visible; 13 | opacity: 1; 14 | -ms-user-select: none; 15 | -webkit-user-select: none; 16 | -moz-user-select: none; 17 | user-select: none; 18 | } 19 | 20 | th[aria-sort=ascending]:not(.no-sort):after { 21 | border-bottom: none; 22 | border-width: 4px 4px 0; 23 | } 24 | 25 | th[aria-sort]:not(.no-sort):after { 26 | visibility: visible; 27 | opacity: 0.4; 28 | } 29 | 30 | th[role=columnheader]:not(.no-sort):hover:after { 31 | visibility: visible; 32 | opacity: 1; 33 | } 34 | -------------------------------------------------------------------------------- /docs/styles/vagrant.css: -------------------------------------------------------------------------------- 1 | .navbar-default .navbar-brand { 2 | background: white; 3 | color: #8d9ba8; 4 | } 5 | 6 | .menu .list { 7 | background: #0c5593; 8 | } 9 | 10 | .menu .chapter { 11 | padding: 0 20px; 12 | } 13 | 14 | .menu ul.list li a[data-type='chapter-link'], 15 | .menu ul.list li.chapter .simple { 16 | color: white; 17 | text-transform: uppercase; 18 | border-bottom: 1px solid rgba(255, 255, 255, 0.4); 19 | } 20 | 21 | .content h1, 22 | .content h2, 23 | .content h3, 24 | .content h4, 25 | .content h5 { 26 | color: #292e31; 27 | font-weight: normal; 28 | } 29 | 30 | .content { 31 | color: #4c555a; 32 | } 33 | 34 | a { 35 | color: #0094bf; 36 | text-decoration: underline; 37 | } 38 | a:hover { 39 | color: #f1362f; 40 | } 41 | 42 | .menu ul.list li.title { 43 | background: white; 44 | padding-bottom: 5px; 45 | } 46 | 47 | .menu ul.list li:nth-child(2) { 48 | margin-top: 0; 49 | } 50 | 51 | .menu ul.list li:nth-last-child(2) { 52 | background: none; 53 | } 54 | 55 | .menu ul.list li.title a { 56 | padding: 10px 15px; 57 | } 58 | 59 | .menu ul.list li.title a, 60 | .navbar a { 61 | color: #8d9ba8; 62 | text-decoration: none; 63 | font-size: 16px; 64 | font-weight: 300; 65 | } 66 | 67 | .menu ul.list li a { 68 | color: white; 69 | padding: 10px; 70 | font-weight: 300; 71 | text-decoration: none; 72 | } 73 | .menu ul.list li a.active { 74 | color: white; 75 | font-weight: bold; 76 | } 77 | 78 | .copyright { 79 | color: white; 80 | background: #000; 81 | } 82 | 83 | code { 84 | box-sizing: border-box; 85 | display: inline-block; 86 | padding: 0 5px; 87 | background: rgba(0, 148, 191, 0.1); 88 | border-radius: 3px; 89 | color: #0094bf; 90 | font-size: 13px; 91 | line-height: 20px; 92 | } 93 | 94 | pre { 95 | margin: 0; 96 | padding: 12px 12px; 97 | background: rgba(238, 238, 238, 0.35); 98 | border-radius: 3px; 99 | font-size: 13px; 100 | line-height: 1.5em; 101 | font-weight: 500; 102 | } 103 | 104 | .dark body { 105 | color: #fafafa; 106 | } 107 | .dark .content h1, 108 | .dark .content h2, 109 | .dark .content h3, 110 | .dark .content h4, 111 | .dark .content h5 { 112 | color: #fafafa; 113 | } 114 | 115 | .dark code { 116 | background: none; 117 | } 118 | 119 | .dark .content { 120 | color: #fafafa; 121 | } 122 | 123 | .dark .menu ul.list li.title a, 124 | .dark .navbar a { 125 | color: #8d9ba8; 126 | } 127 | 128 | .dark .menu ul.list li a { 129 | color: #fafafa; 130 | } 131 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitter-backend-node", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "doc": "npx @compodoc/compodoc -p tsconfig.json -d ./docs", 10 | "prebuild": "rimraf dist", 11 | "postinstall": "node build/postInstall.js", 12 | "build": "nest build", 13 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 14 | "start": "nest start", 15 | "start:dev": "nest start --watch", 16 | "start:debug": "nest start --debug --watch", 17 | "start:prod": "node dist/main", 18 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 19 | "test": "jest", 20 | "test:watch": "jest --watch", 21 | "test:cov": "jest --coverage", 22 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 23 | "test:e2e": "jest --config ./test/jest-e2e.json", 24 | "test:e2e:cov": "jest --config ./test/jest-e2e.json --coverage" 25 | }, 26 | "dependencies": { 27 | "@nestjs/common": "^8.2.3", 28 | "@nestjs/core": "^8.2.3", 29 | "@nestjs/platform-fastify": "^8.2.3", 30 | "@nestjs/swagger": "^5.1.5", 31 | "@nestjs/typeorm": "^8.0.2", 32 | "bcrypt": "^5.0.1", 33 | "fastify-compress": "^3.6.0", 34 | "fastify-swagger": "^4.8.0", 35 | "pg": "^8.6.0", 36 | "pg-hstore": "^2.3.4", 37 | "reflect-metadata": "^0.1.13", 38 | "rimraf": "^3.0.2", 39 | "rxjs": "^7.4.0", 40 | "typeorm": "^0.2.34" 41 | }, 42 | "devDependencies": { 43 | "@compodoc/compodoc": "^1.1.16", 44 | "@nestjs/cli": "^8.1.5", 45 | "@nestjs/schematics": "^8.0.5", 46 | "@nestjs/testing": "^8.2.3", 47 | "@types/bcrypt": "^5.0.0", 48 | "@types/jest": "^27.0.3", 49 | "@types/node": "^16.11.11", 50 | "@types/supertest": "^2.0.10", 51 | "@typescript-eslint/eslint-plugin": "^5.5.0", 52 | "@typescript-eslint/parser": "^5.5.0", 53 | "eslint": "^8.3.0", 54 | "eslint-config-prettier": "^8.1.0", 55 | "eslint-plugin-prettier": "^4.0.0", 56 | "jest": "^27.4.2", 57 | "prettier": "^2.2.1", 58 | "supertest": "^6.1.3", 59 | "ts-jest": "^27.0.7", 60 | "ts-loader": "^9.2.6", 61 | "ts-node": "^10.4.0", 62 | "tsconfig-paths": "^3.9.0", 63 | "typescript": "^4.2.3" 64 | }, 65 | "jest": { 66 | "moduleFileExtensions": [ 67 | "js", 68 | "json", 69 | "ts" 70 | ], 71 | "rootDir": "./", 72 | "roots": [ 73 | "/src" 74 | ], 75 | "testRegex": ".*\\.spec\\.ts$", 76 | "transform": { 77 | "^.+\\.(t|j)s$": "ts-jest" 78 | }, 79 | "collectCoverageFrom": [ 80 | "**/*.(t|j)s" 81 | ], 82 | "coverageDirectory": "./coverage", 83 | "testEnvironment": "node", 84 | "moduleNameMapper": { 85 | "src/(.*)": "/src/$1" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/api.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthModule } from './auth/auth.module'; 3 | import { HashtagsModule } from './hashtags/hashtags.module'; 4 | import { PostsModule } from './posts/posts.module'; 5 | import { UsersModule } from './users/users.module'; 6 | import { LikesModule } from './likes/likes.module'; 7 | 8 | @Module({ 9 | imports: [UsersModule, PostsModule, HashtagsModule, AuthModule, LikesModule], 10 | }) 11 | export class ApiModule {} 12 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get('/hello') 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { ProdDbModule } from './commons/db.module'; 5 | import { ApiModule } from './api.module'; 6 | 7 | @Module({ 8 | imports: [ApiModule, ProdDbModule], 9 | controllers: [AppController], 10 | providers: [AppService], 11 | }) 12 | export class AppModule {} 13 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { 3 | MockPasswordRepositoryProvider, 4 | MockSessionRepositoryProvider, 5 | MockUsersRepositoryProvider, 6 | } from 'src/commons/mocks/mock.providers'; 7 | import { AuthController } from './auth.controller'; 8 | import { AuthService } from './auth.service'; 9 | 10 | describe('AuthController', () => { 11 | let controller: AuthController; 12 | 13 | beforeEach(async () => { 14 | const module: TestingModule = await Test.createTestingModule({ 15 | controllers: [AuthController], 16 | providers: [ 17 | AuthService, 18 | MockUsersRepositoryProvider, 19 | MockPasswordRepositoryProvider, 20 | MockSessionRepositoryProvider, 21 | ], 22 | }).compile(); 23 | 24 | controller = module.get(AuthController); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(controller).toBeDefined(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import { ApiProperty, ApiResponse, ApiTags } from '@nestjs/swagger'; 3 | import { AuthService } from './auth.service'; 4 | 5 | class LoginRequestBody { 6 | @ApiProperty() username: string; 7 | @ApiProperty() password: string; 8 | } 9 | 10 | class LoginResponseBody { 11 | @ApiProperty() token: string; 12 | constructor(token: string) { 13 | this.token = token; 14 | } 15 | } 16 | 17 | @ApiTags('auth') 18 | @Controller('auth') 19 | export class AuthController { 20 | constructor(private authService: AuthService) {} 21 | 22 | @ApiResponse({ type: LoginResponseBody }) 23 | @Post('/login') 24 | async login(@Body() body: LoginRequestBody) { 25 | const session = await this.authService.createNewSession( 26 | body.username, 27 | body.password, 28 | ); 29 | return new LoginResponseBody(session.id); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/auth/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { UserEntity } from 'src/users/users.entity'; 3 | 4 | export const User = createParamDecorator( 5 | (data: unknown, ctx: ExecutionContext): UserEntity => { 6 | const request = ctx.switchToHttp().getRequest(); 7 | return request.user; 8 | }, 9 | ); 10 | -------------------------------------------------------------------------------- /src/auth/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestingModule, Test } from '@nestjs/testing'; 2 | import { 3 | MockUsersRepositoryProvider, 4 | MockPasswordRepositoryProvider, 5 | MockSessionRepositoryProvider, 6 | } from 'src/commons/mocks/mock.providers'; 7 | import { OptionalAuthGuard, RequiredAuthGuard } from './auth.guard'; 8 | import { AuthService } from './auth.service'; 9 | 10 | describe('OptionalAuthGuard', () => { 11 | let authService: AuthService; 12 | 13 | beforeEach(async () => { 14 | const module: TestingModule = await Test.createTestingModule({ 15 | providers: [ 16 | AuthService, 17 | MockUsersRepositoryProvider, 18 | MockPasswordRepositoryProvider, 19 | MockSessionRepositoryProvider, 20 | ], 21 | }).compile(); 22 | 23 | authService = module.get(AuthService); 24 | }); 25 | 26 | it('should be defined', () => { 27 | expect(new OptionalAuthGuard(authService)).toBeDefined(); 28 | }); 29 | }); 30 | 31 | describe('RequiredAuthGuard', () => { 32 | let authService: AuthService; 33 | 34 | beforeEach(async () => { 35 | const module: TestingModule = await Test.createTestingModule({ 36 | providers: [ 37 | AuthService, 38 | MockUsersRepositoryProvider, 39 | MockPasswordRepositoryProvider, 40 | MockSessionRepositoryProvider, 41 | ], 42 | }).compile(); 43 | 44 | authService = module.get(AuthService); 45 | }); 46 | it('should be defined', () => { 47 | expect(new RequiredAuthGuard(authService)).toBeDefined(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Inject, 5 | Injectable, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { AuthService } from './auth.service'; 9 | 10 | class TokenAuthorizer { 11 | constructor(@Inject(AuthService) private readonly authService: AuthService) {} 12 | protected async authorizeToken(context: ExecutionContext): Promise { 13 | const request = context.switchToHttp().getRequest(); 14 | if (!request?.headers?.authorization) { 15 | throw new UnauthorizedException('Missing authorization header'); 16 | } 17 | if (!request.headers.authorization.startsWith('Bearer ')) { 18 | throw new UnauthorizedException('Invalid authorization header'); 19 | } 20 | const token = request.headers.authorization.split(' ')[1]; 21 | if (!token) { 22 | throw new UnauthorizedException('Missing token'); 23 | } 24 | const user = this.authService.getUserFromSessionToken(token); 25 | request.user = user; 26 | return true; 27 | } 28 | } 29 | 30 | @Injectable() 31 | export class OptionalAuthGuard extends TokenAuthorizer implements CanActivate { 32 | async canActivate(context: ExecutionContext): Promise { 33 | try { 34 | return await this.authorizeToken(context); 35 | } catch (e) { 36 | return true; 37 | } 38 | } 39 | } 40 | 41 | @Injectable() 42 | export class RequiredAuthGuard extends TokenAuthorizer implements CanActivate { 43 | async canActivate(context: ExecutionContext): Promise { 44 | return this.authorizeToken(context); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { UserEntity } from 'src/users/users.entity'; 4 | import { AuthController } from './auth.controller'; 5 | import { AuthService } from './auth.service'; 6 | import { PasswordEntity } from './passwords.entity'; 7 | import { SessionsEntity } from './sessions.entity'; 8 | 9 | @Global() 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([ 13 | PasswordEntity, 14 | SessionsEntity, 15 | UserEntity, 16 | PasswordEntity, 17 | ]), 18 | ], 19 | controllers: [AuthController], 20 | providers: [AuthService], 21 | exports: [AuthService], 22 | }) 23 | export class AuthModule {} 24 | -------------------------------------------------------------------------------- /src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { 3 | MockPasswordRepositoryProvider, 4 | MockSessionRepositoryProvider, 5 | MockUsersRepositoryProvider, 6 | } from 'src/commons/mocks/mock.providers'; 7 | import { AuthService } from './auth.service'; 8 | 9 | describe('AuthService', () => { 10 | let service: AuthService; 11 | 12 | beforeEach(async () => { 13 | const module: TestingModule = await Test.createTestingModule({ 14 | providers: [ 15 | AuthService, 16 | MockUsersRepositoryProvider, 17 | MockPasswordRepositoryProvider, 18 | MockSessionRepositoryProvider, 19 | ], 20 | }).compile(); 21 | 22 | service = module.get(AuthService); 23 | }); 24 | 25 | it('should be defined', () => { 26 | expect(service).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NotFoundException, 4 | UnauthorizedException, 5 | } from '@nestjs/common'; 6 | import { InjectRepository } from '@nestjs/typeorm'; 7 | import { compare, hash } from 'bcrypt'; 8 | import { UserEntity } from 'src/users/users.entity'; 9 | import { UsersRepository } from 'src/users/users.repository'; 10 | import { Repository } from 'typeorm'; 11 | import { PasswordEntity } from './passwords.entity'; 12 | import { SessionsEntity } from './sessions.entity'; 13 | 14 | @Injectable() 15 | export class AuthService { 16 | constructor( 17 | @InjectRepository(UserEntity) 18 | private userRepo: UsersRepository, 19 | @InjectRepository(PasswordEntity) 20 | private passwordRepo: Repository, 21 | @InjectRepository(SessionsEntity) 22 | private sessionRepo: Repository, 23 | ) {} 24 | 25 | public static PASSWORD_SALT_ROUNDS = 10; 26 | 27 | async createPasswordForNewUser( 28 | userId: string, 29 | password: string, 30 | ): Promise { 31 | const existing = await this.passwordRepo.findOne({ where: { userId } }); 32 | if (existing) { 33 | throw new UnauthorizedException( 34 | 'This user already has a password, cannot set new password', 35 | ); 36 | } 37 | 38 | const newPassword = new PasswordEntity(); 39 | newPassword.userId = userId; 40 | newPassword.password = await this.passToHash(password); 41 | return await this.passwordRepo.save(newPassword); 42 | } 43 | 44 | async createNewSession(username: string, password: string) { 45 | const user = await this.userRepo.findOne({ where: { username } }); 46 | 47 | if (!user) { 48 | throw new NotFoundException('Username does not exist'); 49 | } 50 | const userPassword = await this.passwordRepo.findOne({ 51 | where: { userId: user.id }, 52 | }); 53 | const passMatch = await this.matchPassHash(password, userPassword.password); 54 | if (!passMatch) { 55 | throw new UnauthorizedException('Password is wrong'); 56 | } 57 | const session = new SessionsEntity(); 58 | session.userId = userPassword.userId; 59 | const savedSession = await this.sessionRepo.save(session); 60 | return savedSession; 61 | } 62 | 63 | async getUserFromSessionToken(token: string): Promise { 64 | const session = await this.sessionRepo.findOne({ where: { id: token } }); 65 | if (!session) { 66 | throw new UnauthorizedException('Session not found'); 67 | } 68 | const user = await session.user; 69 | if (!user) { 70 | throw new UnauthorizedException('User not found'); 71 | } 72 | return user; 73 | } 74 | 75 | private async passToHash(password: string): Promise { 76 | return hash(password, AuthService.PASSWORD_SALT_ROUNDS); 77 | } 78 | 79 | private async matchPassHash( 80 | password: string, 81 | hash: string, 82 | ): Promise { 83 | return (await compare(password, hash)) === true; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/auth/passwords.entity.ts: -------------------------------------------------------------------------------- 1 | import { MooBaseEntity } from 'src/commons/base.entity'; 2 | import { UserEntity } from 'src/users/users.entity'; 3 | import { Column, Entity, JoinColumn, OneToOne } from 'typeorm'; 4 | 5 | @Entity('passwords') 6 | export class PasswordEntity extends MooBaseEntity { 7 | @Column() 8 | userId: string; 9 | 10 | @JoinColumn({ name: 'userId' }) 11 | @OneToOne(() => UserEntity) 12 | user: UserEntity; 13 | 14 | @Column({ nullable: false }) 15 | password: string; 16 | } 17 | -------------------------------------------------------------------------------- /src/auth/sessions.entity.ts: -------------------------------------------------------------------------------- 1 | import { MooBaseEntity } from 'src/commons/base.entity'; 2 | import { UserEntity } from 'src/users/users.entity'; 3 | import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm'; 4 | 5 | @Entity('sessions') 6 | export class SessionsEntity extends MooBaseEntity { 7 | @Column() 8 | userId: string; 9 | 10 | @JoinColumn({ name: 'userId' }) 11 | @ManyToOne(() => UserEntity, { lazy: true }) 12 | user: Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/commons/base.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateDateColumn, 3 | PrimaryGeneratedColumn, 4 | UpdateDateColumn, 5 | } from 'typeorm'; 6 | 7 | /** 8 | * Base entity which is extended by all entities in our application. 9 | */ 10 | export abstract class MooBaseEntity { 11 | @PrimaryGeneratedColumn('uuid') 12 | id: string; 13 | 14 | @CreateDateColumn({ name: 'created_at' }) 15 | createdAt: Date; 16 | 17 | @UpdateDateColumn({ name: 'updated_at' }) 18 | updatedAt: Date; 19 | } 20 | -------------------------------------------------------------------------------- /src/commons/db.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { PasswordEntity } from 'src/auth/passwords.entity'; 4 | import { SessionsEntity } from 'src/auth/sessions.entity'; 5 | import { LikesEntity } from 'src/likes/likes.entity'; 6 | import { PostEntity } from 'src/posts/posts.entity'; 7 | import { UserFollowingEntity } from 'src/users/user-followings.entity'; 8 | import { UserEntity } from 'src/users/users.entity'; 9 | 10 | /** 11 | * Database module for production 12 | */ 13 | @Global() 14 | @Module({ 15 | imports: [ 16 | TypeOrmModule.forRoot({ 17 | type: 'postgres', 18 | username: 'mooadmin', 19 | password: 'moopass', 20 | database: 'moodb', 21 | synchronize: true, 22 | logger: 'advanced-console', 23 | logging: 'all', 24 | entities: [ 25 | UserEntity, 26 | PostEntity, 27 | PasswordEntity, 28 | SessionsEntity, 29 | UserFollowingEntity, 30 | LikesEntity, 31 | ], 32 | }), 33 | ], 34 | }) 35 | export class ProdDbModule {} 36 | 37 | /** 38 | * Database module for testing purposes 39 | */ 40 | @Global() 41 | @Module({ 42 | imports: [ 43 | TypeOrmModule.forRoot({ 44 | type: 'postgres', 45 | username: 'mooadmin', 46 | password: 'moopass', 47 | database: 'moodb_test', 48 | synchronize: true, 49 | dropSchema: true, 50 | logger: 'advanced-console', 51 | logging: 'all', 52 | entities: [ 53 | UserEntity, 54 | PostEntity, 55 | PasswordEntity, 56 | SessionsEntity, 57 | UserFollowingEntity, 58 | LikesEntity, 59 | ], 60 | }), 61 | ], 62 | }) 63 | export class TestDbModule {} 64 | -------------------------------------------------------------------------------- /src/commons/mocks/likes.repository.mock.ts: -------------------------------------------------------------------------------- 1 | import { LikesEntity } from 'src/likes/likes.entity'; 2 | import { Repository } from 'typeorm'; 3 | 4 | export class MockLikesRepository extends Repository {} 5 | -------------------------------------------------------------------------------- /src/commons/mocks/mock.providers.ts: -------------------------------------------------------------------------------- 1 | import { getRepositoryToken } from '@nestjs/typeorm'; 2 | import { PasswordEntity } from 'src/auth/passwords.entity'; 3 | import { SessionsEntity } from 'src/auth/sessions.entity'; 4 | import { LikesEntity } from 'src/likes/likes.entity'; 5 | import { MockLikesRepository } from './likes.repository.mock'; 6 | import { PostEntity } from 'src/posts/posts.entity'; 7 | import { UserFollowingEntity } from 'src/users/user-followings.entity'; 8 | import { UserEntity } from 'src/users/users.entity'; 9 | import { MockPostsRepository } from './posts.repository.mock'; 10 | import { MockUsersRepository } from './users.repository.mock'; 11 | 12 | export const MockUsersRepositoryProvider = { 13 | provide: getRepositoryToken(UserEntity), 14 | useValue: new MockUsersRepository(), 15 | }; 16 | 17 | export const MockUserFollowingsRepositoryProvider = { 18 | provide: getRepositoryToken(UserFollowingEntity), 19 | useValue: {}, 20 | }; 21 | 22 | export const MockPostsRepositoryProvider = { 23 | provide: getRepositoryToken(PostEntity), 24 | useValue: new MockPostsRepository(), 25 | }; 26 | 27 | export const MockLikesRepositoryProvider = { 28 | provide: getRepositoryToken(LikesEntity), 29 | useClass: MockLikesRepository, 30 | }; 31 | 32 | export const MockPasswordRepositoryProvider = { 33 | provide: getRepositoryToken(PasswordEntity), 34 | useValue: {}, 35 | }; 36 | 37 | export const MockSessionRepositoryProvider = { 38 | provide: getRepositoryToken(SessionsEntity), 39 | useValue: {}, 40 | }; 41 | -------------------------------------------------------------------------------- /src/commons/mocks/posts.repository.mock.ts: -------------------------------------------------------------------------------- 1 | import { PostEntity } from 'src/posts/posts.entity'; 2 | import { Repository } from 'typeorm'; 3 | 4 | export class MockPostsRepository extends Repository {} 5 | -------------------------------------------------------------------------------- /src/commons/mocks/users.repository.mock.ts: -------------------------------------------------------------------------------- 1 | import { PasswordEntity } from 'src/auth/passwords.entity'; 2 | import { UserEntity } from 'src/users/users.entity'; 3 | import { Repository } from 'typeorm'; 4 | 5 | export class MockUsersRepository extends Repository { 6 | async findOne() { 7 | const mockUser: UserEntity = { 8 | id: 'test-uuid', 9 | name: 'John Doe', 10 | followeeCount: 1, 11 | followerCount: 1, 12 | updatedAt: new Date('2020-01-01'), 13 | createdAt: new Date('2020-01-01'), 14 | username: 'johndoe', 15 | verified: true, 16 | userPassword: new PasswordEntity(), 17 | }; 18 | return mockUser; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/hashtags/hashtags.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { HashtagsController } from './hashtags.controller'; 3 | 4 | describe('HashtagsController', () => { 5 | let controller: HashtagsController; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | controllers: [HashtagsController], 10 | }).compile(); 11 | 12 | controller = module.get(HashtagsController); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(controller).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/hashtags/hashtags.controller.ts: -------------------------------------------------------------------------------- 1 | import { Param } from '@nestjs/common'; 2 | import { Controller, Get } from '@nestjs/common'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | 5 | @ApiTags('hashtags') 6 | @Controller('hashtags') 7 | export class HashtagsController { 8 | @Get('/') 9 | getHashtags(): string { 10 | // TODO: add actual logic 11 | return 'all top hashtags'; 12 | } 13 | 14 | @Get('/:tag/posts') 15 | getPostsForHashtag(@Param('tag') tag): string { 16 | // TODO: add actual logic 17 | return `show all posts with hashtag ${tag}`; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/hashtags/hashtags.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { HashtagsController } from './hashtags.controller'; 3 | 4 | @Module({ 5 | controllers: [HashtagsController], 6 | }) 7 | export class HashtagsModule {} 8 | -------------------------------------------------------------------------------- /src/likes/likes.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { LikesController } from './likes.controller'; 3 | import { MockLikesModule } from './likes.module.mock'; 4 | import { LikesService } from './likes.service'; 5 | 6 | describe('LikesController', () => { 7 | let controller: LikesController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [LikesController], 12 | providers: [LikesService], 13 | imports: [MockLikesModule], 14 | }).compile(); 15 | 16 | controller = module.get(LikesController); 17 | }); 18 | 19 | it('should be defined', () => { 20 | expect(controller).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/likes/likes.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { LikesService } from './likes.service'; 4 | 5 | @ApiTags('likes') 6 | @Controller('likes') 7 | export class LikesController { 8 | constructor(private readonly likesService: LikesService) {} 9 | } 10 | -------------------------------------------------------------------------------- /src/likes/likes.entity.ts: -------------------------------------------------------------------------------- 1 | import { MooBaseEntity } from 'src/commons/base.entity'; 2 | import { PostEntity } from 'src/posts/posts.entity'; 3 | import { UserEntity } from 'src/users/users.entity'; 4 | import { Entity, JoinColumn, ManyToOne } from 'typeorm'; 5 | 6 | @Entity('likes') 7 | export class LikesEntity extends MooBaseEntity { 8 | @ManyToOne(() => PostEntity) 9 | @JoinColumn({ name: 'post_id' }) 10 | post: PostEntity; 11 | 12 | @ManyToOne(() => UserEntity) 13 | @JoinColumn({ name: 'user_id' }) 14 | user: UserEntity; 15 | } 16 | -------------------------------------------------------------------------------- /src/likes/likes.module.mock.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MockLikesRepositoryProvider } from 'src/commons/mocks/mock.providers'; 3 | import { LikesService } from 'src/likes/likes.service'; 4 | 5 | @Module({ 6 | providers: [MockLikesRepositoryProvider, LikesService], 7 | exports: [MockLikesRepositoryProvider, LikesService], 8 | }) 9 | export class MockLikesModule {} 10 | -------------------------------------------------------------------------------- /src/likes/likes.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { LikesController } from './likes.controller'; 4 | import { LikesRepository } from './likes.repository'; 5 | import { LikesService } from './likes.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([LikesRepository])], 9 | controllers: [LikesController], 10 | providers: [LikesService], 11 | exports: [LikesService], 12 | }) 13 | export class LikesModule {} 14 | -------------------------------------------------------------------------------- /src/likes/likes.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import { LikesEntity } from './likes.entity'; 3 | 4 | @EntityRepository(LikesEntity) 5 | export class LikesRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /src/likes/likes.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MockLikesModule } from './likes.module.mock'; 3 | import { LikesService } from './likes.service'; 4 | 5 | describe('LikesService', () => { 6 | let service: LikesService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [LikesService], 11 | imports: [MockLikesModule], 12 | }).compile(); 13 | 14 | service = module.get(LikesService); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(service).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/likes/likes.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable } from '@nestjs/common'; 2 | import { PostEntity } from 'src/posts/posts.entity'; 3 | import { UserEntity } from 'src/users/users.entity'; 4 | import { LikesEntity } from './likes.entity'; 5 | import { LikesRepository } from './likes.repository'; 6 | 7 | @Injectable() 8 | export class LikesService { 9 | constructor(private likesRepository: LikesRepository) {} 10 | 11 | /** 12 | * @description like a post 13 | */ 14 | async likePost(post: PostEntity, user: UserEntity): Promise { 15 | const alreadyLiked = await this.getLikedPost(post.id, user.id); 16 | 17 | if (alreadyLiked) { 18 | return false; 19 | } 20 | 21 | const newLike = new LikesEntity(); 22 | newLike.post = post; 23 | newLike.user = user; 24 | 25 | const savedLike = await this.likesRepository.save(newLike); 26 | return savedLike !== null; 27 | } 28 | 29 | /** 30 | * @description unlike a post 31 | */ 32 | async unlikePost(postId: string, userId: string): Promise { 33 | const likedPost = await this.getLikedPost(postId, userId); 34 | 35 | if (!likedPost) { 36 | return false; 37 | } 38 | 39 | const unlikePost = await this.likesRepository.delete(likedPost.id); 40 | return unlikePost.affected === 1; 41 | } 42 | 43 | /** 44 | * @description helper method to get a liked post 45 | */ 46 | private async getLikedPost( 47 | postId: string, 48 | userId: string, 49 | ): Promise { 50 | if (!postId || !userId) { 51 | throw new BadRequestException( 52 | 'Post can only be liked/unliked if both user id and post id is provided', 53 | ); 54 | } 55 | 56 | return await this.likesRepository 57 | .createQueryBuilder('likes') 58 | .leftJoinAndSelect('likes.post', 'post') 59 | .leftJoinAndSelect('likes.user', 'user') 60 | .where(`post.id = :postId`, { postId }) 61 | .where(`user.id = :userId`, { userId }) 62 | .getOne(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { 3 | FastifyAdapter, 4 | NestFastifyApplication, 5 | } from '@nestjs/platform-fastify'; 6 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 7 | import { AppModule } from './app.module'; 8 | import compression from 'fastify-compress'; 9 | 10 | async function bootstrap() { 11 | const app = await NestFactory.create( 12 | AppModule, 13 | new FastifyAdapter(), 14 | ); 15 | 16 | const config = new DocumentBuilder() 17 | .setTitle('Moo API') 18 | .setDescription('API for shitposting social network') 19 | .addBearerAuth({ 20 | type: 'http', 21 | scheme: 'bearer', 22 | bearerFormat: 'Token', 23 | }) 24 | .setVersion('1.0') 25 | .addTag('posts') 26 | .build(); 27 | const document = SwaggerModule.createDocument(app, config); 28 | SwaggerModule.setup('api', app, document); 29 | 30 | app.register(compression, { 31 | encodings: ['gzip'], 32 | }); 33 | 34 | await app.listen(3000); 35 | } 36 | void bootstrap(); 37 | -------------------------------------------------------------------------------- /src/posts/posts.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { PostsController } from './posts.controller'; 3 | import { MockPostsModule } from './posts.module.mock'; 4 | import { PostsService } from './posts.service'; 5 | 6 | describe('PostsController', () => { 7 | let controller: PostsController; 8 | 9 | beforeEach(async () => { 10 | const module: TestingModule = await Test.createTestingModule({ 11 | controllers: [PostsController], 12 | providers: [PostsService], 13 | imports: [MockPostsModule], 14 | }).compile(); 15 | 16 | controller = module.get(PostsController); 17 | }); 18 | 19 | it('should be defined', () => { 20 | expect(controller).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/posts/posts.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Delete, 4 | Param, 5 | Post, 6 | Put, 7 | Query, 8 | Req, 9 | UseGuards, 10 | } from '@nestjs/common'; 11 | import { Controller, Get } from '@nestjs/common'; 12 | import { 13 | ApiBearerAuth, 14 | ApiProperty, 15 | ApiPropertyOptional, 16 | ApiTags, 17 | } from '@nestjs/swagger'; 18 | import { User } from 'src/auth/auth.decorator'; 19 | import { RequiredAuthGuard } from 'src/auth/auth.guard'; 20 | import { UserEntity } from 'src/users/users.entity'; 21 | import { PostEntity } from './posts.entity'; 22 | import { PostsService } from './posts.service'; 23 | 24 | class PostCreateRequestBody { 25 | @ApiProperty() text: string; 26 | @ApiPropertyOptional() originalPostId: string; 27 | @ApiPropertyOptional() replyToPostId: string; 28 | } 29 | 30 | class PostDetailsQueryParams { 31 | @ApiPropertyOptional() authorId: string; 32 | @ApiPropertyOptional() hashtags: string[]; 33 | } 34 | 35 | @ApiTags('posts') 36 | @Controller('posts') 37 | export class PostsController { 38 | constructor(private readonly postsService: PostsService) {} 39 | 40 | @Get('/') 41 | async getAllPosts( 42 | @Query() query: PostDetailsQueryParams, 43 | ): Promise { 44 | return await this.postsService.getAllPosts(query.authorId); 45 | } 46 | 47 | @Get('/:postId') 48 | async getPostDetails(@Param('postId') postId: string): Promise { 49 | return await this.postsService.getPost(postId); 50 | } 51 | 52 | @ApiBearerAuth() 53 | @UseGuards(RequiredAuthGuard) 54 | @Post('/') 55 | async createNewPost( 56 | @User() author: UserEntity, 57 | @Body() post: PostCreateRequestBody, 58 | ): Promise { 59 | const createdPost = await this.postsService.createPost( 60 | post, 61 | author, 62 | post.originalPostId, 63 | post.replyToPostId, 64 | ); 65 | return createdPost; 66 | } 67 | 68 | @ApiBearerAuth() 69 | @UseGuards(RequiredAuthGuard) 70 | @Delete('/:postId') 71 | async deletePost(@Param('postId') postId: string) { 72 | const deletedPost = { 73 | id: postId, 74 | deleted: await this.postsService.deletePost(postId), 75 | }; 76 | 77 | return deletedPost; 78 | } 79 | 80 | @ApiBearerAuth() 81 | @UseGuards(RequiredAuthGuard) 82 | @Put('/:postid/like') 83 | async likePost(@Param('postid') postid: string, @Req() req) { 84 | const token = (req.headers.authorization as string).replace('Bearer ', ''); 85 | const likedPost = { 86 | postId: postid, 87 | liked: await this.postsService.likePost(token, postid), 88 | }; 89 | 90 | return likedPost; 91 | } 92 | 93 | @ApiBearerAuth() 94 | @UseGuards(RequiredAuthGuard) 95 | @Delete('/:postid/like') 96 | async unlikePost(@Param('postid') postid: string, @Req() req) { 97 | const token = (req.headers.authorization as string).replace('Bearer ', ''); 98 | const unlikedPost = { 99 | postId: postid, 100 | unliked: await this.postsService.unlikePost(token, postid), 101 | }; 102 | 103 | return unlikedPost; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/posts/posts.entity.ts: -------------------------------------------------------------------------------- 1 | import { MooBaseEntity } from 'src/commons/base.entity'; 2 | import { UserEntity } from 'src/users/users.entity'; 3 | import { Column, Entity, JoinColumn, ManyToOne, OneToOne } from 'typeorm'; 4 | 5 | @Entity('posts') 6 | export class PostEntity extends MooBaseEntity { 7 | @Column({ length: 240, nullable: true }) 8 | text: string; 9 | 10 | @Column('json', { default: [] }) 11 | images: Array; 12 | 13 | @ManyToOne(() => UserEntity) 14 | @JoinColumn({ name: 'author_id' }) 15 | author: UserEntity; 16 | 17 | @Column({ name: 'like_count', default: 0 }) 18 | likeCount: number; 19 | 20 | @Column({ name: 'repost_count', default: 0 }) 21 | repostCount: number; 22 | 23 | @Column('json', { default: [] }) 24 | hashtags: Array; 25 | 26 | @Column('json', { default: [] }) 27 | mentions: Array; 28 | 29 | @OneToOne(() => PostEntity) 30 | @JoinColumn({ name: 'orig_post_id' }) 31 | origPost: PostEntity; 32 | 33 | @OneToOne(() => PostEntity) 34 | @JoinColumn({ name: 'reply_to_id' }) 35 | replyTo: PostEntity; 36 | } 37 | 38 | class Mention { 39 | name: string; 40 | id: string; 41 | } 42 | -------------------------------------------------------------------------------- /src/posts/posts.module.mock.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RequiredAuthGuard } from 'src/auth/auth.guard'; 3 | import { AuthService } from 'src/auth/auth.service'; 4 | import { 5 | MockUsersRepositoryProvider, 6 | MockPostsRepositoryProvider, 7 | MockPasswordRepositoryProvider, 8 | MockSessionRepositoryProvider, 9 | MockLikesRepositoryProvider, 10 | } from 'src/commons/mocks/mock.providers'; 11 | import { LikesService } from 'src/likes/likes.service'; 12 | 13 | @Module({ 14 | providers: [ 15 | MockUsersRepositoryProvider, 16 | MockPostsRepositoryProvider, 17 | MockLikesRepositoryProvider, 18 | MockPasswordRepositoryProvider, 19 | MockSessionRepositoryProvider, 20 | RequiredAuthGuard, 21 | LikesService, 22 | AuthService, 23 | ], 24 | exports: [ 25 | MockUsersRepositoryProvider, 26 | MockPostsRepositoryProvider, 27 | MockLikesRepositoryProvider, 28 | MockPasswordRepositoryProvider, 29 | MockSessionRepositoryProvider, 30 | RequiredAuthGuard, 31 | LikesService, 32 | AuthService, 33 | ], 34 | }) 35 | export class MockPostsModule {} 36 | -------------------------------------------------------------------------------- /src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { LikesModule } from 'src/likes/likes.module'; 4 | import { PostsController } from './posts.controller'; 5 | import { PostEntity } from './posts.entity'; 6 | import { PostsService } from './posts.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([PostEntity]), LikesModule], 10 | controllers: [PostsController], 11 | providers: [PostsService], 12 | }) 13 | export class PostsModule {} 14 | -------------------------------------------------------------------------------- /src/posts/posts.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import { PostEntity } from './posts.entity'; 3 | 4 | @EntityRepository(PostEntity) 5 | export class PostsRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /src/posts/posts.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { MockPostsModule } from './posts.module.mock'; 3 | import { PostsService } from './posts.service'; 4 | 5 | describe('PostsService', () => { 6 | let service: PostsService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | imports: [MockPostsModule], 11 | providers: [PostsService], 12 | }).compile(); 13 | 14 | service = module.get(PostsService); 15 | }); 16 | 17 | it('should be defined', () => { 18 | expect(service).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/posts/posts.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Injectable, 4 | NotFoundException, 5 | } from '@nestjs/common'; 6 | import { AuthService } from 'src/auth/auth.service'; 7 | import { LikesService } from 'src/likes/likes.service'; 8 | import { InjectRepository } from '@nestjs/typeorm'; 9 | import { UserEntity } from 'src/users/users.entity'; 10 | import { PostEntity } from './posts.entity'; 11 | import { PostsRepository } from './posts.repository'; 12 | 13 | @Injectable() 14 | export class PostsService { 15 | constructor( 16 | private readonly likesService: LikesService, 17 | private readonly authService: AuthService, 18 | @InjectRepository(PostEntity) 19 | private postsRepository: PostsRepository, 20 | ) {} 21 | 22 | /** 23 | * @description find all posts 24 | */ 25 | async getAllPosts( 26 | authorId?: string, 27 | hashtags?: string[] | null, 28 | ): Promise> { 29 | // TODO: implementation pagination (size + limit) 30 | // TODO: implement filter by hashtag 31 | const queryBuilder = this.postsRepository 32 | .createQueryBuilder('posts') 33 | .leftJoinAndSelect('posts.author', 'author') 34 | .leftJoinAndSelect('posts.origPost', 'origPost') 35 | .addSelect('origPost.author') 36 | .leftJoinAndSelect('origPost.author', 'origPostAuthor') 37 | .leftJoinAndSelect('posts.replyTo', 'replyTo') 38 | .addSelect('replyTo.author') 39 | .leftJoinAndSelect('replyTo.author', 'replyToAuthor'); 40 | 41 | if (authorId) { 42 | queryBuilder.where(`posts.author = :authorId`, { authorId }); 43 | } 44 | 45 | if (hashtags && hashtags.length > 0) { 46 | // TODO 47 | } 48 | 49 | return queryBuilder 50 | .addSelect('posts.created_at') 51 | .orderBy('posts.created_at', 'DESC') 52 | .limit(100) 53 | .getMany(); 54 | } 55 | 56 | /** 57 | * @description find post by id 58 | */ 59 | async getPost(id: string): Promise { 60 | return this.postsRepository.findOne(id, { 61 | relations: [ 62 | 'author', 63 | 'origPost', 64 | 'origPost.author', 65 | 'replyTo', 66 | 'replyTo.author', 67 | ], 68 | }); 69 | } 70 | 71 | /** 72 | * @description delete post by id 73 | */ 74 | async deletePost(id: string): Promise { 75 | const deleteResult = await this.postsRepository.delete({ id }); 76 | return deleteResult.affected === 1; 77 | } 78 | 79 | /** 80 | * @description create post 81 | */ 82 | async createPost( 83 | post: Partial, 84 | author: UserEntity, 85 | originalPostId: string, 86 | replyToPostId: string, 87 | ): Promise { 88 | // TODO: detect #hashtags in the post and create hashtag entities for them 89 | // TODO: deletect @user mentions in the post 90 | if (!post.text && !originalPostId) { 91 | throw new BadRequestException('Post must contain text or be a repost'); 92 | } 93 | 94 | if (originalPostId && replyToPostId) { 95 | throw new BadRequestException('Post can either be a reply or a repost'); 96 | } 97 | 98 | const newPost = new PostEntity(); 99 | newPost.text = post.text; 100 | newPost.author = author; 101 | 102 | if (originalPostId) { 103 | const origPost = await this.postsRepository.findOne(originalPostId); 104 | if (!origPost) { 105 | throw new NotFoundException('Original post not found'); 106 | } 107 | newPost.origPost = origPost; 108 | } 109 | 110 | if (replyToPostId) { 111 | const replyTo = await this.postsRepository.findOne(replyToPostId); 112 | if (!replyTo) { 113 | throw new NotFoundException('Original post not found'); 114 | } 115 | newPost.replyTo = replyTo; 116 | } 117 | 118 | const savedPost = await this.postsRepository.save(newPost); 119 | return savedPost; 120 | } 121 | 122 | /** 123 | * @description like post by id 124 | */ 125 | async likePost(token: string, postId: string): Promise { 126 | return await this.likeUnlikePostHelper(token, postId, 'like'); 127 | } 128 | 129 | /** 130 | * @description unlike post by id 131 | */ 132 | async unlikePost(token: string, postId: string): Promise { 133 | return await this.likeUnlikePostHelper(token, postId, 'unlike'); 134 | } 135 | 136 | /** 137 | * @description helper method for like/unlike post by id 138 | */ 139 | private async likeUnlikePostHelper( 140 | token: string, 141 | postId: string, 142 | type: 'like' | 'unlike', 143 | ) { 144 | const user = await this.authService.getUserFromSessionToken(token); 145 | 146 | const post = await this.getPost(postId); 147 | if (!post) { 148 | throw new NotFoundException('Post not found'); 149 | } 150 | 151 | return type === 'like' 152 | ? await this.likesService.likePost(post, user) 153 | : await this.likesService.unlikePost(postId, user.id); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/users/user-followings.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, JoinColumn, ManyToOne, Unique } from 'typeorm'; 2 | import { MooBaseEntity } from 'src/commons/base.entity'; 3 | import { UserEntity } from './users.entity'; 4 | 5 | // there can be only 1 row of same follower+followee 6 | @Unique('following_pair', ['follower', 'followee']) 7 | @Entity('user_followings') 8 | export class UserFollowingEntity extends MooBaseEntity { 9 | @JoinColumn({ name: 'follower_id' }) 10 | @ManyToOne(() => UserEntity) 11 | follower: UserEntity; 12 | 13 | @JoinColumn({ name: 'followee_id' }) 14 | @ManyToOne(() => UserEntity) 15 | followee: UserEntity; 16 | } 17 | -------------------------------------------------------------------------------- /src/users/users.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from 'src/auth/auth.service'; 3 | import { 4 | MockPasswordRepositoryProvider, 5 | MockSessionRepositoryProvider, 6 | MockUserFollowingsRepositoryProvider, 7 | MockUsersRepositoryProvider, 8 | } from 'src/commons/mocks/mock.providers'; 9 | import { UsersController } from './users.controller'; 10 | import { UsersService } from './users.service'; 11 | 12 | describe('UsersController', () => { 13 | let controller: UsersController; 14 | 15 | beforeEach(async () => { 16 | const module: TestingModule = await Test.createTestingModule({ 17 | providers: [ 18 | UsersService, 19 | AuthService, 20 | MockUsersRepositoryProvider, 21 | MockUserFollowingsRepositoryProvider, 22 | MockPasswordRepositoryProvider, 23 | MockSessionRepositoryProvider, 24 | ], 25 | controllers: [UsersController], 26 | }).compile(); 27 | 28 | controller = module.get(UsersController); 29 | }); 30 | 31 | it('should be defined', () => { 32 | expect(controller).toBeDefined(); 33 | }); 34 | 35 | it('should be return user', async () => { 36 | const user = await controller.getUserByUserid('test-uuid'); 37 | expect(user).toBeDefined(); 38 | expect(user.id).toBe('test-uuid'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Delete, 4 | ForbiddenException, 5 | NotFoundException, 6 | UseGuards, 7 | } from '@nestjs/common'; 8 | import { Controller, Get, Param, Patch, Post, Put } from '@nestjs/common'; 9 | import { 10 | ApiBearerAuth, 11 | ApiParam, 12 | ApiProperty, 13 | ApiPropertyOptional, 14 | ApiTags, 15 | } from '@nestjs/swagger'; 16 | import { User } from 'src/auth/auth.decorator'; 17 | import { RequiredAuthGuard } from 'src/auth/auth.guard'; 18 | import { UserEntity } from './users.entity'; 19 | import { UsersService } from './users.service'; 20 | 21 | class UserCreateRequestBody { 22 | @ApiProperty() username: string; 23 | @ApiProperty() password: string; 24 | @ApiPropertyOptional() name?: string; 25 | @ApiPropertyOptional() avatar?: string; 26 | @ApiPropertyOptional() bio?: string; 27 | } 28 | 29 | class UserUpdateRequestBody { 30 | @ApiPropertyOptional() password?: string; 31 | @ApiPropertyOptional() name?: string; 32 | @ApiPropertyOptional() avatar?: string; 33 | @ApiPropertyOptional() bio?: string; 34 | } 35 | 36 | @ApiTags('users') 37 | @Controller('users') 38 | export class UsersController { 39 | constructor(private userService: UsersService) {} 40 | 41 | @Get('/@:username') 42 | async getUserByUsername(@Param('username') username: string): Promise { 43 | const user = await this.userService.getUserByUsername(username); 44 | if (!user) { 45 | throw new NotFoundException('User not found'); 46 | } 47 | return user; 48 | } 49 | 50 | @Get('/:userid') 51 | async getUserByUserid(@Param('userid') userid: string): Promise { 52 | const user = await this.userService.getUserByUserId(userid); 53 | 54 | if (!user) { 55 | throw new NotFoundException('User not found'); 56 | } 57 | 58 | return user; 59 | } 60 | 61 | @Post('/') 62 | async createNewUser( 63 | @Body() createUserRequest: UserCreateRequestBody, 64 | ): Promise { 65 | const user = await this.userService.createUser( 66 | createUserRequest, 67 | createUserRequest.password, 68 | ); 69 | return user; 70 | } 71 | 72 | @ApiBearerAuth() 73 | @UseGuards(RequiredAuthGuard) 74 | @Patch('/:userid') 75 | async updateUserDetails( 76 | @User() authdUser: UserEntity, 77 | @Param('userid') userid: string, 78 | @Body() updateUserRequest: UserUpdateRequestBody, 79 | ): Promise { 80 | if (authdUser.id !== userid) { 81 | throw new ForbiddenException('You can only update your own user details'); 82 | } 83 | const user = await this.userService.updateUser(userid, updateUserRequest); 84 | return user; 85 | } 86 | 87 | @ApiBearerAuth() 88 | @UseGuards(RequiredAuthGuard) 89 | @Put('/:userid/follow') 90 | async followUser( 91 | @User() follower: UserEntity, 92 | @Param('userid') followeeId: string, 93 | ): Promise { 94 | const followedUser = await this.userService.createUserFollowRelation( 95 | follower, 96 | followeeId, 97 | ); 98 | return followedUser; 99 | } 100 | 101 | @ApiBearerAuth() 102 | @UseGuards(RequiredAuthGuard) 103 | @Delete('/:userid/follow') 104 | async unfollowUser( 105 | @User() follower: UserEntity, 106 | @Param('userid') followeeId: string, 107 | ): Promise { 108 | const unfollowedUser = await this.userService.deleteUserFollowRelation( 109 | follower, 110 | followeeId, 111 | ); 112 | return unfollowedUser; 113 | } 114 | 115 | @ApiBearerAuth() 116 | @UseGuards(RequiredAuthGuard) 117 | @Get('/:userid/followers') 118 | async getFollowersOfUser(): Promise { 119 | return []; 120 | } 121 | 122 | @ApiBearerAuth() 123 | @UseGuards(RequiredAuthGuard) 124 | @Put('/:userid/followees') 125 | async getFolloweesOfUser(): Promise { 126 | return []; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/users/users.entity.ts: -------------------------------------------------------------------------------- 1 | import { PasswordEntity } from 'src/auth/passwords.entity'; 2 | import { MooBaseEntity } from 'src/commons/base.entity'; 3 | import { Column, Entity, OneToOne } from 'typeorm'; 4 | 5 | @Entity('users') 6 | export class UserEntity extends MooBaseEntity { 7 | @Column({ length: 30, nullable: false, unique: true }) 8 | username: string; 9 | 10 | @Column({ nullable: true, length: 50 }) 11 | name: string; 12 | 13 | @Column({ nullable: true }) 14 | avatar?: string; 15 | 16 | @Column({ nullable: true, length: 240 }) 17 | bio?: string; 18 | 19 | @Column({ name: 'follower_count', default: 0 }) 20 | followerCount: number; 21 | 22 | @Column({ name: 'followee_count', default: 0 }) 23 | followeeCount: number; 24 | 25 | @Column('boolean', { default: false }) 26 | verified: boolean; 27 | 28 | @OneToOne((type) => PasswordEntity, (password) => password.user, { 29 | lazy: true, 30 | cascade: true, 31 | }) 32 | userPassword: PasswordEntity; 33 | } 34 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { PasswordEntity } from 'src/auth/passwords.entity'; 4 | import { UserFollowingEntity } from './user-followings.entity'; 5 | import { UsersController } from './users.controller'; 6 | import { UserEntity } from './users.entity'; 7 | import { UsersService } from './users.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | TypeOrmModule.forFeature([UserEntity, PasswordEntity, UserFollowingEntity]), 12 | ], 13 | controllers: [UsersController], 14 | providers: [UsersService], 15 | }) 16 | export class UsersModule {} 17 | -------------------------------------------------------------------------------- /src/users/users.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository, Repository } from 'typeorm'; 2 | import { UserEntity } from './users.entity'; 3 | 4 | @EntityRepository(UserEntity) 5 | export class UsersRepository extends Repository {} 6 | -------------------------------------------------------------------------------- /src/users/users.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { getRepositoryToken } from '@nestjs/typeorm'; 3 | import { AuthService } from 'src/auth/auth.service'; 4 | import { 5 | MockPasswordRepositoryProvider, 6 | MockSessionRepositoryProvider, 7 | MockUserFollowingsRepositoryProvider, 8 | MockUsersRepositoryProvider, 9 | } from 'src/commons/mocks/mock.providers'; 10 | import { MockUsersRepository } from 'src/commons/mocks/users.repository.mock'; 11 | import { UserEntity } from './users.entity'; 12 | import { UsersService } from './users.service'; 13 | 14 | describe('UsersService', () => { 15 | let service: UsersService; 16 | 17 | beforeEach(async () => { 18 | const module: TestingModule = await Test.createTestingModule({ 19 | providers: [ 20 | MockUsersRepositoryProvider, 21 | MockUserFollowingsRepositoryProvider, 22 | MockPasswordRepositoryProvider, 23 | MockSessionRepositoryProvider, 24 | UsersService, 25 | AuthService, 26 | ], 27 | }).compile(); 28 | 29 | service = module.get(UsersService); 30 | }); 31 | 32 | it('should be defined', () => { 33 | expect(service).toBeDefined(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | ConflictException, 4 | Injectable, 5 | NotFoundException, 6 | } from '@nestjs/common'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { AuthService } from 'src/auth/auth.service'; 9 | import { Repository } from 'typeorm'; 10 | import { UserFollowingEntity } from './user-followings.entity'; 11 | import { UserEntity } from './users.entity'; 12 | import { UsersRepository } from './users.repository'; 13 | 14 | @Injectable() 15 | export class UsersService { 16 | constructor( 17 | @InjectRepository(UserEntity) private userRepo: UsersRepository, 18 | private authService: AuthService, 19 | @InjectRepository(UserFollowingEntity) 20 | private userFollowRepo: Repository, 21 | ) {} 22 | /** 23 | * @description find a user with a given username 24 | * @returns {Promise} user if found 25 | */ 26 | public async getUserByUsername(username: string): Promise { 27 | return await this.userRepo.findOne({ where: { username } }); 28 | } 29 | 30 | /** 31 | * @description find a user with a given userid 32 | * @returns {Promise} user if found 33 | */ 34 | public async getUserByUserId(userId: string): Promise { 35 | return await this.userRepo.findOne({ where: { id: userId } }); 36 | } 37 | 38 | /** 39 | * @description create new user with given details 40 | * @returns {Promise} user if created 41 | */ 42 | public async createUser( 43 | user: Partial, 44 | password: string, 45 | ): Promise { 46 | if (user.username.length < 5) 47 | throw new BadRequestException('Username must be of minimum 5 characters'); 48 | 49 | if (password.length < 8) 50 | throw new BadRequestException('Password must be of minimum 8 characters'); 51 | 52 | if (password.toLowerCase().includes('password')) 53 | throw new BadRequestException( 54 | 'Password cannot contain the word password itself', 55 | ); 56 | 57 | const usernameAlreadyExists = await this.getUserByUsername(user.username); 58 | if (usernameAlreadyExists) 59 | throw new ConflictException('This username is already taken!'); 60 | 61 | const newUser = await this.userRepo.save(user); 62 | 63 | await this.authService.createPasswordForNewUser(newUser.id, password); 64 | 65 | return newUser; 66 | } 67 | 68 | /** 69 | * @description update a user with given details 70 | * @returns {Promise} user if updated 71 | */ 72 | public async updateUser( 73 | userId: string, 74 | newUserDetails: Partial, 75 | ): Promise { 76 | const existingUser = await this.userRepo.findOne({ 77 | where: { id: userId }, 78 | }); 79 | if (!existingUser) { 80 | return null; 81 | } 82 | if (newUserDetails.bio) existingUser.bio = newUserDetails.bio; 83 | if (newUserDetails.avatar) existingUser.avatar = newUserDetails.avatar; 84 | if (newUserDetails.name) existingUser.name = newUserDetails.name; 85 | 86 | return await this.userRepo.save(existingUser); 87 | } 88 | 89 | /** 90 | * create a user-user follow pairing 91 | */ 92 | public async createUserFollowRelation( 93 | follower: UserEntity, 94 | followeeId: string, 95 | ) { 96 | const followee = await this.getUserByUserId(followeeId); 97 | if (!followee) { 98 | throw new NotFoundException('User not found'); 99 | } 100 | const newFollow = await this.userFollowRepo.save({ 101 | follower, 102 | followee, 103 | }); 104 | return newFollow.followee; 105 | } 106 | 107 | /** 108 | * delete a user-user follow pairing 109 | */ 110 | public async deleteUserFollowRelation( 111 | follower: UserEntity, 112 | followeeId: string, 113 | ) { 114 | const followee = await this.getUserByUserId(followeeId); 115 | if (!followee) { 116 | throw new NotFoundException('User not found'); 117 | } 118 | const follow = await this.userFollowRepo.findOne({ 119 | where: { follower, followee }, 120 | }); 121 | if (follow) { 122 | await this.userFollowRepo.delete(follow.id); 123 | // TODO: future: show show that I do not follow them anymore in the response 124 | return followee; 125 | } else { 126 | throw new NotFoundException('No follow relationship found'); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { 3 | FastifyAdapter, 4 | NestFastifyApplication, 5 | } from '@nestjs/platform-fastify'; 6 | import { AppController } from 'src/app.controller'; 7 | import { AppService } from 'src/app.service'; 8 | 9 | describe('AppController (e2e)', () => { 10 | let app: NestFastifyApplication; 11 | 12 | beforeAll(async () => { 13 | const moduleFixture: TestingModule = await Test.createTestingModule({ 14 | controllers: [AppController], 15 | providers: [AppService], 16 | }).compile(); 17 | 18 | app = moduleFixture.createNestApplication( 19 | new FastifyAdapter(), 20 | ); 21 | await app.init(); 22 | }); 23 | 24 | it('/ (GET)', () => { 25 | return app 26 | .inject({ 27 | method: 'GET', 28 | url: '/hello', 29 | }) 30 | .then((response) => { 31 | expect(response.statusCode).toBe(200); 32 | expect(response.payload).toBe('Hello World!'); 33 | }); 34 | }); 35 | 36 | afterAll(async () => { 37 | await app.close(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": "..", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "coverageDirectory": "./coverage-e2e", 10 | "moduleNameMapper": { 11 | "src/(.*)": "/src/$1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/users/users.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { ApiModule } from 'src/api.module'; 3 | import { TestDbModule } from 'src/commons/db.module'; 4 | import { 5 | NestFastifyApplication, 6 | FastifyAdapter, 7 | } from '@nestjs/platform-fastify'; 8 | 9 | describe('UsersController (e2e)', () => { 10 | let app: NestFastifyApplication; 11 | 12 | beforeAll(async () => { 13 | const moduleFixture: TestingModule = await Test.createTestingModule({ 14 | imports: [ApiModule, TestDbModule], 15 | }).compile(); 16 | 17 | app = moduleFixture.createNestApplication( 18 | new FastifyAdapter(), 19 | ); 20 | await app.init(); 21 | await app.getHttpAdapter().getInstance().ready(); 22 | }); 23 | 24 | // test user creation 25 | it('(POST) /users/', () => { 26 | return app 27 | .inject({ 28 | method: 'POST', 29 | url: '/users', 30 | payload: { 31 | username: 'arnav', 32 | password: 'arnav123', 33 | name: 'Arnav Gupta', 34 | bio: 'This is a nice guy!', 35 | }, 36 | }) 37 | .then((response) => { 38 | expect(response.statusCode).toBe(201); 39 | }); 40 | }); 41 | 42 | afterAll(async () => { 43 | await app.close(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "**/*mock.ts"], 4 | "compilerOptions": { 5 | "rootDir": "./", 6 | "paths": { 7 | "src": ["./src"] 8 | } 9 | }, 10 | 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------