├── .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 | 
6 | [](https://deepsource.io/gh/scaleracademy/twitter-backend-node/?ref=repository-badge)
7 | [](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 |
45 | - Classes
46 | - MockPostsRepository
47 |
48 |
49 |
57 |
58 |
59 |
60 |
File
62 |
63 |
66 |
67 |
68 |
69 | Extends
71 |
72 |
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 |
45 | - Classes
46 | - PostsRepository
47 |
48 |
49 |
57 |
58 |
59 |
60 |
File
62 |
63 |
66 |
67 |
68 |
69 | Extends
71 |
72 |
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 |
45 | - Classes
46 | - UsersRepository
47 |
48 |
49 |
57 |
58 |
59 |
60 |
File
62 |
63 |
66 |
67 |
68 |
69 | Extends
71 |
72 |
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 |
48 | - Dependencies
49 |
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 |
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 |
41 | - Injectables
42 | - AppService
43 |
44 |
45 |
53 |
54 |
55 |
56 |
File
58 |
59 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Index
69 |
70 |
71 |
72 |
73 |
74 | Methods
75 | |
76 |
77 |
78 |
79 |
84 | |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | Methods
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | getHello
108 |
109 |
110 | |
111 |
112 |
113 |
114 | getHello()
115 | |
116 |
117 |
118 |
119 |
120 |
121 |
123 | |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | Returns : string
132 |
133 |
134 | |
135 |
136 |
137 |
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="";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 |
50 | - Miscellaneous
51 | - Functions
52 |
53 |
54 |
55 | Index
56 |
57 |
58 |
59 |
60 |
65 | |
66 |
67 |
68 |
69 |
70 |
71 |
src/main.ts
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | bootstrap
80 |
81 |
82 | |
83 |
84 |
85 |
86 | bootstrap()
87 | |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
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 |
77 |
--------------------------------------------------------------------------------
/docs/modules/AppModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
69 |
--------------------------------------------------------------------------------
/docs/modules/AuthModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
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 |
37 | - Modules
38 | - HashtagsModule
39 |
40 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
File
56 |
57 |
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 |
69 |
--------------------------------------------------------------------------------
/docs/modules/PostsModule/dependencies.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
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 |
37 | - Modules
38 | - ProdDbModule
39 |
40 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
File
56 |
57 |
60 |
61 |
62 |
63 |
Description
65 |
66 |
Database module for production
68 |
69 |
70 |
71 |
72 |
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 |
37 | - Modules
38 | - TestDbModule
39 |
40 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
File
56 |
57 |
60 |
61 |
62 |
63 |
Description
65 |
66 |
Database module for testing purposes
68 |
69 |
70 |
71 |
72 |
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 |
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 |
--------------------------------------------------------------------------------