├── .env ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ └── QUESTION.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ ├── dependabot-automerge.yml │ └── typos.yml ├── .gitignore ├── .prettierignore ├── .vscode ├── launch.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── images │ ├── postgresql-installer.png │ └── stackbuilder-installer.png ├── docs ├── ERD.md ├── benchmarks │ └── AMD Ryzen 9 7940HS w Radeon 780M Graphics.md └── requirements │ └── index.md ├── nest-cli.json ├── nestia.config.ts ├── package.json ├── packages └── api │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── swagger.json │ └── tsconfig.json ├── postgres.sh ├── prettier.config.js ├── prisma └── schema │ └── main.prisma ├── src ├── BbsBackend.ts ├── BbsConfiguration.ts ├── BbsGlobal.ts ├── BbsModule.ts ├── api │ ├── HttpError.ts │ ├── IConnection.ts │ ├── functional │ │ ├── bbs │ │ │ ├── articles │ │ │ │ ├── comments │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── monitors │ │ │ ├── health │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── performance │ │ │ └── index.ts │ │ │ └── system │ │ │ └── index.ts │ ├── index.ts │ ├── module.ts │ ├── structures │ │ ├── bbs │ │ │ ├── IBbsArticle.ts │ │ │ └── IBbsArticleComment.ts │ │ ├── common │ │ │ ├── IAttachmentFile.ts │ │ │ ├── IDiagnosis.ts │ │ │ ├── IEntity.ts │ │ │ ├── IPage.ts │ │ │ └── IRecordMerge.ts │ │ └── monitors │ │ │ ├── IPerformance.ts │ │ │ └── ISystem.ts │ └── typings │ │ ├── Atomic.ts │ │ └── Writable.ts ├── controllers │ ├── bbs │ │ ├── BbsArticleCommentsController.ts │ │ └── BbsArticlesController.ts │ └── monitors │ │ ├── MonitorHealthController.ts │ │ ├── MonitorModule.ts │ │ ├── MonitorPerformanceController.ts │ │ └── MonitorSystemController.ts ├── executable │ ├── schema.ts │ ├── server.ts │ └── swagger.ts ├── providers │ ├── bbs │ │ ├── BbsArticleCommentProvider.ts │ │ ├── BbsArticleCommentSnapshotProvider.ts │ │ ├── BbsArticleProvider.ts │ │ ├── BbsArticleSnapshotProvider.ts │ │ ├── EntityMergeProvider.ts │ │ └── ErrorProvider.ts │ ├── common │ │ └── AttachmentFileProvider.ts │ └── monitors │ │ └── SystemProvider.ts ├── setup │ └── BbsSetupWizard.ts └── utils │ ├── BcryptUtil.ts │ ├── DateUtil.ts │ ├── EntityUtil.ts │ ├── ErrorUtil.ts │ ├── JwtTokenManager.ts │ ├── MapUtil.ts │ ├── PaginationUtil.ts │ └── Terminal.ts ├── test ├── TestAutomation.ts ├── benchmark │ ├── index.ts │ └── servant.ts ├── features │ └── api │ │ ├── bbs │ │ ├── internal │ │ │ ├── generate_random_article.ts │ │ │ ├── generate_random_comment.ts │ │ │ ├── prepare_random_article.ts │ │ │ ├── prepare_random_comment.ts │ │ │ └── prepare_random_file.ts │ │ ├── test_api_bbs_article_comment_create.ts │ │ ├── test_api_bbs_article_comment_erase.ts │ │ ├── test_api_bbs_article_comment_index_search.ts │ │ ├── test_api_bbs_article_comment_index_sort.ts │ │ ├── test_api_bbs_article_comment_password.ts │ │ ├── test_api_bbs_article_comment_update.ts │ │ ├── test_api_bbs_article_create.ts │ │ ├── test_api_bbs_article_erase.ts │ │ ├── test_api_bbs_article_index_search.ts │ │ ├── test_api_bbs_article_index_sort.ts │ │ ├── test_api_bbs_article_password.ts │ │ └── test_api_bbs_article_update.ts │ │ └── monitors │ │ ├── test_api_monitor_health_check.ts │ │ └── test_api_monitor_system.ts ├── index.ts ├── internal │ ├── ArgumentParser.ts │ └── StopWatch.ts ├── manual │ ├── password.ts │ ├── update.ts │ └── uuid.ts ├── tsconfig.json └── webpack.ts ├── tsconfig.json ├── typos.toml └── webpack.config.js /.env: -------------------------------------------------------------------------------- 1 | BBS_MODE=local 2 | 3 | BBS_API_PORT=37000 4 | 5 | BBS_POSTGRES_HOST=127.0.0.1 6 | BBS_POSTGRES_PORT=5432 7 | BBS_POSTGRES_DATABASE=samchon 8 | BBS_POSTGRES_SCHEMA=bbs 9 | BBS_POSTGRES_USERNAME=samchon 10 | BBS_POSTGRES_USERNAME_READONLY=samchon_r 11 | BBS_POSTGRES_PASSWORD=samchon 12 | BBS_POSTGRES_URL=postgresql://${BBS_POSTGRES_USERNAME}:${BBS_POSTGRES_PASSWORD}@${BBS_POSTGRES_HOST}:${BBS_POSTGRES_PORT}/${BBS_POSTGRES_DATABASE}?schema=${BBS_POSTGRES_SCHEMA} 13 | 14 | BBS_SYSTEM_PASSWORD=samchon 15 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | plugins: ["@typescript-eslint", "deprecation"], 4 | extends: ["plugin:@typescript-eslint/recommended"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: ["tsconfig.json", "test/tsconfig.json"], 8 | }, 9 | overrides: [ 10 | { 11 | files: ["src/**/*.ts", "test/**/*.ts"], 12 | rules: { 13 | "@typescript-eslint/consistent-type-definitions": "off", 14 | "@typescript-eslint/no-empty-function": "off", 15 | "@typescript-eslint/no-empty-interface": "off", 16 | "@typescript-eslint/no-explicit-any": "off", 17 | "@typescript-eslint/no-inferrable-types": "off", 18 | "@typescript-eslint/no-namespace": "off", 19 | "@typescript-eslint/no-non-null-assertion": "off", 20 | "@typescript-eslint/no-unused-vars": "off", 21 | "@typescript-eslint/no-var-requires": "off", 22 | "@typescript-eslint/no-floating-promises": "error", 23 | }, 24 | }, 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | ## Bug Report 8 | Note that, the bug you're reporting may have registered in the [Issues](https://github.com/samchon/bbs-backend/search?type=Issues) by another user. Even the bug you're reporting may have been fixed in the `@next` version. In those reasons, I recommend you to check the old [Issues](https://github.com/samchon/bbs-backend/search?type=Issues) and reproduct your code with the `@next` version before publishing the bug reporting issue. 9 | 10 | ```bash 11 | `npm install --save @samchon/bbs-api@next` 12 | ``` 13 | 14 | When same error occurs with the `@next` version, then please fill the below template: 15 | 16 | ### Summary 17 | - **SDK Version**: 0.1.0@dev-20210626 18 | - **Expected behavior**: 19 | - **Actual behavior** 20 | 21 | ### Code occuring the bug 22 | ```typescript 23 | import api from "@samchon/bbs-api"; 24 | 25 | /* Demonstration code occuring the bug you're reporting */ 26 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | ## Feature Request 8 | Thanks for your suggestion. Feel free to just state your idea. Writing the issue, it would better to filling the below items: 9 | 10 | - A description of the problem you're trying to solve. 11 | - An overview of the suggested solution. 12 | - Examples of how the suggestion whould work in various places. 13 | - Code examples showing the expected behavior. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/QUESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: What do you want to know? 4 | 5 | --- 6 | 7 | ## Question 8 | I always welcome any questions about this backend proejct. However, half of questions can be solved by traveling [README.md](https://github.com/samchon/bbs-backend) or old [Issues](https://github.com/samchon/bbs-backend/search?type=Issues). Therefore, I recommend you to visit them before. If you have a question have not treated and you're urgently, just contact me. 9 | 10 | - E-mail: samchon.github@gmail.com 11 | - Mobile: 82-10-3627-0016 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before submitting a Pull Request, please test your code. 2 | 3 | If you created a new created a new feature, then create the unit test function, too. 4 | 5 | ```bash 6 | # COMPILE THE BACKEND SERVER 7 | npm run build 8 | 9 | # RUN THE TEST AUTOMATION PROGRAM 10 | npm run test 11 | ``` 12 | 13 | Learn more about the [CONTRIBUTING](CONTRIBUTING.md) -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | ####################################################### 4 | - package-ecosystem: "npm" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | open-pull-requests-limit: 25 9 | versioning-strategy: increase 10 | allow: 11 | - dependency-name: "nestia" 12 | - dependency-name: "@nestjs/*" 13 | - dependency-name: "@nestia/*" 14 | - dependency-name: "@prisma/*" 15 | - dependency-name: "prisma" 16 | - dependency-name: "prisma-markdown" 17 | - dependency-name: "tstl" 18 | - dependency-name: "typia" 19 | - dependency-name: "tgrid" 20 | - dependency-name: "typescript" 21 | - dependency-name: "typescript-transform-paths" 22 | - dependency-name: "ts-patch" 23 | groups: 24 | Samchon: 25 | patterns: 26 | - "@nestia/*" 27 | - "nestia" 28 | - "tstl" 29 | - "tgrid" 30 | - "typia" 31 | - "prisma-markdown" 32 | NestJS: 33 | patterns: 34 | - "@nestjs/*" 35 | Prisma: 36 | patterns: 37 | - "@prisma/*" 38 | - "prisma" 39 | TypeScript: 40 | patterns: 41 | - "typescript" 42 | - "typescript-transform-paths" 43 | - "ts-patch" 44 | ####################################################### 45 | - package-ecosystem: "npm" 46 | directory: "/packages/api" 47 | schedule: 48 | interval: "daily" 49 | open-pull-requests-limit: 25 50 | versioning-strategy: increase 51 | allow: 52 | - dependency-name: "@nestia/fetcher" 53 | - dependency-name: "typia" 54 | groups: 55 | Samchon: 56 | patterns: 57 | - "@nestia/fetcher" 58 | - "typia" 59 | ####################################################### 60 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | pull_request: 4 | paths: 5 | - "prisma/**" 6 | - "src/**" 7 | - "test/**" 8 | - "package.json" 9 | 10 | jobs: 11 | Ubuntu: 12 | runs-on: ubuntu-latest 13 | services: 14 | postgres: 15 | image: postgis/postgis:16-3.4 16 | env: 17 | POSTGRES_PASSWORD: root 18 | ports: 19 | - 5432:5432 20 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | - uses: pnpm/action-setup@v2 25 | with: 26 | version: 8 27 | 28 | - name: Install Backend-Server 29 | run: pnpm install 30 | 31 | - name: Compile Backend-Server 32 | run: npm run build 33 | 34 | - name: Validate through EsLint 35 | run: npm run eslint 36 | 37 | - name: Create DB Schema 38 | run: npm run schema postgres root 39 | 40 | - name: Test Backend-Server 41 | run: npm run test -- --reset true --simultaneous 16 42 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | name: "Dependabot Automerge" 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | types: [opened, synchronize] 7 | paths: 8 | - "package.json" 9 | - "packages/api/package.json" 10 | jobs: 11 | build-and-merge: 12 | if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | steps: 18 | - name: Comment on PR 19 | run: | 20 | gh pr comment "$PR_URL" --body "@dependabot merge" 21 | env: 22 | PR_URL: ${{ github.event.pull_request.html_url }} 23 | GH_TOKEN: ${{ secrets.DEPENDABOT_AUTOMERGE_GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | name: typos 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | typos: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout Actions Repository 10 | uses: actions/checkout@v4 11 | 12 | - uses: crate-ci/typos@master 13 | with: 14 | config: ./typos.toml 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | dist/ 3 | lib/ 4 | node_modules/ 5 | prisma/migrations 6 | 7 | *.DS_Store 8 | package-lock.json 9 | pnpm-lock.yaml 10 | .npmrc -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | bin 3 | lib 4 | 5 | node_modules 6 | packages 7 | 8 | README.md 9 | tsconfig.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "JavaScript Test using SourceMap", 11 | "program": "${workspaceRoot}/test/index.ts", 12 | "cwd": "${workspaceRoot}", 13 | "args": [ 14 | //---- 15 | // Unable to reset DB in debugging mode. 16 | //---- 17 | // Therefore, reset DB first by running 18 | // `npm run reset-for-debugging` command, 19 | // and run debugging mode later. 20 | //---- 21 | "--reset", "false", 22 | //---- 23 | // You can run specific test functions 24 | //---- 25 | // "--include", "something", 26 | // "--exclude", "nothing", 27 | ], 28 | "outFiles": ["${workspaceRoot}/bin/**/*.js"], 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.formatOnSave": true, 4 | "[javascript][typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": "explicit" 8 | } 9 | }, 10 | "[prisma]": { 11 | "editor.defaultFormatter": "Prisma.prisma", 12 | }, 13 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | ## To Publish an issue 3 | Thanks for your advise. Before publishing an issue, please check some components. 4 | 5 | ### 1. Search for duplicates 6 | Before publishing an issue, please check whether the duplicated issue exists or not. 7 | 8 | - [Ordinary Issues](https://github.com/samchon/bbs-backend/issues) 9 | 10 | ### 2. Did you find a bug? 11 | When you reporting a bug, then please write about those items: 12 | 13 | - What version you're using 14 | - If possible, give me an isolated way to reproduce the behavior. 15 | - The behavior your expect to see, and the actual behavior. 16 | 17 | ### 3. Do you have a suggestion? 18 | I always welcome your suggestion. When you publishing a suggestion, then please write such items: 19 | 20 | - A description of the problem you're trying to solve. 21 | - An overview of the suggested solution. 22 | - Examples of how the suggestion would work in various places. 23 | - Code examples showing the expected behavior. 24 | 25 | 26 | 27 | 28 | ## Contributing Code 29 | ### Test your code 30 | Before sending a pull request, please test your new code. You type the command `npm run build &&& npm run test`, then compiling your code and test-automation will be all processed. 31 | 32 | ```bash 33 | # COMPILE 34 | npm run build 35 | 36 | # DO TEST 37 | npm run test 38 | ``` 39 | 40 | If you succeeded to compile, but failed to pass the test-automation, then *debug* the test-automation module. I've configured the `.vscode/launch.json`. You just run the `VSCode` and click the `Start Debugging` button or press `F5` key. By the *debugging*, find the reason why the *test* is failed and fix it. 41 | 42 | ### Adding a Test 43 | If you want to add a testing-logic, then goto the `src/test` directory. It's the directory containing the test-automation module. Declare some functions starting from the prefix `test_`. Then, they will be called after the next testing. 44 | 45 | Note that, the special functions starting from the prefix `test_` must be `export`ed. They also must return one of them: 46 | - `void` 47 | - `Promise` 48 | 49 | When you detect an error, then throw exception such below: 50 | 51 | ```typescript 52 | import { assert } from "typescript-is"; 53 | import api from "../../../../../../api"; 54 | import { IBbsCustomer } from "../../../../../../api/structures/bbs/actors/IBbsCustomer"; 55 | import { IMember } from "../../../../../../api/structures/members/IMember"; 56 | 57 | import { Configuration } from "../../../../../../Configuration"; 58 | import { RandomGenerator } from "../../../../../../utils/RandomGenerator"; 59 | import { exception_must_be_thrown } from "../../../../../internal/exception_must_be_thrown"; 60 | import { prepare_random_citizen } from "../internal/prepare_random_citizen"; 61 | import { test_bbs_customer_activate } from "./test_bbs_customer_activate"; 62 | 63 | export async function test_bbs_customer_join_after_activate 64 | (connection: api.IConnection): Promise 65 | { 66 | const customer: IBbsCustomer = await test_bbs_customer_activate(connection); 67 | 68 | // DIFFERENT CITIZEN 69 | await exception_must_be_thrown 70 | ( 71 | "Customer join after activation with different citizen info", 72 | () => api.functional.bbs.customers.authenticate.join 73 | ( 74 | connection, 75 | { 76 | email: `${RandomGenerator.alphabets(16)}@samchon.org`, 77 | password: Configuration.SYSTEM_PASSWORD(), 78 | citizen: prepare_random_citizen() 79 | } 80 | ) 81 | ); 82 | 83 | // SAME CITIZEN 84 | const member: IMember = await api.functional.bbs.customers.authenticate.join 85 | ( 86 | connection, 87 | { 88 | email: `${RandomGenerator.alphabets(16)}@samchon.org`, 89 | password: Configuration.SYSTEM_PASSWORD(), 90 | citizen: customer.citizen 91 | } 92 | ); 93 | assert(member); 94 | } 95 | ``` 96 | 97 | 98 | 99 | ## Sending a Pull Request 100 | Thanks for your contributing. Before sending a pull request to me, please check those components. 101 | 102 | ### 1. Include enough descriptions 103 | When you send a pull request, please include a description, of what your change intends to do, on the content. Title, make it clear and simple such below: 104 | 105 | - Refactor features 106 | - Fix #17 107 | - Add tests for #28 108 | 109 | ### 2. Include adequate tests 110 | As I've mentioned in the `Contributing Code` section, your PR should pass the test-automation module. Your PR includes *new features* that have not being handled in the ordinary test-automation module, then also update *add the testing unit* please. 111 | 112 | If there're some specific reasons that could not pass the test-automation (not error but *intended*), then please update the ordinary test-automation module or write the reasons on your PR content and *const me update the test-automation module*. 113 | 114 | 115 | 116 | 117 | ## References 118 | I've referenced contribution guidance of the TypeScript. 119 | - https://github.com/Microsoft/TypeScript/blob/master/CONTRIBUTING.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 samchon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backend of Bullet-in Board System 2 | ## 1. Outline 3 | ![Nestia Logo](https://nestia.io/logo.png) 4 | 5 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/bbs-backend/tree/master/LICENSE) 6 | [![npm version](https://img.shields.io/npm/v/@samchon/bbs-api.svg)](https://www.npmjs.com/package/@samchon/bbs-api) 7 | [![Build Status](https://github.com/samchon/bbs-backend/workflows/build/badge.svg)](https://github.com/samchon/bbs-backend/actions?query=workflow%3Abuild) 8 | [![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://nestia.io/docs/) 9 | 10 | Example backend server of Bullet-in Board System for education. 11 | 12 | `@samchon/bbs-backend` is an example backend project of [NestJS](https://nestjs.com) and [Prisma](https://prisma.io) stack. It has been developed to educate how to adapt **functional programming** in the NestJS development. Therefore, it is not the actual bullet-in board service, and implementation of most functions is different from the actual bullet-in board service and may be meaningless. 13 | 14 | Also, `@samchon/bbs-backend` guides how to utilize those 3rd party libraries (what I've developed) in the production, and demonostrates how they are powerful for the productivity. Especially, I have ideally implemented **TDD (Test Driven Development)** through below libraries. I hope this repo would be helpful for your learning. 15 | 16 | - [typia](https://github.com/samchon/typia): Superfast runtime validator 17 | - [nestia](https://github.com/samchon/nestia): NestJS helper libraries like SDK generation 18 | - [prisma-markdown](https://github.com/samchon/prisma-markdown): Markdown generator of Prisma, including ERD and descriptions 19 | 20 | 21 | 22 | 23 | ## 2. Installation 24 | ### 2.1. NodeJS 25 | This backend server has implemented through TypeScript and it runs on the NodeJS. Therefore, to mount this backend server on your local machine, you've to install the NodeJS. 26 | 27 | - https://nodejs.org/en/ 28 | 29 | ### 2.2. PostgreSQL 30 | > ```bash 31 | > bash postgres.sh 32 | >``` 33 | > 34 | > If you've installed Docker, then run the script above. 35 | 36 | Otherwise, visit below PostgreSQL official site and install it manually. 37 | 38 | https://www.enterprisedb.com/downloads/postgres-postgresql-downloads 39 | 40 | After that, run the `npm run schema ` command. 41 | 42 | Database schema for BBS backend system would be automatically constructed. 43 | 44 | ```bash 45 | npm run schema postgres root 46 | ``` 47 | 48 | ### 2.3. Repository 49 | From now on, you can start the backend server development, right now. 50 | 51 | Just download this project through the git clone command and install dependencies by the npm install command. After those preparations, you can start the development by typing the `npm run dev` command. 52 | 53 | ```bash 54 | # CLONE REPOSITORY 55 | git clone https://github.com/samchon/bbs-backend 56 | cd bbs-backend 57 | 58 | # INSTALL DEPENDENCIES 59 | npm install 60 | 61 | # START DEVELOPMENT 62 | npm run dev 63 | ``` 64 | 65 | 66 | 67 | 68 | ## 3. Development 69 | > - A. Definition only 70 | > - Design prisma schema file 71 | > - Build and Share ERD document with your companions 72 | > - Write DTO structures 73 | > - Declare controller method only 74 | > - B. Software Development Kit 75 | > - Build SDK from the declaration only controller files 76 | > - SDK supports mockup simulator, boosting up frontend development 77 | > - SDK is type safe, so development be much safer 78 | > - C. Test Automation Program 79 | > - Build test program earlier than main program development 80 | > - Utilize SDK library in the test program development 81 | > - This is the TDD (Test Driven Development) 82 | > - D. Main Program Development 83 | 84 | ### 3.1. Definition 85 | ![ERD](https://github-production-user-asset-6210df.s3.amazonaws.com/13158709/268175441-80ca9c8e-4c96-4deb-a8cb-674e9845ebf6.png) 86 | 87 | Before developing the main program, define it before. 88 | 89 | At first, design the DB architecture on the Prisma Schema file ([prisma/schema](prisma/schema)). 90 | 91 | Writing the schema definitions, don't forget to write the detailed descriptions on each tables and properties. After that, build ERD (Enterprise Relationship Diagram) document through `npm run build:prisma` command. The ERD document will be generated on the [docs/ERD.md](docs/ERD.md) path. If you share the ERD document with your companions, your team can enjoy increased productivity by standardizing words and entities. 92 | 93 | At second, write DTO structures under the [src/api/structures](src/api/structures) directory and declare API endpoint specs under the [src/controllers](src/controllers) directory. Note that, do not implement the function body of the controller. Just write declaration only. Below code is never pseudo code, but actual code for current step. 94 | 95 | ```typescript 96 | @Controlleer("bbs/articles") 97 | export class BbsArticleController { 98 | @TypedRoute.Patch() 99 | public async index( 100 | @TypedBody() input: IBbsArticle.IRequest 101 | ): Promise> { 102 | input; 103 | return null!; 104 | } 105 | } 106 | ``` 107 | 108 | ### 3.2. Software Development Kit 109 | ![nestia-sdk-demo](https://user-images.githubusercontent.com/13158709/215004990-368c589d-7101-404e-b81b-fbc936382f05.gif) 110 | 111 | [`@samchon/bbs-backend`](https://github.com/samchon/bbs-backend) provides SDK (Software Development Kit) for convenience. 112 | 113 | SDK library means a collection of `fetch` functions with proper types, automatically generated by [nestia](https://github.com/samchon/nestia). As you can see from the above gif image, SDK library boosts up client developments, by providing type hints and auto completions. 114 | 115 | Furthermore, the SDK library supports [Mockup Simulator](https://nestia.io/docs/sdk/simulator/). 116 | 117 | If client developer configures `simulate` option to be `true`, the SDK library will not send HTTP request to your backend server, but simulate the API endpoints by itself. With that feature, frontend developers can directly start the interaction development, even when the [main program development](#34-main-program) has not started. 118 | 119 | ```bash 120 | # BUILD SDK IN LOCAL 121 | npm run build:sdk 122 | 123 | # BUILD SDK AND PUBLISH IT TO THE NPM 124 | npm run package:api 125 | ``` 126 | 127 | ### 3.3. Test Automation Program 128 | > TDD (Test Driven Development) 129 | 130 | After the [Definition](#31-definition) and client [SDK](#32-software-development-kit) generation, you've to design the use-case scenarios and implement a test automation program who represents those use-case scenarios and guarantees the [Main Program](#34-main-program). 131 | 132 | To add a new test function in the Test Automation Program, create a new TS file under the [test/features](test/features) directory following the below category and implement the test scenario function with representative function name and `export` symbol. 133 | 134 | Note that, the Test Automation Program resets the local DB schema whenever being run. Therefore, you've to be careful if import data has been stored in the local DB server. To avoid the resetting the local DB, configure the `reset` option like below. 135 | 136 | Also, the Test Automation Program runs all of the test functions placed into the [test/features](test/features) directory. However, those full testing may consume too much time. Therefore, if you want to reduce the testing time by specializing some test functions, use the `include` option like below. 137 | 138 | - supported options 139 | - `include`: test only restricted functions who is containing the special keyword. 140 | - `exclude`: exclude some functions who is containing the special keyword. 141 | - `reset`: do not reset the DB 142 | 143 | ```bash 144 | # test without db reset 145 | npm run test -- --reset false 146 | 147 | # include or exclude some features 148 | npm run test -- --include something 149 | npm run test -- --include cart order issue 150 | npm run test -- --include cart order issue --exclude index deposit 151 | 152 | # run performance benchmark program 153 | npm run benchmark 154 | ``` 155 | 156 | For reference, if you run `npm run benchmark` command, your test functions defined in the [test/features/api](test/features/api) directory would be utilized for performance benchmarking. If you want to see the performance bench result earlier, visit below link please: 157 | 158 | - [docs/benchmarks/AMD Ryzen 9 7940HS w Radeon 780M Graphics.md](https://github.com/samchon/bbs-backend/blob/master/docs/benchmarks/AMD%20Ryzen%209%207940HS%20w%20Radeon%20780M%20Graphics.md) 159 | 160 | ### 3.4. Main Program 161 | After [Definition](#31-definition), client [SDK](#32-software-development-kit) building and [Test Automation Program](#33-test-automation-program) are all prepared, finally you can develop the Main Program. Also, when you complete the Main Program implementation, it would better to validate the implementation through the pre-built [SDK](#32-software-development-kit) and [Test Automation Program](#33-test-automation-program). 162 | 163 | However, do not commit a mistake that writing source codes only in the [controller](src/controllers) classes. The API Controller must have a role that only intermediation. The main source code should be write down separately following the directory categorizing. For example, source code about DB I/O should be written into the [src/providers](src/providers) directory. 164 | 165 | 166 | 167 | 168 | ## 4. Appendix 169 | ### 4.1. NPM Run Commands 170 | List of the run commands defined in the [package.json](package.json) are like below: 171 | 172 | - Test 173 | - **`test`**: **Run [Test Automation Program](#33-test-automation-program)** 174 | - `benchmark`: Run performance benchmark program 175 | - Build 176 | - `build`: Build every below programs 177 | - `build:sdk`: Build SDK library, but only for local 178 | - `build:test`: Build [Test Automation Program](#33-test-automation-program) 179 | - `build:main`: Build main program 180 | - **`dev`**: **Incremental builder of the [Test Automation Program](#33-test-automation-program)** 181 | - `eslint`: EsLint validator runner 182 | - `pretter`: Adjust prettier to every source codes 183 | - `webpack`: Run webpack bundler 184 | - Deploy 185 | - `package:api`: Build and deploy the SDK library to the NPM 186 | - `schema`: Create DB, users and schemas on local database 187 | - `start`: Start the backend server 188 | - `start:dev`: Start the backend server with incremental build and reload 189 | - Webpack 190 | - `webpack`: Run webpack bundler 191 | - `webpack:start`: Start the backend server built by webpack 192 | - `webpack:test`: Run test program to the webpack built 193 | 194 | ### 4.2. Directories 195 | - [.vscode/launch.json](.vscode/launch.json): Configuration for debugging 196 | - [packages/api/](packages/api): Client [SDK](#32-software-development-kit) library for the client developers 197 | - [**docs/**](docs/): Documents like ERD (Entity Relationship Diagram) 198 | - [**prisma/schema**](prisma/schema): Prisma Schema File 199 | - [src/](src/): TypeScript Source directory 200 | - [src/api/](src/api/): Client SDK that would be published to the `@ORGANIZATION/PROJECT-api` 201 | - [**src/api/functional/**](src/api/functional/): API functions generated by the [`nestia`](https://github.com/samchon/nestia) 202 | - [**src/api/structures/**](src/api/structures/): DTO structures 203 | - [src/controllers/](src/controllers/): Controller classes of the Main Program 204 | - [src/providers/](src/providers/): Service providers (bridge between DB and controllers) 205 | - [src/executable/](src/executable/): Executable programs 206 | - [**test/**](test/): Test Automation Program 207 | -------------------------------------------------------------------------------- /assets/images/postgresql-installer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samchon/bbs-backend/ae9880f032901409332cfcae5963c66b3abd7d8f/assets/images/postgresql-installer.png -------------------------------------------------------------------------------- /assets/images/stackbuilder-installer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samchon/bbs-backend/ae9880f032901409332cfcae5963c66b3abd7d8f/assets/images/stackbuilder-installer.png -------------------------------------------------------------------------------- /docs/ERD.md: -------------------------------------------------------------------------------- 1 | # Bullet-in Board System 2 | 3 | > Generated by [`prisma-markdown`](https://github.com/samchon/prisma-markdown) 4 | 5 | - [Articles](#articles) 6 | 7 | ## Articles 8 | 9 | ```mermaid 10 | erDiagram 11 | "attachment_files" { 12 | String id PK 13 | String name 14 | String extension "nullable" 15 | String url 16 | DateTime created_at 17 | } 18 | "bbs_articles" { 19 | String id PK 20 | String writer 21 | String password 22 | DateTime created_at 23 | DateTime deleted_at "nullable" 24 | } 25 | "bbs_article_snapshots" { 26 | String id PK 27 | String bbs_article_id FK 28 | String format 29 | String title 30 | String body 31 | String ip 32 | DateTime created_at 33 | } 34 | "bbs_article_snapshot_files" { 35 | String id PK 36 | String bbs_article_snapshot_id FK 37 | String attachment_file_id FK 38 | Int sequence 39 | } 40 | "bbs_article_comments" { 41 | String id PK 42 | String bbs_article_id FK 43 | String parent_id FK "nullable" 44 | String writer 45 | String password 46 | DateTime created_at 47 | DateTime deleted_at "nullable" 48 | } 49 | "bbs_article_comment_snapshots" { 50 | String id PK 51 | String bbs_article_comment_id FK 52 | String format 53 | String body 54 | String ip 55 | DateTime created_at 56 | } 57 | "bbs_article_comment_snapshot_files" { 58 | String id PK 59 | String bbs_article_comment_snapshot_id FK 60 | String attachment_file_id FK 61 | Int sequence 62 | } 63 | "bbs_article_snapshots" }|--|| "bbs_articles" : article 64 | "bbs_article_snapshot_files" }o--|| "bbs_article_snapshots" : snapshot 65 | "bbs_article_snapshot_files" }o--|| "attachment_files" : file 66 | "bbs_article_comments" }o--|| "bbs_articles" : article 67 | "bbs_article_comments" }o--o| "bbs_article_comments" : parent 68 | "bbs_article_comment_snapshots" }|--|| "bbs_article_comments" : comment 69 | "bbs_article_comment_snapshot_files" }o--|| "bbs_article_comment_snapshots" : snapshot 70 | "bbs_article_comment_snapshot_files" }o--|| "attachment_files" : file 71 | ``` 72 | 73 | ### `attachment_files` 74 | 75 | Attachment File. 76 | 77 | Every attachment files that are managed in current system. 78 | 79 | Properties as follows: 80 | 81 | - `id`: 82 | - `name`: File name, except extension. 83 | - `extension` 84 | > Extension. 85 | > 86 | > Possible to omit like `README` case. 87 | - `url`: URL path of the real file. 88 | - `created_at`: Creation time of file. 89 | 90 | ### `bbs_articles` 91 | 92 | Article entity. 93 | 94 | `bbs_articles` is a super-type entity of all kinds of articles in the 95 | current backend system, literally shaping individual articles of 96 | the bulletin board. 97 | 98 | And, as you can see, the elements that must inevitably exist in the 99 | article, such as the title or the body, do not exist in the `bbs_articles`, 100 | but exist in the subsidiary entity, [bbs_article_snapshots](#bbs_article_snapshots), as a 101 | 1: N relationship, which is because a new snapshot record is published 102 | every time the article is modified. 103 | 104 | The reason why a new snapshot record is published every time the article 105 | is modified is to preserve the evidence. Due to the nature of e-community, 106 | there is always a threat of dispute among the participants. And it can 107 | happen that disputes arise through articles or comments, and to prevent 108 | such things as modifying existing articles to manipulate the situation, 109 | the article is designed in this structure. 110 | 111 | In other words, to keep evidence, and prevent fraud. 112 | 113 | Properties as follows: 114 | 115 | - `id`: 116 | - `writer`: Writer's name. 117 | - `password`: Password for modification. 118 | - `created_at`: Creation time of article. 119 | - `deleted_at` 120 | > Deletion time of article. 121 | > 122 | > To keep evidence, do not delete the article, but just mark it as 123 | > deleted. 124 | 125 | ### `bbs_article_snapshots` 126 | 127 | Snapshot of article. 128 | 129 | `bbs_article_snapshots` is a snapshot entity that contains the contents of 130 | the article, as mentioned in [bbs_articles](#bbs_articles), the contents of the 131 | article are separated from the article record to keep evidence and prevent 132 | fraud. 133 | 134 | Properties as follows: 135 | 136 | - `id`: 137 | - `bbs_article_id`: Belong article's [bbs_articles.id](#bbs_articles) 138 | - `format` 139 | > Format of body. 140 | > 141 | > Same meaning with extension like `html`, `md`, `txt`. 142 | - `title`: Title of article. 143 | - `body`: Content body of article. 144 | - `ip`: IP address of the snapshot writer. 145 | - `created_at` 146 | > Creation time of record. 147 | > 148 | > It means creation time or update time or article. 149 | 150 | ### `bbs_article_snapshot_files` 151 | 152 | Attachment file of article snapshot. 153 | 154 | `bbs_article_snapshot_files` is an entity that shapes the attached files of 155 | the article snapshot. 156 | 157 | `bbs_article_snapshot_files` is a typical pair relationship table to 158 | resolve the M: N relationship between [bbs_article_snapshots](#bbs_article_snapshots) and 159 | [attachment_files](#attachment_files) tables. Also, to ensure the order of the attached 160 | files, it has an additional `sequence` attribute, which we will continue to 161 | see in this documents. 162 | 163 | Properties as follows: 164 | 165 | - `id`: 166 | - `bbs_article_snapshot_id`: Belonged snapshot's [bbs_article_snapshots.id](#bbs_article_snapshots) 167 | - `attachment_file_id`: Belonged file's [attachment_files.id](#attachment_files) 168 | - `sequence`: Sequence of attachment file in the snapshot. 169 | 170 | ### `bbs_article_comments` 171 | 172 | Comment written on an article. 173 | 174 | `bbs_article_comments` is an entity that shapes the comments written on an 175 | article. 176 | 177 | And for this comment, as in the previous relationship between 178 | [bbs_articles](#bbs_articles) and [bbs_article_snapshots](#bbs_article_snapshots), the content body 179 | of the comment is stored in the sub [bbs_article_comment_snapshots](#bbs_article_comment_snapshots) 180 | table for evidentialism, and a new snapshot record is issued every time 181 | the comment is modified. 182 | 183 | Also, `bbs_article_comments` is expressing the relationship of the 184 | hierarchical reply structure through the `parent_id` attribute. 185 | 186 | Properties as follows: 187 | 188 | - `id`: 189 | - `bbs_article_id`: Belonged article's [bbs_articles.id](#bbs_articles) 190 | - `parent_id` 191 | > Parent comment's [bbs_article_comments.id](#bbs_article_comments) 192 | > 193 | > Used to express the hierarchical reply structure. 194 | - `writer`: Writer's name. 195 | - `password`: Password for modification. 196 | - `created_at`: Creation time of comment. 197 | - `deleted_at` 198 | > Deletion time of comment. 199 | > 200 | > Do not allow to delete the comment, but just mark it as deleted, 201 | > to keep evidence. 202 | 203 | ### `bbs_article_comment_snapshots` 204 | 205 | Snapshot of comment. 206 | 207 | `bbs_article_comment_snapshots` is a snapshot entity that contains the 208 | contents of the comment. 209 | 210 | As mentioned in [bbs_article_comments](#bbs_article_comments), designed to keep evidence 211 | and prevent fraud. 212 | 213 | Properties as follows: 214 | 215 | - `id`: 216 | - `bbs_article_comment_id`: Belonged article's [bbs_article_comments.id](#bbs_article_comments) 217 | - `format` 218 | > Format of content body. 219 | > 220 | > Same meaning with extension like `html`, `md`, `txt`. 221 | - `body`: Content body of comment. 222 | - `ip`: IP address of the snapshot writer. 223 | - `created_at` 224 | > Creation time of record. 225 | > 226 | > It means creation time or update time or comment. 227 | 228 | ### `bbs_article_comment_snapshot_files` 229 | 230 | Attachment file of comment snapshot. 231 | 232 | `bbs_article_comment_snapshot_files` is an entity resolving the M:N 233 | relationship between [bbs_article_comment_snapshots](#bbs_article_comment_snapshots) and 234 | [attachment_files](#attachment_files) tables. 235 | 236 | Properties as follows: 237 | 238 | - `id`: 239 | - `bbs_article_comment_snapshot_id`: Belonged snapshot's [bbs_article_comment_snapshots.id](#bbs_article_comment_snapshots) 240 | - `attachment_file_id`: Belonged file's [attachment_files.id](#attachment_files) 241 | - `sequence` 242 | > Sequence order. 243 | > 244 | > Sequence order of the attached file in the belonged snapshot. 245 | -------------------------------------------------------------------------------- /docs/benchmarks/AMD Ryzen 9 7940HS w Radeon 780M Graphics.md: -------------------------------------------------------------------------------- 1 | # Benchmark Report 2 | > Generated by [`@nestia/benchmark`](https://github.com/samchon/nestia) 3 | 4 | - Specifications 5 | - CPU: AMD Ryzen 9 7940HS w/ Radeon 780M Graphics 6 | - RAM: 31 GB 7 | - NodeJS Version: v20.10.0 8 | - Backend Server: 1 core / 1 thread 9 | - Arguments 10 | - Count: 1,024 11 | - Threads: 4 12 | - Simultaneous: 32 13 | - Time 14 | - Start: 2024-07-26T17:47:22.181Z 15 | - Complete: 2024-07-26T17:48:27.476Z 16 | - Elapsed: 65,295 ms 17 | 18 | Type | Count | Success | Mean. | Stdev. | Minimum | Maximum 19 | ----|----|----|----|----|----|---- 20 | Total | 1,351 | 1,351 | 1,449.36 | 631.37 | 6 | 2,841 21 | 22 | > Unit: milliseconds 23 | 24 | ## Memory Consumptions 25 | ```mermaid 26 | xychart-beta 27 | x-axis "Time (second)" 28 | y-axis "Memory (MB)" 29 | line "Resident Set Size" [71, 82, 85, 88, 91, 95, 94, 95, 97, 97, 97, 99, 99, 104, 106, 109, 114, 114, 114, 115, 115, 116, 116, 117, 116, 118, 117, 118, 117, 118, 118, 120, 119, 120, 120, 121, 122, 121, 122, 123, 123, 125, 126, 125, 126, 123, 123, 90, 93] 30 | line "Heap Total" [32, 37, 47, 47, 47, 47, 47, 47, 48, 48, 48, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 67, 67, 67, 67, 67, 68, 68, 69, 69, 70, 70, 71, 70, 70, 70, 70, 71, 71, 71, 42, 46] 31 | line "Heap Used + External" [29, 31, 34, 37, 35, 36, 37, 37, 38, 38, 37, 34, 36, 44, 37, 42, 36, 40, 46, 40, 45, 36, 42, 35, 42, 47, 38, 43, 36, 43, 50, 43, 50, 43, 50, 44, 49, 43, 50, 42, 37, 36, 45, 45, 41, 42, 42, 32, 34] 32 | line "Heap Used Only" [26, 28, 31, 34, 33, 34, 34, 34, 35, 36, 34, 32, 34, 41, 34, 39, 33, 38, 43, 37, 43, 34, 39, 32, 39, 44, 35, 41, 34, 40, 47, 41, 47, 40, 47, 41, 46, 41, 48, 39, 34, 34, 43, 42, 38, 39, 39, 30, 31] 33 | ``` 34 | 35 | > - 🟦 Resident Set Size 36 | > - 🟢 Heap Total 37 | > - 🔴 Heap Used + External 38 | > - 🟡 Heap Used Only 39 | 40 | ## Endpoints 41 | Type | Count | Success | Mean. | Stdev. | Minimum | Maximum 42 | ----|----|----|----|----|----|---- 43 | PUT /bbs/articles/:id | 30 | 30 | 1,956.03 | 441.51 | 780 | 2,461 44 | POST /bbs/articles/:articleId/comments | 394 | 394 | 1,780.99 | 673.93 | 74 | 2,841 45 | PUT /bbs/articles/:articleId/comments/:id | 68 | 68 | 1,558.64 | 266.8 | 772 | 2,169 46 | POST /bbs/articles | 538 | 538 | 1,534.94 | 400.54 | 130 | 2,176 47 | DELETE /bbs/articles/:articleId/comments/:id | 24 | 24 | 1,410.29 | 382.42 | 818 | 2,195 48 | DELETE /bbs/articles/:id | 17 | 17 | 1,368.82 | 478.81 | 159 | 2,175 49 | GET /bbs/articles/:articleId/comments/:id | 32 | 32 | 1,009.65 | 327.55 | 313 | 1,801 50 | GET /bbs/articles/:id | 24 | 24 | 927.7 | 427.03 | 73 | 1,580 51 | PATCH /bbs/articles/abridges | 49 | 49 | 865.26 | 438.26 | 81 | 1,611 52 | PATCH /bbs/articles | 88 | 88 | 748.94 | 503.52 | 6 | 1,664 53 | PATCH /bbs/articles/:articleId/comments | 65 | 65 | 577.61 | 417.37 | 6 | 1,470 54 | GET /monitors/health | 11 | 11 | 424.72 | 263.94 | 35 | 810 55 | GET /monitors/system | 11 | 11 | 336.45 | 204.68 | 48 | 779 56 | 57 | > Unit: milliseconds 58 | 59 | ## Failures 60 | Method | Path | Count | Failures 61 | -------|------|-------|---------- -------------------------------------------------------------------------------- /docs/requirements/index.md: -------------------------------------------------------------------------------- 1 | I'll provide an English version of the detailed requirements analysis report for the Bulletin Board System. 2 | 3 | # Bulletin Board System Requirements Detailed Analysis Report 4 | 5 | ## 1. System Overview 6 | 7 | This system is a web-based Bulletin Board System that allows users to create posts and communicate through comments. The most distinctive feature of this system is its snapshot-based history management mechanism that preserves all content revision histories to maintain evidence and verify authenticity in case of disputes among users. 8 | 9 | ## 2. Core Requirements 10 | 11 | ### 2.1 Evidence Preservation Mechanism 12 | 13 | The most critical requirement of the system is to thoroughly preserve the revision history of all content (posts and comments). This should be implemented as follows: 14 | 15 | 1. **Snapshot-based History Management**: 16 | - When a post or comment is first created, an initial snapshot is generated 17 | - Each time content is modified, a new snapshot is additionally created 18 | - Existing snapshots are never modified or deleted 19 | - Each snapshot must include information such as content, creation time, IP address, etc. 20 | 21 | 2. **Logical Deletion Method**: 22 | - When posts or comments are deleted, they are not actually removed from the database 23 | - Instead, a deletion time (deleted_at) is recorded for logical deletion 24 | - Deleted items should not be visible to general users but should be accessible for administrative purposes 25 | 26 | 3. **Modification Verification Elements**: 27 | - All snapshots must record the author's IP address 28 | - All actions must have precise timestamps 29 | - Password verification is required for modification/deletion 30 | 31 | ### 2.2 Post Functionality Requirements 32 | 33 | 1. **Post Creation**: 34 | - Users must be able to create posts including title and body 35 | - Author name and password input are required when creating posts 36 | - The body should be writable in various formats (HTML, Markdown, plain text, etc.) 37 | - File attachment functionality must be supported 38 | 39 | 2. **Post Viewing**: 40 | - The post list should display the title of the latest version 41 | - The detail page should primarily display the latest version of content 42 | - It should be possible to view previous versions (snapshots) as needed 43 | - Attached file list and download functionality should be provided 44 | 45 | 3. **Post Modification**: 46 | - Authors can modify posts after password verification 47 | - Modification creates a new snapshot while preserving existing content 48 | - Attachments should also be modifiable (add/delete/reorder) 49 | 50 | 4. **Post Deletion**: 51 | - Authors can delete posts after password verification 52 | - Deletion must be implemented as logical deletion (soft delete) 53 | - Deleted posts should be excluded from general viewing but accessible for administrative purposes 54 | 55 | ### 2.3 Comment Functionality Requirements 56 | 57 | 1. **Comment Creation**: 58 | - Users must be able to create comments on posts 59 | - Author name and password input are required when creating comments 60 | - The body should be writable in various formats 61 | - File attachment functionality must be supported 62 | 63 | 2. **Hierarchical Comment Structure**: 64 | - It should be possible to write replies to comments (nested comments) 65 | - The hierarchical relationship between replies and original comments should be visually represented 66 | - The hierarchical structure should support unlimited depth 67 | 68 | 3. **Comment Modification**: 69 | - Authors can modify comments after password verification 70 | - Modification creates a new snapshot while preserving existing content 71 | - Attachments should also be modifiable 72 | 73 | 4. **Comment Deletion**: 74 | - Authors can delete comments after password verification 75 | - Deletion must be implemented as logical deletion 76 | - Deleted comments should be excluded from general viewing but accessible for administrative purposes 77 | 78 | ### 2.4 File Attachment Requirements 79 | 80 | 1. **File Upload**: 81 | - It should be possible to attach files when creating/modifying posts and comments 82 | - Upload of various file formats should be supported 83 | - Filenames and extensions should be managed separately (files without extensions should also be processable) 84 | 85 | 2. **File Management**: 86 | - Attached files should be displayed in a specific order (sequence) 87 | - Users should be able to specify or change the file order 88 | - Files should be accessible via unique URLs 89 | 90 | 3. **File Download**: 91 | - Users should be able to download attached files 92 | - Files should be provided with the original filename when downloaded 93 | 94 | ## 3. Non-functional Requirements 95 | 96 | ### 3.1 Security Requirements 97 | 98 | 1. **Password Security**: 99 | - Passwords for posts and comments must be stored securely encrypted 100 | - Password verification is required for modification/deletion 101 | 102 | 2. **IP Address Recording**: 103 | - IP addresses must be recorded for all creation and modification actions 104 | - IP address information should have appropriate access restrictions for security reasons 105 | 106 | 3. **Access Control**: 107 | - General users should not be able to access deleted posts/comments 108 | - Only administrators should have access to deleted items 109 | 110 | ### 3.2 Performance Requirements 111 | 112 | 1. **Viewing Performance Optimization**: 113 | - Latest snapshot information should be efficiently retrievable when viewing post lists and details 114 | - Posts with many comments should load quickly 115 | 116 | 2. **Indexing Strategy**: 117 | - Appropriate indexes should be configured for frequently queried fields 118 | - Time-based sorting and filtering should operate quickly 119 | 120 | 3. **File Processing Performance**: 121 | - Large file uploads/downloads should be handled smoothly 122 | - Performance degradation should be minimized even when attaching multiple files 123 | 124 | ### 3.3 Scalability Requirements 125 | 126 | 1. **Data Growth Management**: 127 | - The system should continue to operate stably even as the number of posts and comments increases 128 | - Data growth due to the snapshot approach should be efficiently managed 129 | 130 | 2. **User Growth Management**: 131 | - System performance should be maintained even as the number of simultaneous users increases 132 | 133 | ## 4. User Interface Requirements 134 | 135 | ### 4.1 Post-related UI 136 | 137 | 1. **Post List Screen**: 138 | - Display post title, author, creation date, comment count 139 | - Provide pagination functionality 140 | - Provide search functionality 141 | 142 | 2. **Post Detail Screen**: 143 | - Display title, body, author, creation date 144 | - Provide attached file list and download links 145 | - Include comment section 146 | - Provide options to view previous versions (snapshots) 147 | 148 | 3. **Post Creation/Modification Screen**: 149 | - Title input field 150 | - Body editor (supporting various formats) 151 | - File attachment functionality 152 | - Author name and password input fields 153 | 154 | ### 4.2 Comment-related UI 155 | 156 | 1. **Comment List**: 157 | - Display in hierarchical structure (distinguished by indentation, etc.) 158 | - Show author, creation date, content 159 | - Provide reply creation option 160 | 161 | 2. **Comment Creation/Modification Form**: 162 | - Content input field 163 | - File attachment functionality 164 | - Author name and password input fields 165 | 166 | ### 4.3 History Management UI 167 | 168 | 1. **Snapshot History Screen**: 169 | - Display list of all modification versions of posts/comments 170 | - Provide information on creation time and IP address for each version 171 | - Feature to visually display differences between versions 172 | 173 | ## 5. Data Requirements 174 | 175 | ### 5.1 Post-related Data 176 | 177 | 1. **Post Basic Information**: 178 | - Unique identifier (UUID) 179 | - Author name 180 | - Password (encrypted storage) 181 | - Creation time 182 | - Deletion time (if applicable) 183 | 184 | 2. **Post Snapshot Information**: 185 | - Unique identifier (UUID) 186 | - Associated post ID 187 | - Body format 188 | - Title 189 | - Body content 190 | - Author's IP address 191 | - Creation time 192 | 193 | ### 5.2 Comment-related Data 194 | 195 | 1. **Comment Basic Information**: 196 | - Unique identifier (UUID) 197 | - Associated post ID 198 | - Parent comment ID (for replies) 199 | - Author name 200 | - Password (encrypted storage) 201 | - Creation time 202 | - Deletion time (if applicable) 203 | 204 | 2. **Comment Snapshot Information**: 205 | - Unique identifier (UUID) 206 | - Associated comment ID 207 | - Body format 208 | - Body content 209 | - Author's IP address 210 | - Creation time 211 | 212 | ### 5.3 Attachment-related Data 213 | 214 | 1. **File Basic Information**: 215 | - Unique identifier (UUID) 216 | - Filename (excluding extension) 217 | - Extension (can be null) 218 | - File URL 219 | - Creation time 220 | 221 | 2. **Post-File Connection Information**: 222 | - Associated post snapshot ID 223 | - Associated file ID 224 | - Sequence 225 | 226 | 3. **Comment-File Connection Information**: 227 | - Associated comment snapshot ID 228 | - Associated file ID 229 | - Sequence 230 | 231 | ## 6. Conclusion and Implementation Considerations 232 | 233 | The most prominent feature of this bulletin board system is its mechanism to thoroughly preserve the revision history of all content to maintain evidence and verify authenticity in case of disputes among users. This design considers the risks of disputes that can occur due to the nature of e-communities. 234 | 235 | The following points should be particularly considered during implementation: 236 | 237 | 1. **Data Growth Management**: As data continuously increases due to the snapshot approach, efficient storage and retrieval methods are necessary. 238 | 239 | 2. **Performance Optimization**: Methods to quickly retrieve the latest snapshots are needed. Performance should be optimized using view tables and other techniques. 240 | 241 | 3. **Security Enhancement**: Sensitive information such as passwords and IP addresses must be managed securely. 242 | 243 | 4. **User Experience**: Despite having history management functionality, an intuitive and convenient interface should be provided to general users. 244 | 245 | Based on this requirements analysis report, the development team should be able to implement a highly reliable bulletin board system. The focus should be on building a user-friendly system while achieving the core objectives of evidence preservation and dispute prevention. -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "entryFile": "executable/server", 6 | "compilerOptions": { 7 | "deleteOutDir": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /nestia.config.ts: -------------------------------------------------------------------------------- 1 | import { INestiaConfig } from "@nestia/sdk"; 2 | import { NestFactory } from "@nestjs/core"; 3 | import { FastifyAdapter } from "@nestjs/platform-fastify"; 4 | 5 | import { BbsModule } from "./src/BbsModule"; 6 | 7 | export const NESTIA_CONFIG: INestiaConfig = { 8 | input: () => NestFactory.create(BbsModule, new FastifyAdapter()), 9 | output: "src/api", 10 | swagger: { 11 | output: "packages/api/swagger.json", 12 | servers: [ 13 | { 14 | url: "http://localhost:37001", 15 | description: "Local Server", 16 | }, 17 | ], 18 | beautify: true, 19 | }, 20 | primitive: false, 21 | simulate: true, 22 | }; 23 | export default NESTIA_CONFIG; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@samchon/bbs-backend", 3 | "version": "3.0.2", 4 | "description": "Backend for bbs", 5 | "scripts": { 6 | "benchmark": "rimraf prisma/migrations && node bin/test/benchmark", 7 | "test": "rimraf prisma/migrations && node bin/test", 8 | "------------------------BUILDS------------------------": "", 9 | "build": "npm run build:prisma && npm run build:sdk && npm run build:main && npm run build:test", 10 | "build:api": "rimraf packages/api/lib && nestia sdk && tsc -p packages/api/tsconfig.json && rollup -c packages/api/rollup.config.js", 11 | "build:main": "rimraf lib && tsc", 12 | "build:prisma": "prisma generate --schema=prisma/schema", 13 | "build:sdk": "rimraf src/api/functional && nestia sdk", 14 | "build:swagger": "nestia swagger", 15 | "build:test": "rimraf bin && tsc -p test/tsconfig.json", 16 | "dev": "npm run build:test -- --watch", 17 | "eslint": "eslint src && eslint test", 18 | "eslint:fix": "eslint src --fix && eslint test --fix", 19 | "prepare": "ts-patch install && typia patch && npm run build:prisma", 20 | "prettier": "prettier src --write && prettier test --write", 21 | "------------------------WEBPACK------------------------": "", 22 | "webpack": "rimraf dist && npm run build:prisma && webpack", 23 | "webpack:start": "node dist/server", 24 | "webpack:test": "npm run webpack && rimraf prisma/migrations && node bin/test/webpack.js", 25 | "------------------------DEPLOYS------------------------": "", 26 | "package:api": "npm run build:swagger && npm run build:api && cd packages/api && npm publish --access public", 27 | "start": "node lib/executable/server", 28 | "start:dev": "nest start --watch", 29 | "start:swagger": "ts-node src/executable/swagger.ts", 30 | "schema": "ts-node src/executable/schema" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/samchon/bbs-backend" 35 | }, 36 | "author": "Samchon", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/samchon/bbs-backend/issues" 40 | }, 41 | "homepage": "https://github.com/samchon/bbs-backend", 42 | "devDependencies": { 43 | "@nestia/benchmark": "^7.0.0", 44 | "@nestia/e2e": "^7.0.0", 45 | "@nestia/sdk": "^7.0.0", 46 | "@nestjs/cli": "^11.0.7", 47 | "@rollup/plugin-terser": "^0.4.4", 48 | "@rollup/plugin-typescript": "^11.1.6", 49 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 50 | "@types/bcryptjs": "^2.4.6", 51 | "@types/cli": "^0.11.19", 52 | "@types/cli-progress": "^3.11.5", 53 | "@types/express": "^4.17.21", 54 | "@types/inquirer": "^8.2.5", 55 | "@types/jsonwebtoken": "^9.0.5", 56 | "@types/node": "^20.11.22", 57 | "@types/swagger-ui-express": "^4.1.6", 58 | "@types/uuid": "^10.0.0", 59 | "@typescript-eslint/eslint-plugin": "^6.19.1", 60 | "@typescript-eslint/parser": "^6.19.1", 61 | "cagen": "^1.0.1", 62 | "chalk": "^4.1.2", 63 | "cli": "^1.0.1", 64 | "cli-progress": "^3.12.0", 65 | "copy-webpack-plugin": "^12.0.2", 66 | "eslint-plugin-deprecation": "^2.0.0", 67 | "inquirer": "^8.2.5", 68 | "nestia": "^7.0.0", 69 | "prettier": "^3.2.5", 70 | "prettier-plugin-prisma": "^5.0.0", 71 | "prisma-markdown": "^3.0.1", 72 | "rimraf": "^3.0.2", 73 | "rollup": "^4.18.0", 74 | "sloc": "^0.2.1", 75 | "swagger-ui-express": "^5.0.0", 76 | "ts-loader": "^9.5.1", 77 | "ts-node": "^10.9.1", 78 | "ts-patch": "^3.3.0", 79 | "typescript": "~5.8.3", 80 | "typescript-transform-paths": "^3.5.5", 81 | "webpack": "^5.90.3", 82 | "webpack-cli": "^5.1.4", 83 | "write-file-webpack-plugin": "^4.5.1" 84 | }, 85 | "dependencies": { 86 | "@nestia/core": "^7.0.0", 87 | "@nestia/fetcher": "^7.0.0", 88 | "@nestjs/common": "^11.1.3", 89 | "@nestjs/core": "^11.1.3", 90 | "@nestjs/platform-fastify": "^11.1.3", 91 | "@prisma/client": "^6.11.1", 92 | "bcryptjs": "^2.4.3", 93 | "commander": "10.0.0", 94 | "csv-parse": "^5.5.3", 95 | "dotenv": "^16.3.1", 96 | "dotenv-expand": "^10.0.0", 97 | "fastify": "^4.25.2", 98 | "git-last-commit": "^1.0.0", 99 | "jsonwebtoken": "^9.0.2", 100 | "prisma": "^6.11.1", 101 | "serialize-error": "^4.1.0", 102 | "source-map-support": "^0.5.19", 103 | "tgrid": "^1.1.0", 104 | "tstl": "^3.0.0", 105 | "typia": "^9.3.1", 106 | "uuid": "^10.0.0" 107 | }, 108 | "private": true, 109 | "packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903" 110 | } 111 | -------------------------------------------------------------------------------- /packages/api/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 samchon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/api/README.md: -------------------------------------------------------------------------------- 1 | # SDK for Client Developers 2 | ## Outline 3 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/bbs-backend/tree/master/LICENSE) 4 | [![npm version](https://img.shields.io/npm/v/@samchon/bbs-api.svg)](https://www.npmjs.com/package/@samchon/bbs-api) 5 | [![Build Status](https://github.com/samchon/bbs-backend/workflows/build/badge.svg)](https://github.com/samchon/bbs-backend/actions?query=workflow%3Abuild) 6 | [![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://nestia.io/docs/) 7 | 8 | [`@samchon/bbs-backend`](https://github.com/samchon/bbs-backend) provides SDK (Software Development Kit) for convenience. 9 | 10 | For the client developers who are connecting to this backend server, [`@samchon/bbs-backend`](https://github.com/samchon/bbs-backend) provides not API documents like the Swagger, but provides the API interaction library, one of the typical SDK (Software Development Kit) for the convenience. 11 | 12 | With the SDK, client developers never need to re-define the duplicated API interfaces. Just utilize the provided interfaces and asynchronous functions defined in the SDK. It would be much convenient than any other Rest API solutions. 13 | 14 | ```bash 15 | npm install --save @samchon/bbs-api 16 | ``` 17 | 18 | 19 | 20 | 21 | ## Usage 22 | Import the `@samchon/bbs-api` and enjoy the auto-completion. 23 | 24 | ```typescript 25 | import { ArrayUtil, RandomGenerator, TestValidator } from "@nestia/e2e"; 26 | import { randint } from "tstl"; 27 | import typia from "typia"; 28 | 29 | import api from "@samchon/bbs-api/lib/index"; 30 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 31 | 32 | import { prepare_random_file } from "./internal/prepare_random_file"; 33 | 34 | export const test_api_bbs_article_create = async ( 35 | connection: api.IConnection, 36 | ): Promise => { 37 | // PREPARE INPUT DATA 38 | const input: IBbsArticle.ICreate = { 39 | writer: RandomGenerator.name(), 40 | password: RandomGenerator.alphaNumeric(8), 41 | title: RandomGenerator.paragraph()(), 42 | body: RandomGenerator.content()()(), 43 | format: "md", 44 | files: ArrayUtil.repeat(randint(0, 3))(() => prepare_random_file()), 45 | }; 46 | 47 | // DO CREATE 48 | const article: IBbsArticle = await api.functional.bbs.articles.create( 49 | connection, 50 | input, 51 | ); 52 | typia.assertEquals(article); 53 | 54 | // VALIDATE WHETHER EXACT DATA IS INSERTED 55 | TestValidator.equals("create")({ 56 | snapshots: [ 57 | { 58 | format: input.format, 59 | title: input.title, 60 | body: input.body, 61 | files: input.files, 62 | }, 63 | ], 64 | writer: input.writer, 65 | })(article); 66 | 67 | // COMPARE WITH READ DATA 68 | const read: IBbsArticle = await api.functional.bbs.articles.at( 69 | connection, 70 | article.id, 71 | ); 72 | typia.assertEquals(read); 73 | TestValidator.equals("read")(read)(article); 74 | }; 75 | ``` -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@samchon/bbs-api", 3 | "version": "3.0.3", 4 | "description": "API for bbs", 5 | "main": "lib/index.js", 6 | "module": "lib/index.mjs", 7 | "typings": "lib/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/samchon/bbs-backend" 11 | }, 12 | "author": "Samchon", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/samchon/bbs-backend/issues" 16 | }, 17 | "homepage": "https://github.com/samchon/bbs-backend#readme", 18 | "dependencies": { 19 | "@nestia/fetcher": "^7.0.0", 20 | "typia": "^9.3.1" 21 | }, 22 | "include": [ 23 | "lib", 24 | "package.json", 25 | "swagger.json", 26 | "README.md" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/api/rollup.config.js: -------------------------------------------------------------------------------- 1 | const typescript = require("@rollup/plugin-typescript"); 2 | const terser = require("@rollup/plugin-terser"); 3 | 4 | module.exports = { 5 | input: `${__dirname}/../../src/api/index.ts`, 6 | output: { 7 | dir: `${__dirname}/lib`, 8 | format: "esm", 9 | entryFileNames: "[name].mjs", 10 | sourcemap: true, 11 | }, 12 | plugins: [ 13 | typescript({ 14 | tsconfig: `${__dirname}/tsconfig.json`, 15 | module: "ESNext", 16 | target: "ESNext", 17 | }), 18 | terser({ 19 | format: { 20 | comments: "some", 21 | beautify: true, 22 | ecma: "2020", 23 | }, 24 | compress: false, 25 | mangle: false, 26 | module: true, 27 | }), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES5", 5 | "declaration": true, 6 | "outDir": "lib", 7 | "downlevelIteration": true, 8 | "lib": [ 9 | "DOM", 10 | "ES2015", 11 | ] 12 | }, 13 | "include": ["../../src/api"], 14 | "exclude": ["node_modules"], 15 | } -------------------------------------------------------------------------------- /postgres.sh: -------------------------------------------------------------------------------- 1 | docker pull postgres:latest 2 | docker ps | grep postgres > /dev/null || docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=root -d postgres:latest 3 | npm run build:prisma 4 | npm run schema -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // DEFAULT CONFIGURATIONS 3 | parser: "typescript", 4 | printWidth: 80, 5 | semi: true, 6 | tabWidth: 2, 7 | trailingComma: "all", 8 | 9 | // PLUG-IN CONFIGURATIONS 10 | plugins: ["@trivago/prettier-plugin-sort-imports"], 11 | importOrder: [ 12 | "", 13 | "^@samchon/bbs-api(.*)$", 14 | "(.*)providers/(.*)$", 15 | "^[./]", 16 | ], 17 | importOrderSeparation: true, 18 | importOrderSortSpecifiers: true, 19 | importOrderParserPlugins: ["decorators-legacy", "typescript"], 20 | }; 21 | -------------------------------------------------------------------------------- /src/BbsBackend.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from "@nestjs/core"; 2 | import { 3 | FastifyAdapter, 4 | NestFastifyApplication, 5 | } from "@nestjs/platform-fastify"; 6 | 7 | import { BbsConfiguration } from "./BbsConfiguration"; 8 | import { BbsModule } from "./BbsModule"; 9 | 10 | export class BbsBackend { 11 | private application_?: NestFastifyApplication; 12 | 13 | public async open(): Promise { 14 | // MOUNT CONTROLLERS 15 | this.application_ = await NestFactory.create( 16 | BbsModule, 17 | new FastifyAdapter(), 18 | { logger: false }, 19 | ); 20 | 21 | // DO OPEN 22 | this.application_.enableCors(); 23 | await this.application_.listen(BbsConfiguration.API_PORT(), "0.0.0.0"); 24 | } 25 | 26 | public async close(): Promise { 27 | if (this.application_ === undefined) return; 28 | 29 | // DO CLOSE 30 | await this.application_.close(); 31 | delete this.application_; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/BbsConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionManager } from "@nestia/core"; 2 | import { Prisma } from "@prisma/client"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | 6 | import { ErrorProvider } from "./providers/bbs/ErrorProvider"; 7 | 8 | import { BbsGlobal } from "./BbsGlobal"; 9 | 10 | const EXTENSION = __filename.substr(-2); 11 | if (EXTENSION === "js") require("source-map-support").install(); 12 | 13 | export namespace BbsConfiguration { 14 | export const ROOT = (() => { 15 | const split: string[] = __dirname.split(path.sep); 16 | return split.at(-1) === "src" && split.at(-2) === "bin" 17 | ? path.resolve(__dirname + "/../..") 18 | : fs.existsSync(__dirname + "/.env") 19 | ? __dirname 20 | : path.resolve(__dirname + "/.."); 21 | })(); 22 | 23 | export const API_PORT = () => Number(BbsGlobal.env.BBS_API_PORT); 24 | } 25 | 26 | ExceptionManager.insert(Prisma.PrismaClientKnownRequestError, (exp) => { 27 | switch (exp.code) { 28 | case "P2025": 29 | return ErrorProvider.notFound(exp.message); 30 | case "P2002": // UNIQUE CONSTRAINT 31 | return ErrorProvider.conflict(exp.message); 32 | default: 33 | return ErrorProvider.internal(exp.message); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /src/BbsGlobal.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import dotenv from "dotenv"; 3 | import dotenvExpand from "dotenv-expand"; 4 | import { Singleton } from "tstl"; 5 | import typia from "typia"; 6 | 7 | /** 8 | * Global variables of the server. 9 | * 10 | * @author Samchon 11 | */ 12 | export class BbsGlobal { 13 | public static testing: boolean = false; 14 | 15 | public static readonly prisma: PrismaClient = new PrismaClient(); 16 | 17 | public static get env(): BbsGlobal.IEnvironments { 18 | return environments.get(); 19 | } 20 | 21 | /** 22 | * Current mode. 23 | * 24 | * - local: The server is on your local machine. 25 | * - dev: The server is for the developer. 26 | * - real: The server is for the real service. 27 | */ 28 | public static get mode(): "local" | "dev" | "real" { 29 | return (modeWrapper.value ??= environments.get().BBS_MODE); 30 | } 31 | 32 | /** 33 | * Set current mode. 34 | * 35 | * @param mode The new mode 36 | */ 37 | public static setMode(mode: typeof BbsGlobal.mode): void { 38 | typia.assert(mode); 39 | modeWrapper.value = mode; 40 | } 41 | } 42 | export namespace BbsGlobal { 43 | export interface IEnvironments { 44 | BBS_MODE: "local" | "dev" | "real"; 45 | BBS_API_PORT: `${number}`; 46 | BBS_SYSTEM_PASSWORD: string; 47 | 48 | BBS_POSTGRES_HOST: string; 49 | BBS_POSTGRES_PORT: `${number}`; 50 | BBS_POSTGRES_DATABASE: string; 51 | BBS_POSTGRES_SCHEMA: string; 52 | BBS_POSTGRES_USERNAME: string; 53 | BBS_POSTGRES_USERNAME_READONLY: string; 54 | BBS_POSTGRES_PASSWORD: string; 55 | } 56 | } 57 | 58 | interface IMode { 59 | value?: "local" | "dev" | "real"; 60 | } 61 | const modeWrapper: IMode = {}; 62 | const environments = new Singleton(() => { 63 | const env = dotenv.config(); 64 | dotenvExpand.expand(env); 65 | return typia.assert(process.env); 66 | }); 67 | -------------------------------------------------------------------------------- /src/BbsModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | 3 | import { BbsArticleCommentsController } from "./controllers/bbs/BbsArticleCommentsController"; 4 | import { BbsArticlesController } from "./controllers/bbs/BbsArticlesController"; 5 | import { MonitorModule } from "./controllers/monitors/MonitorModule"; 6 | 7 | @Module({ 8 | imports: [MonitorModule], 9 | controllers: [BbsArticlesController, BbsArticleCommentsController], 10 | }) 11 | export class BbsModule {} 12 | -------------------------------------------------------------------------------- /src/api/HttpError.ts: -------------------------------------------------------------------------------- 1 | export { HttpError } from "@nestia/fetcher"; 2 | -------------------------------------------------------------------------------- /src/api/IConnection.ts: -------------------------------------------------------------------------------- 1 | export type { IConnection } from "@nestia/fetcher"; 2 | -------------------------------------------------------------------------------- /src/api/functional/bbs/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @module api.functional.bbs 4 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 5 | */ 6 | //================================================================ 7 | export * as articles from "./articles"; 8 | -------------------------------------------------------------------------------- /src/api/functional/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @module api.functional 4 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 5 | */ 6 | //================================================================ 7 | export * as bbs from "./bbs"; 8 | export * as monitors from "./monitors"; 9 | -------------------------------------------------------------------------------- /src/api/functional/monitors/health/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @module api.functional.monitors.health 4 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 5 | */ 6 | //================================================================ 7 | import type { IConnection } from "@nestia/fetcher"; 8 | import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; 9 | import typia from "typia"; 10 | 11 | /** 12 | * Health check API. 13 | * 14 | * @tag Monitor 15 | * @author Samchon 16 | * 17 | * @controller MonitorHealthController.get 18 | * @path GET /monitors/health 19 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 20 | */ 21 | export async function get(connection: IConnection): Promise { 22 | return !!connection.simulate 23 | ? get.simulate(connection) 24 | : PlainFetcher.fetch(connection, { 25 | ...get.METADATA, 26 | template: get.METADATA.path, 27 | path: get.path(), 28 | }); 29 | } 30 | export namespace get { 31 | export const METADATA = { 32 | method: "GET", 33 | path: "/monitors/health", 34 | request: null, 35 | response: { 36 | type: "application/json", 37 | encrypted: false, 38 | }, 39 | status: 200, 40 | } as const; 41 | 42 | export const path = () => "/monitors/health"; 43 | export const random = (g?: Partial): void => 44 | typia.random(g); 45 | export const simulate = (connection: IConnection): void => { 46 | return random( 47 | "object" === typeof connection.simulate && null !== connection.simulate 48 | ? connection.simulate 49 | : undefined, 50 | ); 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/api/functional/monitors/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @module api.functional.monitors 4 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 5 | */ 6 | //================================================================ 7 | export * as health from "./health"; 8 | export * as performance from "./performance"; 9 | export * as system from "./system"; 10 | -------------------------------------------------------------------------------- /src/api/functional/monitors/performance/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @module api.functional.monitors.performance 4 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 5 | */ 6 | //================================================================ 7 | import type { IConnection } from "@nestia/fetcher"; 8 | import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; 9 | import typia from "typia"; 10 | 11 | import type { IPerformance } from "../../../structures/monitors/IPerformance"; 12 | 13 | /** 14 | * Get performance information. 15 | * 16 | * Get perofmration information composed with CPU, memory and resource usage. 17 | * 18 | * @returns Performance info 19 | * @tag Monitor 20 | * @author Samchon 21 | * 22 | * @controller MonitorPerformanceController.get 23 | * @path GET /monitors/performance 24 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 25 | */ 26 | export async function get(connection: IConnection): Promise { 27 | return !!connection.simulate 28 | ? get.simulate(connection) 29 | : PlainFetcher.fetch(connection, { 30 | ...get.METADATA, 31 | template: get.METADATA.path, 32 | path: get.path(), 33 | }); 34 | } 35 | export namespace get { 36 | export type Output = IPerformance; 37 | 38 | export const METADATA = { 39 | method: "GET", 40 | path: "/monitors/performance", 41 | request: null, 42 | response: { 43 | type: "application/json", 44 | encrypted: false, 45 | }, 46 | status: 200, 47 | } as const; 48 | 49 | export const path = () => "/monitors/performance"; 50 | export const random = (g?: Partial): IPerformance => 51 | typia.random(g); 52 | export const simulate = (connection: IConnection): Output => { 53 | return random( 54 | "object" === typeof connection.simulate && null !== connection.simulate 55 | ? connection.simulate 56 | : undefined, 57 | ); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/api/functional/monitors/system/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @module api.functional.monitors.system 4 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 5 | */ 6 | //================================================================ 7 | import type { IConnection } from "@nestia/fetcher"; 8 | import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; 9 | import typia from "typia"; 10 | 11 | import type { ISystem } from "../../../structures/monitors/ISystem"; 12 | 13 | /** 14 | * Get system information. 15 | * 16 | * Get system information with commit and package information. 17 | * 18 | * @returns System info 19 | * @tag Monitor 20 | * @author Samchon 21 | * 22 | * @controller MonitorSystemController.get 23 | * @path GET /monitors/system 24 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 25 | */ 26 | export async function get(connection: IConnection): Promise { 27 | return !!connection.simulate 28 | ? get.simulate(connection) 29 | : PlainFetcher.fetch(connection, { 30 | ...get.METADATA, 31 | template: get.METADATA.path, 32 | path: get.path(), 33 | }); 34 | } 35 | export namespace get { 36 | export type Output = ISystem; 37 | 38 | export const METADATA = { 39 | method: "GET", 40 | path: "/monitors/system", 41 | request: null, 42 | response: { 43 | type: "application/json", 44 | encrypted: false, 45 | }, 46 | status: 200, 47 | } as const; 48 | 49 | export const path = () => "/monitors/system"; 50 | export const random = (g?: Partial): ISystem => 51 | typia.random(g); 52 | export const simulate = (connection: IConnection): Output => { 53 | return random( 54 | "object" === typeof connection.simulate && null !== connection.simulate 55 | ? connection.simulate 56 | : undefined, 57 | ); 58 | }; 59 | } 60 | 61 | /** 62 | * @internal 63 | * @controller MonitorSystemController.internal_server_error 64 | * @path GET /monitors/system/internal_server_error 65 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 66 | */ 67 | export async function internal_server_error( 68 | connection: IConnection, 69 | ): Promise { 70 | return !!connection.simulate 71 | ? internal_server_error.simulate(connection) 72 | : PlainFetcher.fetch(connection, { 73 | ...internal_server_error.METADATA, 74 | template: internal_server_error.METADATA.path, 75 | path: internal_server_error.path(), 76 | }); 77 | } 78 | export namespace internal_server_error { 79 | export const METADATA = { 80 | method: "GET", 81 | path: "/monitors/system/internal_server_error", 82 | request: null, 83 | response: { 84 | type: "application/json", 85 | encrypted: false, 86 | }, 87 | status: 200, 88 | } as const; 89 | 90 | export const path = () => "/monitors/system/internal_server_error"; 91 | export const random = (g?: Partial): void => 92 | typia.random(g); 93 | export const simulate = (connection: IConnection): void => { 94 | return random( 95 | "object" === typeof connection.simulate && null !== connection.simulate 96 | ? connection.simulate 97 | : undefined, 98 | ); 99 | }; 100 | } 101 | 102 | /** 103 | * @internal 104 | * @controller MonitorSystemController.uncaught_exception 105 | * @path GET /monitors/system/uncaught_exception 106 | * @nestia Generated by Nestia - https://github.com/samchon/nestia 107 | */ 108 | export async function uncaught_exception( 109 | connection: IConnection, 110 | ): Promise { 111 | return !!connection.simulate 112 | ? uncaught_exception.simulate(connection) 113 | : PlainFetcher.fetch(connection, { 114 | ...uncaught_exception.METADATA, 115 | template: uncaught_exception.METADATA.path, 116 | path: uncaught_exception.path(), 117 | }); 118 | } 119 | export namespace uncaught_exception { 120 | export const METADATA = { 121 | method: "GET", 122 | path: "/monitors/system/uncaught_exception", 123 | request: null, 124 | response: { 125 | type: "application/json", 126 | encrypted: false, 127 | }, 128 | status: 200, 129 | } as const; 130 | 131 | export const path = () => "/monitors/system/uncaught_exception"; 132 | export const random = (g?: Partial): void => 133 | typia.random(g); 134 | export const simulate = (connection: IConnection): void => { 135 | return random( 136 | "object" === typeof connection.simulate && null !== connection.simulate 137 | ? connection.simulate 138 | : undefined, 139 | ); 140 | }; 141 | } 142 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import * as BbsApi from "./module"; 2 | 3 | export * from "./module"; 4 | 5 | export default BbsApi; 6 | -------------------------------------------------------------------------------- /src/api/module.ts: -------------------------------------------------------------------------------- 1 | export type * from "./IConnection"; 2 | export * from "./HttpError"; 3 | 4 | export * as functional from "./functional"; 5 | -------------------------------------------------------------------------------- /src/api/structures/bbs/IBbsArticle.ts: -------------------------------------------------------------------------------- 1 | import { tags } from "typia"; 2 | 3 | import { IAttachmentFile } from "../common/IAttachmentFile"; 4 | import { IPage } from "../common/IPage"; 5 | 6 | /** 7 | * Article entity. 8 | * 9 | * `IBbsArticle* is a super-type entity of all kinds of articles in the current 10 | * backend system, literally shaping individual articles of the bulletin board. 11 | * 12 | * And, as you can see, the elements that must inevitably exist in the article, 13 | * such as the `title` or the `body`, do not exist in the `IBbsArticle`, but exist 14 | * in the subsidiary entity, {@link IBbsArticle.ISnapshot}, as a 1: N relationship, 15 | * which is because a new snapshot record is published every time the article is 16 | * modified. 17 | * 18 | * The reason why a new snapshot record is published every time the article is 19 | * modified is to preserve the evidence. Due to the nature of e-community, there 20 | * is always a threat of dispute among the participants. And it can happen that 21 | * disputes arise through articles or {@link IBbsArticleComment comments}, and to 22 | * prevent such things as modifying existing articles to manipulate the situation, 23 | * the article is designed in this structure. 24 | * 25 | * In other words, to keep evidence, and prevent fraud. 26 | * 27 | * @template Snapshot Snapshot content type of the article 28 | * @author Samchon 29 | */ 30 | export interface IBbsArticle { 31 | /** 32 | * Primary Key. 33 | */ 34 | id: string & tags.Format<"uuid">; 35 | 36 | /** 37 | * Writer of article. 38 | */ 39 | writer: string; 40 | 41 | /** 42 | * List of snapshot contents. 43 | * 44 | * It is created for the first time when an article is created, and is 45 | * accumulated every time the article is modified. 46 | */ 47 | snapshots: IBbsArticle.ISnapshot[] & tags.MinItems<1>; 48 | 49 | /** 50 | * Creation time of article. 51 | */ 52 | created_at: string & tags.Format<"date-time">; 53 | } 54 | export namespace IBbsArticle { 55 | export type Format = "txt" | "md" | "html"; 56 | 57 | /** 58 | * Snapshot of article. 59 | * 60 | * `IBbsArticle.ISnapshot` is a snapshot entity that contains the contents of 61 | * the article, as mentioned in {@link IBbsArticle}, the contents of the article 62 | * are separated from the article record to keep evidence and prevent fraud. 63 | */ 64 | export interface ISnapshot extends Omit { 65 | /** 66 | * Primary Key. 67 | */ 68 | id: string & tags.Format<"uuid">; 69 | 70 | /** 71 | * Creation time of snapshot record. 72 | * 73 | * In other words, creation time or update time or article. 74 | */ 75 | created_at: string & tags.Format<"date-time">; 76 | } 77 | 78 | export interface IRequest extends IPage.IRequest { 79 | /** 80 | * Search condition. 81 | */ 82 | search?: IRequest.ISearch; 83 | 84 | /** 85 | * Sort condition. 86 | */ 87 | sort?: IPage.Sort; 88 | } 89 | export namespace IRequest { 90 | /** 91 | * 검색 정보. 92 | */ 93 | export interface ISearch { 94 | writer?: string; 95 | title?: string; 96 | body?: string; 97 | title_or_body?: string; 98 | 99 | /** 100 | * @format date-time 101 | */ 102 | from?: string; 103 | 104 | /** 105 | * @format date-time 106 | */ 107 | to?: string; 108 | } 109 | 110 | /** 111 | * Sortable columns of {@link IBbsArticle}. 112 | */ 113 | export type SortableColumns = 114 | | "writer" 115 | | "title" 116 | | "created_at" 117 | | "updated_at"; 118 | } 119 | 120 | /** 121 | * Summarized information of the article. 122 | */ 123 | export interface ISummary { 124 | /** 125 | * Primary Key. 126 | */ 127 | id: string & tags.Format<"uuid">; 128 | 129 | /** 130 | * Writer of the article. 131 | */ 132 | writer: string; 133 | 134 | /** 135 | * Title of the last snapshot. 136 | */ 137 | title: string; 138 | 139 | /** 140 | * Creation time of the article. 141 | */ 142 | created_at: string & tags.Format<"date-time">; 143 | 144 | /** 145 | * Modification time of the article. 146 | * 147 | * In other words, the time when the last snapshot was created. 148 | */ 149 | updated_at: string & tags.Format<"date-time">; 150 | } 151 | 152 | /** 153 | * Abriged information of the article. 154 | */ 155 | export interface IAbridge extends ISummary, Omit {} 156 | 157 | /** 158 | * Store content type of the article. 159 | */ 160 | export interface ICreate extends IUpdate { 161 | writer: string; 162 | } 163 | 164 | export interface IUpdate { 165 | /** 166 | * Format of body. 167 | * 168 | * Same meaning with extension like `html`, `md`, `txt`. 169 | */ 170 | format: Format; 171 | 172 | /** 173 | * Title of article. 174 | */ 175 | title: string; 176 | 177 | /** 178 | * Content body of article. 179 | */ 180 | body: string; 181 | 182 | /** 183 | * List of attachment files. 184 | */ 185 | files: IAttachmentFile.ICreate[]; 186 | 187 | /** 188 | * Password for modification. 189 | */ 190 | password: string; 191 | } 192 | 193 | export interface IErase { 194 | password: string; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/api/structures/bbs/IBbsArticleComment.ts: -------------------------------------------------------------------------------- 1 | import { tags } from "typia"; 2 | 3 | import { IAttachmentFile } from "../common/IAttachmentFile"; 4 | import { IPage } from "../common/IPage"; 5 | 6 | /** 7 | * Comment written on an article. 8 | * 9 | * `IBbsArticleComment` is an entity that shapes the comments written on an article. 10 | * 11 | * And for this comment, as in the previous relationship between 12 | * {@link IBbsArticle} and {@link IBbsArticle.ISnapshot}, the content body of the 13 | * comment is stored in the sub {@link IBbsArticleComment.ISnapshot} table for 14 | * evidentialism, and a new snapshot record is issued every time the comment is modified. 15 | * 16 | * Also, `IBbsArticleComment` is expressing the relationship of the hierarchical reply 17 | * structure through the {@link IBbsArticleComment.parent_id} attribute. 18 | * 19 | * @author Samchon 20 | */ 21 | export interface IBbsArticleComment { 22 | /** 23 | * Primary Key. 24 | */ 25 | id: string & tags.Format<"uuid">; 26 | 27 | /** 28 | * Parent comment's ID. 29 | */ 30 | parent_id: null | (string & tags.Format<"uuid">); 31 | 32 | /** 33 | * Writer of comment. 34 | */ 35 | writer: string; 36 | 37 | /** 38 | * List of snapshot contents. 39 | * 40 | * It is created for the first time when a comment being created, and is 41 | * accumulated every time the comment is modified. 42 | */ 43 | snapshots: IBbsArticleComment.ISnapshot[] & tags.MinItems<1>; 44 | 45 | /** 46 | * Creation time of comment. 47 | */ 48 | created_at: string & tags.Format<"date-time">; 49 | } 50 | export namespace IBbsArticleComment { 51 | export type Format = "txt" | "md" | "html"; 52 | 53 | export interface IRequest extends IPage.IRequest { 54 | search?: IRequest.ISearch; 55 | sort?: IPage.Sort; 56 | } 57 | export namespace IRequest { 58 | export interface ISearch { 59 | writer?: string; 60 | body?: string; 61 | } 62 | export type SortableColumns = "writer" | "created_at"; 63 | } 64 | 65 | /** 66 | * Snapshot of comment. 67 | * 68 | * `IBbsArticleComment.ISnapshot` is a snapshot entity that contains 69 | * the contents of the comment. 70 | * 71 | * As mentioned in {@link IBbsArticleComment}, designed to keep evidence 72 | * and prevent fraud. 73 | */ 74 | export interface ISnapshot extends Omit { 75 | /** 76 | * Primary Key. 77 | */ 78 | id: string & tags.Format<"uuid">; 79 | 80 | /** 81 | * Creation time of snapshot record. 82 | * 83 | * In other words, creation time or update time or comment. 84 | */ 85 | created_at: string & tags.Format<"date-time">; 86 | } 87 | 88 | export interface ICreate { 89 | /** 90 | * Writer of comment. 91 | */ 92 | writer: string; 93 | 94 | /** 95 | * Format of body. 96 | * 97 | * Same meaning with extension like `html`, `md`, `txt`. 98 | */ 99 | format: Format; 100 | 101 | /** 102 | * Content body of comment. 103 | */ 104 | body: string; 105 | 106 | /** 107 | * List of attachment files. 108 | */ 109 | files: IAttachmentFile.ICreate[]; 110 | 111 | /** 112 | * Password for modification. 113 | */ 114 | password: string; 115 | } 116 | 117 | export type IUpdate = Omit; 118 | 119 | export interface IErase { 120 | password: string; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/api/structures/common/IAttachmentFile.ts: -------------------------------------------------------------------------------- 1 | import { tags } from "typia"; 2 | 3 | /** 4 | * Attachment File. 5 | * 6 | * Every attachment files that are managed in current system. 7 | * 8 | * @author Samchon 9 | */ 10 | export interface IAttachmentFile extends IAttachmentFile.ICreate { 11 | /** 12 | * Primary Key. 13 | */ 14 | id: string & tags.Format<"uuid">; 15 | 16 | /** 17 | * Creation time of attachment file. 18 | */ 19 | created_at: string & tags.Format<"date-time">; 20 | } 21 | 22 | export namespace IAttachmentFile { 23 | export interface ICreate { 24 | /** 25 | * File name, except extension. 26 | */ 27 | name: string & tags.MaxLength<255>; 28 | 29 | /** 30 | * Extension. 31 | * 32 | * Possible to omit like `README` case. 33 | */ 34 | extension: null | (string & tags.MinLength<1> & tags.MaxLength<8>); 35 | 36 | /** 37 | * URL path of the real file. 38 | */ 39 | url: string & tags.Format<"uri">; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/api/structures/common/IDiagnosis.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Result of diagnosis. 3 | * 4 | * A diagnosis describing which error has been occurred. 5 | * 6 | * @author Samchon 7 | */ 8 | export interface IDiagnosis { 9 | /** 10 | * Access path of variable which caused the problem. 11 | */ 12 | accessor: string; 13 | 14 | /** 15 | * Message of diagnosis. 16 | */ 17 | message: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/api/structures/common/IEntity.ts: -------------------------------------------------------------------------------- 1 | import { tags } from "typia"; 2 | 3 | /** 4 | * Common Entity. 5 | * 6 | * Common entity definition for entities having UUID type primary key value. 7 | * 8 | * @author Samchon 9 | */ 10 | export interface IEntity { 11 | /** 12 | * Primary Key. 13 | */ 14 | id: string & tags.Format<"uuid">; 15 | } 16 | -------------------------------------------------------------------------------- /src/api/structures/common/IPage.ts: -------------------------------------------------------------------------------- 1 | import { tags } from "typia"; 2 | 3 | /** 4 | * A page. 5 | * 6 | * Collection of records with pagination indformation. 7 | * 8 | * @template T Record type 9 | * @author Samchon 10 | */ 11 | export interface IPage { 12 | /** 13 | * Page information. 14 | */ 15 | pagination: IPage.IPagination; 16 | 17 | /** 18 | * List of records. 19 | */ 20 | data: T[]; 21 | } 22 | export namespace IPage { 23 | /** 24 | * Page information. 25 | */ 26 | export interface IPagination { 27 | /** 28 | * Current page number. 29 | */ 30 | current: number & tags.Type<"uint32">; 31 | 32 | /** 33 | * Limitation of records per a page. 34 | * 35 | * @default 100 36 | */ 37 | limit: number & tags.Type<"uint32">; 38 | 39 | /** 40 | * Total records in the database. 41 | */ 42 | records: number & tags.Type<"uint32">; 43 | 44 | /** 45 | * Total pages. 46 | * 47 | * Equal to {@link records} / {@link limit} with ceiling. 48 | */ 49 | pages: number & tags.Type<"uint32">; 50 | } 51 | 52 | /** 53 | * Page request data 54 | */ 55 | export interface IRequest { 56 | /** 57 | * Page number. 58 | */ 59 | page?: number & tags.Type<"uint32">; 60 | 61 | /** 62 | * Limitation of records per a page. 63 | * 64 | * @default 100 65 | */ 66 | limit?: number & tags.Type<"uint32">; 67 | } 68 | 69 | /** 70 | * Sorting column specialization. 71 | * 72 | * The plus means ascending order and the minus means descending order. 73 | */ 74 | export type Sort = Array< 75 | `-${Literal}` | `+${Literal}` 76 | >; 77 | } 78 | -------------------------------------------------------------------------------- /src/api/structures/common/IRecordMerge.ts: -------------------------------------------------------------------------------- 1 | import { tags } from "typia"; 2 | 3 | /** 4 | * Record Merge DTO. 5 | * 6 | * `IRecordMerge` is a structure for merging records. 7 | * 8 | * The `merge` means that merging multiple {@link IRecordMerge.absorbed} 9 | * records into {@link IRecordMerge.keep} instead of deleting 10 | * {@link IRecordMerge.absorbed} records. 11 | * 12 | * If there're some dependent tables of the target `table` having 13 | * unique constraint on foreign key column, such dependent tables 14 | * also perform the merge process, too. 15 | * 16 | * Of course, if there're another dependent tables under those 17 | * dependents, they also perform the merge process recursively as well. 18 | * Such recursive merge process still works for self-recursive 19 | * (tree-structured) tables. 20 | * 21 | * @author Samchon 22 | */ 23 | export interface IRecordMerge { 24 | /** 25 | * Target record to keep after merging. 26 | * 27 | * After merge process, {@link absorbed} records would be merged into 28 | * this {@link keep} record. 29 | */ 30 | keep: string & tags.Format<"uuid">; 31 | 32 | /** 33 | * To be absorbed to {@link keep} after merging. 34 | */ 35 | absorbed: Array>; 36 | } 37 | -------------------------------------------------------------------------------- /src/api/structures/monitors/IPerformance.ts: -------------------------------------------------------------------------------- 1 | export interface IPerformance { 2 | cpu: NodeJS.CpuUsage; 3 | memory: NodeJS.MemoryUsage; 4 | resource: NodeJS.ResourceUsage; 5 | } 6 | -------------------------------------------------------------------------------- /src/api/structures/monitors/ISystem.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * System Information. 3 | * 4 | * @author Samchon 5 | */ 6 | export interface ISystem { 7 | /** 8 | * Random Unique ID. 9 | */ 10 | uid: number; 11 | 12 | /** 13 | * `process.argv` 14 | */ 15 | arguments: string[]; 16 | 17 | /** 18 | * Git commit info. 19 | */ 20 | commit: ISystem.ICommit; 21 | 22 | /** 23 | * `package.json` 24 | */ 25 | package: ISystem.IPackage; 26 | 27 | /** 28 | * Creation time of this server. 29 | */ 30 | created_at: string; 31 | } 32 | 33 | export namespace ISystem { 34 | /** 35 | * Git commit info. 36 | */ 37 | export interface ICommit { 38 | shortHash: string; 39 | branch: string; 40 | hash: string; 41 | subject: string; 42 | sanitizedSubject: string; 43 | body: string; 44 | author: ICommit.IUser; 45 | committer: ICommit.IUser; 46 | authored_at: string; 47 | committed_at: string; 48 | notes?: string; 49 | tags: string[]; 50 | } 51 | export namespace ICommit { 52 | /** 53 | * Git user account info. 54 | */ 55 | export interface IUser { 56 | name: string; 57 | email: string; 58 | } 59 | } 60 | 61 | /** 62 | * NPM package info. 63 | */ 64 | export interface IPackage { 65 | name: string; 66 | version: string; 67 | description: string; 68 | main?: string; 69 | typings?: string; 70 | scripts: Record; 71 | repository: { type: "git"; url: string }; 72 | author: string; 73 | license: string; 74 | bugs: { url: string }; 75 | homepage: string; 76 | devDependencies?: Record; 77 | dependencies: Record; 78 | publishConfig?: { registry: string }; 79 | files?: string[]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/api/typings/Atomic.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @module api.typings 4 | */ 5 | //================================================================ 6 | /** 7 | * 객체 정의로부터 원자 멤버들만의 타입을 추려냄. 8 | * 9 | * @template Instance 대상 객체의 타입 10 | * @author Samchon 11 | */ 12 | export type Atomic = { 13 | [P in keyof Instance]: Instance[P] extends object ? never : Instance[P]; 14 | }; 15 | -------------------------------------------------------------------------------- /src/api/typings/Writable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @packageDocumentation 3 | * @module api.typings 4 | */ 5 | //================================================================ 6 | export type Writable = { 7 | -readonly [P in keyof T]: T[P]; 8 | }; 9 | 10 | export function Writable(elem: Readonly): Writable { 11 | return elem; 12 | } 13 | -------------------------------------------------------------------------------- /src/controllers/bbs/BbsArticleCommentsController.ts: -------------------------------------------------------------------------------- 1 | import core from "@nestia/core"; 2 | import { Controller, Request } from "@nestjs/common"; 3 | import { FastifyRequest } from "fastify"; 4 | import { tags } from "typia"; 5 | 6 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 7 | import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; 8 | 9 | import { BbsArticleCommentProvider } from "../../providers/bbs/BbsArticleCommentProvider"; 10 | 11 | @Controller("bbs/articles/:articleId/comments") 12 | export class BbsArticleCommentsController { 13 | /** 14 | * List up all summarized comments. 15 | * 16 | * List up all summarized comments with pagination and searching options. 17 | * 18 | * @param input Request info of pagination and searching options. 19 | * @returns Paginated summarized comments. 20 | * @tag BBS 21 | * 22 | * @author Samchon 23 | */ 24 | @core.TypedRoute.Patch() 25 | public index( 26 | @core.TypedParam("articleId") articleId: string & tags.Format<"uuid">, 27 | @core.TypedBody() input: IBbsArticleComment.IRequest, 28 | ): Promise> { 29 | return BbsArticleCommentProvider.index({ id: articleId })(input); 30 | } 31 | 32 | /** 33 | * Read individual comment. 34 | * 35 | * Reads a comment with its every {@link IBbsArticleComment.ISnapshot snapshots}. 36 | * 37 | * @param articleId Belonged article's {@link IBbsArticle.id} 38 | * @param id Target comment's {@link IBbsArticleComment.id} 39 | * @returns Comment information 40 | * @tag BBS 41 | * 42 | * @author Samchon 43 | */ 44 | @core.TypedRoute.Get(":id") 45 | public at( 46 | @core.TypedParam("articleId") articleId: string & tags.Format<"uuid">, 47 | @core.TypedParam("id") id: string & tags.Format<"uuid">, 48 | ): Promise { 49 | return BbsArticleCommentProvider.at({ id: articleId })(id); 50 | } 51 | 52 | /** 53 | * Create a new comment. 54 | * 55 | * Create a new comment with its first {@link IBbsArticleComment.ISnapshot snapshot}. 56 | * 57 | * @param articleId Belonged article's {@link IBbsArticle.id} 58 | * @param input Comment information to create. 59 | * @returns Newly created comment. 60 | * @tag BBS 61 | * 62 | * @author Samchon 63 | */ 64 | @core.TypedRoute.Post() 65 | public create( 66 | @Request() request: FastifyRequest, 67 | @core.TypedParam("articleId") articleId: string & tags.Format<"uuid">, 68 | @core.TypedBody() input: IBbsArticleComment.ICreate, 69 | ): Promise { 70 | return BbsArticleCommentProvider.create({ id: articleId })( 71 | input, 72 | request.ip, 73 | ); 74 | } 75 | 76 | /** 77 | * Update a comment. 78 | * 79 | * Accumulate a new {@link IBbsArticleComment.ISnapshot snapshot} record to the comment. 80 | * 81 | * @param articleId Belonged article's {@link IBbsArticle.id} 82 | * @param id Target comment's {@link IBbsArticleComment.id} 83 | * @param input Comment information to update. 84 | * @returns Newly accumulated snapshot information. 85 | * @tag BBS 86 | * 87 | * @author Samchon 88 | */ 89 | @core.TypedRoute.Put(":id") 90 | public update( 91 | @Request() request: FastifyRequest, 92 | @core.TypedParam("articleId") articleId: string & tags.Format<"uuid">, 93 | @core.TypedParam("id") id: string & tags.Format<"uuid">, 94 | @core.TypedBody() input: IBbsArticleComment.IUpdate, 95 | ): Promise { 96 | return BbsArticleCommentProvider.update({ id: articleId })(id)( 97 | input, 98 | request.ip, 99 | ); 100 | } 101 | 102 | /** 103 | * Erase a comment. 104 | * 105 | * Performs soft deletion to the comment. 106 | * 107 | * @param articleId Belonged article's {@link IBbsArticle.id} 108 | * @param id Target comment's {@link IBbsArticleComment.id} 109 | * @param input Password of the comment. 110 | * @tag BBS 111 | * 112 | * @author Samchon 113 | */ 114 | @core.TypedRoute.Delete(":id") 115 | public erase( 116 | @core.TypedParam("articleId") articleId: string & tags.Format<"uuid">, 117 | @core.TypedParam("id") id: string & tags.Format<"uuid">, 118 | @core.TypedBody() input: IBbsArticleComment.IErase, 119 | ): Promise { 120 | return BbsArticleCommentProvider.erase({ id: articleId })(id)(input); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/controllers/bbs/BbsArticlesController.ts: -------------------------------------------------------------------------------- 1 | import core from "@nestia/core"; 2 | import { Controller, Request } from "@nestjs/common"; 3 | import { FastifyRequest } from "fastify"; 4 | import { tags } from "typia"; 5 | 6 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 7 | import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; 8 | 9 | import { BbsArticleProvider } from "../../providers/bbs/BbsArticleProvider"; 10 | 11 | @Controller("bbs/articles") 12 | export class BbsArticlesController { 13 | /** 14 | * List up all summarized articles. 15 | * 16 | * List up all summarized articles with pagination and searching options. 17 | * 18 | * @param input Request info of pagination and searching options. 19 | * @returns Paginated summarized articles. 20 | * @tag BBS 21 | * 22 | * @author Samchon 23 | */ 24 | @core.TypedRoute.Patch() 25 | public index( 26 | @core.TypedBody() input: IBbsArticle.IRequest, 27 | ): Promise> { 28 | return BbsArticleProvider.index(input); 29 | } 30 | 31 | /** 32 | * List up all abridged articles. 33 | * 34 | * List up all abridged articles with pagination and searching options. 35 | * 36 | * @param input Request info of pagination and searching options. 37 | * @returns Paginated abridged articles. 38 | * @tag BBS 39 | * 40 | * @author Samchon 41 | */ 42 | @core.TypedRoute.Patch("abridges") 43 | public abridges( 44 | @core.TypedBody() input: IBbsArticle.IRequest, 45 | ): Promise> { 46 | return BbsArticleProvider.abridges(input); 47 | } 48 | 49 | /** 50 | * Read individual article. 51 | * 52 | * Reads an article with its every {@link IBbsArticle.ISnapshot snapshots}. 53 | * 54 | * @param id Target article's {@link IBbsArticle.id} 55 | * @returns Article information 56 | * @tag BBS 57 | * 58 | * @author Samchon 59 | */ 60 | @core.TypedRoute.Get(":id") 61 | public at( 62 | @core.TypedParam("id") id: string & tags.Format<"uuid">, 63 | ): Promise { 64 | return BbsArticleProvider.at(id); 65 | } 66 | 67 | /** 68 | * Create a new article. 69 | * 70 | * Create a new article with its first {@link IBbsArticle.ISnapshot snapshot}. 71 | * 72 | * @param input Article information to create. 73 | * @returns Newly created article. 74 | * @tag BBS 75 | * 76 | * @author Samchon 77 | */ 78 | @core.TypedRoute.Post() 79 | public create( 80 | @Request() request: FastifyRequest, 81 | @core.TypedBody() input: IBbsArticle.ICreate, 82 | ): Promise { 83 | return BbsArticleProvider.create(input, request.ip); 84 | } 85 | 86 | /** 87 | * Update an article. 88 | * 89 | * Accumulate a new {@link IBbsArticle.ISnapshot snapshot} record to the article. 90 | * 91 | * @param id Target article's {@link IBbsArticle.id} 92 | * @param input Article information to update. 93 | * @returns Newly accumulated snapshot information. 94 | * @tag BBS 95 | * 96 | * @author Samchon 97 | */ 98 | @core.TypedRoute.Put(":id") 99 | public update( 100 | @Request() request: FastifyRequest, 101 | @core.TypedParam("id") id: string & tags.Format<"uuid">, 102 | @core.TypedBody() input: IBbsArticle.IUpdate, 103 | ): Promise { 104 | return BbsArticleProvider.update(id)(input, request.ip); 105 | } 106 | 107 | /** 108 | * Erase an article. 109 | * 110 | * Performs soft deletion to the article. 111 | * 112 | * @param id Target article's {@link IBbsArticle.id} 113 | * @param input Password of the article. 114 | * @tag BBS 115 | * 116 | * @author Samchon 117 | */ 118 | @core.TypedRoute.Delete(":id") 119 | public erase( 120 | @core.TypedParam("id") id: string & tags.Format<"uuid">, 121 | @core.TypedBody() input: IBbsArticle.IErase, 122 | ): Promise { 123 | return BbsArticleProvider.erase(id)(input); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/controllers/monitors/MonitorHealthController.ts: -------------------------------------------------------------------------------- 1 | import core from "@nestia/core"; 2 | import { Controller } from "@nestjs/common"; 3 | 4 | @Controller("monitors/health") 5 | export class MonitorHealthController { 6 | /** 7 | * Health check API. 8 | * 9 | * @tag Monitor 10 | * 11 | * @author Samchon 12 | */ 13 | @core.TypedRoute.Get() 14 | public get(): void {} 15 | } 16 | -------------------------------------------------------------------------------- /src/controllers/monitors/MonitorModule.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | 3 | import { MonitorHealthController } from "./MonitorHealthController"; 4 | import { MonitorPerformanceController } from "./MonitorPerformanceController"; 5 | import { MonitorSystemController } from "./MonitorSystemController"; 6 | 7 | @Module({ 8 | controllers: [ 9 | MonitorHealthController, 10 | MonitorPerformanceController, 11 | MonitorSystemController, 12 | ], 13 | }) 14 | export class MonitorModule {} 15 | -------------------------------------------------------------------------------- /src/controllers/monitors/MonitorPerformanceController.ts: -------------------------------------------------------------------------------- 1 | import core from "@nestia/core"; 2 | import { Controller } from "@nestjs/common"; 3 | 4 | import { IPerformance } from "@samchon/bbs-api/lib/structures/monitors/IPerformance"; 5 | 6 | @Controller("monitors/performance") 7 | export class MonitorPerformanceController { 8 | /** 9 | * Get performance information. 10 | * 11 | * Get perofmration information composed with CPU, memory and resource usage. 12 | * 13 | * @returns Performance info 14 | * @tag Monitor 15 | * 16 | * @author Samchon 17 | */ 18 | @core.TypedRoute.Get() 19 | public async get(): Promise { 20 | return { 21 | cpu: process.cpuUsage(), 22 | memory: process.memoryUsage(), 23 | resource: process.resourceUsage(), 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/controllers/monitors/MonitorSystemController.ts: -------------------------------------------------------------------------------- 1 | import core from "@nestia/core"; 2 | import { Controller } from "@nestjs/common"; 3 | 4 | import { ISystem } from "@samchon/bbs-api/lib/structures/monitors/ISystem"; 5 | 6 | import { SystemProvider } from "../../providers/monitors/SystemProvider"; 7 | 8 | import { DateUtil } from "../../utils/DateUtil"; 9 | 10 | @Controller("monitors/system") 11 | export class MonitorSystemController { 12 | /** 13 | * Get system information. 14 | * 15 | * Get system information with commit and package information. 16 | * 17 | * @returns System info 18 | * @tag Monitor 19 | * 20 | * @author Samchon 21 | */ 22 | @core.TypedRoute.Get() 23 | public async get(): Promise { 24 | return { 25 | uid: SystemProvider.uid, 26 | arguments: process.argv, 27 | commit: await SystemProvider.commit(), 28 | package: await SystemProvider.package(), 29 | created_at: DateUtil.toString(SystemProvider.created_at, true), 30 | }; 31 | } 32 | 33 | /** 34 | * @internal 35 | */ 36 | @core.TypedRoute.Get("internal_server_error") 37 | public async internal_server_error(): Promise { 38 | throw new Error("The manual 500 error for the testing."); 39 | } 40 | 41 | /** 42 | * @internal 43 | */ 44 | @core.TypedRoute.Get("uncaught_exception") 45 | public uncaught_exception(): void { 46 | new Promise(() => { 47 | throw new Error("The manul uncaught exception for the testing."); 48 | }).catch(() => {}); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/executable/schema.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { BbsGlobal } from "../BbsGlobal"; 4 | import { BbsSetupWizard } from "../setup/BbsSetupWizard"; 5 | 6 | async function execute( 7 | database: string, 8 | username: string, 9 | password: string, 10 | script: string, 11 | ): Promise { 12 | try { 13 | const prisma = new PrismaClient({ 14 | datasources: { 15 | db: { 16 | url: `postgresql://${username}:${password}@${BbsGlobal.env.BBS_POSTGRES_HOST}:${BbsGlobal.env.BBS_POSTGRES_PORT}/${database}`, 17 | }, 18 | }, 19 | }); 20 | const queries: string[] = script 21 | .split("\n") 22 | .map((str) => str.trim()) 23 | .filter((str) => !!str); 24 | for (const query of queries) 25 | try { 26 | await prisma.$queryRawUnsafe(query); 27 | } catch (e) { 28 | await prisma.$disconnect(); 29 | } 30 | await prisma.$disconnect(); 31 | } catch (err) { 32 | console.log(err); 33 | } 34 | } 35 | 36 | async function main(): Promise { 37 | const config = { 38 | database: BbsGlobal.env.BBS_POSTGRES_DATABASE, 39 | schema: BbsGlobal.env.BBS_POSTGRES_SCHEMA, 40 | username: BbsGlobal.env.BBS_POSTGRES_USERNAME, 41 | readonlyUsername: BbsGlobal.env.BBS_POSTGRES_USERNAME_READONLY, 42 | password: BbsGlobal.env.BBS_POSTGRES_PASSWORD, 43 | }; 44 | const root = { 45 | account: process.argv[2] ?? "postgres", 46 | password: process.argv[3] ?? "root", 47 | }; 48 | 49 | await execute( 50 | "postgres", 51 | root.account, 52 | root.password, 53 | ` 54 | CREATE USER ${config.username} WITH ENCRYPTED PASSWORD '${config.password}'; 55 | ALTER ROLE ${config.username} WITH CREATEDB 56 | CREATE DATABASE ${config.database} OWNER ${config.username}; 57 | `, 58 | ); 59 | 60 | await execute( 61 | config.database, 62 | root.account, 63 | root.password, 64 | ` 65 | CREATE SCHEMA ${config.schema} AUTHORIZATION ${config.username}; 66 | `, 67 | ); 68 | 69 | await execute( 70 | config.database, 71 | root.account, 72 | root.password, 73 | ` 74 | GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA ${config.schema} TO ${config.username}; 75 | 76 | CREATE USER ${config.readonlyUsername} WITH ENCRYPTED PASSWORD '${config.password}'; 77 | GRANT USAGE ON SCHEMA ${config.schema} TO ${config.readonlyUsername}; 78 | GRANT SELECT ON ALL TABLES IN SCHEMA ${config.schema} TO ${config.readonlyUsername}; 79 | `, 80 | ); 81 | 82 | BbsGlobal.testing = true; 83 | await BbsSetupWizard.schema(); 84 | } 85 | main().catch((exp) => { 86 | console.log(exp); 87 | process.exit(-1); 88 | }); 89 | -------------------------------------------------------------------------------- /src/executable/server.ts: -------------------------------------------------------------------------------- 1 | import { BbsBackend } from "../BbsBackend"; 2 | 3 | const EXTENSION = __filename.substr(-2); 4 | if (EXTENSION === "js") require("source-map-support/register"); 5 | 6 | async function main(): Promise { 7 | // BACKEND SEVER LATER 8 | const backend: BbsBackend = new BbsBackend(); 9 | await backend.open(); 10 | 11 | // POST-PROCESS 12 | process.send?.("ready"); 13 | process.on("SIGTERM", async () => { 14 | await backend.close(); 15 | process.exit(0); 16 | }); 17 | process.on("uncaughtException", console.error); 18 | process.on("unhandledRejection", console.error); 19 | } 20 | main().catch((exp) => { 21 | console.log(exp); 22 | process.exit(-1); 23 | }); 24 | -------------------------------------------------------------------------------- /src/executable/swagger.ts: -------------------------------------------------------------------------------- 1 | import cp from "child_process"; 2 | import express from "express"; 3 | 4 | const execute = (command: string): void => { 5 | console.log(`\n$ ${command}\n`); 6 | cp.execSync(command, { stdio: "inherit" }); 7 | }; 8 | 9 | const main = async (): Promise => { 10 | if (!process.argv.some((str) => str === "--skipBuild")) 11 | execute("npm run build:swagger"); 12 | 13 | const docs = await import("../../packages/api/swagger.json" as any); 14 | 15 | const app = express(); 16 | const swaggerUi = require("swagger-ui-express"); 17 | app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(docs)); 18 | app.listen(37810); 19 | 20 | console.log("\n"); 21 | console.log("-----------------------------------------------------------"); 22 | console.log("\n Swagger UI Address: http://127.0.0.1:37810/api-docs \n"); 23 | console.log("-----------------------------------------------------------"); 24 | }; 25 | main().catch((exp) => { 26 | console.log(exp); 27 | process.exit(-1); 28 | }); 29 | -------------------------------------------------------------------------------- /src/providers/bbs/BbsArticleCommentProvider.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { v4 } from "uuid"; 3 | 4 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 5 | import { IEntity } from "@samchon/bbs-api/lib/structures/common/IEntity"; 6 | import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; 7 | 8 | import { BbsGlobal } from "../../BbsGlobal"; 9 | import { BcryptUtil } from "../../utils/BcryptUtil"; 10 | import { PaginationUtil } from "../../utils/PaginationUtil"; 11 | import { BbsArticleCommentSnapshotProvider } from "./BbsArticleCommentSnapshotProvider"; 12 | import { ErrorProvider } from "./ErrorProvider"; 13 | 14 | export namespace BbsArticleCommentProvider { 15 | /* ----------------------------------------------------------- 16 | TRANSFORMERS 17 | ----------------------------------------------------------- */ 18 | export namespace json { 19 | export const transform = ( 20 | input: Prisma.bbs_article_commentsGetPayload>, 21 | ): IBbsArticleComment => ({ 22 | id: input.id, 23 | parent_id: input.parent_id, 24 | writer: input.writer, 25 | snapshots: input.snapshots 26 | .sort((a, b) => a.created_at.getTime() - b.created_at.getTime()) 27 | .map(BbsArticleCommentSnapshotProvider.json.transform), 28 | created_at: input.created_at.toISOString(), 29 | }); 30 | export const select = () => 31 | ({ 32 | include: { 33 | snapshots: BbsArticleCommentSnapshotProvider.json.select(), 34 | } as const, 35 | }) satisfies Prisma.bbs_article_commentsFindManyArgs; 36 | } 37 | 38 | /* ----------------------------------------------------------- 39 | READERS 40 | ----------------------------------------------------------- */ 41 | export const index = 42 | (article: IEntity) => 43 | async ( 44 | input: IBbsArticleComment.IRequest, 45 | ): Promise> => { 46 | await BbsGlobal.prisma.bbs_articles.findFirstOrThrow({ 47 | where: { 48 | id: article.id, 49 | deleted_at: null, 50 | }, 51 | }); 52 | return PaginationUtil.paginate({ 53 | schema: BbsGlobal.prisma.bbs_article_comments, 54 | payload: json.select(), 55 | transform: json.transform, 56 | })({ 57 | where: { 58 | AND: [{ deleted_at: null }, ...search(input.search ?? {})], 59 | }, 60 | orderBy: input.sort?.length 61 | ? PaginationUtil.orderBy(orderBy)(input.sort) 62 | : [{ created_at: "asc" }], 63 | })(input); 64 | }; 65 | 66 | export const at = 67 | (article: IEntity) => 68 | async (id: string): Promise => { 69 | const record = 70 | await BbsGlobal.prisma.bbs_article_comments.findFirstOrThrow({ 71 | where: { 72 | id, 73 | deleted_at: null, 74 | article: { 75 | id: article.id, 76 | deleted_at: null, 77 | }, 78 | }, 79 | ...json.select(), 80 | }); 81 | return json.transform(record); 82 | }; 83 | 84 | const search = (input: IBbsArticleComment.IRequest.ISearch | undefined) => 85 | [ 86 | ...(input?.writer?.length 87 | ? [ 88 | { 89 | writer: { 90 | contains: input.writer, 91 | }, 92 | }, 93 | ] 94 | : []), 95 | ...(input?.body?.length 96 | ? [ 97 | { 98 | snapshots: { 99 | some: { 100 | body: { 101 | contains: input.body, 102 | }, 103 | }, 104 | }, 105 | }, 106 | ] 107 | : []), 108 | ] satisfies Prisma.bbs_article_commentsWhereInput["AND"]; 109 | 110 | const orderBy = ( 111 | key: IBbsArticleComment.IRequest.SortableColumns, 112 | value: "asc" | "desc", 113 | ) => 114 | (key === "writer" 115 | ? { writer: value } 116 | : { 117 | created_at: value, 118 | }) satisfies Prisma.bbs_article_commentsOrderByWithRelationInput; 119 | 120 | /* ----------------------------------------------------------- 121 | WRITERS 122 | ----------------------------------------------------------- */ 123 | export const create = 124 | (article: IEntity) => 125 | async ( 126 | input: IBbsArticleComment.ICreate, 127 | ip: string, 128 | ): Promise => { 129 | await BbsGlobal.prisma.bbs_articles.findFirstOrThrow({ 130 | where: { 131 | id: article.id, 132 | deleted_at: null, 133 | }, 134 | }); 135 | 136 | const snapshot = BbsArticleCommentSnapshotProvider.collect(input, ip); 137 | const record = await BbsGlobal.prisma.bbs_article_comments.create({ 138 | data: { 139 | id: v4(), 140 | writer: input.writer, 141 | article: { 142 | connect: { id: article.id }, 143 | }, 144 | snapshots: { 145 | create: [snapshot], 146 | }, 147 | created_at: new Date(), 148 | password: await BcryptUtil.hash(input.password), 149 | }, 150 | ...json.select(), 151 | }); 152 | return json.transform(record); 153 | }; 154 | 155 | export const update = 156 | (article: IEntity) => 157 | (id: string) => 158 | async ( 159 | input: IBbsArticleComment.IUpdate, 160 | ip: string, 161 | ): Promise => { 162 | const comment = 163 | await BbsGlobal.prisma.bbs_article_comments.findFirstOrThrow({ 164 | where: { 165 | id, 166 | deleted_at: null, 167 | article: { 168 | id: article.id, 169 | deleted_at: null, 170 | }, 171 | }, 172 | }); 173 | if ( 174 | false === 175 | (await BcryptUtil.equals({ 176 | input: input.password, 177 | hashed: comment.password, 178 | })) 179 | ) 180 | throw ErrorProvider.forbidden({ 181 | accessor: "input.password", 182 | message: "Wrong password.", 183 | }); 184 | return BbsArticleCommentSnapshotProvider.create(comment)(input, ip); 185 | }; 186 | 187 | export const erase = 188 | (article: IEntity) => 189 | (id: string) => 190 | async (input: IBbsArticleComment.IErase): Promise => { 191 | const record = 192 | await BbsGlobal.prisma.bbs_article_comments.findFirstOrThrow({ 193 | where: { 194 | id, 195 | deleted_at: null, 196 | article: { 197 | id: article.id, 198 | deleted_at: null, 199 | }, 200 | }, 201 | }); 202 | if ( 203 | false === 204 | (await BcryptUtil.equals({ 205 | input: input.password, 206 | hashed: record.password, 207 | })) 208 | ) 209 | throw ErrorProvider.forbidden({ 210 | accessor: "input.password", 211 | message: "Wrong password.", 212 | }); 213 | 214 | await BbsGlobal.prisma.bbs_article_comments.update({ 215 | where: { 216 | id, 217 | }, 218 | data: { 219 | deleted_at: new Date(), 220 | }, 221 | }); 222 | }; 223 | } 224 | -------------------------------------------------------------------------------- /src/providers/bbs/BbsArticleCommentSnapshotProvider.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { v4 } from "uuid"; 3 | 4 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 5 | import { IEntity } from "@samchon/bbs-api/lib/structures/common/IEntity"; 6 | 7 | import { BbsGlobal } from "../../BbsGlobal"; 8 | import { AttachmentFileProvider } from "../common/AttachmentFileProvider"; 9 | 10 | export namespace BbsArticleCommentSnapshotProvider { 11 | export namespace json { 12 | export const transform = ( 13 | input: Prisma.bbs_article_comment_snapshotsGetPayload< 14 | ReturnType 15 | >, 16 | ): IBbsArticleComment.ISnapshot => ({ 17 | id: input.id, 18 | format: input.format as any, 19 | body: input.body, 20 | files: input.to_files 21 | .sort((a, b) => a.sequence - b.sequence) 22 | .map((p) => AttachmentFileProvider.json.transform(p.file)), 23 | created_at: input.created_at.toISOString(), 24 | }); 25 | export const select = () => 26 | ({ 27 | include: { 28 | to_files: { 29 | include: { 30 | file: AttachmentFileProvider.json.select(), 31 | }, 32 | }, 33 | } as const, 34 | }) satisfies Prisma.bbs_article_comment_snapshotsFindManyArgs; 35 | } 36 | 37 | export const create = 38 | (comment: IEntity) => 39 | async ( 40 | input: IBbsArticleComment.IUpdate, 41 | ip: string, 42 | ): Promise => { 43 | const snapshot = 44 | await BbsGlobal.prisma.bbs_article_comment_snapshots.create({ 45 | data: { 46 | ...collect(input, ip), 47 | comment: { connect: { id: comment.id } }, 48 | }, 49 | ...json.select(), 50 | }); 51 | return json.transform(snapshot); 52 | }; 53 | 54 | export const collect = (input: IBbsArticleComment.IUpdate, ip: string) => 55 | ({ 56 | id: v4(), 57 | format: input.format, 58 | body: input.body, 59 | ip, 60 | created_at: new Date(), 61 | to_files: { 62 | create: input.files.map((file, i) => ({ 63 | id: v4(), 64 | file: { 65 | create: AttachmentFileProvider.collect(file), 66 | }, 67 | sequence: i, 68 | })), 69 | }, 70 | }) satisfies Prisma.bbs_article_comment_snapshotsCreateWithoutCommentInput; 71 | } 72 | -------------------------------------------------------------------------------- /src/providers/bbs/BbsArticleProvider.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { v4 } from "uuid"; 3 | 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; 6 | 7 | import { BbsGlobal } from "../../BbsGlobal"; 8 | import { BcryptUtil } from "../../utils/BcryptUtil"; 9 | import { PaginationUtil } from "../../utils/PaginationUtil"; 10 | import { AttachmentFileProvider } from "../common/AttachmentFileProvider"; 11 | import { BbsArticleSnapshotProvider } from "./BbsArticleSnapshotProvider"; 12 | import { ErrorProvider } from "./ErrorProvider"; 13 | 14 | export namespace BbsArticleProvider { 15 | /* ----------------------------------------------------------- 16 | TRANSFORMERS 17 | ----------------------------------------------------------- */ 18 | export namespace json { 19 | export const transform = ( 20 | input: Prisma.bbs_articlesGetPayload>, 21 | ): IBbsArticle => ({ 22 | id: input.id, 23 | writer: input.writer, 24 | snapshots: input.snapshots 25 | .sort((a, b) => a.created_at.getTime() - b.created_at.getTime()) 26 | .map(BbsArticleSnapshotProvider.json.transform), 27 | created_at: input.created_at.toISOString(), 28 | }); 29 | 30 | export const select = () => 31 | ({ 32 | include: { 33 | snapshots: BbsArticleSnapshotProvider.json.select(), 34 | } as const, 35 | }) satisfies Prisma.bbs_articlesFindManyArgs; 36 | } 37 | 38 | export namespace abridge { 39 | export const transform = ( 40 | input: Prisma.bbs_articlesGetPayload>, 41 | ): IBbsArticle.IAbridge => ({ 42 | id: input.id, 43 | writer: input.writer, 44 | title: input.mv_last!.snapshot.title, 45 | body: input.mv_last!.snapshot.body, 46 | format: input.mv_last!.snapshot.format as IBbsArticle.Format, 47 | created_at: input.created_at.toISOString(), 48 | updated_at: input.mv_last!.snapshot.created_at.toISOString(), 49 | files: input.mv_last!.snapshot.to_files.map((p) => 50 | AttachmentFileProvider.json.transform(p.file), 51 | ), 52 | }); 53 | export const select = () => 54 | ({ 55 | include: { 56 | mv_last: { 57 | include: { 58 | snapshot: { 59 | include: { 60 | to_files: { 61 | include: { 62 | file: AttachmentFileProvider.json.select(), 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | } as const, 70 | }) satisfies Prisma.bbs_articlesFindManyArgs; 71 | } 72 | 73 | export namespace summarize { 74 | export const transform = ( 75 | input: Prisma.bbs_articlesGetPayload>, 76 | ): IBbsArticle.ISummary => ({ 77 | id: input.id, 78 | writer: input.writer, 79 | title: input.mv_last!.snapshot.title, 80 | created_at: input.created_at.toISOString(), 81 | updated_at: input.mv_last!.snapshot.created_at.toISOString(), 82 | }); 83 | export const select = () => 84 | ({ 85 | include: { 86 | mv_last: { 87 | include: { 88 | snapshot: { 89 | select: { 90 | title: true, 91 | created_at: true, 92 | }, 93 | }, 94 | }, 95 | }, 96 | } as const, 97 | }) satisfies Prisma.bbs_articlesFindManyArgs; 98 | } 99 | 100 | /* ----------------------------------------------------------- 101 | READERS 102 | ----------------------------------------------------------- */ 103 | export const index = ( 104 | input: IBbsArticle.IRequest, 105 | ): Promise> => 106 | PaginationUtil.paginate({ 107 | schema: BbsGlobal.prisma.bbs_articles, 108 | payload: summarize.select(), 109 | transform: summarize.transform, 110 | })({ 111 | where: { 112 | AND: [{ deleted_at: null }, ...search(input.search ?? {})], 113 | }, 114 | orderBy: input.sort?.length 115 | ? PaginationUtil.orderBy(orderBy)(input.sort) 116 | : [{ created_at: "desc" }], 117 | })(input); 118 | 119 | export const abridges = ( 120 | input: IBbsArticle.IRequest, 121 | ): Promise> => 122 | PaginationUtil.paginate({ 123 | schema: BbsGlobal.prisma.bbs_articles, 124 | payload: abridge.select(), 125 | transform: abridge.transform, 126 | })({ 127 | where: { 128 | AND: [{ deleted_at: null }, ...search(input.search ?? {})], 129 | }, 130 | orderBy: input.sort?.length 131 | ? PaginationUtil.orderBy(orderBy)(input.sort) 132 | : [{ created_at: "desc" }], 133 | })(input); 134 | 135 | export const at = async (id: string): Promise => { 136 | const record = await BbsGlobal.prisma.bbs_articles.findFirstOrThrow({ 137 | where: { 138 | id, 139 | deleted_at: null, 140 | }, 141 | ...json.select(), 142 | }); 143 | return json.transform(record); 144 | }; 145 | 146 | const search = (input: IBbsArticle.IRequest.ISearch | undefined) => 147 | [ 148 | ...(input?.writer?.length 149 | ? [{ writer: { contains: input.writer } }] 150 | : []), 151 | ...(input?.title?.length 152 | ? [ 153 | { 154 | mv_last: { 155 | snapshot: { 156 | title: { contains: input.title }, 157 | }, 158 | }, 159 | }, 160 | ] 161 | : []), 162 | ...(input?.body?.length 163 | ? [ 164 | { 165 | mv_last: { 166 | snapshot: { 167 | body: { 168 | contains: input.body, 169 | }, 170 | }, 171 | }, 172 | }, 173 | ] 174 | : []), 175 | ...(input?.title_or_body?.length 176 | ? [ 177 | { 178 | OR: [ 179 | { 180 | mv_last: { 181 | snapshot: { 182 | title: { 183 | contains: input.title_or_body, 184 | }, 185 | }, 186 | }, 187 | }, 188 | { 189 | mv_last: { 190 | snapshot: { 191 | body: { 192 | contains: input.title_or_body, 193 | }, 194 | }, 195 | }, 196 | }, 197 | ], 198 | }, 199 | ] 200 | : []), 201 | ...(input?.from?.length 202 | ? [ 203 | { 204 | created_at: { 205 | gte: new Date(input.from), 206 | }, 207 | }, 208 | ] 209 | : []), 210 | ...(input?.to?.length 211 | ? [ 212 | { 213 | created_at: { 214 | lte: new Date(input.to), 215 | }, 216 | }, 217 | ] 218 | : []), 219 | ] satisfies Prisma.bbs_articlesWhereInput["AND"]; 220 | 221 | const orderBy = ( 222 | key: IBbsArticle.IRequest.SortableColumns, 223 | value: "asc" | "desc", 224 | ) => 225 | (key === "writer" 226 | ? { writer: value } 227 | : key === "title" 228 | ? { mv_last: { snapshot: { title: value } } } 229 | : key === "created_at" 230 | ? { created_at: value } 231 | : // updated_at 232 | { 233 | mv_last: { snapshot: { created_at: value } }, 234 | }) satisfies Prisma.bbs_articlesOrderByWithRelationInput; 235 | 236 | /* ----------------------------------------------------------- 237 | WRITERS 238 | ----------------------------------------------------------- */ 239 | export const create = async ( 240 | input: IBbsArticle.ICreate, 241 | ip: string, 242 | ): Promise => { 243 | const snapshot = BbsArticleSnapshotProvider.collect(input, ip); 244 | const record = await BbsGlobal.prisma.bbs_articles.create({ 245 | data: { 246 | id: v4(), 247 | writer: input.writer, 248 | created_at: new Date(), 249 | password: await BcryptUtil.hash(input.password), 250 | snapshots: { 251 | create: [snapshot], 252 | }, 253 | mv_last: { 254 | create: { 255 | bbs_article_snapshot_id: snapshot.id, 256 | }, 257 | }, 258 | }, 259 | ...json.select(), 260 | }); 261 | return json.transform(record); 262 | }; 263 | 264 | export const update = 265 | (id: string) => 266 | async ( 267 | input: IBbsArticle.IUpdate, 268 | ip: string, 269 | ): Promise => { 270 | const record = await BbsGlobal.prisma.bbs_articles.findFirstOrThrow({ 271 | where: { 272 | id, 273 | deleted_at: null, 274 | }, 275 | ...json.select(), 276 | }); 277 | if ( 278 | false === 279 | (await BcryptUtil.equals({ 280 | input: input.password, 281 | hashed: record.password, 282 | })) 283 | ) 284 | throw ErrorProvider.forbidden({ 285 | accessor: "input.password", 286 | message: "Wrong password.", 287 | }); 288 | return BbsArticleSnapshotProvider.create({ id })(input, ip); 289 | }; 290 | 291 | export const erase = 292 | (id: string) => 293 | async (input: IBbsArticle.IErase): Promise => { 294 | const record = await BbsGlobal.prisma.bbs_articles.findFirstOrThrow({ 295 | where: { 296 | id, 297 | deleted_at: null, 298 | }, 299 | }); 300 | if ( 301 | false === 302 | (await BcryptUtil.equals({ 303 | input: input.password, 304 | hashed: record.password, 305 | })) 306 | ) 307 | throw ErrorProvider.forbidden({ 308 | accessor: "input.password", 309 | message: "Wrong password.", 310 | }); 311 | await BbsGlobal.prisma.bbs_articles.update({ 312 | where: { id }, 313 | data: { 314 | deleted_at: new Date(), 315 | }, 316 | }); 317 | }; 318 | } 319 | -------------------------------------------------------------------------------- /src/providers/bbs/BbsArticleSnapshotProvider.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { v4 } from "uuid"; 3 | 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | import { IEntity } from "@samchon/bbs-api/lib/structures/common/IEntity"; 6 | 7 | import { BbsGlobal } from "../../BbsGlobal"; 8 | import { AttachmentFileProvider } from "../common/AttachmentFileProvider"; 9 | 10 | export namespace BbsArticleSnapshotProvider { 11 | export namespace json { 12 | export const transform = ( 13 | input: Prisma.bbs_article_snapshotsGetPayload>, 14 | ): IBbsArticle.ISnapshot => ({ 15 | id: input.id, 16 | title: input.title, 17 | format: input.format as any, 18 | body: input.body, 19 | files: input.to_files 20 | .sort((a, b) => a.sequence - b.sequence) 21 | .map((p) => AttachmentFileProvider.json.transform(p.file)), 22 | created_at: input.created_at.toISOString(), 23 | }); 24 | export const select = () => 25 | ({ 26 | include: { 27 | to_files: { 28 | include: { 29 | file: AttachmentFileProvider.json.select(), 30 | }, 31 | }, 32 | } as const, 33 | }) satisfies Prisma.bbs_article_snapshotsFindManyArgs; 34 | } 35 | 36 | export const create = 37 | (article: IEntity) => 38 | async ( 39 | input: IBbsArticle.IUpdate, 40 | ip: string, 41 | ): Promise => { 42 | const snapshot = await BbsGlobal.prisma.bbs_article_snapshots.create({ 43 | data: { 44 | ...collect(input, ip), 45 | article: { connect: { id: article.id } }, 46 | }, 47 | ...json.select(), 48 | }); 49 | await BbsGlobal.prisma.mv_bbs_article_last_snapshots.update({ 50 | where: { 51 | bbs_article_id: article.id, 52 | }, 53 | data: { 54 | bbs_article_snapshot_id: snapshot.id, 55 | }, 56 | }); 57 | return json.transform(snapshot); 58 | }; 59 | 60 | export const collect = (input: IBbsArticle.IUpdate, ip: string) => 61 | ({ 62 | id: v4(), 63 | title: input.title, 64 | format: input.format, 65 | body: input.body, 66 | ip, 67 | created_at: new Date(), 68 | to_files: { 69 | create: input.files.map((file, i) => ({ 70 | id: v4(), 71 | file: { 72 | create: AttachmentFileProvider.collect(file), 73 | }, 74 | sequence: i, 75 | })), 76 | }, 77 | }) satisfies Prisma.bbs_article_snapshotsCreateWithoutArticleInput; 78 | } 79 | -------------------------------------------------------------------------------- /src/providers/bbs/EntityMergeProvider.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | 3 | import { IRecordMerge } from "@samchon/bbs-api/lib/structures/common/IRecordMerge"; 4 | 5 | import { BbsGlobal } from "../../BbsGlobal"; 6 | import { EntityUtil } from "../../utils/EntityUtil"; 7 | import { ErrorProvider } from "./ErrorProvider"; 8 | 9 | export namespace EntityMergeProvider { 10 | export const merge = 11 | ( 12 | table: Prisma.ModelName, 13 | finder?: (input: IRecordMerge) => Promise, 14 | ) => 15 | async (input: IRecordMerge): Promise => { 16 | // VALIDATE TABLE 17 | const primary: Prisma.DMMF.Field | undefined = 18 | Prisma.dmmf.datamodel.models 19 | .find((model) => model.name === table) 20 | ?.fields.find((field) => field.isId === true); 21 | if (primary === undefined) throw ErrorProvider.internal("Invalid table."); 22 | 23 | // FIND MATCHED RECORDS 24 | const count: number = finder 25 | ? await finder(input) 26 | : await (BbsGlobal.prisma[table] as any).count({ 27 | where: { 28 | [primary.name]: { 29 | in: [input.keep, ...input.absorbed], 30 | }, 31 | }, 32 | }); 33 | if (count !== input.absorbed.length + 1) 34 | throw ErrorProvider.notFound({ 35 | accessor: "input.keep | input.absorbed", 36 | message: "Unable to find matched record.", 37 | }); 38 | 39 | // DO MERGE 40 | await EntityUtil.merge(BbsGlobal.prisma)(table)(input); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/providers/bbs/ErrorProvider.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from "@nestjs/common"; 2 | 3 | import { IDiagnosis } from "@samchon/bbs-api/lib/structures/common/IDiagnosis"; 4 | 5 | export namespace ErrorProvider { 6 | const http = 7 | (status: number) => 8 | (reason: string | IDiagnosis | IDiagnosis[]): HttpException => { 9 | const diagnoses: IDiagnosis[] = 10 | typeof reason === "string" 11 | ? [ 12 | { 13 | message: reason, 14 | accessor: "unknown", 15 | }, 16 | ] 17 | : Array.isArray(reason) 18 | ? reason 19 | : [reason]; 20 | return new HttpException(diagnoses, status); 21 | }; 22 | 23 | export const badRequest = http(400); 24 | export const unauthorized = http(401); 25 | export const paymentRequired = http(402); 26 | export const forbidden = http(403); 27 | export const notFound = http(404); 28 | export const conflict = http(409); 29 | export const gone = http(410); 30 | export const unprocessable = http(422); 31 | export const iamTeaPot = http(418); 32 | export const internal = http(500); 33 | } 34 | -------------------------------------------------------------------------------- /src/providers/common/AttachmentFileProvider.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { v4 } from "uuid"; 3 | 4 | import { IAttachmentFile } from "@samchon/bbs-api/lib/structures/common/IAttachmentFile"; 5 | 6 | export namespace AttachmentFileProvider { 7 | export namespace json { 8 | export const transform = ( 9 | input: Prisma.attachment_filesGetPayload>, 10 | ): IAttachmentFile => ({ 11 | id: input.id, 12 | name: input.name, 13 | extension: input.extension, 14 | url: input.url, 15 | created_at: input.created_at.toISOString(), 16 | }); 17 | export const select = () => 18 | ({}) satisfies Prisma.attachment_filesFindManyArgs; 19 | } 20 | 21 | export const collect = (input: IAttachmentFile.ICreate) => 22 | ({ 23 | id: v4(), 24 | name: input.name, 25 | extension: input.extension, 26 | url: input.url, 27 | created_at: new Date(), 28 | }) satisfies Prisma.attachment_filesCreateInput; 29 | } 30 | -------------------------------------------------------------------------------- /src/providers/monitors/SystemProvider.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import git from "git-last-commit"; 3 | import { Singleton, randint } from "tstl"; 4 | 5 | import { ISystem } from "@samchon/bbs-api/lib/structures/monitors/ISystem"; 6 | 7 | import { BbsConfiguration } from "../../BbsConfiguration"; 8 | import { DateUtil } from "../../utils/DateUtil"; 9 | 10 | export class SystemProvider { 11 | public static readonly uid: number = randint(0, Number.MAX_SAFE_INTEGER); 12 | public static readonly created_at: Date = new Date(); 13 | 14 | public static package(): Promise { 15 | return package_.get(); 16 | } 17 | 18 | public static commit(): Promise { 19 | return commit_.get(); 20 | } 21 | } 22 | 23 | const commit_: Singleton> = new Singleton( 24 | () => 25 | new Promise((resolve, reject) => { 26 | git.getLastCommit((err, commit) => { 27 | if (err) reject(err); 28 | else 29 | resolve({ 30 | ...commit, 31 | authored_at: DateUtil.toString( 32 | new Date(Number(commit.authoredOn) * 1000), 33 | true, 34 | ), 35 | committed_at: DateUtil.toString( 36 | new Date(Number(commit.committedOn) * 1000), 37 | true, 38 | ), 39 | }); 40 | }); 41 | }), 42 | ); 43 | const package_: Singleton> = new Singleton( 44 | async () => { 45 | const content: string = await fs.promises.readFile( 46 | `${BbsConfiguration.ROOT}/package.json`, 47 | "utf8", 48 | ); 49 | return JSON.parse(content); 50 | }, 51 | ); 52 | 53 | commit_.get().catch(() => {}); 54 | package_.get().catch(() => {}); 55 | -------------------------------------------------------------------------------- /src/setup/BbsSetupWizard.ts: -------------------------------------------------------------------------------- 1 | import cp from "child_process"; 2 | 3 | import { BbsGlobal } from "../BbsGlobal"; 4 | 5 | export namespace BbsSetupWizard { 6 | export async function schema(): Promise { 7 | if (BbsGlobal.testing === false) 8 | throw new Error( 9 | "Erron on SetupWizard.schema(): unable to reset database in non-test mode.", 10 | ); 11 | const execute = (type: string) => (argv: string) => 12 | cp.execSync( 13 | `npx prisma migrate ${type} --schema=prisma/schema ${argv}`, 14 | { stdio: "inherit" }, 15 | ); 16 | execute("reset")("--force"); 17 | execute("dev")("--name init"); 18 | 19 | await BbsGlobal.prisma.$executeRawUnsafe( 20 | `GRANT SELECT ON ALL TABLES IN SCHEMA ${BbsGlobal.env.BBS_POSTGRES_SCHEMA} TO ${BbsGlobal.env.BBS_POSTGRES_USERNAME_READONLY}`, 21 | ); 22 | } 23 | 24 | export async function seed(): Promise {} 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/BcryptUtil.ts: -------------------------------------------------------------------------------- 1 | import * as bcrypt from "bcryptjs"; 2 | 3 | export namespace BcryptUtil { 4 | export const hash = async (input: string) => { 5 | const salt: string = await bcrypt.genSalt(); 6 | return bcrypt.hash(input, salt); 7 | }; 8 | 9 | export const equals = async (props: { input: string; hashed: string }) => 10 | bcrypt.compare(props.input, props.hashed); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/DateUtil.ts: -------------------------------------------------------------------------------- 1 | export namespace DateUtil { 2 | export const SECOND = 1_000; 3 | export const MINUTE = 60 * SECOND; 4 | export const HOUR = 60 * MINUTE; 5 | export const DAY = 24 * HOUR; 6 | export const WEEK = 7 * DAY; 7 | export const MONTH = 30 * DAY; 8 | 9 | export function toString(date: Date, hms: boolean = false): string { 10 | const ymd: string = [ 11 | date.getFullYear(), 12 | date.getMonth() + 1, 13 | date.getDate(), 14 | ] 15 | .map((value) => _To_cipher_string(value)) 16 | .join("-"); 17 | if (hms === false) return ymd; 18 | 19 | return ( 20 | `${ymd} ` + 21 | [date.getHours(), date.getMinutes(), date.getSeconds()] 22 | .map((value) => _To_cipher_string(value)) 23 | .join(":") 24 | ); 25 | } 26 | 27 | // export function to_uuid(date: Date = new Date()): string { 28 | // const elements: number[] = [ 29 | // date.getFullYear(), 30 | // date.getMonth() + 1, 31 | // date.getDate(), 32 | // date.getHours(), 33 | // date.getMinutes(), 34 | // date.getSeconds(), 35 | // ]; 36 | // return ( 37 | // elements.map((value) => _To_cipher_string(value)).join('') + 38 | // ':' + 39 | // Math.random().toString().substr(2) 40 | // ); 41 | // } 42 | 43 | export interface IDifference { 44 | year: number; 45 | month: number; 46 | date: number; 47 | } 48 | 49 | export function diff(x: Date | string, y: Date | string): IDifference { 50 | x = _To_date(x); 51 | y = _To_date(y); 52 | 53 | // FIRST DIFFERENCES 54 | const ret: IDifference = { 55 | year: x.getFullYear() - y.getFullYear(), 56 | month: x.getMonth() - y.getMonth(), 57 | date: x.getDate() - y.getDate(), 58 | }; 59 | 60 | //---- 61 | // HANDLE NEGATIVE ELEMENTS 62 | //---- 63 | // DATE 64 | if (ret.date < 0) { 65 | const last: number = lastDate(y.getFullYear(), y.getMonth()); 66 | 67 | --ret.month; 68 | ret.date = x.getDate() + (last - y.getDate()); 69 | } 70 | 71 | // MONTH 72 | if (ret.month < 0) { 73 | --ret.year; 74 | ret.month = 12 + ret.month; 75 | } 76 | return ret; 77 | } 78 | 79 | export function lastDate(year: number, month: number): number { 80 | // LEAP MONTH 81 | if (month == 1 && year % 4 == 0 && !(year % 100 == 0 && year % 400 != 0)) 82 | return 29; 83 | else return LAST_DATES[month]; 84 | } 85 | 86 | export function addYears(date: Date, value: number): Date { 87 | date = new Date(date); 88 | date.setFullYear(date.getFullYear() + value); 89 | 90 | return date; 91 | } 92 | 93 | export function addMonths(date: Date, value: number): Date { 94 | date = new Date(date); 95 | 96 | const year: number = 97 | date.getFullYear() + Math.floor((date.getMonth() + value) / 12); 98 | const month: number = (date.getMonth() + value) % 12; 99 | const last: number = lastDate(year, month - 1); 100 | 101 | if (last < date.getDate()) date.setDate(last); 102 | 103 | date.setMonth(value - 1); 104 | return date; 105 | } 106 | 107 | export function addDays(date: Date, value: number): Date { 108 | date = new Date(); 109 | date.setDate(date.getDate() + value); 110 | 111 | return date; 112 | } 113 | 114 | function _To_date(date: string | Date): Date { 115 | if (date instanceof Date) return date; 116 | else return new Date(date); 117 | } 118 | function _To_cipher_string(val: number): string { 119 | if (val < 10) return "0" + val; 120 | else return String(val); 121 | } 122 | const LAST_DATES: number[] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 123 | } 124 | -------------------------------------------------------------------------------- /src/utils/EntityUtil.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { HashMap, hash } from "tstl"; 3 | import { ranges } from "tstl"; 4 | import typia from "typia"; 5 | 6 | /** 7 | * Utility for database entity. 8 | * 9 | * @author Samchon 10 | */ 11 | export namespace EntityUtil { 12 | /** 13 | * Properties of {@link merge} function. 14 | */ 15 | export interface IMergeProps { 16 | /** 17 | * Target record to keep after merging. 18 | * 19 | * After merge process, {@link absorbed} records would be merged into 20 | * this {@link keep} record. 21 | */ 22 | keep: Key; 23 | 24 | /** 25 | * To be merged to {@link keep} after merging. 26 | */ 27 | absorbed: Key[]; 28 | } 29 | 30 | /** 31 | * Merge multiple records into one. 32 | * 33 | * Merge multiple {@link IMergeProps.absorbed} records into 34 | * {@link IMergeProps.keep} record, instead of deleting them. 35 | * 36 | * If there're some dependent tables of the target `table` having 37 | * unique constraint on foreign key column, such dependent tables 38 | * also perform the merge process, too. 39 | * 40 | * Of course, if there're another dependent tables under those 41 | * dependents, they also perform the merge process recursively as well. 42 | * Such recursive merge process still works for self-recursive 43 | * (tree-structured) tables. 44 | * 45 | * @param client Prisma Client 46 | * @param table Target table to perform merge 47 | * @param props Target records to merge 48 | */ 49 | export const merge = 50 | (client: PrismaClient) => 51 | (table: Table) => 52 | async ( 53 | props: IMergeProps, 54 | ): Promise => { 55 | // FIND TARGET MODEL AND PRIMARY KEY 56 | const model: Prisma.DMMF.Model | undefined = 57 | Prisma.dmmf.datamodel.models.find((model) => model.name === table); 58 | if (model === undefined) 59 | throw new Error( 60 | `Error on EntityUtil.unify(): table ${table} does not exist.`, 61 | ); 62 | const key: Prisma.DMMF.Field | undefined = model.fields.find( 63 | (field) => field.isId === true, 64 | ); 65 | if (key === undefined) 66 | throw new Error( 67 | `Error on EntityUtil.unify(): table ${table} does not have single columned primary key.`, 68 | ); 69 | 70 | // LIST UP DEPENDENCIES 71 | const dependencies: Prisma.DMMF.Field[] = model.fields.filter( 72 | (field) => 73 | field.kind === "object" && 74 | typia.is(field.type) && 75 | !field.relationFromFields?.length, 76 | ); 77 | for (const dep of dependencies) { 78 | // GET TARGET TABLE MODEL AND FOREIGN COLUMN 79 | const target: Prisma.DMMF.Model = Prisma.dmmf.datamodel.models.find( 80 | (model) => model.name === dep.type, 81 | )!; 82 | const relation: Prisma.DMMF.Field = target.fields.find( 83 | (field) => field.relationName === dep.relationName, 84 | )!; 85 | if (relation.relationFromFields?.length !== 1) 86 | throw new Error( 87 | `Error on EntityUtil.unify(): table ${getName( 88 | target, 89 | )} has multiple columned foreign key.`, 90 | ); 91 | const foreign: Prisma.DMMF.Field = target.fields.find( 92 | (f) => f.name === relation.relationFromFields![0], 93 | )!; 94 | 95 | // CONSIDER UNIQUE CONSTRAINT -> CASCADE MERGING 96 | const uniqueMatrix: (readonly string[])[] = target.uniqueFields.filter( 97 | (columns) => columns.includes(foreign.name), 98 | ); 99 | if (uniqueMatrix.length) 100 | for (const unique of uniqueMatrix) 101 | await _Merge_unique_children(client)({ 102 | table, 103 | ...props, 104 | })({ 105 | model: target, 106 | unique, 107 | foreign, 108 | }); 109 | else 110 | await (client as any)[getName(target)].updateMany({ 111 | where: { 112 | [foreign.name]: { in: props.absorbed }, 113 | }, 114 | data: { 115 | [foreign.name]: props.keep, 116 | }, 117 | }); 118 | } 119 | 120 | // REMOVE TO BE MERGED RECORD 121 | await (client[table] as any).deleteMany({ 122 | where: { 123 | [key.name]: { in: props.absorbed }, 124 | }, 125 | }); 126 | }; 127 | 128 | const _Merge_unique_children = 129 | (client: PrismaClient) => 130 | (parent: IMergeProps & { table: Prisma.ModelName }) => 131 | async (current: { 132 | model: Prisma.DMMF.Model; 133 | foreign: Prisma.DMMF.Field; 134 | unique: readonly string[]; 135 | }) => { 136 | // GET PRIMARY KEY AND OTHER UNIQUE COLUMNS 137 | const primary: Prisma.DMMF.Field = current.model.fields.find( 138 | (column) => column.isId === true, 139 | )!; 140 | const group: string[] = current.unique.filter( 141 | (column) => column !== current.foreign.name, 142 | ); 143 | 144 | // COMPOSE GROUPS OF OTHER UNIQUE COLUMNS 145 | const dict: HashMap = new HashMap( 146 | (elements) => hash(...elements.map((e) => JSON.stringify(e))), 147 | (x, y) => 148 | ranges.equal(x, y, (a, b) => JSON.stringify(a) === JSON.stringify(b)), 149 | ); 150 | const recordList: any[] = await (client as any)[ 151 | current.model.name 152 | ].findMany({ 153 | where: { 154 | [current.foreign.name]: { 155 | in: [parent.keep, ...parent.absorbed], 156 | }, 157 | }, 158 | orderBy: [ 159 | { 160 | [primary.name]: "asc", 161 | }, 162 | ], 163 | }); 164 | for (const record of recordList) { 165 | const key: any[] = group.map((column) => record[column]); 166 | const array: any[] = dict.take(key, () => []); 167 | array.push(record); 168 | } 169 | 170 | // MERGE THEM 171 | for (const it of dict) { 172 | const index: number = it.second.findIndex( 173 | (rec) => rec[current.foreign.name] === parent.keep, 174 | ); 175 | if (index === -1) continue; 176 | 177 | const master: any = it.second[index]; 178 | const slaves: any[] = it.second.filter((_r, i) => i !== index); 179 | if (slaves.length) 180 | await merge(client)(current.model.name as Prisma.ModelName)({ 181 | keep: master[primary.name], 182 | absorbed: slaves.map((slave) => slave[primary.name]), 183 | }); 184 | } 185 | }; 186 | 187 | const getName = (x: { dbName?: string | null; name: string }): string => 188 | x.dbName ?? x.name; 189 | } 190 | -------------------------------------------------------------------------------- /src/utils/ErrorUtil.ts: -------------------------------------------------------------------------------- 1 | import serializeError = require("serialize-error"); 2 | 3 | export namespace ErrorUtil { 4 | export function toJSON(err: any): object { 5 | return err instanceof Object && err.toJSON instanceof Function 6 | ? err.toJSON() 7 | : serializeError(err); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/JwtTokenManager.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | export namespace JwtTokenManager { 4 | export type Type = "access" | "refresh"; 5 | export interface IPassword { 6 | access: string; 7 | refresh: string; 8 | } 9 | export interface IProps { 10 | table: string; 11 | id: string; 12 | readonly: boolean; 13 | expired_at?: string; 14 | refreshable_until?: string; 15 | } 16 | export type IAsset = Required; 17 | export interface IOutput extends IAsset { 18 | access: string; 19 | refresh: string; 20 | } 21 | 22 | export const generate = 23 | (password: IPassword) => 24 | async (props: IProps): Promise => { 25 | const asset: IAsset = { 26 | table: props.table, 27 | id: props.id, 28 | readonly: props.readonly, 29 | expired_at: 30 | props.expired_at ?? 31 | new Date(Date.now() + EXPIRATIONS.ACCESS).toISOString(), 32 | refreshable_until: 33 | props.refreshable_until ?? 34 | new Date(Date.now() + EXPIRATIONS.REFRESH).toISOString(), 35 | }; 36 | const [access, refresh] = [password.access, password.refresh].map((key) => 37 | jwt.sign(asset, key), 38 | ); 39 | return { 40 | ...asset, 41 | access, 42 | refresh, 43 | }; 44 | }; 45 | 46 | export const verify = 47 | (password: IPassword) => 48 | (type: Type) => 49 | async (token: string): Promise => 50 | jwt.verify(token, password[type]) as IAsset; 51 | 52 | export const EXPIRATIONS = { 53 | ACCESS: 3 * 60 * 60 * 1000, 54 | REFRESH: 2 * 7 * 24 * 60 * 60 * 1000, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/MapUtil.ts: -------------------------------------------------------------------------------- 1 | export namespace MapUtil { 2 | export const take = 3 | (dict: Map) => 4 | (key: Key, generator: () => T): T => { 5 | const oldbie: T | undefined = dict.get(key); 6 | if (oldbie) return oldbie; 7 | 8 | const value: T = generator(); 9 | dict.set(key, value); 10 | return value; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/PaginationUtil.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil } from "@nestia/e2e"; 2 | 3 | import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; 4 | 5 | export namespace PaginationUtil { 6 | export interface Transformer { 7 | (records: Input): Output | Promise; 8 | } 9 | 10 | export interface IProps< 11 | Where extends object, 12 | OrderBy extends object, 13 | Payload extends object, 14 | Raw extends object, 15 | Output extends object, 16 | > { 17 | schema: { 18 | findMany( 19 | input: Payload & { 20 | skip?: number; 21 | take?: number; 22 | where?: Where; 23 | orderBy?: OrderBy | OrderBy[]; 24 | }, 25 | ): Promise; 26 | count(arg: { where: Where }): Promise; 27 | }; 28 | payload: Payload; 29 | transform: Transformer; 30 | } 31 | export namespace IProps { 32 | export type DeduceWhere> = 33 | T extends IProps ? U : never; 34 | export type DeduceOrderBy> = 35 | T extends IProps ? U : never; 36 | export type DeducePayload> = 37 | T extends IProps ? U : never; 38 | export type DeduceRaw> = 39 | T extends IProps ? U : never; 40 | export type DeduceOutput> = 41 | T extends IProps ? U : never; 42 | } 43 | 44 | export const paginate = 45 | >(props: T) => 46 | (spec: { 47 | where: IProps.DeduceWhere; 48 | orderBy: IProps.DeduceOrderBy[]; 49 | }) => 50 | async (input: IPage.IRequest): Promise>> => { 51 | input.limit ??= 100; 52 | 53 | const records: number = await props.schema.count({ 54 | where: spec.where, 55 | }); 56 | const pages: number = 57 | input.limit !== 0 ? Math.ceil(records / input.limit) : 1; 58 | input.page = input.page ? Math.max(1, Math.min(input.page, pages)) : 1; 59 | 60 | const data: IProps.DeduceRaw[] = await props.schema.findMany({ 61 | ...props.payload, 62 | skip: (input.page - 1) * input.limit, 63 | take: input.limit || records, 64 | where: spec.where, 65 | orderBy: spec.orderBy, 66 | }); 67 | return { 68 | data: await ArrayUtil.asyncMap(data)(async (elem) => 69 | props.transform(elem), 70 | ), 71 | pagination: { 72 | records, 73 | pages, 74 | current: input.page, 75 | limit: input.limit, 76 | }, 77 | }; 78 | }; 79 | 80 | export const orderBy = 81 | ( 82 | transform: (column: Column, direction: "asc" | "desc") => Output | null, 83 | ) => 84 | (columns: IPage.Sort): Output[] => 85 | columns 86 | .map((col) => 87 | transform( 88 | col.substring(1) as Column, 89 | col[0] === "+" ? "asc" : "desc", 90 | ), 91 | ) 92 | .filter((elem) => elem !== null) as Output[]; 93 | } 94 | -------------------------------------------------------------------------------- /src/utils/Terminal.ts: -------------------------------------------------------------------------------- 1 | import cp from "child_process"; 2 | import { Pair } from "tstl"; 3 | 4 | export namespace Terminal { 5 | export function execute( 6 | ...commands: string[] 7 | ): Promise> { 8 | return new Promise((resolve, reject) => { 9 | cp.exec( 10 | commands.join(" && "), 11 | (error: Error | null, stdout: string, stderr: string) => { 12 | if (error) reject(error); 13 | else resolve(new Pair(stdout, stderr)); 14 | }, 15 | ); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/TestAutomation.ts: -------------------------------------------------------------------------------- 1 | import { DynamicExecutor } from "@nestia/e2e"; 2 | import chalk from "chalk"; 3 | 4 | import { BbsConfiguration } from "../src/BbsConfiguration"; 5 | import api from "../src/api"; 6 | import { BbsSetupWizard } from "../src/setup/BbsSetupWizard"; 7 | import { ArgumentParser } from "./internal/ArgumentParser"; 8 | import { StopWatch } from "./internal/StopWatch"; 9 | 10 | export namespace TestAutomation { 11 | export interface IProps { 12 | open(options: IOptions): Promise; 13 | close(backend: T): Promise; 14 | } 15 | 16 | export interface IOptions { 17 | reset: boolean; 18 | simultaneous: number; 19 | include?: string[]; 20 | exclude?: string[]; 21 | trace: boolean; 22 | } 23 | 24 | export const execute = async (props: IProps): Promise => { 25 | // CONFIGURE 26 | const options: IOptions = await getOptions(); 27 | if (options.reset) { 28 | await StopWatch.trace("Reset DB")(BbsSetupWizard.schema); 29 | await StopWatch.trace("Seed Data")(BbsSetupWizard.seed); 30 | } 31 | 32 | // DO TEST 33 | const backend: T = await props.open(options); 34 | const connection: api.IConnection = { 35 | host: `http://127.0.0.1:${BbsConfiguration.API_PORT()}`, 36 | }; 37 | const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ 38 | prefix: "test", 39 | location: __dirname + "/features", 40 | parameters: () => [ 41 | { 42 | host: connection.host, 43 | encryption: connection.encryption, 44 | }, 45 | ], 46 | filter: (func) => 47 | (!options.include?.length || 48 | (options.include ?? []).some((str) => func.includes(str))) && 49 | (!options.exclude?.length || 50 | (options.exclude ?? []).every((str) => !func.includes(str))), 51 | onComplete: (exec) => { 52 | const trace = (str: string) => 53 | console.log(` - ${chalk.green(exec.name)}: ${str}`); 54 | if (exec.error === null) { 55 | const elapsed: number = 56 | new Date(exec.completed_at).getTime() - 57 | new Date(exec.started_at).getTime(); 58 | trace(`${chalk.yellow(elapsed.toLocaleString())} ms`); 59 | } else trace(chalk.red(exec.error.name)); 60 | }, 61 | }); 62 | 63 | // TERMINATE 64 | await props.close(backend); 65 | 66 | const exceptions: Error[] = report.executions 67 | .filter((exec) => exec.error !== null) 68 | .map((exec) => exec.error!); 69 | if (exceptions.length === 0) { 70 | console.log("Success"); 71 | console.log("Elapsed time", report.time.toLocaleString(), `ms`); 72 | } else { 73 | if (options.trace !== false) 74 | for (const exp of exceptions) console.log(exp); 75 | console.log("Failed"); 76 | console.log("Elapsed time", report.time.toLocaleString(), `ms`); 77 | process.exit(-1); 78 | } 79 | }; 80 | } 81 | 82 | const getOptions = () => 83 | ArgumentParser.parse( 84 | async (command, prompt, action) => { 85 | command.option("--reset ", "reset local DB or not"); 86 | command.option( 87 | "--simultaneous ", 88 | "number of simultaneous requests to make", 89 | ); 90 | command.option("--include ", "include feature files"); 91 | command.option("--exclude ", "exclude feature files"); 92 | command.option("--trace ", "trace detailed errors"); 93 | 94 | return action(async (options) => { 95 | if (typeof options.reset === "string") 96 | options.reset = options.reset === "true"; 97 | options.reset ??= await prompt.boolean("reset")("Reset local DB"); 98 | options.simultaneous = Number( 99 | options.simultaneous ?? 100 | (await prompt.number("simultaneous")( 101 | "Number of simultaneous requests to make", 102 | )), 103 | ); 104 | options.trace = options.trace !== ("false" as any); 105 | return options as TestAutomation.IOptions; 106 | }); 107 | }, 108 | ); 109 | -------------------------------------------------------------------------------- /test/benchmark/index.ts: -------------------------------------------------------------------------------- 1 | import { DynamicBenchmarker } from "@nestia/benchmark"; 2 | import cliProgress from "cli-progress"; 3 | import fs from "fs"; 4 | import os from "os"; 5 | import { IPointer } from "tstl"; 6 | 7 | import { BbsBackend } from "../../src/BbsBackend"; 8 | import { BbsConfiguration } from "../../src/BbsConfiguration"; 9 | import { BbsGlobal } from "../../src/BbsGlobal"; 10 | import { BbsSetupWizard } from "../../src/setup/BbsSetupWizard"; 11 | import { ArgumentParser } from "../internal/ArgumentParser"; 12 | import { StopWatch } from "../internal/StopWatch"; 13 | 14 | interface IOptions { 15 | reset: boolean; 16 | include?: string[]; 17 | exclude?: string[]; 18 | trace: boolean; 19 | count: number; 20 | threads: number; 21 | simultaneous: number; 22 | } 23 | 24 | const getOptions = () => 25 | ArgumentParser.parse(async (command, prompt, action) => { 26 | command.option("--mode ", "target mode"); 27 | command.option("--reset ", "reset local DB or not"); 28 | command.option("--include ", "include feature files"); 29 | command.option("--exclude ", "exclude feature files"); 30 | command.option("--count ", "number of requests to make"); 31 | command.option("--threads ", "number of threads to use"); 32 | command.option( 33 | "--simultaneous ", 34 | "number of simultaneous requests to make", 35 | ); 36 | return action(async (options) => { 37 | if (typeof options.reset === "string") 38 | options.reset = options.reset === "true"; 39 | options.reset ??= await prompt.boolean("reset")("Reset local DB"); 40 | options.trace = options.trace !== ("false" as any); 41 | options.count = Number( 42 | options.count ?? 43 | (await prompt.number("count")("Number of requests to make")), 44 | ); 45 | options.threads = Number( 46 | options.threads ?? 47 | (await prompt.number("threads")("Number of threads to use")), 48 | ); 49 | options.simultaneous = Number( 50 | options.simultaneous ?? 51 | (await prompt.number("simultaneous")( 52 | "Number of simultaneous requests to make", 53 | )), 54 | ); 55 | return options as IOptions; 56 | }); 57 | }); 58 | 59 | const main = async (): Promise => { 60 | // CONFIGURATIONS 61 | const options: IOptions = await getOptions(); 62 | BbsGlobal.testing = true; 63 | 64 | if (options.reset) { 65 | await StopWatch.trace("Reset DB")(BbsSetupWizard.schema); 66 | await StopWatch.trace("Seed Data")(BbsSetupWizard.seed); 67 | } 68 | 69 | // BACKEND SERVER 70 | const backend: BbsBackend = new BbsBackend(); 71 | await backend.open(); 72 | 73 | // DO BENCHMARK 74 | const prev: IPointer = { value: 0 }; 75 | const bar: cliProgress.SingleBar = new cliProgress.SingleBar( 76 | {}, 77 | cliProgress.Presets.shades_classic, 78 | ); 79 | bar.start(options.count, 0); 80 | 81 | const report: DynamicBenchmarker.IReport = await DynamicBenchmarker.master({ 82 | servant: `${__dirname}/servant.js`, 83 | count: options.count, 84 | threads: options.threads, 85 | simultaneous: options.simultaneous, 86 | filter: (func) => 87 | (!options.include?.length || 88 | (options.include ?? []).some((str) => func.includes(str))) && 89 | (!options.exclude?.length || 90 | (options.exclude ?? []).every((str) => !func.includes(str))), 91 | progress: (value: number) => { 92 | if (value >= 100 + prev.value) { 93 | bar.update(value); 94 | prev.value = value; 95 | } 96 | }, 97 | stdio: "ignore", 98 | }); 99 | bar.stop(); 100 | 101 | // DOCUMENTATION 102 | try { 103 | await fs.promises.mkdir(`${BbsConfiguration.ROOT}/docs/benchmarks`, { 104 | recursive: true, 105 | }); 106 | } catch {} 107 | await fs.promises.writeFile( 108 | `${BbsConfiguration.ROOT}/docs/benchmarks/${os 109 | .cpus()[0] 110 | .model.trim() 111 | .split("\\") 112 | .join("") 113 | .split("/") 114 | .join("")}.md`, 115 | DynamicBenchmarker.markdown(report), 116 | "utf8", 117 | ); 118 | 119 | // CLOSE 120 | await backend.close(); 121 | }; 122 | main().catch((exp) => { 123 | console.error(exp); 124 | process.exit(-1); 125 | }); 126 | -------------------------------------------------------------------------------- /test/benchmark/servant.ts: -------------------------------------------------------------------------------- 1 | import { DynamicBenchmarker } from "@nestia/benchmark"; 2 | 3 | import { BbsConfiguration } from "../../src/BbsConfiguration"; 4 | 5 | DynamicBenchmarker.servant({ 6 | connection: { 7 | host: `http://127.0.0.1:${BbsConfiguration.API_PORT()}`, 8 | }, 9 | location: `${__dirname}/../features`, 10 | parameters: (connection) => [connection], 11 | prefix: "test_api_", 12 | }).catch((exp) => { 13 | console.error(exp); 14 | process.exit(-1); 15 | }); 16 | -------------------------------------------------------------------------------- /test/features/api/bbs/internal/generate_random_article.ts: -------------------------------------------------------------------------------- 1 | import { RandomGenerator } from "@nestia/e2e"; 2 | import typia from "typia"; 3 | 4 | import BbsApi from "@samchon/bbs-api/lib/index"; 5 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 6 | 7 | import { prepare_random_article } from "./prepare_random_article"; 8 | 9 | export const generate_random_article = async ( 10 | connection: BbsApi.IConnection, 11 | password?: string, 12 | ): Promise => { 13 | const article: IBbsArticle = await BbsApi.functional.bbs.articles.create( 14 | connection, 15 | { 16 | writer: RandomGenerator.name(), 17 | ...prepare_random_article(password ?? RandomGenerator.alphaNumeric(8)), 18 | }, 19 | ); 20 | return typia.assertEquals(article); 21 | }; 22 | -------------------------------------------------------------------------------- /test/features/api/bbs/internal/generate_random_comment.ts: -------------------------------------------------------------------------------- 1 | import { RandomGenerator } from "@nestia/e2e"; 2 | import typia from "typia"; 3 | 4 | import BbsApi from "@samchon/bbs-api/lib/index"; 5 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 6 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 7 | 8 | import { prepare_random_comment } from "./prepare_random_comment"; 9 | 10 | export const generate_random_comment = async ( 11 | connection: BbsApi.IConnection, 12 | article: IBbsArticle, 13 | password?: string, 14 | ): Promise => { 15 | const comment: IBbsArticleComment = 16 | await BbsApi.functional.bbs.articles.comments.create( 17 | connection, 18 | article.id, 19 | { 20 | writer: RandomGenerator.name(), 21 | ...prepare_random_comment(password ?? RandomGenerator.alphaNumeric(8)), 22 | }, 23 | ); 24 | return typia.assertEquals(comment); 25 | }; 26 | -------------------------------------------------------------------------------- /test/features/api/bbs/internal/prepare_random_article.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, RandomGenerator } from "@nestia/e2e"; 2 | import { randint } from "tstl"; 3 | 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | 6 | import { prepare_random_file } from "./prepare_random_file"; 7 | 8 | export const prepare_random_article = ( 9 | password: string, 10 | ): IBbsArticle.IUpdate => ({ 11 | password, 12 | title: RandomGenerator.paragraph()(), 13 | body: RandomGenerator.content()()(), 14 | format: "txt", 15 | files: ArrayUtil.repeat(randint(0, 3))(() => prepare_random_file()), 16 | }); 17 | -------------------------------------------------------------------------------- /test/features/api/bbs/internal/prepare_random_comment.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, RandomGenerator } from "@nestia/e2e"; 2 | import { randint } from "tstl"; 3 | 4 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 5 | 6 | import { prepare_random_file } from "./prepare_random_file"; 7 | 8 | export const prepare_random_comment = ( 9 | password: string, 10 | ): IBbsArticleComment.IUpdate => ({ 11 | password, 12 | body: RandomGenerator.content()()(), 13 | format: "txt", 14 | files: ArrayUtil.repeat(randint(0, 3))(() => prepare_random_file()), 15 | }); 16 | -------------------------------------------------------------------------------- /test/features/api/bbs/internal/prepare_random_file.ts: -------------------------------------------------------------------------------- 1 | import { RandomGenerator } from "@nestia/e2e"; 2 | import { randint } from "tstl"; 3 | 4 | import { IAttachmentFile } from "@samchon/bbs-api/lib/structures/common/IAttachmentFile"; 5 | 6 | export function prepare_random_file( 7 | extension?: string, 8 | ): IAttachmentFile.ICreate { 9 | const name: string = RandomGenerator.alphabets(randint(5, 16)); 10 | if (extension === undefined) extension = RandomGenerator.alphabets(3); 11 | 12 | const url: string = `https://picsum.photos/200/300?random`; 13 | 14 | return { 15 | name, 16 | extension, 17 | url, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_comment_create.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, RandomGenerator, TestValidator } from "@nestia/e2e"; 2 | import { randint } from "tstl"; 3 | 4 | import BbsApi from "@samchon/bbs-api/lib/index"; 5 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 6 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 7 | 8 | import { generate_random_article } from "./internal/generate_random_article"; 9 | import { prepare_random_file } from "./internal/prepare_random_file"; 10 | 11 | export const test_api_bbs_article_comment_create = async ( 12 | connection: BbsApi.IConnection, 13 | ): Promise => { 14 | const article: IBbsArticle = await generate_random_article(connection); 15 | 16 | const input: IBbsArticleComment.ICreate = { 17 | writer: RandomGenerator.name(), 18 | password: RandomGenerator.alphaNumeric(8), 19 | body: RandomGenerator.content()()(), 20 | format: "md", 21 | files: ArrayUtil.repeat(randint(0, 3))(() => prepare_random_file()), 22 | }; 23 | const comment: IBbsArticleComment = 24 | await BbsApi.functional.bbs.articles.comments.create( 25 | connection, 26 | article.id, 27 | input, 28 | ); 29 | 30 | TestValidator.equals("create")({ 31 | snapshots: [ 32 | { 33 | format: input.format, 34 | body: input.body, 35 | files: input.files, 36 | }, 37 | ], 38 | writer: input.writer, 39 | })(comment); 40 | 41 | const read: IBbsArticleComment = 42 | await BbsApi.functional.bbs.articles.comments.at( 43 | connection, 44 | article.id, 45 | comment.id, 46 | ); 47 | TestValidator.equals("read")(read)(comment); 48 | }; 49 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_comment_erase.ts: -------------------------------------------------------------------------------- 1 | import { RandomGenerator, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 6 | 7 | import { generate_random_article } from "./internal/generate_random_article"; 8 | import { generate_random_comment } from "./internal/generate_random_comment"; 9 | 10 | export const test_api_bbs_article_comment_erase = async ( 11 | connection: BbsApi.IConnection, 12 | ): Promise => { 13 | const article: IBbsArticle = await generate_random_article(connection); 14 | const password: string = RandomGenerator.alphaNumeric(8); 15 | const comment: IBbsArticleComment = await generate_random_comment( 16 | connection, 17 | article, 18 | password, 19 | ); 20 | await BbsApi.functional.bbs.articles.comments.erase( 21 | connection, 22 | article.id, 23 | comment.id, 24 | { password }, 25 | ); 26 | await TestValidator.httpError("erase")(404)(() => 27 | BbsApi.functional.bbs.articles.comments.at( 28 | connection, 29 | article.id, 30 | comment.id, 31 | ), 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_comment_index_search.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 6 | import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; 7 | 8 | import { generate_random_article } from "./internal/generate_random_article"; 9 | import { generate_random_comment } from "./internal/generate_random_comment"; 10 | 11 | export const test_api_bbs_article_comment_index_search = async ( 12 | connection: BbsApi.IConnection, 13 | ): Promise => { 14 | const article: IBbsArticle = await generate_random_article(connection); 15 | await ArrayUtil.asyncRepeat(REPEAT)(() => 16 | generate_random_comment(connection, article), 17 | ); 18 | 19 | const expected: IPage = 20 | await BbsApi.functional.bbs.articles.comments.index( 21 | connection, 22 | article.id, 23 | { 24 | limit: REPEAT, 25 | }, 26 | ); 27 | 28 | const validator = TestValidator.search("search")( 29 | async (search: IBbsArticle.IRequest.ISearch) => { 30 | const page: IPage = 31 | await BbsApi.functional.bbs.articles.comments.index( 32 | connection, 33 | article.id, 34 | { 35 | search, 36 | limit: REPEAT, 37 | }, 38 | ); 39 | return page.data; 40 | }, 41 | )(expected.data, 2); 42 | 43 | await validator({ 44 | fields: ["writer"], 45 | values: (arc) => [arc.writer], 46 | request: ([writer]) => ({ writer }), 47 | filter: (arc, [name]) => arc.writer.includes(name), 48 | }); 49 | await validator({ 50 | fields: ["body"], 51 | values: (arc) => [arc.snapshots.at(-1)!.body], 52 | request: ([body]) => ({ body }), 53 | filter: (arc, [title]) => arc.snapshots.some((s) => s.body.includes(title)), 54 | }); 55 | }; 56 | 57 | const REPEAT = 25; 58 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_comment_index_sort.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, GaffComparator, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 6 | import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; 7 | 8 | import { generate_random_article } from "./internal/generate_random_article"; 9 | import { generate_random_comment } from "./internal/generate_random_comment"; 10 | 11 | export const test_api_bbs_article_comment_index_sort = async ( 12 | connection: BbsApi.IConnection, 13 | ): Promise => { 14 | const article: IBbsArticle = await generate_random_article(connection); 15 | 16 | await ArrayUtil.asyncRepeat(REPEAT)(() => 17 | generate_random_comment(connection, article), 18 | ); 19 | 20 | const validator = TestValidator.sort("questions.index")< 21 | IBbsArticleComment, 22 | IBbsArticleComment.IRequest.SortableColumns, 23 | IPage.Sort 24 | >(async (input: IPage.Sort) => { 25 | const page: IPage = 26 | await BbsApi.functional.bbs.articles.comments.index( 27 | connection, 28 | article.id, 29 | { 30 | limit: REPEAT, 31 | sort: input, 32 | }, 33 | ); 34 | return page.data; 35 | }); 36 | 37 | const components = [ 38 | validator("created_at")(GaffComparator.dates((x) => x.created_at)), 39 | validator("writer")(GaffComparator.strings((x) => x.writer)), 40 | ]; 41 | for (const comp of components) { 42 | await comp("+"); 43 | await comp("-"); 44 | } 45 | }; 46 | 47 | const REPEAT = 25; 48 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_comment_password.ts: -------------------------------------------------------------------------------- 1 | import { RandomGenerator, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 6 | 7 | import { generate_random_article } from "./internal/generate_random_article"; 8 | import { generate_random_comment } from "./internal/generate_random_comment"; 9 | import { prepare_random_comment } from "./internal/prepare_random_comment"; 10 | 11 | export const test_api_bbs_article_comment_password = async ( 12 | connection: BbsApi.IConnection, 13 | ): Promise => { 14 | const article: IBbsArticle = await generate_random_article(connection); 15 | 16 | const password: string = RandomGenerator.alphaNumeric(8); 17 | const comment: IBbsArticleComment = await generate_random_comment( 18 | connection, 19 | article, 20 | password, 21 | ); 22 | 23 | await TestValidator.httpError("update")(403)(() => 24 | BbsApi.functional.bbs.articles.comments.update( 25 | connection, 26 | article.id, 27 | comment.id, 28 | prepare_random_comment("invalid-password"), 29 | ), 30 | ); 31 | 32 | await TestValidator.httpError("erase")(403)(() => 33 | BbsApi.functional.bbs.articles.comments.erase( 34 | connection, 35 | article.id, 36 | comment.id, 37 | { 38 | password: "invalid-password", 39 | }, 40 | ), 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_comment_update.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, RandomGenerator, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | import { IBbsArticleComment } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticleComment"; 6 | 7 | import { generate_random_article } from "./internal/generate_random_article"; 8 | import { generate_random_comment } from "./internal/generate_random_comment"; 9 | import { prepare_random_comment } from "./internal/prepare_random_comment"; 10 | 11 | export const test_api_bbs_article_comment_update = async ( 12 | connection: BbsApi.IConnection, 13 | ): Promise => { 14 | const article: IBbsArticle = await generate_random_article(connection); 15 | 16 | const password: string = RandomGenerator.alphaNumeric(8); 17 | const comment: IBbsArticleComment = await generate_random_comment( 18 | connection, 19 | article, 20 | password, 21 | ); 22 | 23 | const inputs: IBbsArticleComment.IUpdate[] = ArrayUtil.repeat(4)(() => 24 | prepare_random_comment(password), 25 | ); 26 | for (const i of inputs) { 27 | const snapshot: IBbsArticleComment.ISnapshot = 28 | await BbsApi.functional.bbs.articles.comments.update( 29 | connection, 30 | article.id, 31 | comment.id, 32 | i, 33 | ); 34 | comment.snapshots.push(snapshot); 35 | TestValidator.equals("snapshot")({ 36 | format: i.format, 37 | body: i.body, 38 | files: i.files, 39 | })(snapshot); 40 | } 41 | 42 | const read: IBbsArticleComment = 43 | await BbsApi.functional.bbs.articles.comments.at( 44 | connection, 45 | article.id, 46 | comment.id, 47 | ); 48 | TestValidator.equals("read")(read)(comment); 49 | }; 50 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_create.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, RandomGenerator, TestValidator } from "@nestia/e2e"; 2 | import { randint } from "tstl"; 3 | 4 | import BbsApi from "@samchon/bbs-api/lib/index"; 5 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 6 | 7 | import { prepare_random_file } from "./internal/prepare_random_file"; 8 | 9 | export const test_api_bbs_article_create = async ( 10 | connection: BbsApi.IConnection, 11 | ): Promise => { 12 | // PREPARE INPUT DATA 13 | const input: IBbsArticle.ICreate = { 14 | writer: RandomGenerator.name(), 15 | password: RandomGenerator.alphaNumeric(8), 16 | title: RandomGenerator.paragraph()(), 17 | body: RandomGenerator.content()()(), 18 | format: "md", 19 | files: ArrayUtil.repeat(randint(0, 3))(() => prepare_random_file()), 20 | }; 21 | 22 | // DO CREATE 23 | const article: IBbsArticle = await BbsApi.functional.bbs.articles.create( 24 | connection, 25 | input, 26 | ); 27 | 28 | // VALIDATE WHETHER EXACT DATA IS INSERTED 29 | TestValidator.equals("create")({ 30 | snapshots: [ 31 | { 32 | format: input.format, 33 | title: input.title, 34 | body: input.body, 35 | files: input.files, 36 | }, 37 | ], 38 | writer: input.writer, 39 | })(article); 40 | 41 | // COMPARE WITH READ DATA 42 | const read: IBbsArticle = await BbsApi.functional.bbs.articles.at( 43 | connection, 44 | article.id, 45 | ); 46 | TestValidator.equals("read")(read)(article); 47 | }; 48 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_erase.ts: -------------------------------------------------------------------------------- 1 | import { RandomGenerator, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | 6 | import { generate_random_article } from "./internal/generate_random_article"; 7 | 8 | export const test_api_bbs_article_erase = async ( 9 | connection: BbsApi.IConnection, 10 | ): Promise => { 11 | const password: string = RandomGenerator.alphaNumeric(8); 12 | const article: IBbsArticle = await generate_random_article( 13 | connection, 14 | password, 15 | ); 16 | await BbsApi.functional.bbs.articles.erase(connection, article.id, { 17 | password, 18 | }); 19 | await TestValidator.httpError("erase")(404)(() => 20 | BbsApi.functional.bbs.articles.at(connection, article.id), 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_index_search.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, RandomGenerator, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; 6 | 7 | import { generate_random_article } from "./internal/generate_random_article"; 8 | 9 | export const test_api_bbs_article_index_search = async ( 10 | connection: BbsApi.IConnection, 11 | ): Promise => { 12 | await ArrayUtil.asyncRepeat(REPEAT)(() => 13 | generate_random_article(connection), 14 | ); 15 | 16 | const expected: IPage = 17 | await BbsApi.functional.bbs.articles.abridges(connection, { 18 | limit: REPEAT, 19 | }); 20 | 21 | const validator = TestValidator.search("search")( 22 | async (search: IBbsArticle.IRequest.ISearch) => { 23 | const page: IPage = 24 | await BbsApi.functional.bbs.articles.abridges(connection, { 25 | search, 26 | limit: REPEAT, 27 | }); 28 | return page.data; 29 | }, 30 | )(expected.data, 2); 31 | 32 | await validator({ 33 | fields: ["writer"], 34 | values: (arc) => [arc.writer], 35 | request: ([writer]) => ({ writer }), 36 | filter: (arc, [name]) => arc.writer.includes(name), 37 | }); 38 | await validator({ 39 | fields: ["title"], 40 | values: (arc) => [arc.title], 41 | request: ([title]) => ({ title }), 42 | filter: (arc, [title]) => arc.title.includes(title), 43 | }); 44 | await validator({ 45 | fields: ["title_or_body"], 46 | values: (arc) => [RandomGenerator.pick([arc.title, arc.body])], 47 | request: ([title_or_body]) => ({ title_or_body }), 48 | filter: (arc, [value]) => 49 | arc.title.includes(value) || arc.body.includes(value), 50 | }); 51 | }; 52 | 53 | const REPEAT = 25; 54 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_index_sort.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, GaffComparator, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | import { IPage } from "@samchon/bbs-api/lib/structures/common/IPage"; 6 | 7 | import { generate_random_article } from "./internal/generate_random_article"; 8 | 9 | export const test_api_bbs_article_index_sort = async ( 10 | connection: BbsApi.IConnection, 11 | ): Promise => { 12 | await ArrayUtil.asyncRepeat(REPEAT)(() => 13 | generate_random_article(connection), 14 | ); 15 | 16 | const validator = TestValidator.sort("questions.index")< 17 | IBbsArticle.ISummary, 18 | IBbsArticle.IRequest.SortableColumns, 19 | IPage.Sort 20 | >(async (input: IPage.Sort) => { 21 | const page: IPage = 22 | await BbsApi.functional.bbs.articles.index(connection, { 23 | limit: REPEAT, 24 | sort: input, 25 | }); 26 | return page.data; 27 | }); 28 | 29 | const components = [ 30 | validator("created_at")(GaffComparator.dates((x) => x.created_at)), 31 | validator("updated_at")(GaffComparator.dates((x) => x.updated_at)), 32 | validator("title")(GaffComparator.strings((x) => x.title)), 33 | validator("writer")(GaffComparator.strings((x) => x.writer)), 34 | ]; 35 | for (const comp of components) { 36 | await comp("+"); 37 | await comp("-"); 38 | } 39 | }; 40 | 41 | const REPEAT = 25; 42 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_password.ts: -------------------------------------------------------------------------------- 1 | import { RandomGenerator, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | 6 | import { generate_random_article } from "./internal/generate_random_article"; 7 | import { prepare_random_article } from "./internal/prepare_random_article"; 8 | 9 | export const test_api_bbs_article_password = async ( 10 | connection: BbsApi.IConnection, 11 | ): Promise => { 12 | const password: string = RandomGenerator.alphaNumeric(8); 13 | const article: IBbsArticle = await generate_random_article( 14 | connection, 15 | password, 16 | ); 17 | 18 | await TestValidator.httpError("update")(403)(() => 19 | BbsApi.functional.bbs.articles.update( 20 | connection, 21 | article.id, 22 | prepare_random_article("invalid-password"), 23 | ), 24 | ); 25 | 26 | await TestValidator.httpError("erase")(403)(() => 27 | BbsApi.functional.bbs.articles.erase(connection, article.id, { 28 | password: "invalid-password", 29 | }), 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /test/features/api/bbs/test_api_bbs_article_update.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil, RandomGenerator, TestValidator } from "@nestia/e2e"; 2 | 3 | import BbsApi from "@samchon/bbs-api/lib/index"; 4 | import { IBbsArticle } from "@samchon/bbs-api/lib/structures/bbs/IBbsArticle"; 5 | 6 | import { generate_random_article } from "./internal/generate_random_article"; 7 | import { prepare_random_article } from "./internal/prepare_random_article"; 8 | 9 | export const test_api_bbs_article_update = async ( 10 | connection: BbsApi.IConnection, 11 | ): Promise => { 12 | const password: string = RandomGenerator.alphaNumeric(8); 13 | const article: IBbsArticle = await generate_random_article( 14 | connection, 15 | password, 16 | ); 17 | 18 | const inputs: IBbsArticle.IUpdate[] = ArrayUtil.repeat(4)(() => 19 | prepare_random_article(password), 20 | ); 21 | for (const i of inputs) { 22 | const snapshot: IBbsArticle.ISnapshot = 23 | await BbsApi.functional.bbs.articles.update(connection, article.id, i); 24 | article.snapshots.push(snapshot); 25 | TestValidator.equals("snapshot")({ 26 | format: i.format, 27 | title: i.title, 28 | body: i.body, 29 | files: i.files, 30 | })(snapshot); 31 | } 32 | 33 | const read: IBbsArticle = await BbsApi.functional.bbs.articles.at( 34 | connection, 35 | article.id, 36 | ); 37 | TestValidator.equals("read")(read)(article); 38 | }; 39 | -------------------------------------------------------------------------------- /test/features/api/monitors/test_api_monitor_health_check.ts: -------------------------------------------------------------------------------- 1 | import BbsApi from "@samchon/bbs-api"; 2 | 3 | export async function test_api_monitor_health_check( 4 | connection: BbsApi.IConnection, 5 | ): Promise { 6 | await BbsApi.functional.monitors.health.get(connection); 7 | } 8 | -------------------------------------------------------------------------------- /test/features/api/monitors/test_api_monitor_system.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "typia"; 2 | 3 | import BbsApi from "@samchon/bbs-api"; 4 | import { ISystem } from "@samchon/bbs-api/lib/structures/monitors/ISystem"; 5 | 6 | export async function test_api_monitor_system( 7 | connection: BbsApi.IConnection, 8 | ): Promise { 9 | const system: ISystem = 10 | await BbsApi.functional.monitors.system.get(connection); 11 | assert(system); 12 | } 13 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { BbsBackend } from "../src/BbsBackend"; 2 | import { BbsGlobal } from "../src/BbsGlobal"; 3 | import { TestAutomation } from "./TestAutomation"; 4 | 5 | const main = async (): Promise => { 6 | BbsGlobal.testing = true; 7 | await TestAutomation.execute({ 8 | open: async () => { 9 | const backend: BbsBackend = new BbsBackend(); 10 | await backend.open(); 11 | return backend; 12 | }, 13 | close: (backend) => backend.close(), 14 | }); 15 | }; 16 | main().catch((exp) => { 17 | console.log(exp); 18 | process.exit(-1); 19 | }); 20 | -------------------------------------------------------------------------------- /test/internal/ArgumentParser.ts: -------------------------------------------------------------------------------- 1 | import commander from "commander"; 2 | import * as inquirer from "inquirer"; 3 | 4 | export namespace ArgumentParser { 5 | export type Inquiry = ( 6 | command: commander.Command, 7 | prompt: (opt?: inquirer.StreamOptions) => inquirer.PromptModule, 8 | action: (closure: (options: Partial) => Promise) => Promise, 9 | ) => Promise; 10 | 11 | export interface Prompt { 12 | select: ( 13 | name: string, 14 | ) => ( 15 | message: string, 16 | ) => (choices: Choice[]) => Promise; 17 | boolean: (name: string) => (message: string) => Promise; 18 | number: (name: string) => (message: string) => Promise; 19 | } 20 | 21 | export const parse = async ( 22 | inquiry: ( 23 | command: commander.Command, 24 | prompt: Prompt, 25 | action: (closure: (options: Partial) => Promise) => Promise, 26 | ) => Promise, 27 | ): Promise => { 28 | // TAKE OPTIONS 29 | const action = (closure: (options: Partial) => Promise) => 30 | new Promise((resolve, reject) => { 31 | commander.program.action(async (options) => { 32 | try { 33 | resolve(await closure(options)); 34 | } catch (exp) { 35 | reject(exp); 36 | } 37 | }); 38 | commander.program.parseAsync().catch(reject); 39 | }); 40 | 41 | const select = 42 | (name: string) => 43 | (message: string) => 44 | async (choices: Choice[]): Promise => 45 | ( 46 | await inquirer.createPromptModule()({ 47 | type: "list", 48 | name, 49 | message, 50 | choices, 51 | }) 52 | )[name]; 53 | const boolean = (name: string) => async (message: string) => 54 | ( 55 | await inquirer.createPromptModule()({ 56 | type: "confirm", 57 | name, 58 | message, 59 | }) 60 | )[name] as boolean; 61 | const number = (name: string) => async (message: string) => 62 | Number( 63 | ( 64 | await inquirer.createPromptModule()({ 65 | type: "number", 66 | name, 67 | message, 68 | }) 69 | )[name], 70 | ); 71 | 72 | const output: T | Error = await (async () => { 73 | try { 74 | return await inquiry( 75 | commander.program, 76 | { select, boolean, number }, 77 | action, 78 | ); 79 | } catch (error) { 80 | return error as Error; 81 | } 82 | })(); 83 | 84 | // RETURNS 85 | if (output instanceof Error) throw output; 86 | return output; 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /test/internal/StopWatch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Elapsed time measurement utility. 3 | * 4 | * @author Sachon 5 | */ 6 | export namespace StopWatch { 7 | /** 8 | * Type of task. 9 | */ 10 | export type Task = () => Promise; 11 | 12 | /** 13 | * 14 | * @param task 15 | * @returns 16 | */ 17 | export const measure = async (task: Task): Promise<[T, number]> => { 18 | const time: number = Date.now(); 19 | const output: T = await task(); 20 | return [output, Date.now() - time]; 21 | }; 22 | 23 | /** 24 | * 25 | * @param title 26 | * @param task 27 | * @returns 28 | */ 29 | export const trace = 30 | (title: string) => 31 | async (task: Task): Promise<[T, number]> => { 32 | process.stdout.write(` - ${title}: `); 33 | const res: [T, number] = await measure(task); 34 | 35 | console.log(`${res[1].toLocaleString()} ms`); 36 | return res; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /test/manual/password.ts: -------------------------------------------------------------------------------- 1 | import { RandomGenerator } from "@nestia/e2e"; 2 | 3 | console.log(RandomGenerator.alphaNumeric(32)); 4 | console.log(RandomGenerator.alphaNumeric(16)); 5 | console.log(RandomGenerator.alphaNumeric(16)); 6 | -------------------------------------------------------------------------------- /test/manual/update.ts: -------------------------------------------------------------------------------- 1 | import { ArrayUtil } from "@nestia/e2e"; 2 | import { sleep_for } from "tstl"; 3 | 4 | import api from "@samchon/bbs-api"; 5 | 6 | import { BbsConfiguration } from "../../src/BbsConfiguration"; 7 | import { Terminal } from "../../src/utils/Terminal"; 8 | 9 | async function main(): Promise { 10 | //---- 11 | // PREPARATIONS 12 | //---- 13 | // START UPDATOR SERVER 14 | await Terminal.execute("npm run start:updator:master"); 15 | await sleep_for(1000); 16 | 17 | // START BACKEND SERVER 18 | await Terminal.execute("npm run start local xxxx yyyy zzzz"); 19 | await sleep_for(4000); 20 | 21 | // API LIBRARY 22 | const connection: api.IConnection = { 23 | host: `http://127.0.0.1:${BbsConfiguration.API_PORT()}`, 24 | }; 25 | 26 | sleep_for(1000) 27 | .then(async () => { 28 | console.log("Start updating"); 29 | await Terminal.execute("npm run update"); 30 | console.log("The update has been completed"); 31 | }) 32 | .catch(() => {}); 33 | 34 | try { 35 | await Promise.all( 36 | ArrayUtil.repeat(600)(async (i) => { 37 | await sleep_for(i * 10); 38 | await api.functional.monitors.system.get(connection); 39 | }), 40 | ); 41 | } catch (exp) { 42 | throw exp; 43 | } 44 | await Terminal.execute("npm run stop"); 45 | await Terminal.execute("npm run stop:updator:master"); 46 | } 47 | main().catch((exp) => { 48 | console.log(exp); 49 | process.exit(-1); 50 | }); 51 | -------------------------------------------------------------------------------- /test/manual/uuid.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | 3 | console.log(v4()); 4 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../bin", 5 | }, 6 | "include": [".", "../src"] 7 | } -------------------------------------------------------------------------------- /test/webpack.ts: -------------------------------------------------------------------------------- 1 | import cp from "child_process"; 2 | import { sleep_for } from "tstl"; 3 | 4 | import { BbsConfiguration } from "../src/BbsConfiguration"; 5 | import { BbsGlobal } from "../src/BbsGlobal"; 6 | import api from "../src/api"; 7 | import { TestAutomation } from "./TestAutomation"; 8 | 9 | const wait = async (): Promise => { 10 | const connection: api.IConnection = { 11 | host: `http://localhost:${BbsConfiguration.API_PORT()}`, 12 | }; 13 | while (true) 14 | try { 15 | await api.functional.monitors.health.get(connection); 16 | return; 17 | } catch { 18 | await sleep_for(100); 19 | } 20 | }; 21 | 22 | const main = async (): Promise => { 23 | BbsGlobal.testing = true; 24 | await TestAutomation.execute({ 25 | open: async () => { 26 | const backend: cp.ChildProcess = cp.fork( 27 | `${BbsConfiguration.ROOT}/dist/server.js`, 28 | { 29 | cwd: `${BbsConfiguration.ROOT}/dist`, 30 | }, 31 | ); 32 | await wait(); 33 | return backend; 34 | }, 35 | close: async (backend) => { 36 | backend.kill(); 37 | }, 38 | }); 39 | }; 40 | main().catch((exp) => { 41 | console.log(exp); 42 | process.exit(-1); 43 | }); 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 7 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./lib", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | /* Additional Checks */ 35 | "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 40 | /* Module Resolution Options */ 41 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 43 | "paths": { 44 | "@samchon/bbs-models/lib/*": ["./src/models/*"], 45 | "@samchon/bbs-api/lib/*": ["./src/api/*"], 46 | "@samchon/bbs-api/lib/": ["./src/api"], 47 | "@samchon/bbs-api": ["./src/api"], 48 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | /* Experimental Options */ 62 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | /* Advanced Options */ 65 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 66 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 67 | "plugins": [ 68 | { "transform": "typescript-transform-paths" }, 69 | { "transform": "typia/lib/transform" }, 70 | { 71 | "transform": "@nestia/core/lib/transform", 72 | /** 73 | * Validate request body. 74 | * 75 | * - "assert": Use typia.assert() function 76 | * - "is": Use typia.is() function 77 | * - "validate": Use typia.validate() function 78 | * - "assertEquals": Use typia.assertEquals() function 79 | * - "equals": Use typia.equals() function 80 | * - "validateEquals": Use typia.validateEquals() function 81 | */ 82 | "validate": "validate", 83 | /** 84 | * Validate JSON typed response body. 85 | * 86 | * - "assert": Use typia.assertStringify() function 87 | * - "is": Use typia.isStringify() function 88 | * - "validate": Use typia.validateStringify() function 89 | * - "validate.log": typia.validateStringify(), but do not throw and just log it 90 | * - "stringify": Use typia.stringify() function, but dangerous 91 | * - null: Just use JSON.stringify() function, without boosting 92 | */ 93 | "stringify": "assert", 94 | }, 95 | ], 96 | }, 97 | "include": [ 98 | "src" 99 | ] 100 | } -------------------------------------------------------------------------------- /typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | locale = 'en-us' 3 | extend-ignore-re = [ 4 | "(?Rm)^.*(|\n)?$", 5 | "(?s)(|\n).*?(