├── .all-contributorsrc ├── .commit-template ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── boring-cyborg.yml ├── checkgroup.yml └── workflows │ ├── achievements-unit-tests.yml │ ├── add-pr-deploy-badge.yml │ ├── api-tests.yml │ ├── build.yml │ ├── client-unit-tests.yml │ ├── e2e-tests.yml │ ├── lint-achievements.yml │ ├── lint-server.yml │ ├── release.yml │ └── server-unit-tests.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── prepare-commit-msg ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── achievements ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── package-lock.json ├── package.json ├── src │ ├── __snapshots__ │ │ ├── cutting-edges.achievement.spec.ts.snap │ │ ├── dont-yell-at-me.achievement.spec.ts.snap │ │ ├── double-review.achievement.spec.ts.snap │ │ ├── dr-claw.achievement.spec.ts.snap │ │ ├── helping-hand.achievement.spec.ts.snap │ │ ├── inspector-gadget.achievement.spec.ts.snap │ │ ├── label-baby-junior.achievement.spec.ts.snap │ │ ├── meeseek.achievement.spec.ts.snap │ │ ├── member.achievement.spec.ts.snap │ │ ├── mr-miyagi.achievement.spec.ts.snap │ │ ├── never-go-full-retard.achievement.spec.ts.snap │ │ ├── optimus-prime.achievement.spec.ts.snap │ │ ├── reaction-on-every-comment.achievement.spec.ts.snap │ │ ├── the-godfather-consigliere.achievement.spec.ts.snap │ │ ├── use-github-bot.achievement.spec.ts.snap │ │ └── used-all-reactions-in-comment.achievement.spec.ts.snap │ ├── achievement.abstract.ts │ ├── bi-winning.achievement.ts │ ├── breaking-bad.achievement.ts │ ├── cutting-edges.achievement.spec.ts │ ├── cutting-edges.achievement.ts │ ├── dev-tools │ │ ├── mocks.ts │ │ └── utils.ts │ ├── dont-yell-at-me.achievement.spec.ts │ ├── dont-yell-at-me.achievement.ts │ ├── double-review.achievement.spec.ts │ ├── double-review.achievement.ts │ ├── dr-claw.achievement.spec.ts │ ├── dr-claw.achievement.ts │ ├── helping-hand.achievement.spec.ts │ ├── helping-hand.achievement.ts │ ├── index.ts │ ├── inspector-gadget.achievement.spec.ts │ ├── inspector-gadget.achievement.ts │ ├── label-baby-junior.achievement.spec.ts │ ├── label-baby-junior.achievement.ts │ ├── meeseek.achievement.spec.ts │ ├── meeseek.achievement.ts │ ├── member.achievement.spec.ts │ ├── member.achievement.ts │ ├── mr-miyagi.achievement.spec.ts │ ├── mr-miyagi.achievement.ts │ ├── never-go-full-retard.achievement.spec.ts │ ├── never-go-full-retard.achievement.ts │ ├── optimus-prime.achievement.spec.ts │ ├── optimus-prime.achievement.ts │ ├── reaction-on-every-comment.achievement.spec.ts │ ├── reaction-on-every-comment.achievement.ts │ ├── the-godfather-consigliere.achievement.spec.ts │ ├── the-godfather-consigliere.achievement.ts │ ├── use-github-bot.achievement.spec.ts │ ├── use-github-bot.achievement.ts │ ├── used-all-reactions-in-comment.achievement.spec.ts │ └── used-all-reactions-in-comment.achievement.ts └── tsconfig.json ├── client ├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── angular.json ├── e2e │ ├── protractor.conf.js │ ├── src │ │ ├── app.e2e-spec.ts │ │ └── app.po.ts │ └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── proxy.conf.json ├── src │ ├── app │ │ ├── angular-material │ │ │ └── angular-material.module.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ └── app.module.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ├── codecov.yml ├── commitlint.config.js ├── env.schema.json ├── package-lock.json ├── package.json ├── server ├── .eslintignore ├── .eslintrc.js ├── README.md ├── nest-cli.json ├── package-lock.json ├── package.json ├── src │ ├── abstracts │ │ ├── __snapshots__ │ │ │ ├── base.model.abstract.spec.ts.snap │ │ │ └── base.service.abstract.spec.ts.snap │ │ ├── base.model.abstract.spec.ts │ │ ├── base.model.abstract.ts │ │ ├── base.service.abstract.spec.ts │ │ ├── base.service.abstract.ts │ │ ├── engine.abstract.ts │ │ ├── index.ts │ │ └── readme.md │ ├── api │ │ ├── api.controller.spec.ts │ │ ├── api.controller.ts │ │ ├── api.module.ts │ │ ├── index.ts │ │ ├── pull-request │ │ │ ├── pull-request.controller.spec.ts │ │ │ ├── pull-request.controller.ts │ │ │ ├── pull-request.module.ts │ │ │ ├── pull-request.service.spec.ts │ │ │ └── pull-request.service.ts │ │ ├── readme.md │ │ ├── repo │ │ │ ├── __snapshots__ │ │ │ │ └── repo.controller.spec.ts.snap │ │ │ ├── repo.controller.spec.ts │ │ │ ├── repo.controller.ts │ │ │ ├── repo.module.ts │ │ │ ├── repo.service.spec.ts │ │ │ └── repo.service.ts │ │ ├── user │ │ │ ├── __snapshots__ │ │ │ │ └── user.controller.spec.ts.snap │ │ │ ├── user.controller.spec.ts │ │ │ ├── user.controller.ts │ │ │ ├── user.module.ts │ │ │ ├── user.service.spec.ts │ │ │ └── user.service.ts │ │ └── webhook-event-manager │ │ │ ├── webhook-event-manager.controller.spec.ts │ │ │ ├── webhook-event-manager.controller.ts │ │ │ ├── webhook-event-manager.module.ts │ │ │ ├── webhook-event-manager.service.spec.ts │ │ │ └── webhook-event-manager.service.ts │ ├── app-root.ts │ ├── app │ │ ├── app.controller.spec.ts │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── index.ts │ │ └── readme.md │ ├── bootstrap-application.ts │ ├── config │ │ ├── __mocks__ │ │ │ └── winston.config.ts │ │ ├── __snapshots__ │ │ │ └── config.service.spec.ts.snap │ │ ├── achievibit-config.model.ts │ │ ├── config.module.ts │ │ ├── config.service.spec.ts │ │ ├── config.service.ts │ │ ├── index.ts │ │ ├── json-schema.validator.ts │ │ └── winston.config.ts │ ├── decorators │ │ ├── get-all.decorator.ts │ │ ├── get-one.decorator.ts │ │ ├── index.ts │ │ ├── kb-api-validation-error-response.decorator.ts │ │ ├── kb-delete.decorator.ts │ │ ├── kb-meature.decorator.ts │ │ ├── kb-patch.decorator.ts │ │ ├── kb-post.decorator.ts │ │ ├── kb-put.decorator.ts │ │ ├── readme.md │ │ └── task-health-check.decorator.ts │ ├── dev-tools │ │ ├── captured-events │ │ │ ├── pull-request-assignee-added.event.ts │ │ │ ├── pull-request-assignee-removed.event.ts │ │ │ ├── pull-request-closed.event.ts │ │ │ ├── pull-request-created-organization.event.ts │ │ │ ├── pull-request-created.event.ts │ │ │ ├── pull-request-edited.event.ts │ │ │ ├── pull-request-label-added.event.ts │ │ │ ├── pull-request-label-removed.event.ts │ │ │ ├── pull-request-labels-initialized.event.ts │ │ │ ├── pull-request-merged.event.ts │ │ │ ├── pull-request-review-comment-added.event.ts │ │ │ ├── pull-request-review-comment-deleted.event.ts │ │ │ ├── pull-request-review-comment-edited.event.ts │ │ │ ├── pull-request-review-request-added.event.ts │ │ │ ├── pull-request-review-request-removed.event.ts │ │ │ ├── pull-request-review-submitted.event.ts │ │ │ ├── review-comment-added.event.ts │ │ │ ├── review-comment-edited.event.ts │ │ │ ├── review-comment-removed.event.ts │ │ │ ├── review-submitted.event.ts │ │ │ └── wehbook-ping.event.ts │ │ ├── common-mocks.ts │ │ ├── dto.mock-generator.ts │ │ ├── in-memory-database.module.ts │ │ └── index.ts │ ├── engines │ │ ├── github.engine.spec.ts │ │ ├── github.engine.ts │ │ └── index.ts │ ├── errors │ │ ├── config.errors.ts │ │ └── index.ts │ ├── events │ │ ├── events.gateway.spec.ts │ │ ├── events.gateway.ts │ │ ├── events.module.ts │ │ ├── index.ts │ │ └── readme.md │ ├── filters │ │ ├── __snapshots__ │ │ │ ├── kb-not-found-exception.filter.spec.ts.snap │ │ │ └── kb-validation-exception.filter.spec.ts.snap │ │ ├── index.ts │ │ ├── kb-not-found-exception.filter.spec.ts │ │ ├── kb-not-found-exception.filter.ts │ │ ├── kb-validation-exception.filter.spec.ts │ │ ├── kb-validation-exception.filter.ts │ │ └── readme.md │ ├── interfaces │ │ ├── github-pr-payload.model.ts │ │ └── index.ts │ ├── main.ts │ ├── models │ │ ├── api.model.ts │ │ ├── index.ts │ │ ├── public-error.model.ts │ │ ├── pull-request.model.ts │ │ ├── readme.md │ │ ├── repo.model.ts │ │ └── user.model.ts │ ├── swagger.ts │ └── tasks │ │ ├── index.ts │ │ ├── readme.md │ │ ├── tasks.module.ts │ │ ├── tasks.service.spec.ts │ │ └── tasks.service.ts ├── test-tools │ └── jest.setup.ts ├── test │ ├── __snapshots__ │ │ ├── app.e2e-spec.ts.snap │ │ └── github-events.e2e-spec.ts.snap │ ├── app.e2e-spec.ts │ ├── github-events.e2e-spec.ts │ ├── jest-e2e.json │ ├── socket.service.ts │ ├── sockets.e2e-spec.ts │ ├── tasks.e2e-spec.ts │ └── utils.ts ├── tsconfig.build.json └── tsconfig.json ├── test.env.json └── tools ├── data └── names-dictionary.js ├── initialize.js ├── readme.md └── scripts ├── get-all-contributors.js ├── monorepo-commit-analyze.js ├── prune-untrackted-branches.js └── replace-template-string.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "achievibit", 3 | "projectOwner": "Kibibit", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "badgeTemplate": "-orange.svg?style=flat-square\" alt=\"All Contributors\">", 10 | "imageSize": 100, 11 | "commit": false, 12 | "commitConvention": "angular", 13 | "contributors": [ 14 | { 15 | "login": "Thatkookooguy", 16 | "name": "Neil Kalman", 17 | "avatar_url": "https://avatars3.githubusercontent.com/u/10427304?v=4", 18 | "profile": "http://thatkookooguy.kibibit.io/", 19 | "contributions": [ 20 | "code", 21 | "doc", 22 | "design", 23 | "maintenance", 24 | "infra", 25 | "test" 26 | ] 27 | }, 28 | { 29 | "login": "ortichon", 30 | "name": "O T", 31 | "avatar_url": "https://avatars.githubusercontent.com/u/10263615?v=4", 32 | "profile": "https://github.com/ortichon", 33 | "contributions": [ 34 | "code", 35 | "test" 36 | ] 37 | }, 38 | { 39 | "login": "dunaevsky", 40 | "name": "Michael Dunaevsky", 41 | "avatar_url": "https://avatars.githubusercontent.com/u/19394324?v=4", 42 | "profile": "https://github.com/dunaevsky", 43 | "contributions": [ 44 | "code", 45 | "test", 46 | "bug" 47 | ] 48 | }, 49 | { 50 | "login": "andrearosr", 51 | "name": "Andrea Rosales", 52 | "avatar_url": "https://avatars.githubusercontent.com/u/12634807?v=4", 53 | "profile": "https://github.com/andrearosr", 54 | "contributions": [ 55 | "bug", 56 | "code" 57 | ] 58 | }, 59 | { 60 | "login": "DanielRuf", 61 | "name": "Daniel Ruf", 62 | "avatar_url": "https://avatars.githubusercontent.com/u/827205?v=4", 63 | "profile": "https://daniel-ruf.de/", 64 | "contributions": [ 65 | "bug", 66 | "doc" 67 | ] 68 | } 69 | ], 70 | "contributorsPerLine": 7, 71 | "skipCi": true 72 | } 73 | -------------------------------------------------------------------------------- /.commit-template: -------------------------------------------------------------------------------- 1 | type(scope): subject 2 | 3 | description -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | environment: 6 | - NODE_ENV=devcontainer 7 | build: 8 | context: . 9 | dockerfile: Dockerfile 10 | args: 11 | # [Choice] Node.js version: 14, 12, 10 12 | VARIANT: 14 13 | # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. 14 | USER_UID: 1000 15 | USER_GID: 1000 16 | 17 | volumes: 18 | - ..:/workspace:cached 19 | 20 | # Overrides default command so things don't shut down after the process ends. 21 | command: sleep infinity 22 | 23 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 24 | network_mode: service:db 25 | 26 | # Uncomment the next line to use a non-root user for all processes. 27 | # user: node 28 | 29 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 30 | # (Adding the "ports" property to this file will not forward from a Codespace.) 31 | 32 | db: 33 | image: mongo:latest 34 | restart: unless-stopped 35 | volumes: 36 | - mongodb-data:/data/db 37 | 38 | # Uncomment to change startup options 39 | # environment: 40 | # MONGO_INITDB_ROOT_USERNAME: root 41 | # MONGO_INITDB_ROOT_PASSWORD: example 42 | # MONGO_INITDB_DATABASE: your-database-here 43 | 44 | # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally. 45 | # (Adding the "ports" property to this file will not forward from a Codespace.) 46 | 47 | volumes: 48 | mongodb-data: -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This is a comment. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in 5 | # the repo. Unless a later match takes precedence, 6 | # @global-owner1 and @global-owner2 will be requested for 7 | # review when someone opens a pull request. 8 | * @thatkookooguy @ortichon @dunaevsky @zimgil 9 | 10 | # Server Owners 11 | /server/ @thatkookooguy 12 | 13 | # Client Owners 14 | /client/ @thatkookooguy 15 | 16 | # Achievements Owners 17 | # These are the people that understand the pattern to create 18 | # and work with achievements 19 | /achievements/ @thatkookooguy @ortichon @dunaevsky @zimgil 20 | 21 | # Build\Github Actions Owners 22 | /tools/ @thatkookooguy 23 | /.github/ @thatkookooguy 24 | /.devcontainer/ @thatkookooguy 25 | /.vscode/ @thatkookooguy 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: 2 | - https://paypal.me/thatkookooguy?locale.x=en_US -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Subject of the issue 2 | Describe your issue here. 3 | 4 | ### Your environment 5 | * version of achievibit 6 | * if it's a client issue, which browser and its version 7 | 8 | ### Steps to reproduce 9 | Tell us how to reproduce this issue 10 | 11 | ### Expected behaviour 12 | Tell us what should happen 13 | 14 | ### Actual behaviour 15 | Tell us what happens instead 16 | 17 | ### Additional Info & Screenshots 18 | Add any related information like logs or screenshots 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 🚨 Review the [guidelines for contributing](../CONTRIBUTING.md) to this repository. 🚨 3 | 4 | Please explain the changes you made here. 5 | 6 | You can explain individual changes as a list: 7 | 8 | - [ ] feature name: extra details 9 | - [ ] bug: extra details (resolves #`issue_number`) 10 | 11 | ### Checklist 12 | Please check if your PR fulfills the following requirements: 13 | - [ ] Code compiles correctly (`npm run build`) 14 | - [ ] Code is linted 15 | - [ ] Created tests which fail without the change (if possible) 16 | - All **relevant** tests are passing 17 | - [ ] Server Unit Tests 18 | - [ ] Client Unit Tests 19 | - [ ] Achievements Unit Tests 20 | - [ ] API Tests 21 | - [ ] E2E Tests 22 | - [ ] Extended the README / documentation, if necessary 23 | -------------------------------------------------------------------------------- /.github/boring-cyborg.yml: -------------------------------------------------------------------------------- 1 | ##### Labeler ############################################################## 2 | labelPRBasedOnFilePath: 3 | Client: 4 | - client/**/* 5 | Server: 6 | - server/**/* 7 | achievements: 8 | - achievements/**/* 9 | tests: 10 | - server/**/*.spec.ts 11 | - client/**/*.spec.ts 12 | - achievements/**/*.spec.ts -------------------------------------------------------------------------------- /.github/checkgroup.yml: -------------------------------------------------------------------------------- 1 | subprojects: 2 | - id: Server 3 | paths: 4 | - "server/**" 5 | checks: 6 | - "Run linters Server" 7 | - "Server Unit Tests" 8 | - "API Tests" 9 | - "codecov/project" 10 | - "codecov/project/unit-test-server" 11 | - "codecov/project/api-tests" 12 | - id: Client 13 | paths: 14 | - "clients/**" 15 | checks: 16 | - "Client Unit Tests" 17 | - "codecov/project" 18 | - "codecov/project/unit-test-client" 19 | - id: Achievements 20 | paths: 21 | - "achievements/**" 22 | checks: 23 | - "Run linters Achievements" 24 | - "codecov/project" 25 | - "codecov/project/unit-test-achievements" -------------------------------------------------------------------------------- /.github/workflows/achievements-unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Achievements Unit Tests 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | paths: 6 | - 'achievements/**' 7 | push: 8 | paths: 9 | - 'achievements/**' 10 | branches: 11 | - master 12 | - beta 13 | 14 | jobs: 15 | achievementsunittest: 16 | name: Achievements Unit Tests 17 | runs-on: ubuntu-18.04 18 | steps: 19 | - name: Checkout Commit 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 12 27 | - name: Cache node modules 28 | uses: actions/cache@v2 29 | env: 30 | cache-name: cache-node-modules 31 | with: 32 | path: '**/node_modules' 33 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 34 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 35 | - name: Install Dependencies 36 | run: npm install 37 | - name: Run Achievements Tests 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | run: npm run test:ach-unit 42 | - name: Archive test results & coverage 43 | uses: actions/upload-artifact@v2 44 | with: 45 | name: achievements-unit-tests 46 | path: test-results/achievements 47 | - name: Upload coverage to Codecov 48 | uses: codecov/codecov-action@v1 49 | with: 50 | token: ${{ secrets.CODECOV_TOKEN }} 51 | directory: ./test-results/achievements/coverage 52 | flags: unit-test-achievements 53 | fail_ci_if_error: true -------------------------------------------------------------------------------- /.github/workflows/add-pr-deploy-badge.yml: -------------------------------------------------------------------------------- 1 | name: Add PR Deploy Badge 2 | # https://docs.github.com/en/actions/reference/events-that-trigger-workflows 3 | on: [deployment_status] 4 | 5 | jobs: 6 | badge: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | pull-requests: write 10 | # only runs this job on successful deploy 11 | if: github.event.deployment_status.state == 'success' 12 | steps: 13 | - name: Kb Pull Request Deployment Badges 14 | uses: kibibit/kb-badger-action@v2 15 | with: 16 | github-token: ${{secrets.GITHUB_TOKEN}} 17 | badge-left: demo 18 | badge-right: application 19 | badge-logo: heroku 20 | badge-path: api 21 | badge2-left: demo 22 | badge2-right: api-docs 23 | badge2-color: 85EA2D 24 | badge2-logo: swagger 25 | badge2-path: api/docs 26 | -------------------------------------------------------------------------------- /.github/workflows/api-tests.yml: -------------------------------------------------------------------------------- 1 | name: API Tests 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | paths: 6 | - 'server/**' 7 | push: 8 | branches: 9 | - master 10 | - beta 11 | jobs: 12 | unittest: 13 | name: API Tests 14 | runs-on: ubuntu-18.04 15 | steps: 16 | - name: Checkout Commit 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 12 24 | - name: Start MongoDB 25 | uses: supercharge/mongodb-github-action@1.3.0 26 | with: 27 | mongodb-version: 4.2 28 | - name: Cache node modules 29 | uses: actions/cache@v2 30 | env: 31 | cache-name: cache-node-modules 32 | with: 33 | path: '**/node_modules' 34 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 35 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 36 | - name: Install Dependencies 37 | run: npm install 38 | - name: Build 39 | run: npm run build --if-present 40 | - name: Run API Tests 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | run: npm run test:api 45 | - name: Upload coverage to Codecov 46 | uses: codecov/codecov-action@v1 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | directory: ./test-results/api/coverage 50 | flags: api-test 51 | fail_ci_if_error: true 52 | - name: Archive test results & coverage 53 | uses: actions/upload-artifact@v2 54 | with: 55 | name: api-tests 56 | path: test-results/api -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Production 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - beta 9 | 10 | jobs: 11 | build: 12 | name: Build Production 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout commit 16 | uses: actions/checkout@v2 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 12 21 | - name: Cache node modules 22 | uses: actions/cache@v2 23 | env: 24 | cache-name: cache-node-modules 25 | with: 26 | path: '**/node_modules' 27 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 29 | - name: Install Dependencies 30 | run: npm install 31 | - name: Build 32 | run: npm run build -------------------------------------------------------------------------------- /.github/workflows/client-unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Client Unit Tests 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | paths: 6 | - 'client/**' 7 | push: 8 | paths: 9 | - 'client/**' 10 | branches: 11 | - master 12 | - beta 13 | 14 | jobs: 15 | clientunittest: 16 | name: Client Unit Tests 17 | runs-on: ubuntu-18.04 18 | steps: 19 | - name: Checkout Commit 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 12 27 | - name: Cache node modules 28 | uses: actions/cache@v2 29 | env: 30 | cache-name: cache-node-modules 31 | with: 32 | path: '**/node_modules' 33 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 34 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 35 | - name: Install Dependencies 36 | run: npm install 37 | - name: Run Client Tests 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | run: npm run test:client-unit 42 | - name: Archive test Results & Coverage 43 | uses: actions/upload-artifact@v2 44 | with: 45 | name: client-unit-tests 46 | path: test-results/client 47 | - name: Upload coverage to Codecov 48 | uses: codecov/codecov-action@v1 49 | with: 50 | token: ${{ secrets.CODECOV_TOKEN }} 51 | directory: ./test-results/client/coverage/report-json 52 | flags: unit-test-client 53 | fail_ci_if_error: true 54 | -------------------------------------------------------------------------------- /.github/workflows/e2e-tests.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | paths: 6 | - 'client/**' 7 | push: 8 | branches: 9 | - master 10 | - beta 11 | jobs: 12 | unittest: 13 | name: E2E Tests 14 | runs-on: ubuntu-18.04 15 | steps: 16 | - name: Checkout Commit 17 | uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 12 24 | - name: Start MongoDB 25 | uses: supercharge/mongodb-github-action@1.3.0 26 | with: 27 | mongodb-version: 4.2 28 | - name: Cache node modules 29 | uses: actions/cache@v2 30 | env: 31 | cache-name: cache-node-modules 32 | with: 33 | path: '**/node_modules' 34 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 35 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 36 | - name: Install Dependencies 37 | run: npm install 38 | - name: Build 39 | run: npm run build --if-present 40 | - name: Run E2E Tests 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | run: npm run test:e2e 45 | # - name: Upload coverage to Codecov 46 | # uses: codecov/codecov-action@v1 47 | # with: 48 | # token: ${{ secrets.CODECOV_TOKEN }} 49 | # directory: ./test-results/e2e/coverage 50 | # flags: clientunittests 51 | # fail_ci_if_error: true 52 | # verbose: true -------------------------------------------------------------------------------- /.github/workflows/lint-achievements.yml: -------------------------------------------------------------------------------- 1 | name: Lint Achievements 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: # Run on local PRs 6 | paths: 7 | - 'achievements/**' 8 | pull_request_target: # Run on fork PRs 9 | paths: 10 | - 'achievements/**' 11 | push: 12 | paths: 13 | - 'achievements/**' 14 | branches: 15 | - master 16 | - beta 17 | 18 | jobs: 19 | run-linters: 20 | name: Run linters Achievements 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout Commit 25 | uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 0 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v1 30 | with: 31 | node-version: 12 32 | - name: Cache node modules 33 | uses: actions/cache@v2 34 | env: 35 | cache-name: cache-node-modules 36 | with: 37 | path: '**/node_modules' 38 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 39 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 40 | - name: Install Dependencies 41 | run: npm install 42 | - name: Run linters 43 | uses: wearerequired/lint-action@v1.9.0 44 | with: 45 | github_token: ${{ secrets.github_token }} 46 | # Enable linters 47 | eslint: true 48 | # Eslint options 49 | eslint_dir: achievements/ 50 | eslint_extensions: js,ts -------------------------------------------------------------------------------- /.github/workflows/lint-server.yml: -------------------------------------------------------------------------------- 1 | name: Lint Server 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: # Run on local PRs 6 | paths: 7 | - 'server/**' 8 | pull_request_target: # Run on fork PRs 9 | paths: 10 | - 'server/**' 11 | push: 12 | paths: 13 | - 'server/**' 14 | branches: 15 | - master 16 | - beta 17 | 18 | jobs: 19 | run-linters: 20 | name: Run linters Server 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout Commit 25 | uses: actions/checkout@v2 26 | with: 27 | fetch-depth: 0 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v1 30 | with: 31 | node-version: 12 32 | - name: Cache node modules 33 | uses: actions/cache@v2 34 | env: 35 | cache-name: cache-node-modules 36 | with: 37 | path: '**/node_modules' 38 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 39 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 40 | - name: Install Dependencies 41 | run: npm install 42 | - name: Run linters 43 | uses: wearerequired/lint-action@v1.9.0 44 | with: 45 | github_token: ${{ secrets.BOT_TOKEN }} 46 | # Enable linters 47 | eslint: true 48 | # Eslint options 49 | eslint_dir: server/ 50 | eslint_extensions: js,ts 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | # workflow_run: 4 | # workflows: 5 | # - API Tests 6 | # - Client Unit Tests 7 | # - E2E Tests 8 | # - Server Unit Tests 9 | # types: [completed] 10 | push: 11 | branches: 12 | - master 13 | - beta 14 | jobs: 15 | release: 16 | name: Release 17 | runs-on: ubuntu-18.04 18 | steps: 19 | - name: Checkout Commit 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | token: ${{ secrets.BOT_TOKEN }} 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: 12 28 | - name: Cache node modules 29 | uses: actions/cache@v2 30 | env: 31 | cache-name: cache-node-modules 32 | with: 33 | path: '**/node_modules' 34 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 35 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 36 | - name: Install Dependencies 37 | run: npm install 38 | - name: Build 39 | run: npm run build --if-present 40 | - name: Release 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} 43 | GIT_AUTHOR_NAME: k1b1b0t 44 | GIT_AUTHOR_EMAIL: k1b1b0t@kibibit.io 45 | GIT_COMMITTER_NAME: k1b1b0t 46 | GIT_COMMITTER_EMAIL: k1b1b0t@kibibit.io 47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | run: npm run semantic-release -------------------------------------------------------------------------------- /.github/workflows/server-unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Server Unit Tests 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | paths: 6 | - 'server/**' 7 | push: 8 | paths: 9 | - 'server/**' 10 | branches: 11 | - master 12 | - beta 13 | 14 | jobs: 15 | serverunittest: 16 | name: Server Unit Tests 17 | runs-on: ubuntu-18.04 18 | steps: 19 | - name: Checkout Commit 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 12 27 | - name: Cache node modules 28 | uses: actions/cache@v2 29 | env: 30 | cache-name: cache-node-modules 31 | with: 32 | path: '**/node_modules' 33 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 34 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 35 | - name: Install Dependencies 36 | run: npm install 37 | - name: Run Server Tests 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | run: npm run test:server-unit 42 | - name: Archive test results & coverage 43 | uses: actions/upload-artifact@v2 44 | with: 45 | name: server-unit-tests 46 | path: test-results/server 47 | - name: Upload coverage to Codecov 48 | uses: codecov/codecov-action@v1 49 | with: 50 | token: ${{ secrets.CODECOV_TOKEN }} 51 | directory: ./test-results/server/coverage 52 | flags: unit-test-server 53 | fail_ci_if_error: true 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | test-results 3 | *.env.json 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | .parcel-cache 82 | 83 | # Next.js build output 84 | .next 85 | out 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and not Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | 115 | # yarn v2 116 | .yarn/cache 117 | .yarn/unplugged 118 | .yarn/build-state.yml 119 | .yarn/install-state.gz 120 | .pnp.* 121 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . "$(dirname "$0")/_/husky.sh" 3 | exec < /dev/tty && node_modules/.bin/cz --hook || true -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | // "rbbit.typescript-hero", 4 | "coenraads.bracket-pair-colorizer", 5 | "wix.vscode-import-cost", 6 | "orta.vscode-jest", 7 | "dbaeumer.vscode-eslint", 8 | "actboy168.tasks", 9 | "johnpapa.vscode-peacock", 10 | "angular.ng-template", 11 | "abhijoybasak.nestjs-files", 12 | "eamodio.gitlens", 13 | "codeandstuff.package-json-upgrade", 14 | "mongodb.mongodb-vscode", 15 | "ms-azuretools.vscode-docker", 16 | "jock.svg", 17 | "firsttris.vscode-jest-runner", 18 | "wakatime.vscode-wakatime", 19 | "cschleiden.vscode-github-actions", 20 | "hirse.vscode-ungit", 21 | "github.vscode-pull-request-github", 22 | "wayou.vscode-todo-highlight", 23 | "mhutchie.git-graph" 24 | ] 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 kibibit 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. 22 | -------------------------------------------------------------------------------- /achievements/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .eslintrc.js -------------------------------------------------------------------------------- /achievements/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | ignorePatterns: [ '.eslintrc.js' ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | 'eol-last': [ 2, 'windows' ], 20 | 'comma-dangle': [ 'error', 'never' ], 21 | 'max-len': [ 'error', { 'code': 80, "ignoreComments": true } ], 22 | 'quotes': ["error", "single"], 23 | '@typescript-eslint/no-empty-interface': 'error', 24 | '@typescript-eslint/member-delimiter-style': 'error', 25 | '@typescript-eslint/explicit-function-return-type': 'off', 26 | '@typescript-eslint/explicit-module-boundary-types': 'off', 27 | '@typescript-eslint/naming-convention': [ 28 | "error", 29 | { 30 | "selector": "interface", 31 | "format": ["PascalCase"], 32 | "custom": { 33 | "regex": "^I[A-Z]", 34 | "match": true 35 | } 36 | } 37 | ] 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /achievements/.gitignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /achievements/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kb-achievements", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "prebuild": "rimraf lib", 9 | "build": "tsc", 10 | "generate-barrels": "barrelsby --delete -d ./src -l top -q --exclude spec.ts", 11 | "lint": "eslint -c ./.eslintrc.js \"{src,apps,libs,test}/**/*.ts\"", 12 | "lint:fix": "eslint -c ./.eslintrc.js \"{src,apps,libs,test}/**/*.ts\" --fix", 13 | "test": "jest", 14 | "test:watch": "jest --coverage --watch --verbose", 15 | "test:cov": "jest --coverage --verbose" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "@types/jest": "^26.0.22", 21 | "jest": "^26.6.3", 22 | "rimraf": "^3.0.2", 23 | "ts-jest": "^26.5.5", 24 | "ts-loader": "^9.0.2", 25 | "ts-node": "^9.1.1", 26 | "@typescript-eslint/eslint-plugin": "^4.14.2", 27 | "@typescript-eslint/parser": "^4.14.2", 28 | "eslint": "^7.19.0", 29 | "eslint-config-prettier": "^7.2.0", 30 | "eslint-plugin-prettier": "^3.3.1" 31 | }, 32 | "jest": { 33 | "moduleFileExtensions": [ 34 | "js", 35 | "json", 36 | "ts" 37 | ], 38 | "modulePathIgnorePatterns": [ 39 | "node_modules" 40 | ], 41 | "rootDir": "src", 42 | "testRegex": ".*\\.spec\\.ts$", 43 | "transform": { 44 | "^.+\\.(t|j)s$": "ts-jest" 45 | }, 46 | "collectCoverageFrom": [ 47 | "**/*.(t|j)s", 48 | "!**/index.ts", 49 | "!**/dev-tools/**/*.(t|j)s" 50 | ], 51 | "reporters": [ 52 | "default", 53 | [ 54 | "jest-stare", 55 | { 56 | "resultDir": "../test-results/achievements", 57 | "reportTitle": "jest-stare!", 58 | "additionalResultsProcessors": [ 59 | "jest-junit" 60 | ], 61 | "coverageLink": "./coverage/lcov-report/index.html" 62 | } 63 | ] 64 | ], 65 | "coverageReporters": [ 66 | "json", 67 | "lcov", 68 | "text", 69 | "clover", 70 | "html" 71 | ], 72 | "coverageDirectory": "../../test-results/achievements/coverage", 73 | "testEnvironment": "node" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/cutting-edges.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Cutting Edges achievement should be granted if pull request was merged without approvals 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/cuttingEdges.achievement.jpg", 6 | "description": "You've merged a pull request without a reviewer confirming", 7 | "name": "Cutting Edges", 8 | "relatedPullRequest": "test", 9 | "short": "Cutting corners? I also like to live dangerously", 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/dont-yell-at-me.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`dontYellAtMe achievement should be granted to PR creator if both reasons 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/dontYellAtMe.achievement.jpg", 6 | "description": "You've used ALL CAPS and 3 or more exclamation marks in your Pull Request title", 7 | "name": "Don't Yell At Me!!!", 8 | "relatedPullRequest": "test", 9 | "short": "I don't know what we're yelling about", 10 | } 11 | `; 12 | 13 | exports[`dontYellAtMe achievement should be granted to PR creator if more than 2 '!' 1`] = ` 14 | Object { 15 | "avatar": "images/achievements/dontYellAtMe.achievement.jpg", 16 | "description": "You've used 3 or more exclamation marks in your Pull Request title", 17 | "name": "Don't Yell At Me!!!", 18 | "relatedPullRequest": "test", 19 | "short": "I don't know what we're yelling about", 20 | } 21 | `; 22 | 23 | exports[`dontYellAtMe achievement should be granted to PR creator if title is all caps 1`] = ` 24 | Object { 25 | "avatar": "images/achievements/dontYellAtMe.achievement.jpg", 26 | "description": "You've used ALL CAPS in your Pull Request title", 27 | "name": "Don't Yell At Me!!!", 28 | "relatedPullRequest": "test", 29 | "short": "I don't know what we're yelling about", 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/double-review.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`doubleReview achievement should be granted if 2 reviewers excluding creator 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/doubleReview.achievement.gif", 6 | "description": "double headed code review.
It doesn't matter who added you, apparently, both of you are needed for a one man job 😇", 7 | "name": "We're ready, master", 8 | "relatedPullRequest": "test", 9 | "short": ""This way!"-"No, that way!"", 10 | } 11 | `; 12 | 13 | exports[`doubleReview achievement should be granted if 2 reviewers excluding creator 2`] = ` 14 | Object { 15 | "avatar": "images/achievements/doubleReview.achievement.gif", 16 | "description": "double headed code review.
It doesn't matter who added you, apparently, both of you are needed for a one man job 😇", 17 | "name": "We're ready, master", 18 | "relatedPullRequest": "test", 19 | "short": ""This way!"-"No, that way!"", 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/dr-claw.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`drClaw achievement should be granted to PR creator if coverage decreased by 2+ 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/drClaw.achievement.gif", 6 | "description": "You've decreased a project coverage by -6.2%", 7 | "name": "Dr. Claw", 8 | "relatedPullRequest": "test", 9 | "short": "I'll get you next time, Gadget... next time!!", 10 | } 11 | `; 12 | 13 | exports[`drClaw achievement should parse only last coverall comment 1`] = ` 14 | Object { 15 | "avatar": "images/achievements/drClaw.achievement.gif", 16 | "description": "You've decreased a project coverage by -6.2%", 17 | "name": "Dr. Claw", 18 | "relatedPullRequest": "test", 19 | "short": "I'll get you next time, Gadget... next time!!", 20 | } 21 | `; 22 | 23 | exports[`drClaw achievement should write in description last decreased percentage 1`] = ` 24 | Object { 25 | "avatar": "images/achievements/drClaw.achievement.gif", 26 | "description": "You've decreased a project coverage by -10.6%", 27 | "name": "Dr. Claw", 28 | "relatedPullRequest": "test", 29 | "short": "I'll get you next time, Gadget... next time!!", 30 | } 31 | `; 32 | 33 | exports[`drClaw achievement should write in description last decreased percentage 2`] = ` 34 | Object { 35 | "avatar": "images/achievements/drClaw.achievement.gif", 36 | "description": "You've decreased a project coverage by -6.2%", 37 | "name": "Dr. Claw", 38 | "relatedPullRequest": "test", 39 | "short": "I'll get you next time, Gadget... next time!!", 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/helping-hand.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`helpingHand achievement should add indication if more than one PR reviewer 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/helpingHandManInBlack.achievement.jpg", 6 | "description": "Your reviewers reviewer, 2nd committed to your Pull Request", 7 | "name": "Helping Hand", 8 | "relatedPullRequest": "test", 9 | "short": "Look, I don't mean to be rude but this is not as easy as it looks", 10 | } 11 | `; 12 | 13 | exports[`helpingHand achievement should add indication if more than one PR reviewer 2`] = ` 14 | Object { 15 | "avatar": "images/achievements/helpingHandHelloThere.achievement.jpg", 16 | "description": "You've committed to creator's Pull Request you are reviewing", 17 | "name": "Helping Hand", 18 | "relatedPullRequest": "test", 19 | "short": "Hello there. Slow going?", 20 | } 21 | `; 22 | 23 | exports[`helpingHand achievement should add indication if more than one PR reviewer 3`] = ` 24 | Object { 25 | "avatar": "images/achievements/helpingHandHelloThere.achievement.jpg", 26 | "description": "You've committed to creator's Pull Request you are reviewing", 27 | "name": "Helping Hand", 28 | "relatedPullRequest": "test", 29 | "short": "Hello there. Slow going?", 30 | } 31 | `; 32 | 33 | exports[`helpingHand achievement should be granted if PR reviewer added commit 1`] = ` 34 | Object { 35 | "avatar": "images/achievements/helpingHandManInBlack.achievement.jpg", 36 | "description": "Your reviewer reviewer committed to your Pull Request", 37 | "name": "Helping Hand", 38 | "relatedPullRequest": "test", 39 | "short": "Look, I don't mean to be rude but this is not as easy as it looks", 40 | } 41 | `; 42 | 43 | exports[`helpingHand achievement should be granted if PR reviewer added commit 2`] = ` 44 | Object { 45 | "avatar": "images/achievements/helpingHandHelloThere.achievement.jpg", 46 | "description": "You've committed to creator's Pull Request you are reviewing", 47 | "name": "Helping Hand", 48 | "relatedPullRequest": "test", 49 | "short": "Hello there. Slow going?", 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/inspector-gadget.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`inspectorGadget achievement should be granted to PR creator if coverage increased by 2+ 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/inspectorGadget.achievement.jpg", 6 | "description": "You've increased a project coverage by +2.6%", 7 | "name": "Inspector Gadget", 8 | "relatedPullRequest": "test", 9 | "short": "I'm always careful, Penny. That's what makes me a great inspector.", 10 | } 11 | `; 12 | 13 | exports[`inspectorGadget achievement should parse only last coverall comment 1`] = ` 14 | Object { 15 | "avatar": "images/achievements/inspectorGadget.achievement.jpg", 16 | "description": "You've increased a project coverage by +2.6%", 17 | "name": "Inspector Gadget", 18 | "relatedPullRequest": "test", 19 | "short": "I'm always careful, Penny. That's what makes me a great inspector.", 20 | } 21 | `; 22 | 23 | exports[`inspectorGadget achievement should write in description last increased percentage 1`] = ` 24 | Object { 25 | "avatar": "images/achievements/inspectorGadget.achievement.jpg", 26 | "description": "You've increased a project coverage by +10.6%", 27 | "name": "Inspector Gadget", 28 | "relatedPullRequest": "test", 29 | "short": "I'm always careful, Penny. That's what makes me a great inspector.", 30 | } 31 | `; 32 | 33 | exports[`inspectorGadget achievement should write in description last increased percentage 2`] = ` 34 | Object { 35 | "avatar": "images/achievements/inspectorGadget.achievement.jpg", 36 | "description": "You've increased a project coverage by +2.6%", 37 | "name": "Inspector Gadget", 38 | "relatedPullRequest": "test", 39 | "short": "I'm always careful, Penny. That's what makes me a great inspector.", 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/label-baby-junior.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`labelBabyJunior achievement should be granted to PR creator if more than 5 labels 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/labelBabyJunior.achievement.jpg", 6 | "description": "You've put many labels, thank you for organizing. You're a gift that keeps on re-giving", 7 | "name": "The Label Maker", 8 | "relatedPullRequest": "test", 9 | "short": "Is this a label maker?", 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/member.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`member achievement should be granted if PR opened more than 2 weeks ago 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/member.achievement.jpg", 6 | "description": "A pull request you've created 2 weeks ago is finally merged", 7 | "name": "Member pull request #undefined?", 8 | "relatedPullRequest": "test", 9 | "short": "Member Commits? member Push? member PR? ohh I member", 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/mr-miyagi.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mrMiyagi achievement should be granted to PR creator if coverage increased to 100% 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/mrMiyagi.achievement.jpg", 6 | "description": "You're the ultimate zen master. You increased a project coverage to 100%. It was a long journey... but you know...
First learn stand, then learn fly. Nature rule, creator-san, not mine", 7 | "name": "Mr Miyagi", 8 | "relatedPullRequest": "test", 9 | "short": "Never put passion in front of principle, even if you win, you’ll lose", 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/never-go-full-retard.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`neverGoFullRetard achievement should be granted for all supported files 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/neverGoFullRetard.achievement.png", 6 | "description": "merged a pull request containing only pictures. pretty!", 7 | "name": "never go full retard", 8 | "relatedPullRequest": "test", 9 | "short": "Nigga, You Just Went Full Retard", 10 | } 11 | `; 12 | 13 | exports[`neverGoFullRetard achievement should be granted to creator and reviewers 1`] = ` 14 | Object { 15 | "avatar": "images/achievements/neverGoFullRetard.achievement.png", 16 | "description": "merged a pull request containing only pictures. pretty!", 17 | "name": "never go full retard", 18 | "relatedPullRequest": "test", 19 | "short": "Nigga, You Just Went Full Retard", 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/optimus-prime.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`optimusPrime achievement should be granted to PR creator if PR number is prime 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/optimusPrime.achievement.jpeg", 6 | "description": "Pull requests with prime numbers are very rare! yours was 3", 7 | "name": "optimus prime", 8 | "relatedPullRequest": "test", 9 | "short": "Fate rarely calls upon us at a moment of our choosing", 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/reaction-on-every-comment.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`reactionOnEveryComment achievement should be granted if PR and existing comments have reactions should work with both PR comments & inline comments 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/reactionOnEveryComment.achievement.png", 6 | "description": "got for having at least one comment\\\\inline comment, and all of them (including the PR description) had reactions", 7 | "name": "royal flush", 8 | "relatedPullRequest": "test", 9 | "short": "emojis on all of the comments", 10 | } 11 | `; 12 | 13 | exports[`reactionOnEveryComment achievement should be granted if PR and existing comments have reactions should work with only PR comments 1`] = ` 14 | Object { 15 | "avatar": "images/achievements/reactionOnEveryComment.achievement.png", 16 | "description": "got for having at least one comment\\\\inline comment, and all of them (including the PR description) had reactions", 17 | "name": "royal flush", 18 | "relatedPullRequest": "test", 19 | "short": "emojis on all of the comments", 20 | } 21 | `; 22 | 23 | exports[`reactionOnEveryComment achievement should be granted if PR and existing comments have reactions should work with only inline comments 1`] = ` 24 | Object { 25 | "avatar": "images/achievements/reactionOnEveryComment.achievement.png", 26 | "description": "got for having at least one comment\\\\inline comment, and all of them (including the PR description) had reactions", 27 | "name": "royal flush", 28 | "relatedPullRequest": "test", 29 | "short": "emojis on all of the comments", 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/the-godfather-consigliere.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`theGodfatherConsigliere achievement should be granted to PR creator if organization is Kibibit 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/theGodfatherConsigliere.achievement.jpg", 6 | "description": "

You have contributed to Kibibit! We really appreciate it!

Accept this achievement as gift on my daughter's wedding day

", 7 | "name": "The Godfather Consigliere", 8 | "relatedPullRequest": "test", 9 | "short": "Great men are not born great, they contribute to Kibibit . . .", 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/use-github-bot.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`githubBot achievement should be granted if committer username is web-flow 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/useGithubBot.achievement.jpeg", 6 | "description": "used github to create a pull request, using the web-flow bot", 7 | "name": "Why not bots?", 8 | "relatedPullRequest": "test", 9 | "short": "Hey sexy mama, wanna kill all humans?", 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /achievements/src/__snapshots__/used-all-reactions-in-comment.achievement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`usedAllReactionsInComment achievement should be granted if PR has a message with all reactions should work if all reactions + author reaction 1`] = ` 4 | Object { 5 | "avatar": "images/achievements/gladiator.achievement.gif", 6 | "description": "Your message got all the possible reactions. Enjoy your 15 minutes of fame", 7 | "name": "Gladiator", 8 | "relatedPullRequest": "test", 9 | "short": "Are you not ENTERTAINED?!", 10 | } 11 | `; 12 | 13 | exports[`usedAllReactionsInComment achievement should be granted if PR has a message with all reactions should work with PR comments 1`] = ` 14 | Object { 15 | "avatar": "images/achievements/gladiator.achievement.gif", 16 | "description": "Your message got all the possible reactions. Enjoy your 15 minutes of fame", 17 | "name": "Gladiator", 18 | "relatedPullRequest": "test", 19 | "short": "Are you not ENTERTAINED?!", 20 | } 21 | `; 22 | 23 | exports[`usedAllReactionsInComment achievement should be granted if PR has a message with all reactions should work with PR description 1`] = ` 24 | Object { 25 | "avatar": "images/achievements/gladiator.achievement.gif", 26 | "description": "Your message got all the possible reactions. Enjoy your 15 minutes of fame", 27 | "name": "Gladiator", 28 | "relatedPullRequest": "test", 29 | "short": "Are you not ENTERTAINED?!", 30 | } 31 | `; 32 | 33 | exports[`usedAllReactionsInComment achievement should be granted if PR has a message with all reactions should work with inline comments 1`] = ` 34 | Object { 35 | "avatar": "images/achievements/gladiator.achievement.gif", 36 | "description": "Your message got all the possible reactions. Enjoy your 15 minutes of fame", 37 | "name": "Gladiator", 38 | "relatedPullRequest": "test", 39 | "short": "Are you not ENTERTAINED?!", 40 | } 41 | `; 42 | -------------------------------------------------------------------------------- /achievements/src/achievement.abstract.ts: -------------------------------------------------------------------------------- 1 | export interface IShall { 2 | grant(username: string, achievement: IUserAchievement): void; 3 | } 4 | 5 | export interface IAchievement { 6 | name: string; 7 | check(pullRequest: any, shall: IShall): void; 8 | } 9 | 10 | export interface IUserAchievement { 11 | name: string; 12 | avatar: string; 13 | short: string; 14 | description: string; 15 | relatedPullRequest: string; 16 | } 17 | -------------------------------------------------------------------------------- /achievements/src/bi-winning.achievement.ts: -------------------------------------------------------------------------------- 1 | import { every, isEmpty } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const biWinning: IAchievement = { 6 | name: 'bi-winning', 7 | check: function(pullRequest, shall) { 8 | if (!isEmpty(pullRequest.commits) && 9 | every(pullRequest.commits, allStatusesPassed)) { 10 | 11 | const achievement: IUserAchievement = { 12 | avatar: 'images/achievements/biWinning.achievement.jpg', 13 | name: 'BI-WINNING!', 14 | short: 'I\'m bi-winning. I win here and I win there', 15 | description: [ 16 | '

All the commits in your pull-request have passing statuses! ', 17 | 'WINNING!

', 18 | '

I\'m different. I have a different constitution, I have a ', 19 | 'different brain, I have a different heart. I got tiger blood, man. ', 20 | 'Dying\'s for fools, dying\'s for amateurs.

' 21 | ].join(''), 22 | relatedPullRequest: pullRequest._id 23 | }; 24 | 25 | shall.grant(pullRequest.creator.username, achievement); 26 | } 27 | } 28 | }; 29 | 30 | function allStatusesPassed(commit) { 31 | return !isEmpty(commit.statuses) && 32 | every(commit.statuses, { state: 'success' }); 33 | } 34 | -------------------------------------------------------------------------------- /achievements/src/breaking-bad.achievement.ts: -------------------------------------------------------------------------------- 1 | import { forEach, isEqual } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const breakingBad: IAchievement = { 6 | name: 'Breaking Bad', 7 | check: function(pullRequest, shall) { 8 | if (atLeast80PrecentCommitsFailedBuild(pullRequest)) { 9 | 10 | const achievement: IUserAchievement = { 11 | avatar: 'images/achievements/breakingBad.achievement.jpg', 12 | name: 'Breaking Bad', 13 | short: [ 14 | 'Look, let\'s start with some tough love. ', 15 | 'You two suck at peddling meth. Period.' 16 | ].join(''), 17 | description: [ 18 | 'You merged a Pull Request with 5 or more commits with failing status' 19 | ].join(''), 20 | relatedPullRequest: pullRequest._id 21 | }; 22 | 23 | shall.grant(pullRequest.creator.username, achievement); 24 | } 25 | } 26 | }; 27 | 28 | function atLeast80PrecentCommitsFailedBuild(pullRequest) { 29 | let failedCommits = 0; 30 | const totalCommits = pullRequest.commits.length; 31 | forEach(pullRequest.commits, function(commit) { 32 | const TRAVIS_PR = 'continuous-integration/travis-ci/pr'; 33 | const TRAVIS_PUSH = 'continuous-integration/travis-ci/push'; 34 | const prBuildStatus = commit.statuses[TRAVIS_PR]; 35 | const pushBuildStatus = commit.statuses[TRAVIS_PUSH]; 36 | if ((prBuildStatus && isEqual(prBuildStatus.state, 'error')) || 37 | (pushBuildStatus && isEqual(pushBuildStatus.state, 'error'))) { 38 | failedCommits++; 39 | } 40 | }); 41 | 42 | return ((failedCommits / totalCommits) * 100) >= 80; 43 | } 44 | -------------------------------------------------------------------------------- /achievements/src/cutting-edges.achievement.spec.ts: -------------------------------------------------------------------------------- 1 | import { cuttingEdge } from './cutting-edges.achievement'; 2 | import { PullRequest, Shall } from './dev-tools/mocks'; 3 | 4 | describe('Cutting Edges achievement', () => { 5 | it('should not be granted if pull request is not merged', () => { 6 | const testShall = new Shall(); 7 | const pullRequest = new PullRequest(); 8 | 9 | cuttingEdge.check(pullRequest, testShall); 10 | expect(testShall.grantedAchievements).toBeUndefined(); 11 | }); 12 | 13 | it('should not be granted if pull request was merged with approvals', () => { 14 | const testShall = new Shall(); 15 | const pullRequest = new PullRequest(); 16 | 17 | pullRequest.merged = true; 18 | pullRequest.reviews = [ 19 | { 20 | id: 'review', 21 | state: 'APPROVED' 22 | } 23 | ]; 24 | 25 | cuttingEdge.check(pullRequest, testShall); 26 | expect(testShall.grantedAchievements).toBeUndefined(); 27 | }); 28 | 29 | it('should be granted if pull request was merged without approvals', () => { 30 | const testShall = new Shall(); 31 | const pullRequest = new PullRequest(); 32 | 33 | pullRequest.merged = true; 34 | pullRequest.reviews = []; 35 | 36 | cuttingEdge.check(pullRequest, testShall); 37 | expect(testShall.grantedAchievements).toBeDefined(); 38 | expect(testShall.grantedAchievements.creator).toMatchSnapshot(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /achievements/src/cutting-edges.achievement.ts: -------------------------------------------------------------------------------- 1 | import { some } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const cuttingEdge: IAchievement = { 6 | name: 'Cutting Edges', 7 | check: function(pullRequest, shall) { 8 | if (pullRequest.merged) { 9 | const anyApprovals = some(pullRequest.reviews, function(review) { 10 | return review.state === 'APPROVED'; 11 | }); 12 | 13 | if (!anyApprovals) { 14 | const achieve: IUserAchievement = { 15 | avatar: 'images/achievements/cuttingEdges.achievement.jpg', 16 | name: 'Cutting Edges', 17 | short: 'Cutting corners? I also like to live dangerously', 18 | description: 19 | 'You\'ve merged a pull request without a reviewer confirming', 20 | relatedPullRequest: pullRequest.id 21 | }; 22 | 23 | shall.grant(pullRequest.creator.username, achieve); 24 | } 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /achievements/src/dev-tools/mocks.ts: -------------------------------------------------------------------------------- 1 | export class Shall { 2 | grantedAchievements: { [username: string]: any }; 3 | 4 | grant(username, achievementObject) { 5 | this.grantedAchievements = this.grantedAchievements || {}; 6 | this.grantedAchievements[username] = achievementObject; 7 | } 8 | } 9 | 10 | export class PullRequest { 11 | title = 'this is a happy little title'; 12 | id = 'test'; 13 | number: number; 14 | url = 'url'; 15 | organization: { username: string }; 16 | description = ''; 17 | creator = { 18 | username: 'creator' 19 | }; 20 | reviewers = [ { 21 | 'username': 'reviewer' 22 | } ]; 23 | merged: any; 24 | reviews: any; 25 | comments: any[]; 26 | inlineComments: any[]; 27 | reactions: any[]; 28 | commits: any[]; 29 | labels: string[]; 30 | createdOn: Date; 31 | files: { name: string }[]; 32 | } 33 | -------------------------------------------------------------------------------- /achievements/src/dev-tools/utils.ts: -------------------------------------------------------------------------------- 1 | export function createComment(username: string, message: string) { 2 | return { 3 | 'author': { 4 | 'username': username 5 | }, 6 | 'message': message 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /achievements/src/dont-yell-at-me.achievement.ts: -------------------------------------------------------------------------------- 1 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 2 | 3 | export const dontYellAtMe: IAchievement = { 4 | name: 'Don\'t Yell At Me!!!', 5 | check: function(pullRequest, shall) { 6 | const reason = isCreatorJustMean(pullRequest); 7 | 8 | if (reason) { 9 | 10 | const achieve: IUserAchievement = { 11 | avatar: 'images/achievements/dontYellAtMe.achievement.jpg', 12 | name: 'Don\'t Yell At Me!!!', 13 | short: 'I don\'t know what we\'re yelling about', 14 | description: 'You\'ve used ' + reason + ' in your Pull Request title', 15 | relatedPullRequest: pullRequest.id 16 | }; 17 | 18 | shall.grant(pullRequest.creator.username, achieve); 19 | } 20 | } 21 | }; 22 | 23 | function isCreatorJustMean(pullRequest) { 24 | const cleanedTitle = pullRequest.title.replace(/\[.*?\]/g, ''); 25 | const isTitleContainLetters = /[a-zA-Z]/.test(cleanedTitle); 26 | const isNoLowerCase = /^[^a-z]*$/.test(cleanedTitle); 27 | const isOverExclamation = /!{3}/.test(cleanedTitle); 28 | 29 | let reason = ''; 30 | let comboPotential = ''; 31 | 32 | if (isTitleContainLetters) { 33 | if (isNoLowerCase) { 34 | reason += 'ALL CAPS'; 35 | comboPotential = ' and '; 36 | } 37 | 38 | if (isOverExclamation) { 39 | reason += comboPotential + '3 or more exclamation marks'; 40 | } 41 | } 42 | 43 | return reason; 44 | } 45 | -------------------------------------------------------------------------------- /achievements/src/double-review.achievement.spec.ts: -------------------------------------------------------------------------------- 1 | import { PullRequest, Shall } from './dev-tools/mocks'; 2 | import { doubleReview } from './double-review.achievement'; 3 | 4 | describe('doubleReview achievement', function() { 5 | it('should not be granted if 1 reviewer', function() { 6 | const testShall = new Shall(); 7 | const pullRequest = new PullRequest(); 8 | 9 | doubleReview.check(pullRequest, testShall); 10 | expect(testShall.grantedAchievements).toBeUndefined(); 11 | }); 12 | 13 | it('should not be granted if more than 2 reviewers', function() { 14 | const testShall = new Shall(); 15 | const pullRequest = new PullRequest(); 16 | 17 | pullRequest.reviewers.push({ 18 | username: 'reviewerTwo' 19 | }); 20 | 21 | pullRequest.reviewers.push({ 22 | username: 'reviewerThree' 23 | }); 24 | 25 | doubleReview.check(pullRequest, testShall); 26 | expect(testShall.grantedAchievements).toBeUndefined(); 27 | }); 28 | 29 | it('should not be granted if 2 reviewers including creator', function() { 30 | const testShall = new Shall(); 31 | const pullRequest = new PullRequest(); 32 | 33 | pullRequest.reviewers.push({ 34 | username: 'creator' 35 | }); 36 | 37 | doubleReview.check(pullRequest, testShall); 38 | expect(testShall.grantedAchievements).toBeUndefined(); 39 | }); 40 | 41 | it('should be granted if 2 reviewers excluding creator', function() { 42 | const testShall = new Shall(); 43 | const pullRequest = new PullRequest(); 44 | 45 | pullRequest.reviewers.push({ 46 | username: 'reviewerTwo' 47 | }); 48 | 49 | doubleReview.check(pullRequest, testShall); 50 | expect(testShall.grantedAchievements).toBeDefined(); 51 | expect(testShall.grantedAchievements.creator).toBeUndefined(); 52 | expect(testShall.grantedAchievements.reviewer).toBeDefined(); 53 | expect(testShall.grantedAchievements.reviewer).toMatchSnapshot(); 54 | expect(testShall.grantedAchievements.reviewerTwo).toBeDefined(); 55 | expect(testShall.grantedAchievements.reviewerTwo).toMatchSnapshot() 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /achievements/src/double-review.achievement.ts: -------------------------------------------------------------------------------- 1 | import { clone, escape, forEach, remove } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const doubleReview: IAchievement = { 6 | name: 'doubleReview', 7 | check: function(pullRequest, shall) { 8 | // clone the reviewers to not mutate the original pullRequest 9 | const reviewers = clone(pullRequest.reviewers); 10 | remove(reviewers, { 11 | username: pullRequest.creator.username 12 | }); 13 | if (reviewers && reviewers.length === 2) { 14 | 15 | const achieve: IUserAchievement = { 16 | avatar: 'images/achievements/doubleReview.achievement.gif', 17 | name: 'We\'re ready, master', 18 | short: escape('"This way!"-"No, that way!"'), 19 | description: [ 20 | 'double headed code review.
It doesn\'t matter who added you, ', 21 | 'apparently, both of you are needed for a one man job 😇' 22 | ].join(''), 23 | relatedPullRequest: pullRequest.id 24 | }; 25 | 26 | forEach(reviewers, function(reviewer) { 27 | shall.grant(reviewer.username, achieve); 28 | }); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /achievements/src/dr-claw.achievement.ts: -------------------------------------------------------------------------------- 1 | import { findLast, get, parseInt, replace } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const drClaw: IAchievement = { 6 | name: 'Dr. Claw', 7 | check: function(pullRequest, shall) { 8 | 9 | const coveragePercentageDecreased = coverageDecreased(pullRequest); 10 | 11 | if (coveragePercentageDecreased) { 12 | 13 | const achievement: IUserAchievement = { 14 | avatar: 'images/achievements/drClaw.achievement.gif', 15 | name: 'Dr. Claw', 16 | short: 'I\'ll get you next time, Gadget... next time!!', 17 | description: [ 18 | 'You\'ve decreased a project coverage by ', 19 | coveragePercentageDecreased 20 | ].join(''), 21 | relatedPullRequest: pullRequest.id 22 | }; 23 | 24 | shall.grant(pullRequest.creator.username, achievement); 25 | } 26 | } 27 | }; 28 | 29 | function coverageDecreased(pullRequest) { 30 | const lastCoverageUpdate = findLast(pullRequest.comments, 31 | ['author.username', 'coveralls']); 32 | 33 | const lastCoverageUpdateMessage = get(lastCoverageUpdate, 'message'); 34 | const getDecreasedPercentageRegexp = /Coverage decreased \((.*?)\)/g; 35 | const match = getDecreasedPercentageRegexp.exec(lastCoverageUpdateMessage); 36 | const percentageString = get(match, 1); 37 | const percentageNumberOnly = replace(percentageString, /[-%]/g, ''); 38 | 39 | return parseInt(percentageNumberOnly, 10) >= 2 ? percentageString : false; 40 | 41 | } 42 | -------------------------------------------------------------------------------- /achievements/src/helping-hand.achievement.ts: -------------------------------------------------------------------------------- 1 | import { find, forEach, isEmpty, map } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const helpingHand: IAchievement = { 6 | name: 'Helping Hand', 7 | check: function(pullRequest, shall) { 8 | const committedReviewers = reviewersWhoCommittedToPullRequest(pullRequest); 9 | 10 | if (!isEmpty(committedReviewers)) { 11 | 12 | const isMultipleCommittedReviewers = 13 | committedReviewers.length > 1 ? 's ' : ' '; 14 | 15 | const reviewerAchievement: IUserAchievement = { 16 | avatar: 'images/achievements/helpingHandHelloThere.achievement.jpg', 17 | name: 'Helping Hand', 18 | short: 'Hello there. Slow going?', 19 | description: [ 20 | 'You\'ve committed to ', pullRequest.creator.username, 21 | '\'s Pull Request you are reviewing' 22 | ].join(''), 23 | relatedPullRequest: pullRequest.id 24 | }; 25 | 26 | const committerAchievement = { 27 | avatar: 'images/achievements/helpingHandManInBlack.achievement.jpg', 28 | name: 'Helping Hand', 29 | short: [ 30 | 'Look, I don\'t mean to be rude but this is not as easy as it looks' 31 | ].join(''), 32 | description: [ 33 | 'Your reviewer', isMultipleCommittedReviewers, 34 | map(committedReviewers, 'username').join(', '), 35 | ' committed to your Pull Request' 36 | ].join(''), 37 | relatedPullRequest: pullRequest.id 38 | }; 39 | 40 | forEach(committedReviewers, function(reviewer) { 41 | shall.grant(reviewer.username, reviewerAchievement); 42 | }); 43 | 44 | shall.grant(pullRequest.creator.username, committerAchievement); 45 | } 46 | } 47 | }; 48 | 49 | 50 | function reviewersWhoCommittedToPullRequest(pullRequest) { 51 | const committedReviewers = []; 52 | 53 | forEach(pullRequest.commits, function(commit) { 54 | if (commit.author.username !== pullRequest.creator.username && 55 | find(pullRequest.reviewers, {username: commit.author.username})) { 56 | committedReviewers.push(commit.author); 57 | } 58 | }); 59 | 60 | return committedReviewers; 61 | } 62 | -------------------------------------------------------------------------------- /achievements/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './achievement.abstract'; 6 | export * from './bi-winning.achievement'; 7 | export * from './breaking-bad.achievement'; 8 | export * from './cutting-edges.achievement'; 9 | export * from './dont-yell-at-me.achievement'; 10 | export * from './double-review.achievement'; 11 | export * from './dr-claw.achievement'; 12 | export * from './helping-hand.achievement'; 13 | export * from './inspector-gadget.achievement'; 14 | export * from './label-baby-junior.achievement'; 15 | export * from './meeseek.achievement'; 16 | export * from './member.achievement'; 17 | export * from './mr-miyagi.achievement'; 18 | export * from './never-go-full-retard.achievement'; 19 | export * from './optimus-prime.achievement'; 20 | export * from './reaction-on-every-comment.achievement'; 21 | export * from './the-godfather-consigliere.achievement'; 22 | export * from './use-github-bot.achievement'; 23 | export * from './used-all-reactions-in-comment.achievement'; 24 | export * from './dev-tools/mocks'; 25 | export * from './dev-tools/utils'; 26 | -------------------------------------------------------------------------------- /achievements/src/inspector-gadget.achievement.ts: -------------------------------------------------------------------------------- 1 | import { findLast, get, parseInt, replace } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const inspectorGadget: IAchievement = { 6 | name: 'Inspector Gadget', 7 | check: function(pullRequest, shall) { 8 | 9 | const coveragePercentageIncreased = coverageIncreased(pullRequest); 10 | 11 | if (coveragePercentageIncreased) { 12 | 13 | const achievement: IUserAchievement = { 14 | avatar: 'images/achievements/inspectorGadget.achievement.jpg', 15 | name: 'Inspector Gadget', 16 | short: [ 17 | 'I\'m always careful, Penny. That\'s what makes me ', 18 | 'a great inspector.' 19 | ].join(''), 20 | description: [ 21 | 'You\'ve increased a project coverage by ', 22 | coveragePercentageIncreased 23 | ].join(''), 24 | relatedPullRequest: pullRequest.id 25 | }; 26 | 27 | shall.grant(pullRequest.creator.username, achievement); 28 | } 29 | } 30 | }; 31 | 32 | function coverageIncreased(pullRequest) { 33 | const lastCoverageUpdate = findLast(pullRequest.comments, 34 | ['author.username', 'coveralls']); 35 | 36 | const lastCoverageUpdateMessage = get(lastCoverageUpdate, 'message'); 37 | const getIncreasedPercentageRegexp = /Coverage increased \((.*?)\)/g; 38 | const match = getIncreasedPercentageRegexp.exec(lastCoverageUpdateMessage); 39 | const percentageString = get(match, 1); 40 | const percentageNumberOnly = replace(percentageString, /[+%]/g, ''); 41 | 42 | return parseInt(percentageNumberOnly, 10) >= 2 ? percentageString : false; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /achievements/src/label-baby-junior.achievement.spec.ts: -------------------------------------------------------------------------------- 1 | import { PullRequest, Shall } from './dev-tools/mocks'; 2 | import { labelBabyJunior } from './label-baby-junior.achievement'; 3 | 4 | describe('labelBabyJunior achievement', () => { 5 | it('should not be granted if PR labels are undefined', () => { 6 | const testShall = new Shall(); 7 | const pullRequest = new PullRequest(); 8 | 9 | labelBabyJunior.check(pullRequest, testShall); 10 | expect(testShall.grantedAchievements).toBeUndefined(); 11 | }); 12 | 13 | it('should not be granted if PR has no labels', () => { 14 | const testShall = new Shall(); 15 | const pullRequest = new PullRequest(); 16 | 17 | pullRequest.labels = []; 18 | 19 | labelBabyJunior.check(pullRequest, testShall); 20 | expect(testShall.grantedAchievements).toBeUndefined(); 21 | }); 22 | 23 | it('should not be granted if less than 6 labels', () => { 24 | const testShall = new Shall(); 25 | const pullRequest = new PullRequest(); 26 | 27 | pullRequest.labels = [ 28 | 'label1', 29 | 'label2', 30 | 'label3', 31 | 'label4', 32 | 'label5' 33 | ]; 34 | 35 | labelBabyJunior.check(pullRequest, testShall); 36 | expect(testShall.grantedAchievements).toBeUndefined(); 37 | }); 38 | 39 | it('should be granted to PR creator if more than 5 labels', () => { 40 | const testShall = new Shall(); 41 | const pullRequest = new PullRequest(); 42 | 43 | pullRequest.labels = [ 44 | 'label1', 45 | 'label2', 46 | 'label3', 47 | 'label4', 48 | 'label5', 49 | 'label6' 50 | ]; 51 | 52 | labelBabyJunior.check(pullRequest, testShall); 53 | expect(testShall.grantedAchievements.creator).toMatchSnapshot(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /achievements/src/label-baby-junior.achievement.ts: -------------------------------------------------------------------------------- 1 | import { IAchievement } from './achievement.abstract'; 2 | 3 | export const labelBabyJunior: IAchievement = { 4 | name: 'Label Baby Junior', 5 | check: function(pullRequest, shall) { 6 | if (isManyLabels(pullRequest)) { 7 | const achievement = { 8 | avatar: 'images/achievements/labelBabyJunior.achievement.jpg', 9 | name: 'The Label Maker', 10 | short: 'Is this a label maker?', 11 | description: [ 12 | 'You\'ve put many labels, thank you for organizing. ', 13 | 'You\'re a gift that keeps on re-giving' 14 | ].join(''), 15 | relatedPullRequest: pullRequest.id 16 | }; 17 | 18 | shall.grant(pullRequest.creator.username, achievement); 19 | } 20 | } 21 | }; 22 | 23 | function isManyLabels(pullRequest) { 24 | const labels = pullRequest.labels; 25 | return labels && labels.length > 5; 26 | } 27 | -------------------------------------------------------------------------------- /achievements/src/meeseek.achievement.ts: -------------------------------------------------------------------------------- 1 | import { uniqBy } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const meeseek: IAchievement = { 6 | name: 'I\'m Mr. Meeseeks! Look at me!', 7 | check: function(pullRequest, shall) { 8 | if (checkIfResolvesManyIssues(pullRequest)) { 9 | 10 | const achievement: IUserAchievement = { 11 | avatar: 'images/achievements/meeseek.achievement.gif', 12 | name: 'I\'m Mr. Meeseeks! Look at me!', 13 | short: 'Knock yourselves out. Just eh-keep your requests simple.', 14 | description: [ 15 | '

Congrats on resolving so many issues at ones! Shouldn\'t ', 16 | 'pull requests be kept simple?

', 17 | '

Pull requests don\'t usually ', 18 | 'have to exist this long. It\'s getting weird.

' 19 | ].join(''), 20 | relatedPullRequest: pullRequest.id 21 | }; 22 | 23 | shall.grant(pullRequest.creator.username, achievement); 24 | } 25 | } 26 | }; 27 | 28 | function checkIfResolvesManyIssues(pullRequest) { 29 | let desc = pullRequest.description.toLowerCase(); 30 | 31 | const keywordsRegexString = 32 | '(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved) \\#(\\d+)'; 33 | 34 | const keywordsWithPrefix = new RegExp([ 35 | '\\w', 36 | keywordsRegexString 37 | ].join(''), 'g'); 38 | 39 | const keywordsWithSuffix = new RegExp([ 40 | keywordsRegexString, 41 | '\\w' 42 | ].join(''), 'g'); 43 | 44 | // remove unqualified sub-strings 45 | desc = desc 46 | .replace(keywordsWithPrefix, '') 47 | .replace(keywordsWithSuffix, ''); 48 | 49 | //these keywords resolve issues in github 50 | const resolveIssueRegex = new RegExp(keywordsRegexString, 'g'); 51 | 52 | // check uniqueness by bug number only 53 | const result = uniqBy( 54 | desc.match(resolveIssueRegex), 55 | (keyword: string) => keyword.replace(/^\D+/g, '') 56 | ); 57 | 58 | //resolved more than 3 issue in on pull request 59 | return result && result.length > 3; 60 | } 61 | -------------------------------------------------------------------------------- /achievements/src/member.achievement.spec.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { PullRequest, Shall } from './dev-tools/mocks'; 4 | import { member } from './member.achievement'; 5 | 6 | describe('member achievement', () => { 7 | it('should not be granted if PR opened less than 2 weeks ago', () => { 8 | const testShall = new Shall(); 9 | const pullRequest = new PullRequest(); 10 | 11 | pullRequest.createdOn = moment().subtract(13, 'days').toDate(); 12 | 13 | member.check(pullRequest, testShall); 14 | expect(testShall.grantedAchievements).toBeUndefined(); 15 | }); 16 | 17 | it('should be granted if PR opened more than 2 weeks ago', () => { 18 | const testShall = new Shall(); 19 | const pullRequest = new PullRequest(); 20 | 21 | pullRequest.createdOn = moment().subtract(15, 'days').toDate(); 22 | 23 | member.check(pullRequest, testShall); 24 | expect(testShall.grantedAchievements.creator).toMatchSnapshot(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /achievements/src/member.achievement.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { IAchievement } from './achievement.abstract'; 4 | 5 | export const member: IAchievement = { 6 | name: 'Member?', 7 | check: function(pullRequest, shall) { 8 | if (isWaitingLongTime(pullRequest)) { 9 | 10 | const achieve = { 11 | avatar: 'images/achievements/member.achievement.jpg', 12 | name: 'Member pull request #' + pullRequest.number + '?', 13 | short: 'Member Commits? member Push? member PR? ohh I member', 14 | description: [ 15 | 'A pull request you\'ve created 2 weeks ago', 16 | ' is finally merged' 17 | ].join(''), 18 | relatedPullRequest: pullRequest.id 19 | }; 20 | 21 | shall.grant(pullRequest.creator.username, achieve); 22 | } 23 | } 24 | }; 25 | 26 | function isWaitingLongTime(pullRequest) { 27 | const backThen = moment(pullRequest.createdOn); 28 | const now = moment(); 29 | 30 | return now.diff(backThen, 'days') > 14; 31 | } 32 | -------------------------------------------------------------------------------- /achievements/src/mr-miyagi.achievement.ts: -------------------------------------------------------------------------------- 1 | import { findLast, get } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const mrMiyagi: IAchievement = { 6 | name: 'Mr Miyagi', 7 | check: function(pullRequest, shall) { 8 | 9 | if (isCoverageTurened100(pullRequest)) { 10 | 11 | const achievement: IUserAchievement = { 12 | avatar: 'images/achievements/mrMiyagi.achievement.jpg', 13 | name: 'Mr Miyagi', 14 | short: [ 15 | 'Never put passion in front of principle, even if you win, ', 16 | 'you’ll lose' 17 | ].join(''), 18 | description: [ 19 | 'You\'re the ultimate zen master. You increased a project coverage ', 20 | 'to 100%. It was a long journey... but you know...
', 21 | '', 22 | 'First learn stand, then learn fly. Nature rule, ', 23 | pullRequest.creator.username, '-san, not mine', 24 | '' 25 | ].join(''), 26 | relatedPullRequest: pullRequest.id 27 | }; 28 | 29 | shall.grant(pullRequest.creator.username, achievement); 30 | } 31 | } 32 | }; 33 | 34 | function isCoverageTurened100(pullRequest) { 35 | const lastCoverageUpdate = findLast(pullRequest.comments, 36 | ['author.username', 'coveralls']); 37 | 38 | const lastCoverageUpdateMessage = get(lastCoverageUpdate, 'message'); 39 | const getTotalPercentageRegexp = /Coverage increased \(.*?\) to (.*?%)/g; 40 | const match = getTotalPercentageRegexp.exec(lastCoverageUpdateMessage); 41 | const totalCoverageString = get(match, 1); 42 | 43 | return totalCoverageString === '100.0%'; 44 | 45 | } 46 | -------------------------------------------------------------------------------- /achievements/src/never-go-full-retard.achievement.ts: -------------------------------------------------------------------------------- 1 | import { endsWith, every, forEach } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const neverGoFullRetard: IAchievement = { 6 | name: 'never go full retard', 7 | check: function(pullRequest, shall) { 8 | if (pullRequest.files && pullRequest.files.length > 0 && 9 | every(pullRequest.files, isAnImage)) { 10 | 11 | const achieve: IUserAchievement = { 12 | avatar: 'images/achievements/neverGoFullRetard.achievement.png', 13 | name: 'never go full retard', 14 | short: 'Nigga, You Just Went Full Retard', 15 | description: 'merged a pull request containing only pictures. pretty!', 16 | relatedPullRequest: pullRequest.id 17 | }; 18 | shall.grant(pullRequest.creator.username, achieve); 19 | forEach(pullRequest.reviewers, function(reviewer) { 20 | shall.grant(reviewer.username, achieve); 21 | }); 22 | } 23 | } 24 | }; 25 | 26 | function isAnImage(file: any) { 27 | return typeof file === 'object' && file.name && 28 | (endsWith(file.name, '.png') || 29 | endsWith(file.name, '.jpg') || 30 | endsWith(file.name, '.jpeg') || 31 | endsWith(file.name, '.ico') || 32 | endsWith(file.name, '.svg') || 33 | endsWith(file.name, '.gif') || 34 | endsWith(file.name, '.icns')); 35 | } 36 | -------------------------------------------------------------------------------- /achievements/src/optimus-prime.achievement.spec.ts: -------------------------------------------------------------------------------- 1 | import { PullRequest, Shall } from './dev-tools/mocks'; 2 | import { optimusPrime } from './optimus-prime.achievement'; 3 | 4 | describe('optimusPrime achievement', () => { 5 | it('should be granted to PR creator if PR number is prime', () => { 6 | const testShall = new Shall(); 7 | const pullRequest = new PullRequest(); 8 | 9 | pullRequest.number = 3; 10 | 11 | optimusPrime.check(pullRequest, testShall); 12 | expect(testShall.grantedAchievements).toBeDefined(); 13 | expect(testShall.grantedAchievements.creator).toMatchSnapshot(); 14 | }); 15 | 16 | it('should not grant if PR number is 1', () => { 17 | const testShall = new Shall(); 18 | const pullRequest = new PullRequest(); 19 | 20 | pullRequest.number = 1; 21 | 22 | optimusPrime.check(pullRequest, testShall); 23 | expect(testShall.grantedAchievements).toBeUndefined(); 24 | }); 25 | 26 | it('should not grant if PR number is not prime', () => { 27 | const testShall = new Shall(); 28 | const pullRequest = new PullRequest(); 29 | 30 | pullRequest.number = 40; 31 | 32 | optimusPrime.check(pullRequest, testShall); 33 | expect(testShall.grantedAchievements).toBeUndefined(); 34 | }); 35 | 36 | it('should not fail if PR number does not exist', () => { 37 | const testShall = new Shall(); 38 | const pullRequest = new PullRequest(); 39 | 40 | optimusPrime.check(pullRequest, testShall); 41 | expect(testShall.grantedAchievements).toBeUndefined(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /achievements/src/optimus-prime.achievement.ts: -------------------------------------------------------------------------------- 1 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 2 | 3 | export const optimusPrime: IAchievement = { 4 | name: 'optimus prime', 5 | check: function(pullRequest, shall) { 6 | if (isPrime(pullRequest.number)) { 7 | 8 | const achieve: IUserAchievement = { 9 | avatar: 'images/achievements/optimusPrime.achievement.jpeg', 10 | name: 'optimus prime', 11 | short: 'Fate rarely calls upon us at a moment of our choosing', 12 | description: [ 13 | 'Pull requests with prime numbers are very rare! yours was ', 14 | pullRequest.number 15 | ].join(''), 16 | relatedPullRequest: pullRequest.id 17 | }; 18 | shall.grant(pullRequest.creator.username, achieve); 19 | } 20 | } 21 | }; 22 | 23 | function isPrime(n) { 24 | 25 | // If n is less than 2 or not an integer then by definition cannot be prime. 26 | if (n < 2) { 27 | return false; 28 | } 29 | 30 | if (n !== Math.round(n)) { 31 | return false; 32 | } 33 | 34 | // Now assume that n is prime, we will try to prove that it is not. 35 | let isPrime = true; 36 | 37 | // Now check every whole number from 2 to the square root of n. 38 | // If any of these divides n exactly, n cannot be prime. 39 | for (let i = 2; i <= Math.sqrt(n); i++) { 40 | if (n % i === 0) {isPrime = false;} 41 | } 42 | 43 | // Finally return whether n is prime or not. 44 | return isPrime; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /achievements/src/reaction-on-every-comment.achievement.ts: -------------------------------------------------------------------------------- 1 | import { every, get, isArray } from 'lodash'; 2 | 3 | import { IAchievement } from './achievement.abstract'; 4 | 5 | export const reactionOnEveryComment: IAchievement = { 6 | name: 'Royal Flush', 7 | check: function(pullRequest, shall) { 8 | const doesCommentsExist = 9 | get(pullRequest.inlineComments, 'length') > 0 || 10 | get(pullRequest.comments, 'length') > 0; 11 | const isReactionOnEverything = 12 | every(pullRequest.inlineComments, haveReactions) && 13 | every(pullRequest.comments, haveReactions) && 14 | every([ pullRequest ], haveReactions); 15 | 16 | if (doesCommentsExist && isReactionOnEverything) { 17 | 18 | shall.grant(pullRequest.creator.username, { 19 | avatar: 'images/achievements/reactionOnEveryComment.achievement.png', 20 | name: 'royal flush', 21 | short: 'emojis on all of the comments', 22 | description: [ 23 | 'got for having at least one comment\\inline comment, ', 24 | 'and all of them (including the PR description) had reactions' 25 | ].join(''), 26 | relatedPullRequest: pullRequest.id 27 | }); 28 | 29 | } 30 | } 31 | }; 32 | 33 | function haveReactions(comment) { 34 | return isArray(comment.reactions) && 35 | get(comment.reactions, 'length') !== 0; 36 | } 37 | -------------------------------------------------------------------------------- /achievements/src/the-godfather-consigliere.achievement.spec.ts: -------------------------------------------------------------------------------- 1 | import { PullRequest, Shall } from './dev-tools/mocks'; 2 | import { 3 | theGodfatherConsigliere 4 | } from './the-godfather-consigliere.achievement'; 5 | 6 | describe('theGodfatherConsigliere achievement', () => { 7 | it('should be granted to PR creator if organization is Kibibit', () => { 8 | const testShall = new Shall(); 9 | const pullRequest = new PullRequest(); 10 | 11 | const organization = { 12 | 'username': 'Kibibit' 13 | }; 14 | 15 | pullRequest.organization = organization; 16 | 17 | theGodfatherConsigliere.check(pullRequest, testShall); 18 | expect(testShall.grantedAchievements).toBeDefined(); 19 | expect(testShall.grantedAchievements.creator).toMatchSnapshot(); 20 | }); 21 | 22 | it('should not grant if no organization', () => { 23 | const testShall = new Shall(); 24 | const pullRequest = new PullRequest(); 25 | 26 | theGodfatherConsigliere.check(pullRequest, testShall); 27 | expect(testShall.grantedAchievements).toBeUndefined(); 28 | }); 29 | 30 | it('should not grant if organization is not Kibibit', () => { 31 | const testShall = new Shall(); 32 | const pullRequest = new PullRequest(); 33 | 34 | const organization = { 35 | 'username': 'Gibibit' 36 | }; 37 | 38 | pullRequest.organization = organization; 39 | 40 | theGodfatherConsigliere.check(pullRequest, testShall); 41 | expect(testShall.grantedAchievements).toBeUndefined(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /achievements/src/the-godfather-consigliere.achievement.ts: -------------------------------------------------------------------------------- 1 | import { result } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const theGodfatherConsigliere: IAchievement = { 6 | name: 'The Godfather Consigliere', 7 | check: function(pullRequest, shall) { 8 | if (result(pullRequest, 'organization.username') === 'Kibibit') { 9 | 10 | const achievement: IUserAchievement = { 11 | avatar: 'images/achievements/theGodfatherConsigliere.achievement.jpg', 12 | name: 'The Godfather Consigliere', 13 | short: 'Great men are not born great, they contribute to Kibibit . . .', 14 | description: [ 15 | '

You have contributed to Kibibit! We really ', 16 | 'appreciate it!

', 17 | '

Accept this achievement as gift on ', 18 | 'my daughter\'s wedding day

' 19 | ].join(''), 20 | relatedPullRequest: pullRequest.id 21 | }; 22 | 23 | shall.grant(pullRequest.creator.username, achievement); 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /achievements/src/use-github-bot.achievement.spec.ts: -------------------------------------------------------------------------------- 1 | import { PullRequest, Shall } from './dev-tools/mocks'; 2 | import { githubBot } from './use-github-bot.achievement'; 3 | 4 | const mockCommits = [ 5 | { 6 | author: { 7 | username: 'commit-author' 8 | }, 9 | committer: { 10 | username: 'web-flow' 11 | } 12 | } 13 | ]; 14 | 15 | describe('githubBot achievement', () => { 16 | it('should be granted if committer username is web-flow', () => { 17 | const testShall = new Shall(); 18 | const pullRequest = new PullRequest(); 19 | pullRequest.commits = mockCommits; 20 | 21 | githubBot.check(pullRequest, testShall); 22 | expect(testShall.grantedAchievements).toBeDefined(); 23 | expect(testShall.grantedAchievements.creator).toMatchSnapshot(); 24 | }); 25 | 26 | it('should not grant if committer is not web-flow', () => { 27 | const testShall = new Shall(); 28 | const pullRequest = new PullRequest(); 29 | pullRequest.commits = mockCommits; 30 | pullRequest.commits[0].committer.username = 'not-web-flow'; 31 | 32 | githubBot.check(pullRequest, testShall); 33 | expect(testShall.grantedAchievements).toBeUndefined(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /achievements/src/use-github-bot.achievement.ts: -------------------------------------------------------------------------------- 1 | import { find } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | export const githubBot: IAchievement = { 6 | name: 'use github bot', 7 | check: function(pullRequest, shall) { 8 | const isComitterGitHubWebFlow = find(pullRequest.commits, { 9 | committer: { 10 | username: 'web-flow' 11 | } 12 | }); 13 | if (pullRequest.commits && 14 | pullRequest.commits.length > 0 && 15 | isComitterGitHubWebFlow) { 16 | 17 | const achieve: IUserAchievement = { 18 | avatar: 'images/achievements/useGithubBot.achievement.jpeg', 19 | name: 'Why not bots?', 20 | short: 'Hey sexy mama, wanna kill all humans?', 21 | description: [ 22 | 'used github to create a pull request, using the web-flow bot' 23 | ].join(''), 24 | relatedPullRequest: pullRequest.id 25 | }; 26 | 27 | shall.grant(pullRequest.creator.username, achieve); 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /achievements/src/used-all-reactions-in-comment.achievement.ts: -------------------------------------------------------------------------------- 1 | import { concat, forEach, isEmpty, map, reject, uniq } from 'lodash'; 2 | 3 | import { IAchievement, IUserAchievement } from './achievement.abstract'; 4 | 5 | // Gives achievement to comment authors who got all reactions 6 | // without reacting themselves 7 | 8 | export const usedAllReactionsInComment: IAchievement = { 9 | name: 'Gladiator', 10 | check: function(pullRequest, shall) { 11 | const topAuthorsUsernames = getCommentAuthorsWithAllReactions(pullRequest); 12 | if (!isEmpty(topAuthorsUsernames)) { 13 | const achievement: IUserAchievement = { 14 | avatar: 'images/achievements/gladiator.achievement.gif', 15 | name: 'Gladiator', 16 | short: 'Are you not ENTERTAINED?!', 17 | description: [ 18 | 'Your message got all the possible reactions. ', 19 | 'Enjoy your 15 minutes of fame' 20 | ].join(''), 21 | relatedPullRequest: pullRequest.id 22 | }; 23 | 24 | forEach(topAuthorsUsernames, function(author) { 25 | shall.grant(author, achievement); 26 | }); 27 | } 28 | } 29 | }; 30 | 31 | function getCommentAuthorsWithAllReactions(pullRequest) { 32 | const allComments = concat(pullRequest.comments, pullRequest.inlineComments); 33 | const authors = map(allComments, 'author.username'); 34 | const AllCommentsReactions = map(allComments, 'reactions'); 35 | 36 | // also add pull request description reactions 37 | authors.push(pullRequest.creator.username); 38 | AllCommentsReactions.push(pullRequest.reactions); 39 | 40 | getOnlyUniqueReactionsWithoutAuthors(AllCommentsReactions, authors); 41 | 42 | return onlyUsersWithAllReactions(authors, AllCommentsReactions); 43 | } 44 | 45 | function reactionsWithoutAuthor(reactions, author) { 46 | return map(reject(reactions, ['user.username', author]), 'reaction'); 47 | } 48 | 49 | function getOnlyUniqueReactionsWithoutAuthors(AllCommentsReactions, authors) { 50 | forEach(AllCommentsReactions, function(reactions, index) { 51 | AllCommentsReactions[index] = 52 | uniq(reactionsWithoutAuthor(reactions, authors[index])); 53 | 54 | }); 55 | } 56 | 57 | function onlyUsersWithAllReactions(authors, AllCommentsReactions) { 58 | const commentAuthorsWithAllReactions = []; 59 | 60 | forEach(authors, function(author, index) { 61 | if (AllCommentsReactions[index].length === 6) { 62 | commentAuthorsWithAllReactions.push(author); 63 | } 64 | }); 65 | 66 | return commentAuthorsWithAllReactions; 67 | } 68 | -------------------------------------------------------------------------------- /achievements/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "esModuleInterop": true, 12 | "outDir": "./lib", 13 | "baseUrl": "./src", 14 | "paths": {}, 15 | "incremental": true, 16 | "skipLibCheck": true 17 | }, 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /client/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json", 14 | "e2e/tsconfig.json" 15 | ], 16 | "tsconfigRootDir": __dirname, 17 | "createDefaultProgram": true 18 | }, 19 | "extends": [ 20 | "plugin:@angular-eslint/ng-cli-compat", 21 | "plugin:@angular-eslint/ng-cli-compat--formatting-add-on", 22 | "plugin:@angular-eslint/template/process-inline-templates" 23 | ], 24 | "rules": { 25 | 'eol-last': [ 2, 'windows' ], 26 | 'comma-dangle': [ 'error', 'never' ], 27 | 'max-len': [ 'error', { 'code': 80, "ignoreComments": true } ], 28 | "@angular-eslint/component-selector": [ 29 | "error", 30 | { 31 | "type": "element", 32 | "prefix": "app", 33 | "style": "kebab-case" 34 | } 35 | ], 36 | "@angular-eslint/directive-selector": [ 37 | "error", 38 | { 39 | "type": "attribute", 40 | "prefix": "app", 41 | "style": "camelCase" 42 | } 43 | ] 44 | } 45 | }, 46 | { 47 | "files": [ 48 | "*.html" 49 | ], 50 | "extends": [ 51 | "plugin:@angular-eslint/template/recommended" 52 | ], 53 | "rules": {} 54 | } 55 | ] 56 | }; 57 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.2.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /client/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome', 17 | chromeOptions: { 18 | args: [ '--headless', '--disable-gpu', '--window-size=800,600', '--log-level=3', '--no-sandbox' ] 19 | } 20 | }, 21 | directConnect: true, 22 | baseUrl: 'http://localhost:10101/', 23 | framework: 'jasmine', 24 | jasmineNodeOpts: { 25 | showColors: true, 26 | defaultTimeoutInterval: 30000, 27 | print: function() {} 28 | }, 29 | onPrepare() { 30 | require('ts-node').register({ 31 | project: require('path').join(__dirname, './tsconfig.json') 32 | }); 33 | jasmine.getEnv().addReporter(new SpecReporter({ 34 | spec: { 35 | displayStacktrace: StacktraceOption.PRETTY 36 | } 37 | })); 38 | } 39 | }; -------------------------------------------------------------------------------- /client/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging } from 'protractor'; 2 | 3 | import { AppPage } from './app.po'; 4 | 5 | describe('workspace-project App', () => { 6 | let page: AppPage; 7 | 8 | beforeEach(() => { 9 | page = new AppPage(); 10 | }); 11 | 12 | it('should display welcome message', () => { 13 | page.navigateTo(); 14 | expect(page.getTitleText()).toEqual('kibibit Client Side'); 15 | }); 16 | 17 | afterEach(async () => { 18 | // Assert that there are no errors emitted from the browser 19 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 20 | expect(logs).not.toContain(jasmine.objectContaining({ 21 | level: logging.Level.SEVERE 22 | } as logging.Entry)); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /client/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root #site-name')) 10 | .getText() as Promise; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | require('karma-spec-reporter'), 15 | require('karma-htmlfile-reporter') 16 | ], 17 | client: { 18 | captureConsole: false, 19 | clearContext: false // leave Jasmine Spec Runner output visible in browser 20 | }, 21 | preprocessors: { 22 | 'src/**/*!(test|spec).ts': ['coverage'] 23 | }, 24 | // coverageIstanbulReporter: { 25 | // dir: require('path').join(__dirname, '../coverage/client'), 26 | // reports: ['html', 'lcovonly', 'text-summary'], 27 | // fixWebpackSourcePaths: true 28 | // }, 29 | coverageReporter: { 30 | // type : 'html', 31 | dir : '../test-results/client/coverage', 32 | reporters: [ 33 | { type: 'lcov', subdir: 'report-lcov' }, 34 | { type: 'html', subdir: 'report-html' }, 35 | { type: 'json', subdir: 'report-json' } 36 | ], 37 | fixWebpackSourcePaths: true 38 | }, 39 | 40 | reporters: ['spec', 'html'], 41 | htmlReporter: { 42 | outputFile: '../test-results/client/index.html', 43 | 44 | // Optional 45 | pageTitle: 'Client-Side Unit Tests', 46 | subPageTitle: 'A summary of test results', 47 | groupSuites: true, 48 | useCompactStyle: true, 49 | useLegacyStyle: true, 50 | showOnlyFailed: false 51 | }, 52 | port: 9876, 53 | colors: true, 54 | logLevel: config.LOG_INFO, 55 | autoWatch: true, 56 | browsers: [ 'Chrome_without_sandbox' ], 57 | customLaunchers: { 58 | Chrome_without_sandbox: { 59 | base: 'ChromeHeadless', 60 | flags: [ '--no-sandbox' ] // with sandbox it fails under Docker 61 | } 62 | }, 63 | singleRun: false, 64 | restartOnFileChange: true 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --port 10101 --poll=2000", 7 | "start:proxy": "ng serve --port 10101 --proxy-config ./proxy.conf.json --poll=2000", 8 | "build": "ng build", 9 | "test:watch": "ng test", 10 | "test": "ng test --no-watch --browsers Chrome_without_sandbox", 11 | "test:cov": "ng test --no-watch --code-coverage --browsers Chrome_without_sandbox", 12 | "test:headed": "ng test --no-watch --code-coverage --browsers=Chrome", 13 | "lint": "ng lint", 14 | "lint:fix": "ng lint client --fix", 15 | "lint:html": "prettyhtml ./src/**/*.html --tab-width 2 --print-width 80 --wrapAttributes true --sortAttributes", 16 | "e2e": "ng e2e", 17 | "e2e:headed": "ng e2e --browsers Chrome" 18 | }, 19 | "private": true, 20 | "dependencies": { 21 | "@angular/animations": "~11.1.2", 22 | "@angular/cdk": "^11.1.2", 23 | "@angular/common": "~11.1.2", 24 | "@angular/compiler": "~11.1.2", 25 | "@angular/core": "~11.1.2", 26 | "@angular/forms": "~11.1.2", 27 | "@angular/material": "^11.1.2", 28 | "@angular/platform-browser": "~11.1.2", 29 | "@angular/platform-browser-dynamic": "~11.1.2", 30 | "@angular/router": "~11.1.2", 31 | "@kibibit/consologo": "^1.2.0", 32 | "rxjs": "~6.6.0", 33 | "tslib": "^2.0.0", 34 | "zone.js": "~0.10.2" 35 | }, 36 | "devDependencies": { 37 | "@angular-devkit/build-angular": "~0.1101.4", 38 | "@angular-eslint/builder": "1.2.0", 39 | "@angular-eslint/eslint-plugin": "1.2.0", 40 | "@angular-eslint/eslint-plugin-template": "1.2.0", 41 | "@angular-eslint/schematics": "1.2.0", 42 | "@angular-eslint/template-parser": "1.2.0", 43 | "@angular/cli": "^11.1.4", 44 | "@angular/compiler-cli": "~11.1.2", 45 | "@starptech/prettyhtml": "^0.10.0", 46 | "@types/jasmine": "~3.5.0", 47 | "@types/jasminewd2": "~2.0.3", 48 | "@types/node": "^14.14.25", 49 | "@typescript-eslint/eslint-plugin": "4.14.2", 50 | "@typescript-eslint/parser": "4.14.2", 51 | "codelyzer": "^6.0.0", 52 | "eslint": "^7.19.0", 53 | "eslint-plugin-import": "2.22.1", 54 | "eslint-plugin-jsdoc": "31.6.1", 55 | "eslint-plugin-prefer-arrow": "1.2.2", 56 | "jasmine-core": "~3.6.0", 57 | "jasmine-spec-reporter": "~5.0.0", 58 | "karma": "^5.2.3", 59 | "karma-chrome-launcher": "~3.1.0", 60 | "karma-coverage": "^2.0.3", 61 | "karma-coverage-istanbul-reporter": "~3.0.2", 62 | "karma-jasmine": "~4.0.0", 63 | "karma-jasmine-html-reporter": "^1.5.0", 64 | "karma-spec-reporter": "0.0.32", 65 | "protractor": "~7.0.0", 66 | "ts-node": "~9.1.1", 67 | "tslint": "~6.1.0", 68 | "typescript": "~4.1.3" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /client/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": { 3 | "target": "http://localhost:10102", 4 | "secure": false, 5 | "logLevel": "debug", 6 | "changeOrigin": true 7 | } 8 | } -------------------------------------------------------------------------------- /client/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })], 8 | exports: [RouterModule] 9 | }) 10 | export class AppRoutingModule { } 11 | -------------------------------------------------------------------------------- /client/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | kibibit Client Side 3 | 4 | 5 |
6 |

Start Writing your app!

7 | 8 |
9 | 10 | -------------------------------------------------------------------------------- /client/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/achievibit/727415636b31e44079b4cc16c6352ed50741fbd2/client/src/app/app.component.scss -------------------------------------------------------------------------------- /client/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | 4 | import { 5 | AngularMaterialModule 6 | } from './angular-material/angular-material.module'; 7 | import { AppComponent } from './app.component'; 8 | 9 | describe('AppComponent', () => { 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [ 13 | RouterTestingModule, 14 | AngularMaterialModule 15 | ], 16 | declarations: [ 17 | AppComponent 18 | ] 19 | }).compileComponents(); 20 | }); 21 | 22 | it('should create the app', () => { 23 | const fixture = TestBed.createComponent(AppComponent); 24 | const app = fixture.componentInstance; 25 | expect(app).toBeTruthy(); 26 | }); 27 | 28 | it(`should have as title 'kibibit client'`, () => { 29 | const fixture = TestBed.createComponent(AppComponent); 30 | const app = fixture.componentInstance; 31 | expect(app.title).toEqual('kibibit client'); 32 | }); 33 | 34 | it('should render title', () => { 35 | const fixture = TestBed.createComponent(AppComponent); 36 | fixture.detectChanges(); 37 | const compiled = fixture.nativeElement; 38 | expect(compiled.querySelector('#site-name').textContent) 39 | .toContain('kibibit Client Side'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { webConsolelogo } from '@kibibit/consologo'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'] 8 | }) 9 | export class AppComponent { 10 | title = 'kibibit client'; 11 | 12 | constructor() { 13 | webConsolelogo('kibibit client template', [ 14 | 'kibibit server-client template', 15 | 'change this up in app.component.ts' 16 | ]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | 5 | import { 6 | AngularMaterialModule 7 | } from './angular-material/angular-material.module'; 8 | import { AppRoutingModule } from './app-routing.module'; 9 | import { AppComponent } from './app.component'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | AppComponent 14 | ], 15 | imports: [ 16 | BrowserModule, 17 | AppRoutingModule, 18 | BrowserAnimationsModule, 19 | AngularMaterialModule 20 | ], 21 | providers: [], 22 | bootstrap: [AppComponent], 23 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 24 | }) 25 | export class AppModule { } 26 | -------------------------------------------------------------------------------- /client/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/achievibit/727415636b31e44079b4cc16c6352ed50741fbd2/client/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/achievibit/727415636b31e44079b4cc16c6352ed50741fbd2/client/src/favicon.ico -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Client 6 | 7 | 11 | 16 | 20 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /client/src/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | // Custom Theming for Angular Material 3 | // For more information: https://material.angular.io/guide/theming 4 | @import '~@angular/material/theming'; 5 | // Plus imports for other components in your app. 6 | 7 | // Define a custom typography config that overrides the font-family as well as the 8 | // `headlines` and `body-1` levels. 9 | $custom-typography: mat-typography-config( 10 | $font-family: "'Comfortaa', cursive", 11 | $headline: mat-typography-level(32px, 48px, 700), 12 | $body-1: mat-typography-level(16px, 24px, 500) 13 | ); 14 | 15 | 16 | // Include the common styles for Angular Material. We include this here so that you only 17 | // have to load a single css file for Angular Material in your app. 18 | // Be sure that you only ever include this mixin once! 19 | @include mat-core($custom-typography); 20 | 21 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 22 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 23 | // hue. Available color palettes: https://material.io/design/color/ 24 | $client-primary: mat-palette($mat-indigo); 25 | $client-accent: mat-palette($mat-pink, A200, A100, A400); 26 | 27 | // The warn palette is optional (defaults to red). 28 | $client-warn: mat-palette($mat-red); 29 | 30 | // Create the theme object. A theme consists of configurations for individual 31 | // theming systems such as "color" or "typography". 32 | $client-theme: mat-dark-theme(( 33 | color: ( 34 | primary: $client-primary, 35 | accent: $client-accent, 36 | warn: $client-warn, 37 | ) 38 | )); 39 | 40 | // Include theme styles for core and each component used in your app. 41 | // Alternatively, you can import and @include the theme mixins for each component 42 | // that you are using. 43 | @include angular-material-theme($client-theme); 44 | 45 | /* You can add global styles to this file, and also import other style files */ 46 | 47 | html, body { height: 100%; } 48 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; background: #212121; } 49 | -------------------------------------------------------------------------------- /client/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads 2 | // recursively all the .spec and framework files 3 | import 'zone.js/dist/zone-testing'; 4 | 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | 12 | declare const require: { 13 | context(path: string, deep?: boolean, filter?: RegExp): { 14 | keys(): string[]; 15 | (id: string): T; 16 | }; 17 | }; 18 | 19 | // First, initialize the Angular testing environment. 20 | getTestBed().initTestEnvironment( 21 | BrowserDynamicTestingModule, 22 | platformBrowserDynamicTesting() 23 | ); 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().map(context); 28 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "disableTypeScriptVersionCheck": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | show_carryforward_flags: true 3 | # Setting coverage targets per flag 4 | coverage: 5 | status: 6 | patch: off 7 | project: 8 | default: 9 | target: 80% #overall project/ repo coverage 10 | api-tests: 11 | threshold: 5% 12 | target: auto 13 | flags: 14 | - api-test 15 | unit-test-server: 16 | threshold: 5% 17 | target: auto 18 | flags: 19 | - unit-test-server 20 | unit-test-client: 21 | threshold: 5% 22 | target: auto 23 | flags: 24 | - unit-test-client 25 | unit-test-achievements: 26 | threshold: 5% 27 | target: auto 28 | flags: 29 | - unit-test-achievements 30 | 31 | flag_management: 32 | default_rules: 33 | carryforward: true -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ '@commitlint/config-angular' ], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', [ 7 | 'build', 8 | 'chore', 9 | 'ci', 10 | 'docs', 11 | 'feat', 12 | 'fix', 13 | 'perf', 14 | 'refactor', 15 | 'revert', 16 | 'style', 17 | 'test' 18 | ] 19 | ] 20 | } 21 | }; -------------------------------------------------------------------------------- /env.schema.json: -------------------------------------------------------------------------------- 1 | {"properties":{"port":{"description":"Set server port","type":"number"},"dbUrl":{"description":"DB connection URL. Expects a mongodb db for connections","format":"url","type":"string"},"webhookProxyUrl":{"description":"Used to create a custom repeatable smee webhook url instead of Generating a random one","format":"url","type":"string","pattern":"^https:\\/\\/(?:www\\.)?smee\\.io\\/[a-zA-Z0-9_-]+\\/?"},"webhookDestinationUrl":{"description":"proxy should sent events to this url for achievibit","pattern":"^([\\w]+)?(\\/[\\w-]+)*$","type":"string"},"saveToFile":{"description":"Create a file made out of the internal config. This is mostly for merging command line, environment, and file variables to a single instance","type":"boolean"},"deletePRsHealthId":{"description":"cron job monitoring id","type":"string"},"githubAccessToken":{"description":"GitHub Access Token","type":"string"}},"type":"object","required":["port","webhookProxyUrl","webhookDestinationUrl","saveToFile"]} 2 | -------------------------------------------------------------------------------- /server/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .eslintrc.js -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: [ 8 | '@typescript-eslint/eslint-plugin', 9 | 'unused-imports', 10 | 'simple-import-sort', 11 | 'import' 12 | ], 13 | extends: [ 14 | 'plugin:@typescript-eslint/eslint-recommended', 15 | 'plugin:@typescript-eslint/recommended', 16 | ], 17 | ignorePatterns: [ 18 | '.eslintrc.js', 19 | '**/*.event.ts' 20 | ], 21 | root: true, 22 | env: { 23 | node: true, 24 | jest: true, 25 | }, 26 | rules: { 27 | 'unused-imports/no-unused-imports': 'error', 28 | 'simple-import-sort/imports': ['error', { 29 | groups: [ 30 | // 1. built-in node.js modules 31 | [`^(${require("module").builtinModules.join("|")})(/|$)`], 32 | // 2.1. package that start without @ 33 | // 2.2. package that start with @ 34 | ['^\\w', '^@\\w'], 35 | // 3. @nestjs packages 36 | ['^@nestjs\/'], 37 | // 4. @kibibit external packages 38 | ['^@kibibit\/'], 39 | // 5. Internal kibibit packages (inside this project) 40 | ['^@kb-'], 41 | // 6. Parent imports. Put `..` last. 42 | // Other relative imports. Put same-folder imports and `.` last. 43 | ["^\\.\\.(?!/?$)", "^\\.\\./?$", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], 44 | // 7. Side effect imports. 45 | // https://riptutorial.com/javascript/example/1618/importing-with-side-effects 46 | ["^\\u0000"] 47 | ] 48 | }], 49 | 'import/first': 'error', 50 | 'import/newline-after-import': 'error', 51 | 'import/no-duplicates': 'error', 52 | 'eol-last': [ 2, 'windows' ], 53 | 'comma-dangle': [ 'error', 'never' ], 54 | 'max-len': [ 'error', { 'code': 80, "ignoreComments": true } ], 55 | 'quotes': ["error", "single"], 56 | '@typescript-eslint/no-empty-interface': 'error', 57 | '@typescript-eslint/member-delimiter-style': 'error', 58 | '@typescript-eslint/explicit-function-return-type': 'off', 59 | '@typescript-eslint/explicit-module-boundary-types': 'off', 60 | '@typescript-eslint/naming-convention': [ 61 | "error", 62 | { 63 | "selector": "interface", 64 | "format": ["PascalCase"], 65 | "custom": { 66 | "regex": "^I[A-Z]", 67 | "match": true 68 | } 69 | } 70 | ], 71 | "semi": "off", 72 | "@typescript-eslint/semi": ["error"] 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /server/src/abstracts/__snapshots__/base.model.abstract.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Base Model should obfuscate and convert to string 1`] = `"{\\"mockAttribute\\":\\"nice\\"}"`; 4 | -------------------------------------------------------------------------------- /server/src/abstracts/__snapshots__/base.service.abstract.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BaseService create should fail creating model with missing required attribute 1`] = `"MockModel validation failed: mockAttribute: Path \`mockAttribute\` is required."`; 4 | 5 | exports[`BaseService delete item should throw error when deleting a missing item 1`] = `"Cast to ObjectId failed for value \\"???\\" at path \\"_id\\" for model \\"MockModel\\""`; 6 | -------------------------------------------------------------------------------- /server/src/abstracts/base.model.abstract.spec.ts: -------------------------------------------------------------------------------- 1 | import { MockModel } from '@kb-dev-tools'; 2 | 3 | describe('Base Model', () => { 4 | let model: MockModel; 5 | 6 | beforeEach(() => model = new MockModel({ 7 | mockAttribute: 'nice', 8 | mockPrivateAttribute: 'bad' 9 | })); 10 | 11 | it ('should allow extending', () => { 12 | expect(model).toBeDefined(); 13 | }); 14 | 15 | it('should obfuscate and convert to plain object', () => { 16 | const asJson = model.toJSON(); 17 | 18 | expect(model.mockAttribute).toBe('nice'); 19 | expect(model.mockPrivateAttribute).toBe('bad'); 20 | expect(asJson).toBeDefined(); 21 | expect(asJson.mockPrivateAttribute).toBeUndefined(); 22 | expect(asJson.mockAttribute).toBe('nice'); 23 | }); 24 | 25 | it('should obfuscate and convert to string', () => { 26 | const asString = model.toString(); 27 | expect(asString).toMatchSnapshot(); 28 | expect(asString).not.toMatch(/mockPrivateAttribute/g); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /server/src/abstracts/base.model.abstract.ts: -------------------------------------------------------------------------------- 1 | import { classToPlain, Exclude, Expose } from 'class-transformer'; 2 | import { Schema } from 'mongoose'; 3 | import { buildSchema, prop as PersistInDb } from '@typegoose/typegoose'; 4 | 5 | @Exclude() 6 | export abstract class BaseModel { 7 | @PersistInDb() 8 | createdDate?: Date; // provided by timestamps 9 | @Expose() 10 | @PersistInDb() 11 | updatedDate?: Date; // provided by timestamps 12 | 13 | // @Expose({ name: 'id' }) 14 | // @Transform(({ value }) => value && value.toString()) 15 | // tslint:disable-next-line: variable-name 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | _id?: any; 18 | 19 | id?: string; // is actually model._id getter 20 | 21 | // tslint:disable-next-line: variable-name 22 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 23 | _v?: any; 24 | 25 | // add more to a base model if you want. 26 | 27 | toJSON() { 28 | return classToPlain(this); 29 | } 30 | 31 | toString() { 32 | return JSON.stringify(this.toJSON()); 33 | } 34 | 35 | static get schema(): Schema { 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 37 | return buildSchema(this as any, { 38 | timestamps: true, 39 | toJSON: { 40 | getters: true, 41 | virtuals: true 42 | } 43 | }); 44 | } 45 | 46 | static get modelName(): string { 47 | return this.name; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/src/abstracts/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './base.model.abstract'; 6 | export * from './base.service.abstract'; 7 | export * from './engine.abstract'; 8 | -------------------------------------------------------------------------------- /server/src/abstracts/readme.md: -------------------------------------------------------------------------------- 1 | ## ABSTRACTS 2 | 3 | Contains all the abstracts of this project. Usually these are common parts of the application that repeat a lot (like services for database items or the database items themselves). 4 | 5 | Currently, this contain an abstract base service for DB items, and a base DB model to define new models based on. -------------------------------------------------------------------------------- /server/src/api/api.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import findRoot from 'find-root'; 2 | import fs from 'fs-extra'; 3 | 4 | import { Test, TestingModule } from '@nestjs/testing'; 5 | 6 | import { ApiController } from '@kb-api'; 7 | import { ConfigService } from '@kb-config'; 8 | 9 | jest.mock('fs-extra'); 10 | jest.mock('find-root', () => () => 'app-root'); 11 | 12 | findRoot; 13 | 14 | describe('ApiController', () => { 15 | // const MockedAppService = mocked(AppService, true); 16 | let controller: ApiController; 17 | 18 | beforeEach(async () => { 19 | // Clears the record of calls to the mock constructor function and its methods 20 | // MockedAppService.mockClear(); 21 | const module: TestingModule = await Test.createTestingModule({ 22 | controllers: [ApiController], 23 | providers: [ 24 | { 25 | provide: ConfigService, 26 | useValue: { 27 | appRoot: 'app-root' 28 | } 29 | } 30 | ] 31 | }).compile(); 32 | 33 | controller = module.get(ApiController); 34 | }); 35 | 36 | it('should be defined', () => { 37 | expect(controller).toBeDefined(); 38 | }); 39 | 40 | it('should return package.json object', async () => { 41 | const packageInfo = { 42 | name: 'nice', 43 | description: 'nice', 44 | version: 'nice', 45 | license: 'nice', 46 | repository: 'nice', 47 | author: 'nice', 48 | bugs: 'nice' 49 | }; 50 | fs.readJSON = jest.fn().mockResolvedValue(packageInfo); 51 | jest.spyOn(fs, 'readJSON'); 52 | expect(await controller.getAPI()).toEqual(packageInfo); 53 | expect(fs.readJSON).toHaveBeenCalledTimes(1); 54 | expect(fs.readJSON).toHaveBeenCalledWith('app-root/package.json'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /server/src/api/api.controller.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { readJSON } from 'fs-extra'; 4 | import { chain } from 'lodash'; 5 | 6 | import { Controller, Get } from '@nestjs/common'; 7 | import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; 8 | 9 | import { WinstonLogger } from '@kibibit/nestjs-winston'; 10 | 11 | import { ConfigService } from '@kb-config'; 12 | import { KbMeasure } from '@kb-decorators'; 13 | import { ApiInfo } from '@kb-models'; 14 | 15 | @Controller('api') 16 | export class ApiController { 17 | readonly appRoot: string; 18 | private readonly logger = new WinstonLogger(ApiController.name); 19 | 20 | constructor(private readonly configService: ConfigService) { 21 | this.appRoot = this.configService.appRoot; 22 | } 23 | 24 | @Get() 25 | @ApiOperation({ summary: 'Get API Information' }) 26 | @ApiOkResponse({ 27 | description: 'Returns API info as a JSON', 28 | type: ApiInfo 29 | }) 30 | async getAPI() { 31 | const packageInfo = await readJSON( 32 | join(this.appRoot, './package.json') 33 | ); 34 | const details = new ApiInfo( 35 | chain(packageInfo) 36 | .pick([ 37 | 'name', 38 | 'description', 39 | 'version', 40 | 'license', 41 | 'repository', 42 | 'author', 43 | 'bugs' 44 | ]) 45 | .mapValues((val) => val.url ? val.url : val) 46 | .value() 47 | ); 48 | this.logger.info('Api information requested'); 49 | return details; 50 | } 51 | 52 | @Get('/nana') 53 | @ApiOperation({ 54 | deprecated: true 55 | }) 56 | @KbMeasure(ApiController.name) 57 | async deprecationTest() { 58 | return new Promise((resolve) => setTimeout(() => resolve('hello'), 6000)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/src/api/api.module.ts: -------------------------------------------------------------------------------- 1 | import { Logger, Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | 4 | import { ConfigModule, ConfigService } from '@kb-config'; 5 | 6 | import { 7 | createInMemoryDatabaseModule 8 | } from '../dev-tools/in-memory-database.module'; 9 | import { PullRequestModule } from './pull-request/pull-request.module'; 10 | import { RepoModule } from './repo/repo.module'; 11 | import { UserModule } from './user/user.module'; 12 | import { 13 | WebhookEventManagerModule 14 | } from './webhook-event-manager/webhook-event-manager.module'; 15 | import { ApiController } from './api.controller'; 16 | 17 | const config = new ConfigService(); 18 | @Module({ 19 | controllers: [ApiController], 20 | imports: [ 21 | config.dbUrl ? 22 | MongooseModule.forRoot(config.dbUrl, { 23 | useFindAndModify: false, 24 | useNewUrlParser: true, 25 | useUnifiedTopology: true 26 | }) : 27 | createInMemoryDatabaseModule(), 28 | UserModule, 29 | RepoModule, 30 | WebhookEventManagerModule, 31 | PullRequestModule, 32 | ConfigModule 33 | ] 34 | }) 35 | export class ApiModule { 36 | logger: Logger = new Logger('ApiModule'); 37 | 38 | constructor() { 39 | this.logger.log(config.dbUrl ? 40 | `Connecting to database: ${ config.dbUrl }` : 41 | 'No DB address given. Using in-memory DB' 42 | ); 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /server/src/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './api.controller'; 6 | export * from './api.module'; 7 | export * from './pull-request/pull-request.controller'; 8 | export * from './pull-request/pull-request.module'; 9 | export * from './pull-request/pull-request.service'; 10 | export * from './repo/repo.controller'; 11 | export * from './repo/repo.module'; 12 | export * from './repo/repo.service'; 13 | export * from './user/user.controller'; 14 | export * from './user/user.module'; 15 | export * from './user/user.service'; 16 | export * from './webhook-event-manager/webhook-event-manager.controller'; 17 | export * from './webhook-event-manager/webhook-event-manager.module'; 18 | export * from './webhook-event-manager/webhook-event-manager.service'; 19 | -------------------------------------------------------------------------------- /server/src/api/pull-request/pull-request.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | 5 | import { PullRequestController } from './pull-request.controller'; 6 | import { PullRequestService } from './pull-request.service'; 7 | 8 | describe('PullRequestController', () => { 9 | let controller: PullRequestController; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | controllers: [PullRequestController], 14 | providers: [{ 15 | provide: PullRequestService, 16 | useValue: { findAllRepos: noop, findByName: noop } 17 | }] 18 | }).compile(); 19 | 20 | controller = module.get(PullRequestController); 21 | }); 22 | 23 | it('should be defined', () => { 24 | expect(controller).toBeDefined(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /server/src/api/pull-request/pull-request.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, UseFilters } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | 4 | import { GetAll } from '@kb-decorators'; 5 | import { KbValidationExceptionFilter } from '@kb-filters'; 6 | import { PullRequest } from '@kb-models'; 7 | 8 | import { PullRequestService } from './pull-request.service'; 9 | 10 | @Controller('api/pull-request') 11 | @ApiTags('Pull Request') 12 | @UseFilters(new KbValidationExceptionFilter()) 13 | export class PullRequestController { 14 | constructor(private prService: PullRequestService) { } 15 | 16 | @GetAll(PullRequest) 17 | async getAll(): Promise { 18 | const allPRs = await this.prService.findAllAsync(); 19 | const allPRsParsed = allPRs.map((dbPR) => new PullRequest(dbPR.toObject())); 20 | 21 | return allPRsParsed; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/src/api/pull-request/pull-request.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Type } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | 4 | import { WinstonLogger } from '@kibibit/nestjs-winston'; 5 | 6 | import { ConfigService } from '@kb-config'; 7 | import { PullRequest } from '@kb-models'; 8 | 9 | import { PullRequestController } from './pull-request.controller'; 10 | import { PullRequestService } from './pull-request.service'; 11 | 12 | const devControllers: Type[] = [PullRequestController]; 13 | const logger = new WinstonLogger('PullRequestModule'); 14 | 15 | const config = new ConfigService(); 16 | const controllers = (() => { 17 | if (config.nodeEnv === 'production') { 18 | return []; 19 | } else { 20 | logger.log('Not running in production mode!'); 21 | logger.debug('Attaching Pull Request controller for development'); 22 | return devControllers; 23 | } 24 | })(); 25 | 26 | @Module({ 27 | imports: [ 28 | MongooseModule.forFeature([ 29 | { name: PullRequest.modelName, schema: PullRequest.schema } 30 | ]) 31 | ], 32 | providers: [PullRequestService], 33 | controllers, 34 | exports: [PullRequestService] 35 | }) 36 | export class PullRequestModule {} 37 | -------------------------------------------------------------------------------- /server/src/api/readme.md: -------------------------------------------------------------------------------- 1 | ## API 2 | 3 | Contains the main API of the application under `/api`. As a rule of thumbs, each part of the api should be a module of itself. -------------------------------------------------------------------------------- /server/src/api/repo/__snapshots__/repo.controller.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`RepoController should throw error if repo not found 1`] = `"Repo with name mockname not found"`; 4 | -------------------------------------------------------------------------------- /server/src/api/repo/repo.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | 5 | import { RepoService } from '@kb-api'; 6 | import { DtoMockGenerator } from '@kb-dev-tools'; 7 | 8 | import { RepoController } from './repo.controller'; 9 | 10 | describe('RepoController', () => { 11 | let controller: RepoController; 12 | let repoService: RepoService; 13 | 14 | beforeEach(async () => { 15 | const module: TestingModule = await Test.createTestingModule({ 16 | controllers: [RepoController], 17 | providers: [{ 18 | provide: RepoService, 19 | useValue: { findAllRepos: noop, findByName: noop } 20 | }] 21 | }).compile(); 22 | 23 | controller = module.get(RepoController); 24 | repoService = module.get(RepoService); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(controller).toBeDefined(); 29 | }); 30 | 31 | it('should call service on get all repos', async () => { 32 | const findAllResponse = DtoMockGenerator.repos(); 33 | 34 | // console.log('mock result: ', findAllResponse); 35 | 36 | const spyFindAll = jest.spyOn(repoService, 'findAllRepos') 37 | .mockImplementation(() => Promise.resolve(findAllResponse)); 38 | 39 | const result = await controller.getAllRepos(); 40 | 41 | expect(result).toEqual(findAllResponse); 42 | expect(spyFindAll).toHaveBeenCalledTimes(1); 43 | }); 44 | 45 | it('should get repo by name', async () => { 46 | const findByNameResponse = DtoMockGenerator.repo(); 47 | 48 | // console.log('mock result: ', findAllResponse); 49 | const spyFindByUsername = jest.spyOn(repoService, 'findByName') 50 | .mockImplementation(() => Promise.resolve(findByNameResponse)); 51 | 52 | const result = await controller.getRepo(findByNameResponse.name); 53 | 54 | expect(result).toEqual(findByNameResponse); 55 | expect(spyFindByUsername).toHaveBeenCalledTimes(1); 56 | }); 57 | 58 | it('should throw error if repo not found', async () => { 59 | const spyFindByName = jest.spyOn(repoService, 'findByName') 60 | .mockImplementation(() => Promise.resolve(undefined)); 61 | 62 | expect(controller.getRepo('mockname')) 63 | .rejects.toThrowErrorMatchingSnapshot(); 64 | expect(spyFindByName).toHaveBeenCalledTimes(1); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /server/src/api/repo/repo.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Logger, 4 | NotFoundException, 5 | Param, 6 | UseFilters 7 | } from '@nestjs/common'; 8 | import { ApiTags } from '@nestjs/swagger'; 9 | 10 | import { GetAll, GetOne } from '@kb-decorators'; 11 | import { KbValidationExceptionFilter } from '@kb-filters'; 12 | import { Repo } from '@kb-models'; 13 | 14 | import { RepoService } from './repo.service'; 15 | 16 | @Controller('api/repo') 17 | @ApiTags('repo') 18 | @UseFilters(new KbValidationExceptionFilter()) 19 | export class RepoController { 20 | private readonly logger = new Logger(RepoController.name); 21 | 22 | constructor(private readonly repoService: RepoService) {} 23 | 24 | @GetAll(Repo) 25 | async getAllRepos() { 26 | const repos = await this.repoService.findAllRepos(); 27 | 28 | return repos; 29 | } 30 | 31 | @GetOne(Repo, ':name') 32 | async getRepo(@Param('name') name: string) { 33 | const repo = await this.repoService.findByName(name); 34 | 35 | if (!repo) { 36 | throw new NotFoundException(`Repo with name ${ name } not found`); 37 | } 38 | 39 | // will show secret fields as well! 40 | this.logger.log('Full Repo'); 41 | // will log only public fields! 42 | this.logger.log(repo); 43 | // DANGER! WILL LOG EVERYTHING! 44 | // console.log(repo); 45 | 46 | // will only include exposed fields 47 | return repo; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/src/api/repo/repo.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | 4 | import { Repo } from '@kb-models'; 5 | 6 | import { RepoController } from './repo.controller'; 7 | import { RepoService } from './repo.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | MongooseModule.forFeature([ 12 | { name: Repo.modelName, schema: Repo.schema } 13 | ]) 14 | ], 15 | providers: [RepoService], 16 | controllers: [RepoController], 17 | exports: [RepoService] 18 | }) 19 | export class RepoModule {} 20 | -------------------------------------------------------------------------------- /server/src/api/repo/repo.service.ts: -------------------------------------------------------------------------------- 1 | import { ReturnModelType } from '@typegoose/typegoose'; 2 | 3 | import { Injectable } from '@nestjs/common'; 4 | import { InjectModel } from '@nestjs/mongoose'; 5 | 6 | import { BaseService } from '@kb-abstracts'; 7 | import { Repo } from '@kb-models'; 8 | 9 | @Injectable() 10 | export class RepoService extends BaseService { 11 | constructor( 12 | @InjectModel(Repo.modelName) 13 | private readonly repoModel: ReturnModelType 14 | ) { 15 | super(repoModel, Repo); 16 | } 17 | 18 | async findAllRepos(): Promise { 19 | const dbRepos = await this.findAll().exec(); 20 | 21 | return dbRepos.map((repo) => new Repo(repo.toObject())); 22 | } 23 | 24 | async findByName(name: string): Promise { 25 | const dbRepo = await this.findOne({ name }).exec(); 26 | 27 | if (!dbRepo) { 28 | return; 29 | } 30 | 31 | return new Repo(dbRepo.toObject()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/src/api/user/__snapshots__/user.controller.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`UserController should throw error if user not found 1`] = `"User with username mockusername not found"`; 4 | -------------------------------------------------------------------------------- /server/src/api/user/user.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | 5 | import { DtoMockGenerator } from '@kb-dev-tools'; 6 | 7 | import { UserController } from './user.controller'; 8 | import { UserService } from './user.service'; 9 | 10 | describe.only('UserController', () => { 11 | let controller: UserController; 12 | let userService: UserService; 13 | 14 | beforeEach(async () => { 15 | const module: TestingModule = await Test.createTestingModule({ 16 | controllers: [UserController], 17 | providers: [{ 18 | provide: UserService, 19 | useValue: { findByUsername: noop, findAllUsers: noop } 20 | }] 21 | }).compile(); 22 | 23 | controller = module.get(UserController); 24 | userService = module.get(UserService); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(controller).toBeDefined(); 29 | }); 30 | 31 | it('should call service on get all users', async () => { 32 | const findAllResponse = DtoMockGenerator.users(); 33 | 34 | // console.log('mock result: ', findAllResponse); 35 | 36 | const spyFindAll = jest.spyOn(userService, 'findAllUsers') 37 | .mockImplementation(() => Promise.resolve(findAllResponse)); 38 | 39 | const result = await controller.getAllUsers(); 40 | 41 | expect(result).toEqual(findAllResponse); 42 | expect(spyFindAll).toHaveBeenCalledTimes(1); 43 | }); 44 | 45 | it('should get user by username', async () => { 46 | const findByUsernameResponse = DtoMockGenerator.user(); 47 | 48 | // console.log('mock result: ', findAllResponse); 49 | const spyFindByUsername = jest.spyOn(userService, 'findByUsername') 50 | .mockImplementation(() => Promise.resolve(findByUsernameResponse)); 51 | 52 | const result = await controller.getUser(findByUsernameResponse.username); 53 | 54 | expect(result).toEqual(findByUsernameResponse); 55 | expect(spyFindByUsername).toHaveBeenCalledTimes(1); 56 | }); 57 | 58 | it('should throw error if user not found', async () => { 59 | const spyFindByUsername = jest.spyOn(userService, 'findByUsername') 60 | .mockImplementation(() => Promise.resolve(undefined)); 61 | 62 | expect(controller.getUser('mockusername')) 63 | .rejects.toThrowErrorMatchingSnapshot(); 64 | expect(spyFindByUsername).toHaveBeenCalledTimes(1); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /server/src/api/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Logger, 4 | NotFoundException, 5 | Param, 6 | UseFilters 7 | } from '@nestjs/common'; 8 | import { ApiTags } from '@nestjs/swagger'; 9 | 10 | import { GetAll, GetOne } from '@kb-decorators'; 11 | import { KbValidationExceptionFilter } from '@kb-filters'; 12 | import { User } from '@kb-models'; 13 | 14 | import { UserService } from './user.service'; 15 | 16 | @Controller('api/user') 17 | @ApiTags('user') 18 | @UseFilters(new KbValidationExceptionFilter()) 19 | export class UserController { 20 | private readonly logger = new Logger(UserController.name); 21 | 22 | constructor(private readonly userService: UserService) {} 23 | 24 | @GetAll(User) 25 | async getAllUsers() { 26 | const users = await this.userService.findAllUsers(); 27 | 28 | return users; 29 | } 30 | 31 | @GetOne(User, ':username') 32 | async getUser(@Param('username') username: string) { 33 | const user = await this.userService.findByUsername(username); 34 | 35 | if (!user) { 36 | throw new NotFoundException(`User with username ${ username } not found`); 37 | } 38 | 39 | // will show secret fields as well! 40 | this.logger.log('Full User'); 41 | // will log only public fields! 42 | this.logger.log(user); 43 | // DANGER! WILL LOG EVERYTHING! 44 | // console.log(user); 45 | 46 | // will only include exposed fields 47 | return user; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/src/api/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | 4 | import { User } from '@kb-models'; 5 | 6 | import { UserController } from './user.controller'; 7 | import { UserService } from './user.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | MongooseModule.forFeature([ 12 | { name: User.modelName, schema: User.schema } 13 | ]) 14 | ], 15 | providers: [UserService], 16 | controllers: [UserController], 17 | exports: [UserService] 18 | }) 19 | export class UserModule {} 20 | -------------------------------------------------------------------------------- /server/src/api/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { ReturnModelType } from '@typegoose/typegoose'; 2 | 3 | import { Injectable } from '@nestjs/common'; 4 | import { InjectModel } from '@nestjs/mongoose'; 5 | 6 | import { BaseService } from '@kb-abstracts'; 7 | import { User } from '@kb-models'; 8 | 9 | @Injectable() 10 | export class UserService extends BaseService { 11 | constructor( 12 | @InjectModel(User.modelName) 13 | private readonly userModel: ReturnModelType 14 | ) { 15 | super(userModel, User); 16 | } 17 | 18 | async findAllUsers(): Promise { 19 | const dbUsers = await this.findAll().exec(); 20 | 21 | return dbUsers.map((user) => new User(user.toObject())); 22 | } 23 | 24 | async findByUsername(username: string): Promise { 25 | const dbUser = await this.findOne({ username }).exec(); 26 | 27 | if (!dbUser) { 28 | return; 29 | } 30 | 31 | return new User(dbUser.toObject()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/src/api/webhook-event-manager/webhook-event-manager.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { 4 | WebhookEventManagerController 5 | } from './webhook-event-manager.controller'; 6 | import { WebhookEventManagerService } from './webhook-event-manager.service'; 7 | 8 | describe('WebhookEventManagerController', () => { 9 | let controller: WebhookEventManagerController; 10 | 11 | beforeEach(async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | providers: [{ 14 | provide: WebhookEventManagerService, 15 | useValue: {} 16 | }], 17 | controllers: [WebhookEventManagerController] 18 | }).compile(); 19 | 20 | controller = module 21 | .get(WebhookEventManagerController); 22 | }); 23 | 24 | it('should be defined', () => { 25 | expect(controller).toBeDefined(); 26 | }); 27 | 28 | it.todo('add more tests...'); 29 | }); 30 | -------------------------------------------------------------------------------- /server/src/api/webhook-event-manager/webhook-event-manager.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Headers, 5 | Logger, 6 | Post, 7 | UseFilters 8 | } from '@nestjs/common'; 9 | import { ApiOperation, ApiTags } from '@nestjs/swagger'; 10 | 11 | import { KbValidationExceptionFilter } from '@kb-filters'; 12 | import { IGithubPullRequestEvent } from '@kb-interfaces'; 13 | 14 | import { WebhookEventManagerService } from './webhook-event-manager.service'; 15 | 16 | @Controller('api/webhook-event-manager') 17 | @ApiTags('Webhook Event Manager') 18 | @UseFilters(new KbValidationExceptionFilter()) 19 | export class WebhookEventManagerController { 20 | private readonly logger = new Logger(WebhookEventManagerController.name); 21 | 22 | constructor( 23 | private readonly webhookEventManagerService: WebhookEventManagerService 24 | ) {} 25 | 26 | @Post() 27 | @ApiOperation({ summary: 'Recieve GitHub Webhooks' }) 28 | async recieveGitHubWebhooks( 29 | @Headers('x-github-event') githubEvent: string, 30 | @Body() eventBody: IGithubPullRequestEvent 31 | ): Promise { 32 | const eventName = await this.webhookEventManagerService 33 | .notifyAchievements(githubEvent, eventBody); 34 | 35 | return eventName; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/src/api/webhook-event-manager/webhook-event-manager.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PullRequestModule } from '../pull-request/pull-request.module'; 4 | import { RepoModule } from '../repo/repo.module'; 5 | import { UserModule } from '../user/user.module'; 6 | import { 7 | WebhookEventManagerController 8 | } from './webhook-event-manager.controller'; 9 | import { WebhookEventManagerService } from './webhook-event-manager.service'; 10 | 11 | @Module({ 12 | imports: [ UserModule, RepoModule, PullRequestModule ], 13 | controllers: [WebhookEventManagerController], 14 | providers: [ 15 | WebhookEventManagerService 16 | ] 17 | }) 18 | export class WebhookEventManagerModule {} 19 | -------------------------------------------------------------------------------- /server/src/app-root.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/achievibit/727415636b31e44079b4cc16c6352ed50741fbd2/server/src/app-root.ts -------------------------------------------------------------------------------- /server/src/app/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { mockResponse } from 'jest-mock-req-res'; 2 | 3 | import { Test, TestingModule } from '@nestjs/testing'; 4 | 5 | import { AppController } from '@kb-app'; 6 | import { ConfigModule } from '@kb-config'; 7 | 8 | describe('AppController', () => { 9 | let appController: AppController; 10 | 11 | beforeEach(async () => { 12 | const app: TestingModule = await Test.createTestingModule({ 13 | imports: [ ConfigModule ], 14 | controllers: [AppController] 15 | }).compile(); 16 | 17 | appController = app.get(AppController); 18 | }); 19 | 20 | describe('root', () => { 21 | it('should return an HTML page', async () => { 22 | const mocRes = mockResponse(); 23 | appController.sendWebClient(mocRes); 24 | expect(mocRes.sendFile.mock.calls.length).toBe(1); 25 | const param = mocRes.sendFile.mock.calls[0][0] as string; 26 | expect(param.endsWith('dist/client/index.html')).toBeTruthy(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /server/src/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { Response } from 'express'; 4 | 5 | import { Controller, Get, Res } from '@nestjs/common'; 6 | import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; 7 | 8 | import { ConfigService } from '@kb-config'; 9 | 10 | @Controller() 11 | export class AppController { 12 | constructor(private readonly configService: ConfigService) {} 13 | 14 | @Get() 15 | @ApiOperation({ summary: 'Get Web Client (HTML)' }) 16 | @ApiOkResponse({ 17 | description: 'Returns the Web Client\'s HTML File' 18 | }) 19 | sendWebClient(@Res() res: Response): void { 20 | res.sendFile(join(this.configService.appRoot, '/dist/client/index.html')); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Module } from '@nestjs/common'; 3 | import { ScheduleModule } from '@nestjs/schedule'; 4 | 5 | import { WinstonModule } from '@kibibit/nestjs-winston'; 6 | 7 | import { ApiModule } from '@kb-api'; 8 | import { ConfigModule } from '@kb-config'; 9 | import { EventsGateway, EventsModule } from '@kb-events'; 10 | import { TasksModule } from '@kb-tasks'; 11 | 12 | import { AppController } from './app.controller'; 13 | 14 | @Module({ 15 | imports: [ 16 | WinstonModule.forRoot({}), 17 | ApiModule, 18 | ScheduleModule.forRoot(), 19 | EventsModule, 20 | ConfigModule, 21 | TasksModule 22 | ], 23 | controllers: [AppController], 24 | providers: [EventsGateway] 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /server/src/app/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './app.controller'; 6 | export * from './app.module'; 7 | -------------------------------------------------------------------------------- /server/src/app/readme.md: -------------------------------------------------------------------------------- 1 | ## APP 2 | 3 | The main app definition. Everything is defined here (all the submodules are imported in this module as well). -------------------------------------------------------------------------------- /server/src/bootstrap-application.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { ValidationPipe } from '@nestjs/common'; 4 | import { NestFactory } from '@nestjs/core'; 5 | import { NestExpressApplication } from '@nestjs/platform-express'; 6 | import { WsAdapter } from '@nestjs/platform-ws'; 7 | 8 | import { terminalConsoleLogo } from '@kibibit/consologo'; 9 | import { WINSTON_MODULE_NEST_PROVIDER } from '@kibibit/nestjs-winston'; 10 | 11 | import { AppModule } from '@kb-app'; 12 | import { ConfigService } from '@kb-config'; 13 | import { KbNotFoundExceptionFilter } from '@kb-filters'; 14 | 15 | import { Swagger } from './swagger'; 16 | 17 | const config = new ConfigService(); 18 | const appRoot = config.appRoot; 19 | 20 | export async function bootstrap(): Promise { 21 | terminalConsoleLogo(config.packageDetails.name, [ 22 | `version ${ config.packageDetails.version }` 23 | ]); 24 | const app = await NestFactory.create(AppModule, { 25 | logger: false 26 | }); 27 | app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); 28 | app.useWebSocketAdapter(new WsAdapter(app)); 29 | app.useGlobalFilters(new KbNotFoundExceptionFilter(appRoot)); 30 | app.useGlobalPipes(new ValidationPipe()); 31 | app.useStaticAssets(join(appRoot, './dist/client')); 32 | 33 | await Swagger.addSwagger(app); 34 | 35 | return app; 36 | } 37 | -------------------------------------------------------------------------------- /server/src/config/__mocks__/winston.config.ts: -------------------------------------------------------------------------------- 1 | 2 | import winston, { createLogger } from 'winston'; 3 | 4 | import { 5 | winstonInstance 6 | } from '@kibibit/nestjs-winston'; 7 | 8 | export function initializeWinston() { 9 | winstonInstance.logger = createLogger({ 10 | transports: [ 11 | new winston.transports.Console({ 12 | silent: true, 13 | level: 'debug' 14 | }) 15 | ] 16 | }); 17 | } 18 | 19 | initializeWinston(); 20 | -------------------------------------------------------------------------------- /server/src/config/__snapshots__/config.service.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ConfigService Forced Singleton and baypass should set default values to everything that needs one 1`] = ` 4 | Object { 5 | "dbUrl": undefined, 6 | "deletePRsHealthId": undefined, 7 | "githubAccessToken": undefined, 8 | "nodeEnv": "development", 9 | "port": 10101, 10 | "webhookDestinationUrl": "events", 11 | "webhookProxyUrl": "https://smee.io/achievibit-test", 12 | } 13 | `; 14 | 15 | exports[`ConfigService Validations dbUrl should REJECT non-mongodb URLS 1`] = ` 16 | " 17 | ============================ 18 | property: dbUrl 19 | value: https://google.com/ 20 | ============================ 21 | - dbUrl should be a valid mongodb URL 22 | 23 | " 24 | `; 25 | 26 | exports[`ConfigService Validations nodeEnv should REJECT other values 1`] = ` 27 | " 28 | ========================== 29 | property: nodeEnv 30 | value: value_not_allowed 31 | ========================== 32 | - nodeEnv must be one of the following values: development, production, test, devcontainer 33 | 34 | " 35 | `; 36 | 37 | exports[`ConfigService Validations nodeEnv should REJECT other values 2`] = ` 38 | " 39 | =================== 40 | property: nodeEnv 41 | value: 4 42 | =================== 43 | - nodeEnv must be one of the following values: development, production, test, devcontainer 44 | - nodeEnv must be a string 45 | 46 | " 47 | `; 48 | 49 | exports[`ConfigService Validations port should REJECT values other than numbers 1`] = ` 50 | " 51 | ================ 52 | property: port 53 | value: hello 54 | ================ 55 | - port must be a number conforming to the specified constraints 56 | 57 | " 58 | `; 59 | 60 | exports[`ConfigService Validations port should REJECT values other than numbers 2`] = ` 61 | " 62 | ======================== 63 | property: port 64 | value: [object Object] 65 | ======================== 66 | - port must be a number conforming to the specified constraints 67 | 68 | " 69 | `; 70 | 71 | exports[`ConfigService Validations webhookProxyUrl should REJECT non-URLS 1`] = ` 72 | " 73 | =========================== 74 | property: webhookProxyUrl 75 | value: hello world 76 | =========================== 77 | - webhookProxyUrl must be an URL address 78 | - webhookProxyUrl must match /^https:\\\\/\\\\/(?:www\\\\.)?smee\\\\.io\\\\/[a-zA-Z0-9_-]+\\\\/?/ regular expression 79 | 80 | " 81 | `; 82 | 83 | exports[`ConfigService Validations webhookProxyUrl should REJECT non-smee URLS 1`] = ` 84 | " 85 | ============================ 86 | property: webhookProxyUrl 87 | value: https://google.com/ 88 | ============================ 89 | - webhookProxyUrl must match /^https:\\\\/\\\\/(?:www\\\\.)?smee\\\\.io\\\\/[a-zA-Z0-9_-]+\\\\/?/ regular expression 90 | 91 | " 92 | `; 93 | -------------------------------------------------------------------------------- /server/src/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { ConfigService } from './config.service'; 4 | 5 | @Module({ 6 | providers: [ 7 | { 8 | provide: ConfigService, 9 | useValue: new ConfigService() 10 | } 11 | ], 12 | exports: [ ConfigService ] 13 | }) 14 | export class ConfigModule { } 15 | -------------------------------------------------------------------------------- /server/src/config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './achievibit-config.model'; 6 | export * from './config.module'; 7 | export * from './config.service'; 8 | export * from './json-schema.validator'; 9 | export * from './winston.config'; 10 | -------------------------------------------------------------------------------- /server/src/config/json-schema.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ValidatorConstraint, 3 | ValidatorConstraintInterface 4 | } from 'class-validator'; 5 | 6 | @ValidatorConstraint({ name: 'JsonSchema', async: false }) 7 | export class JsonSchema implements ValidatorConstraintInterface { 8 | validate() { 9 | return true; 10 | } 11 | 12 | defaultMessage() { 13 | return ''; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/config/winston.config.ts: -------------------------------------------------------------------------------- 1 | import { basename, join } from 'path'; 2 | 3 | import bytes from 'bytes'; 4 | import winston, { createLogger } from 'winston'; 5 | 6 | import { 7 | nestLike, 8 | winstonInstance 9 | } from '@kibibit/nestjs-winston'; 10 | 11 | const fiveMegaBytes = bytes('5MB'); 12 | 13 | const omitMeta = [ 14 | 'file', 15 | 'env' 16 | ]; 17 | 18 | export function initializeWinston(rootFolder: string) { 19 | winstonInstance.logger = createLogger({ 20 | transports: [ 21 | new winston.transports.Console({ 22 | level: 'debug', 23 | format: winston.format.combine( 24 | winston.format.timestamp(), 25 | winston.format.ms(), 26 | nestLike('achievibit', omitMeta), 27 | ) 28 | }), 29 | new winston.transports.File({ 30 | level: 'debug', 31 | filename: join(rootFolder, '/logs/server.log'), 32 | maxsize: fiveMegaBytes, 33 | maxFiles: 5, 34 | tailable: true 35 | }) 36 | ], 37 | exceptionHandlers: [ 38 | new winston.transports.File({ 39 | level: 'debug', 40 | filename: join(rootFolder, '/logs/exceptions.log'), 41 | maxsize: fiveMegaBytes, 42 | maxFiles: 5, 43 | tailable: true 44 | }) 45 | ], 46 | handleExceptions: true, 47 | format: winston.format.combine( 48 | winston.format((info) => { 49 | info.env = process.env.NODE_ENV; 50 | const filename = getCallerFile(); 51 | 52 | if (filename) { 53 | info.file = basename(getCallerFile()); 54 | } 55 | return info; 56 | })(), 57 | winston.format.timestamp(), 58 | winston.format.splat(), 59 | winston.format.json() 60 | ) 61 | }); 62 | 63 | function getCallerFile(): string { 64 | try { 65 | const err = new Error(); 66 | let callerfile; 67 | Error.prepareStackTrace = function (_err, stack) { return stack; }; 68 | const currentfile = (err.stack as any).shift().getFileName(); 69 | 70 | while (err.stack.length) { 71 | callerfile = (err.stack as any).shift().getFileName(); 72 | 73 | if (currentfile !== callerfile && 74 | !callerfile.includes('node_modules') && 75 | !callerfile.includes('internal/process')) return callerfile; 76 | } 77 | } catch (err) { } 78 | return ''; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/src/decorators/get-all.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applyDecorators, 3 | ClassSerializerInterceptor, 4 | Get, 5 | UseInterceptors 6 | } from '@nestjs/common'; 7 | import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | export function GetAll(model: any, path?: string | string[]) { 11 | return applyDecorators( 12 | Get(path), 13 | ApiOperation({ summary: `Get all ${ model.name }s` }), 14 | ApiOkResponse({ description: `Return a list of all ${ model.name }s` }), 15 | UseInterceptors(ClassSerializerInterceptor) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /server/src/decorators/get-one.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applyDecorators, 3 | ClassSerializerInterceptor, 4 | Get, 5 | UseInterceptors 6 | } from '@nestjs/common'; 7 | import { 8 | ApiBadRequestResponse, 9 | ApiNotFoundResponse, 10 | ApiOkResponse, 11 | ApiOperation 12 | } from '@nestjs/swagger'; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | export function GetOne(type: any, path?: string | string[]) { 16 | return applyDecorators( 17 | Get(path), 18 | ApiOperation({ summary: `Get an existing ${ type.name }` }), 19 | ApiOkResponse({ description: `Return a single ${ type.name }`, type }), 20 | ApiNotFoundResponse({ description: `${ type.name } not found` }), 21 | ApiBadRequestResponse({ description: 'Invalid identifier supplied' }), 22 | UseInterceptors(ClassSerializerInterceptor) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /server/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './get-all.decorator'; 6 | export * from './get-one.decorator'; 7 | export * from './kb-api-validation-error-response.decorator'; 8 | export * from './kb-delete.decorator'; 9 | export * from './kb-meature.decorator'; 10 | export * from './kb-patch.decorator'; 11 | export * from './kb-post.decorator'; 12 | export * from './kb-put.decorator'; 13 | export * from './task-health-check.decorator'; 14 | -------------------------------------------------------------------------------- /server/src/decorators/kb-api-validation-error-response.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators } from '@nestjs/common'; 2 | import { ApiResponse } from '@nestjs/swagger'; 3 | 4 | import { PublicError } from '@kb-models'; 5 | 6 | export const KbApiValidateErrorResponse = () => { 7 | return applyDecorators( 8 | ApiResponse({ 9 | description: 'Some validation error as accured on the given model', 10 | status: 405, 11 | type: PublicError 12 | }) 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /server/src/decorators/kb-delete.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applyDecorators, 3 | ClassSerializerInterceptor, 4 | Delete, 5 | UseInterceptors 6 | } from '@nestjs/common'; 7 | import { 8 | ApiBadRequestResponse, 9 | ApiNotFoundResponse, 10 | ApiOkResponse, 11 | ApiOperation 12 | } from '@nestjs/swagger'; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | export function KbDelete(type: any, path?: string | string[]) { 16 | return applyDecorators( 17 | Delete(path), 18 | ApiOperation({ 19 | summary: `Delete an existing ${ type.name }` 20 | }), 21 | ApiOkResponse({ type: type, description: `${ type.name } deleted` }), 22 | ApiNotFoundResponse({ 23 | description: `${ type.name } not found` 24 | }), 25 | ApiBadRequestResponse({ description: 'Invalid identifier supplied' }), 26 | UseInterceptors(ClassSerializerInterceptor) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /server/src/decorators/kb-meature.decorator.ts: -------------------------------------------------------------------------------- 1 | import { WinstonLogger } from '@kibibit/nestjs-winston'; 2 | 3 | const logger = new WinstonLogger('KbMeasure'); 4 | 5 | export const KbMeasure = (controlerName?: string) => ( 6 | target: unknown, 7 | propertyKey: string, 8 | descriptor: PropertyDescriptor 9 | ) => { 10 | const originalMethod = descriptor.value; 11 | 12 | descriptor.value = async function (...args) { 13 | logger.verbose(generateLogMessagge('START')); 14 | const start = logger.startTimer(); 15 | const result = await Promise.resolve(originalMethod.apply(this, args)); 16 | start.done({ 17 | level: 'verbose', 18 | message: generateLogMessagge('END') 19 | }); 20 | return result; 21 | 22 | function generateLogMessagge(msg: string) { 23 | return [ 24 | `${ controlerName ? controlerName + '.' : '' }${ originalMethod.name }`, 25 | `(${ args && args.length ? '...' : '' }) ${ msg }` 26 | ].join(''); 27 | } 28 | }; 29 | 30 | return descriptor; 31 | }; 32 | -------------------------------------------------------------------------------- /server/src/decorators/kb-patch.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applyDecorators, 3 | ClassSerializerInterceptor, 4 | Patch, 5 | UseInterceptors 6 | } from '@nestjs/common'; 7 | import { 8 | ApiBadRequestResponse, 9 | ApiNotFoundResponse, 10 | ApiOkResponse, 11 | ApiOperation 12 | } from '@nestjs/swagger'; 13 | 14 | import { KbApiValidateErrorResponse } from '@kb-decorators'; 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | export function KbPatch(type: any, path?: string | string[]) { 18 | return applyDecorators( 19 | Patch(path), 20 | ApiOperation({ 21 | summary: `Update an existing ${ type.name }`, 22 | description: `Expects a partial ${ type.name }` 23 | }), 24 | ApiOkResponse({ type: type, description: `${ type.name } updated` }), 25 | ApiNotFoundResponse({ 26 | description: `${ type.name } not found` 27 | }), 28 | ApiBadRequestResponse({ description: 'Invalid identifier supplied' }), 29 | KbApiValidateErrorResponse(), 30 | UseInterceptors(ClassSerializerInterceptor) 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /server/src/decorators/kb-post.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applyDecorators, 3 | ClassSerializerInterceptor, 4 | Post, 5 | UseInterceptors 6 | } from '@nestjs/common'; 7 | import { ApiCreatedResponse, ApiOperation } from '@nestjs/swagger'; 8 | 9 | import { KbApiValidateErrorResponse } from '@kb-decorators'; 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | export function KbPost(type: any, path?: string | string[]) { 13 | return applyDecorators( 14 | Post(path), 15 | ApiOperation({ summary: `Create a new ${ type.name }` }), 16 | ApiCreatedResponse({ 17 | description: `The ${ type.name } has been successfully created.`, 18 | type 19 | }), 20 | KbApiValidateErrorResponse(), 21 | UseInterceptors(ClassSerializerInterceptor) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /server/src/decorators/kb-put.decorator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applyDecorators, 3 | ClassSerializerInterceptor, 4 | Put, 5 | UseInterceptors 6 | } from '@nestjs/common'; 7 | import { 8 | ApiBadRequestResponse, 9 | ApiNotFoundResponse, 10 | ApiOkResponse, 11 | ApiOperation 12 | } from '@nestjs/swagger'; 13 | 14 | import { KbApiValidateErrorResponse } from '@kb-decorators'; 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | export function KbPut(type: any, path?: string | string[]) { 18 | return applyDecorators( 19 | Put(path), 20 | ApiOperation({ 21 | summary: `Update an existing ${ type.name }`, 22 | description: `Expects a full ${ type.name }` 23 | }), 24 | ApiOkResponse({ type: type, description: `${ type.name } updated` }), 25 | ApiNotFoundResponse({ 26 | description: `${ type.name } not found` 27 | }), 28 | ApiBadRequestResponse({ description: 'Invalid identifier supplied' }), 29 | KbApiValidateErrorResponse(), 30 | UseInterceptors(ClassSerializerInterceptor) 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /server/src/decorators/readme.md: -------------------------------------------------------------------------------- 1 | ## DECORATORS 2 | 3 | Mostly using `applyDecorators` to combine decorators that are commonly being used together across the project -------------------------------------------------------------------------------- /server/src/decorators/task-health-check.decorator.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // import { SetMetadata } from '@nestjs/common'; 4 | 5 | export const TaskHealthCheck = function (healthCheckId?: string) { 6 | return function ( 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | target: any, 9 | propertyKey: string, 10 | descriptor: PropertyDescriptor 11 | ) { 12 | const originalMethod = descriptor.value; 13 | descriptor.value = async function(...args) { 14 | await originalMethod.apply(this, args); 15 | if (healthCheckId) { 16 | await pingHealthCheck(healthCheckId); 17 | } 18 | }; 19 | 20 | return descriptor; 21 | }; 22 | }; 23 | 24 | async function pingHealthCheck(healthCheckId: string) { 25 | await axios.get(`https://hc-ping.com/${ healthCheckId }`); 26 | } 27 | -------------------------------------------------------------------------------- /server/src/dev-tools/common-mocks.ts: -------------------------------------------------------------------------------- 1 | import { Exclude, Expose } from 'class-transformer'; 2 | import { prop as PersistInDb, ReturnModelType } from '@typegoose/typegoose'; 3 | 4 | import { ArgumentsHost, Injectable } from '@nestjs/common'; 5 | import { InjectModel } from '@nestjs/mongoose'; 6 | 7 | import { BaseModel, BaseService } from '@kb-abstracts'; 8 | 9 | export const mockResponse = { 10 | status: jest.fn().mockReturnThis(), 11 | json: jest.fn().mockReturnThis(), 12 | sendFile: jest.fn().mockReturnThis(), 13 | mockClear: () => { 14 | mockResponse.status.mockClear(); 15 | mockResponse.json.mockClear(); 16 | mockResponse.sendFile.mockClear(); 17 | } 18 | }; 19 | 20 | export const hostMock = (req, res, roles?: string[]): ArgumentsHost => { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | const ctx: any = {}; 23 | ctx.switchToHttp = jest.fn().mockReturnValue({ 24 | getRequest: () => req, 25 | getResponse: () => res 26 | }); 27 | ctx.getHandler = jest.fn().mockReturnValue({ roles }); 28 | 29 | return ctx; 30 | }; 31 | 32 | @Exclude() 33 | export class MockModel extends BaseModel { 34 | @PersistInDb() 35 | mockPrivateAttribute: string; 36 | 37 | @Expose() 38 | @PersistInDb({ required: true }) 39 | mockAttribute: string; 40 | 41 | @Exclude() 42 | @PersistInDb() 43 | updatedDate?: Date; 44 | 45 | constructor(partial: Partial = {}) { 46 | super(); 47 | Object.assign(this, partial); 48 | } 49 | } 50 | 51 | @Injectable() 52 | export class MockService extends BaseService { 53 | constructor( 54 | @InjectModel(MockModel.modelName) 55 | private readonly injectedModel: ReturnModelType 56 | ) { 57 | super(injectedModel, MockModel); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/src/dev-tools/in-memory-database.module.ts: -------------------------------------------------------------------------------- 1 | import { MongoMemoryServer } from 'mongodb-memory-server'; 2 | import mongoose from 'mongoose'; 3 | 4 | import { MongooseModule, MongooseModuleOptions } from '@nestjs/mongoose'; 5 | 6 | let mongod: MongoMemoryServer; 7 | 8 | export const createInMemoryDatabaseModule = 9 | (options: MongooseModuleOptions = {}) => MongooseModule.forRootAsync({ 10 | useFactory: async () => { 11 | mongod = new MongoMemoryServer(); 12 | const mongoUri = await mongod.getUri(); 13 | return { 14 | uri: mongoUri, 15 | ...options 16 | }; 17 | } 18 | }); 19 | 20 | export const closeInMemoryDatabaseConnection = async () => { 21 | await mongoose.disconnect(); 22 | if (mongod) await mongod.stop(); 23 | }; 24 | -------------------------------------------------------------------------------- /server/src/dev-tools/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './common-mocks'; 6 | export * from './dto.mock-generator'; 7 | export * from './in-memory-database.module'; 8 | export * from './captured-events/pull-request-assignee-added.event'; 9 | export * from './captured-events/pull-request-assignee-removed.event'; 10 | export * from './captured-events/pull-request-closed.event'; 11 | export * from './captured-events/pull-request-created-organization.event'; 12 | export * from './captured-events/pull-request-created.event'; 13 | export * from './captured-events/pull-request-edited.event'; 14 | export * from './captured-events/pull-request-label-added.event'; 15 | export * from './captured-events/pull-request-label-removed.event'; 16 | export * from './captured-events/pull-request-labels-initialized.event'; 17 | export * from './captured-events/pull-request-merged.event'; 18 | export * from './captured-events/pull-request-review-comment-added.event'; 19 | export * from './captured-events/pull-request-review-comment-deleted.event'; 20 | export * from './captured-events/pull-request-review-comment-edited.event'; 21 | export * from './captured-events/pull-request-review-request-added.event'; 22 | export * from './captured-events/pull-request-review-request-removed.event'; 23 | export * from './captured-events/pull-request-review-submitted.event'; 24 | export * from './captured-events/review-comment-added.event'; 25 | export * from './captured-events/review-comment-edited.event'; 26 | export * from './captured-events/review-comment-removed.event'; 27 | export * from './captured-events/review-submitted.event'; 28 | export * from './captured-events/wehbook-ping.event'; 29 | -------------------------------------------------------------------------------- /server/src/engines/github.engine.spec.ts: -------------------------------------------------------------------------------- 1 | describe('GitHub Engine', () => { 2 | it.todo('should create user and repo on new connection'); 3 | it.todo('should create user, repo, & pr on new pull request opened'); 4 | it.todo('should edit pr on new pull request edited'); 5 | }); 6 | -------------------------------------------------------------------------------- /server/src/engines/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './github.engine'; 6 | -------------------------------------------------------------------------------- /server/src/errors/config.errors.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from 'class-validator'; 2 | import { times, values } from 'lodash'; 3 | 4 | export class ConfigValidationError extends Error { 5 | constructor(validationErrors: ValidationError[]) { 6 | const message = validationErrors 7 | .map((validationError) => { 8 | const productLine = ` property: ${ validationError.property } `; 9 | const valueLine = ` value: ${ validationError.value } `; 10 | const longerLineLength = Math.max(productLine.length, valueLine.length); 11 | const deco = times(longerLineLength, () => '=').join(''); 12 | return [ 13 | '', 14 | deco, 15 | ` property: ${ validationError.property }`, 16 | ` value: ${ validationError.value }`, 17 | deco, 18 | values(validationError.constraints) 19 | .map((value) => ` - ${ value }`).join('\n') 20 | ].join('\n'); 21 | }).join('') + '\n\n'; 22 | 23 | super(message); 24 | this.name = 'ConfigValidationError'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './config.errors'; 6 | -------------------------------------------------------------------------------- /server/src/events/events.gateway.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { EventsGateway } from '@kb-events'; 4 | 5 | describe('EventsGateway', () => { 6 | let gateway: EventsGateway; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [EventsGateway] 11 | }).compile(); 12 | 13 | gateway = module.get(EventsGateway); 14 | }); 15 | 16 | it('should be defined', () => { 17 | expect(gateway).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /server/src/events/events.gateway.ts: -------------------------------------------------------------------------------- 1 | import { from, Observable } from 'rxjs'; 2 | import { map } from 'rxjs/operators'; 3 | import { Server } from 'socket.io'; 4 | 5 | import { Logger } from '@nestjs/common'; 6 | import { 7 | MessageBody, 8 | OnGatewayConnection, 9 | SubscribeMessage, 10 | WebSocketGateway, 11 | WebSocketServer, 12 | WsResponse 13 | } from '@nestjs/websockets'; 14 | 15 | @WebSocketGateway() 16 | export class EventsGateway implements OnGatewayConnection { 17 | private logger: Logger = new Logger('AppGateway'); 18 | 19 | @WebSocketServer() 20 | server: Server; 21 | 22 | constructor() { 23 | this.logger.log('testing, testing...'); 24 | } 25 | 26 | @SubscribeMessage('message') 27 | handleMessage(@MessageBody() data: unknown): WsResponse { 28 | this.logger.log('got here!'); 29 | 30 | return { 31 | event: 'message-response', 32 | data 33 | }; 34 | } 35 | 36 | @SubscribeMessage('events') 37 | onEvent(/* @MessageBody() data: unknown */): Observable> { 38 | const event = 'events'; 39 | const response = [1, 2, 3]; 40 | 41 | return from(response).pipe( 42 | map(data => ({ event, data })) 43 | ); 44 | } 45 | 46 | handleDisconnect() { 47 | this.logger.log('Client disconnected:'); 48 | // console.log(client); 49 | } 50 | 51 | handleConnection() { 52 | this.logger.log('Client connected:'); 53 | // console.log(client); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/src/events/events.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { EventsGateway } from './events.gateway'; 4 | 5 | @Module({ 6 | providers: [EventsGateway] 7 | }) 8 | export class EventsModule {} 9 | -------------------------------------------------------------------------------- /server/src/events/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './events.gateway'; 6 | export * from './events.module'; 7 | -------------------------------------------------------------------------------- /server/src/events/readme.md: -------------------------------------------------------------------------------- 1 | ## EVENTS 2 | 3 | This module is in charge of everything related to async events on top of websockets -------------------------------------------------------------------------------- /server/src/filters/__snapshots__/kb-not-found-exception.filter.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`NotFoundExceptionFilterFilter should return HTML file for undefined routes not inside /api/ 1`] = `"app-root/dist/client/index.html"`; 4 | 5 | exports[`NotFoundExceptionFilterFilter should return exepction if route starts with /api/ 1`] = ` 6 | Object { 7 | "error": "test title", 8 | "name": "Error", 9 | "statusCode": 404, 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /server/src/filters/__snapshots__/kb-validation-exception.filter.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`KbValidationExceptionFilter should return pretty validation errors 1`] = ` 4 | PublicError { 5 | "error": "Bad Request", 6 | "name": "BadRequestException", 7 | "path": "https://server.com/mock-api-path", 8 | "statusCode": 405, 9 | "timestamp": "2000-05-04T00:00:00.000Z", 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /server/src/filters/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './kb-not-found-exception.filter'; 6 | export * from './kb-validation-exception.filter'; 7 | -------------------------------------------------------------------------------- /server/src/filters/kb-not-found-exception.filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus, NotFoundException } from '@nestjs/common'; 2 | 3 | import { hostMock, mockResponse } from '@kb-dev-tools'; 4 | import { KbNotFoundExceptionFilter } from '@kb-filters'; 5 | 6 | describe('NotFoundExceptionFilterFilter', () => { 7 | beforeEach(() => { 8 | mockResponse.mockClear(); 9 | }); 10 | 11 | it('should be defined', () => { 12 | expect(new KbNotFoundExceptionFilter('')).toBeDefined(); 13 | }); 14 | 15 | it('should return exepction if route starts with /api/', async () => { 16 | const mockRequest = { 17 | path: '/api/pizza' 18 | }; 19 | const host = hostMock(mockRequest, mockResponse); 20 | const filter = new KbNotFoundExceptionFilter(''); 21 | filter.catch(new NotFoundException('test title', 'test description'), host); 22 | expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.NOT_FOUND); 23 | expect(mockResponse.json).toHaveBeenCalledTimes(1); 24 | expect(mockResponse.json.mock.calls[0][0]).toMatchSnapshot(); 25 | }); 26 | 27 | it('should return HTML file for undefined routes not inside /api/', 28 | async () => { 29 | const mockRequest = { 30 | path: '/pizza' 31 | }; 32 | const host = hostMock(mockRequest, mockResponse); 33 | const filter = new KbNotFoundExceptionFilter('app-root'); 34 | filter.catch(new NotFoundException('test title', 'test description'), host); 35 | expect(mockResponse.sendFile).toHaveBeenCalledTimes(1); 36 | expect(mockResponse.sendFile.mock.calls[0][0]).toMatchSnapshot(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /server/src/filters/kb-not-found-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { 4 | ArgumentsHost, 5 | Catch, 6 | ExceptionFilter, 7 | NotFoundException 8 | } from '@nestjs/common'; 9 | 10 | @Catch(NotFoundException) 11 | export class KbNotFoundExceptionFilter implements ExceptionFilter { 12 | constructor(private readonly appRoot: string) {} 13 | 14 | catch(exception: NotFoundException, host: ArgumentsHost) { 15 | const ctx = host.switchToHttp(); 16 | const response = ctx.getResponse(); 17 | const request = ctx.getRequest(); 18 | const path: string = request.path; 19 | 20 | if (path.startsWith('/api/')) { 21 | response.status(exception.getStatus()).json({ 22 | statusCode: exception.getStatus(), 23 | name: exception.name, 24 | error: exception.message 25 | }); 26 | 27 | return; 28 | } 29 | 30 | response.sendFile( 31 | join(this.appRoot, './dist/client/index.html') 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/filters/kb-validation-exception.filter.spec.ts: -------------------------------------------------------------------------------- 1 | import MockDate from 'mockdate'; 2 | 3 | import { BadRequestException, HttpStatus } from '@nestjs/common'; 4 | 5 | import { hostMock, mockResponse } from '@kb-dev-tools'; 6 | import { KbValidationExceptionFilter } from '@kb-filters'; 7 | 8 | describe('KbValidationExceptionFilter', () => { 9 | it('should be defined', () => { 10 | expect(new KbValidationExceptionFilter()).toBeDefined(); 11 | }); 12 | 13 | it('should return pretty validation errors', async () => { 14 | MockDate.set('2000-05-04'); 15 | const filter = new KbValidationExceptionFilter(); 16 | const req = { 17 | path: '/mock-api-path', 18 | url: 'https://server.com/mock-api-path' 19 | }; 20 | 21 | filter.catch(new BadRequestException(), hostMock(req, mockResponse)); 22 | 23 | expect(mockResponse.status) 24 | .toHaveBeenCalledWith(HttpStatus.METHOD_NOT_ALLOWED); 25 | expect(mockResponse.json).toHaveBeenCalledTimes(1); 26 | expect(mockResponse.json.mock.calls[0][0]).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /server/src/filters/kb-validation-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentsHost, 3 | BadRequestException, 4 | Catch, 5 | ExceptionFilter, 6 | HttpStatus 7 | } from '@nestjs/common'; 8 | 9 | import { PublicError } from '@kb-models'; 10 | 11 | @Catch(BadRequestException) 12 | export class KbValidationExceptionFilter implements ExceptionFilter { 13 | catch(exception: BadRequestException, host: ArgumentsHost) { 14 | const ctx = host.switchToHttp(); 15 | const response = ctx.getResponse(); 16 | const request = ctx.getRequest(); 17 | 18 | response 19 | .status(HttpStatus.METHOD_NOT_ALLOWED) 20 | // you can manipulate the response here 21 | .json(new PublicError({ 22 | statusCode: HttpStatus.METHOD_NOT_ALLOWED, 23 | timestamp: new Date().toISOString(), 24 | path: request.url, 25 | name: 'BadRequestException', 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | error: (exception.getResponse() as any).message as string[] 28 | })); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/filters/readme.md: -------------------------------------------------------------------------------- 1 | ## FILTERS 2 | 3 | Nest.js filters -------------------------------------------------------------------------------- /server/src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './github-pr-payload.model'; 6 | -------------------------------------------------------------------------------- /server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '@kb-config'; 2 | 3 | import { bootstrap } from './bootstrap-application'; 4 | 5 | const config = new ConfigService(); 6 | 7 | bootstrap() 8 | .then((app) => app.listen(config.port)); 9 | -------------------------------------------------------------------------------- /server/src/models/api.model.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class ApiInfo { 4 | @ApiProperty() 5 | name: string; 6 | 7 | @ApiProperty() 8 | description: string; 9 | 10 | @ApiProperty() 11 | version: string; 12 | 13 | @ApiProperty() 14 | license: string; 15 | 16 | @ApiProperty() 17 | repository: string; 18 | 19 | @ApiProperty() 20 | author: string; 21 | 22 | @ApiProperty() 23 | bugs: string; 24 | 25 | constructor(partial: Partial = {}) { 26 | Object.assign(this, partial); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/src/models/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './api.model'; 6 | export * from './public-error.model'; 7 | export * from './pull-request.model'; 8 | export * from './repo.model'; 9 | export * from './user.model'; 10 | -------------------------------------------------------------------------------- /server/src/models/public-error.model.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@nestjs/common'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class PublicError { 5 | @ApiProperty() 6 | statusCode: HttpStatus; 7 | 8 | // toISOString formatted Date 9 | @ApiProperty() 10 | timestamp: string; 11 | 12 | @ApiProperty() 13 | path: string; 14 | 15 | @ApiProperty() 16 | name: string; 17 | 18 | @ApiProperty({ 19 | oneOf: [{ type: 'string' }, { type: '[string]' }] 20 | }) 21 | error: string | string[]; 22 | 23 | constructor(partial: Partial = {}) { 24 | Object.assign(this, partial); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/models/readme.md: -------------------------------------------------------------------------------- 1 | ## MODELS 2 | 3 | Every `item` in our system should be a model. We should have two types of items: persistent items, which will be saved to the database, and non-persistent items, which resign in-memory -------------------------------------------------------------------------------- /server/src/models/repo.model.ts: -------------------------------------------------------------------------------- 1 | import { Exclude, Expose } from 'class-transformer'; 2 | import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; 3 | import { index, modelOptions, prop as PersistInDb } from '@typegoose/typegoose'; 4 | 5 | import { ApiProperty } from '@nestjs/swagger'; 6 | 7 | import { BaseModel } from '@kb-abstracts'; 8 | 9 | @Exclude() 10 | @modelOptions({ 11 | schemaOptions: { 12 | collation: { locale: 'en_US', strength: 2 }, 13 | timestamps: true 14 | } 15 | }) 16 | @index({ fullname: 1 }, { unique: true }) 17 | export class Repo extends BaseModel { 18 | 19 | @Expose() 20 | @ApiProperty() 21 | @IsNotEmpty() 22 | @PersistInDb({ required: true }) 23 | readonly name: string; 24 | 25 | @Expose() 26 | @ApiProperty() 27 | @IsNotEmpty() 28 | @PersistInDb({ required: true, unique: true }) 29 | readonly fullname: string; 30 | 31 | @Expose() 32 | @ApiProperty() 33 | @IsString() 34 | @PersistInDb({ required: true }) 35 | readonly url: string; 36 | 37 | @Expose() 38 | @ApiProperty() 39 | @IsString() 40 | @IsOptional() 41 | @PersistInDb() 42 | readonly organization: string; 43 | 44 | constructor(partial: Partial = {}) { 45 | super(); 46 | Object.assign(this, partial); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Exclude, Expose } from 'class-transformer'; 2 | import { 3 | IsArray, 4 | IsBoolean, 5 | IsNotEmpty, 6 | IsOptional, 7 | IsString 8 | } from 'class-validator'; 9 | import { 10 | index, 11 | modelOptions,prop as PersistInDb, 12 | Severity 13 | } from '@typegoose/typegoose'; 14 | 15 | import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 16 | 17 | import { BaseModel } from '@kb-abstracts'; 18 | 19 | export interface IUserAchievement { 20 | name: string; 21 | avatar: string; 22 | short: string; 23 | description: string; 24 | relatedPullRequest: string; 25 | } 26 | 27 | @Exclude() 28 | @modelOptions({ 29 | options: { 30 | allowMixed: Severity.ALLOW 31 | }, 32 | schemaOptions: { 33 | collation: { locale: 'en_US', strength: 2 }, 34 | timestamps: true 35 | } 36 | }) 37 | @index({ username: 1 }, { unique: true }) 38 | export class User extends BaseModel { 39 | 40 | @Expose() 41 | @IsNotEmpty() 42 | @ApiProperty() 43 | @PersistInDb({ required: true, unique: true }) 44 | readonly username: string; 45 | 46 | @Expose() 47 | @ApiProperty() 48 | @IsNotEmpty() 49 | @PersistInDb({ required: true }) 50 | readonly url: string; 51 | 52 | @Expose() 53 | @ApiProperty() 54 | @IsString() 55 | @PersistInDb({ required: true }) 56 | readonly avatar: string; 57 | 58 | @Expose() 59 | @ApiProperty() 60 | @IsBoolean() 61 | @IsOptional() 62 | @PersistInDb() 63 | readonly organization: boolean; 64 | 65 | @Expose() 66 | @ApiPropertyOptional() 67 | @IsArray() 68 | @IsOptional() 69 | @PersistInDb({ type: () => [String] }) 70 | readonly users?: string[]; 71 | 72 | @Expose() 73 | @ApiPropertyOptional() 74 | @IsArray() 75 | @PersistInDb({ type: () => [String] }) 76 | readonly repos: string[]; 77 | 78 | @Expose() 79 | @ApiPropertyOptional() 80 | @IsArray() 81 | @PersistInDb({ type: () => [String] }) 82 | readonly organizations: string[]; 83 | 84 | @Expose() 85 | @IsArray() 86 | @IsOptional() 87 | @PersistInDb() 88 | achievements: IUserAchievement[]; 89 | 90 | @Exclude() 91 | @PersistInDb() 92 | readonly token: string; 93 | 94 | constructor(partial: Partial = {}) { 95 | super(); 96 | Object.assign(this, partial); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/src/tasks/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from './tasks.module'; 6 | export * from './tasks.service'; 7 | -------------------------------------------------------------------------------- /server/src/tasks/readme.md: -------------------------------------------------------------------------------- 1 | ## TASKS 2 | 3 | This module is in charge of everything related to scheduled tasks. Think of cron jobs that the application should perform. This can include database backup, or updating data once a day and things like that -------------------------------------------------------------------------------- /server/src/tasks/tasks.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { PullRequestModule } from '@kb-api'; 4 | 5 | import { TasksService } from './tasks.service'; 6 | 7 | @Module({ 8 | imports: [PullRequestModule], 9 | providers: [TasksService] 10 | }) 11 | export class TasksModule {} 12 | -------------------------------------------------------------------------------- /server/src/tasks/tasks.service.spec.ts: -------------------------------------------------------------------------------- 1 | import MockDate from 'mockdate'; 2 | 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | import { Test, TestingModule } from '@nestjs/testing'; 5 | 6 | import { PullRequestService } from '@kb-api'; 7 | import { 8 | closeInMemoryDatabaseConnection, 9 | createInMemoryDatabaseModule, 10 | DtoMockGenerator 11 | } from '@kb-dev-tools'; 12 | import { PullRequest } from '@kb-models'; 13 | import { TasksService } from '@kb-tasks'; 14 | 15 | describe('TasksService', () => { 16 | let service: TasksService; 17 | let prService: PullRequestService; 18 | 19 | beforeEach(async () => { 20 | const module: TestingModule = await Test.createTestingModule({ 21 | imports: [ 22 | createInMemoryDatabaseModule(), 23 | MongooseModule.forFeature([{ 24 | name: PullRequest.modelName, 25 | schema: PullRequest.schema 26 | }]) 27 | ], 28 | providers: [ 29 | TasksService, 30 | PullRequestService 31 | ] 32 | }).compile(); 33 | 34 | service = module.get(TasksService); 35 | prService = module.get(PullRequestService); 36 | }); 37 | 38 | afterEach(async () => await closeInMemoryDatabaseConnection()); 39 | 40 | it('should be defined', () => { 41 | expect(service).toBeDefined(); 42 | }); 43 | 44 | it('should ignore PR that is younger than 100 days', async () => { 45 | const stalePr = DtoMockGenerator.pullRequest(); 46 | const aMonthAgo = new Date(); 47 | aMonthAgo.setDate(aMonthAgo.getDate() - 99); 48 | MockDate.set(aMonthAgo); 49 | await prService.create(stalePr); 50 | MockDate.reset(); 51 | const spy = jest.spyOn(prService, 'deleteAsync'); 52 | await service.removeStalePullRequests(); 53 | expect(spy).not.toHaveBeenCalledTimes(1); 54 | }); 55 | 56 | it('should delete PR that is old or equal to 100 days', async () => { 57 | const stalePr = DtoMockGenerator.pullRequest(); 58 | const aMonthAgo = new Date(); 59 | aMonthAgo.setDate(aMonthAgo.getDate() - 100); 60 | MockDate.set(aMonthAgo); 61 | await prService.create(stalePr); 62 | MockDate.reset(); 63 | const spy = jest.spyOn(prService, 'deleteAsync'); 64 | await service.removeStalePullRequests(); 65 | expect(spy).toHaveBeenCalledTimes(1); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /server/src/tasks/tasks.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Cron, CronExpression } from '@nestjs/schedule'; 3 | 4 | import { WinstonLogger } from '@kibibit/nestjs-winston'; 5 | 6 | import { PullRequestService } from '@kb-api'; 7 | import { ConfigService } from '@kb-config'; 8 | import { TaskHealthCheck } from '@kb-decorators'; 9 | import { PRStatus, PullRequest } from '@kb-models'; 10 | 11 | const configService = new ConfigService; 12 | @Injectable() 13 | export class TasksService { 14 | private readonly logger = new WinstonLogger(TasksService.name); 15 | 16 | constructor(private prService: PullRequestService) { 17 | this.logger.log('Tasks Service Initialized'); 18 | } 19 | 20 | /** At 00:00 on Sunday */ 21 | /** https://crontab.guru/every-week */ 22 | @Cron(CronExpression.EVERY_WEEK) 23 | @TaskHealthCheck(configService.deletePRsHealthId) 24 | async removeStalePullRequests() { 25 | const d100daysAgo = new Date(); 26 | d100daysAgo.setDate(d100daysAgo.getDate() - 100); 27 | const d14daysAgo = new Date(); 28 | d14daysAgo.setDate(d14daysAgo.getDate() - 14); 29 | 30 | this.logger.debug(`Removing Stale OPEN PRs older than: ${ d100daysAgo }`); 31 | const openPRsForDeletion = await this.getPRsBy( 32 | d100daysAgo, 33 | PRStatus.OPEN 34 | ); 35 | await this.deletePRsByIds(openPRsForDeletion); 36 | 37 | this.logger.debug(`Removing CLOSED PRs older than: ${ d14daysAgo }`); 38 | const closedPRsForDeletion = await this.getPRsBy( 39 | d14daysAgo, 40 | PRStatus.CLOSED 41 | ); 42 | await this.deletePRsByIds(closedPRsForDeletion); 43 | 44 | this.logger.debug(`Removing MERGED PRs older than: ${ d14daysAgo }`); 45 | const mergedPRsForDeletion = await this.getPRsBy( 46 | d14daysAgo, 47 | PRStatus.MERGED 48 | ); 49 | await this.deletePRsByIds(mergedPRsForDeletion); 50 | } 51 | 52 | private async getPRsBy(deleteBefore: Date, status: PRStatus) { 53 | const prs = await this.prService.findAllAsync({ 54 | updatedAt: { 55 | $lte : deleteBefore 56 | }, 57 | status: status 58 | }); 59 | 60 | return prs.map((pr) => new PullRequest(pr.toObject()).prid); 61 | } 62 | 63 | private async deletePRsByIds(ids: string[]) { 64 | if (ids.length) { 65 | this.logger.log('Removing PRs:'); 66 | this.logger.log('IDs', { ids }); 67 | await this.prService.deleteAsync({ 68 | prid: { $in: ids } 69 | }); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/test-tools/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '../src/config/winston.config'; 2 | 3 | jest.mock('../src/config/winston.config'); 4 | 5 | const nativeConsoleError = global.console.warn; 6 | 7 | global.console.warn = (...args) => { 8 | const msg = args.join(''); 9 | if ( 10 | msg.includes('Using Unsupported mongoose version') || 11 | msg.includes('Setting "Mixed" for property') 12 | ) { 13 | return; 14 | } 15 | return nativeConsoleError(...args); 16 | }; 17 | -------------------------------------------------------------------------------- /server/test/__snapshots__/app.e2e-spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AppController (e2e) /api (GET) API Information 1`] = ` 4 | Object { 5 | "author": "thatkookooguy ", 6 | "bugs": "https://github.com/Kibibit/achievibit/issues", 7 | "description": "", 8 | "license": "MIT", 9 | "name": "@kibibit/achievibit", 10 | "repository": "https://github.com/Kibibit/achievibit.git", 11 | "version": "SEMVER", 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { valid } from 'semver'; 2 | import request from 'supertest'; 3 | 4 | import { INestApplication } from '@nestjs/common'; 5 | import { Test, TestingModule } from '@nestjs/testing'; 6 | 7 | import { AppModule } from '@kb-app'; 8 | 9 | describe('AppController (e2e)', () => { 10 | let app: INestApplication; 11 | let server; 12 | 13 | beforeEach(async () => { 14 | const moduleFixture: TestingModule = await Test.createTestingModule({ 15 | imports: [AppModule] 16 | }).compile(); 17 | 18 | app = moduleFixture.createNestApplication(); 19 | await app.init(); 20 | server = app.getHttpServer(); 21 | }); 22 | 23 | afterEach(async() => { 24 | await app.close(); 25 | }); 26 | 27 | test('/api (GET) API Information', async () => { 28 | const response = await request(server).get('/api'); 29 | expect(response.status).toBe(200); 30 | expect(response.body.version).toBeDefined(); 31 | expect(valid(response.body.version)).toBeTruthy(); 32 | response.body.version = 'SEMVER'; 33 | expect(response.body).toMatchSnapshot(); 34 | }); 35 | 36 | test('/ (GET) HTML of client application', async () => { 37 | const response = await request(server).get('/'); 38 | // console.log(response.body); 39 | expect(response.status).toBe(200); 40 | expect(response.type).toMatch(/html/); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": "..", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "setupFilesAfterEnv": [ 10 | "./test-tools/jest.setup.ts" 11 | ], 12 | "collectCoverage": true, 13 | "collectCoverageFrom": [ 14 | "src/**/*.(t|j)s", 15 | "!src/**/*.decorator.ts", 16 | "!src/**/index.ts", 17 | "!src/**/dev-tools/**/*.ts", 18 | "!src/bootstrap-application.ts", 19 | "!src/swagger.ts", 20 | "!src/main.ts" 21 | ], 22 | "coveragePathIgnorePatterns": [ 23 | ".module.ts$", 24 | ".spec.ts$" 25 | ], 26 | "coverageDirectory": "../test-results/api/coverage", 27 | "reporters": [ 28 | "default", 29 | [ 30 | "jest-stare", 31 | { 32 | "resultDir": "../test-results/api", 33 | "reportTitle": "jest-stare!", 34 | "additionalResultsProcessors": [ 35 | "jest-junit" 36 | ], 37 | "coverageLink": "./coverage/lcov-report/index.html" 38 | } 39 | ] 40 | ], 41 | "moduleNameMapper": { 42 | "^@kb-server$": "/src", 43 | "^@kb-models$": "/src/models/index", 44 | "^@kb-abstracts$": "/src/abstracts/index", 45 | "^@kb-decorators$": "/src/decorators/index", 46 | "^@kb-filters$": "/src/filters/index", 47 | "^@kb-api$": "/src/api/index", 48 | "^@kb-events$": "/src/events/index", 49 | "^@kb-app$": "/src/app/index", 50 | "^@kb-tasks$": "/src/tasks/index", 51 | "^@kb-engines$": "/src/engines/index", 52 | "^@kb-dev-tools$": "/src/dev-tools/index", 53 | "^@kb-config$": "/src/config/index", 54 | "^@kb-errors$": "/src/errors/index" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/test/socket.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { Observable } from 'rxjs'; 3 | import { debounceTime, first, share, tap } from 'rxjs/operators'; 4 | import { connect, Socket } from 'socket.io-client'; 5 | 6 | enum NATIVE_EVENTS { 7 | CONNECT = 'connect', 8 | DISCONNECT = 'disconnect', 9 | ERROR = 'error', 10 | RECONNECT = 'reconnect', 11 | } 12 | 13 | export class SocketService { 14 | private host = 'ws://localhost:10109'; 15 | private events$: Record> = {}; 16 | socket: typeof Socket; 17 | 18 | errors$: Observable; 19 | isConnected$: Observable; 20 | 21 | constructor(defer?: boolean) { 22 | this.socket = connect( 23 | this.host, 24 | { 25 | autoConnect: !defer, 26 | forceNew: true 27 | }, 28 | ); 29 | 30 | this.errors$ = this.on('disconnect-reason'); 31 | this.on(NATIVE_EVENTS.DISCONNECT) 32 | .pipe( 33 | tap(reason => `disconnected: ${reason}`), 34 | debounceTime(5000), 35 | ) 36 | .subscribe(reason => { 37 | if (reason === 'io server disconnect') { 38 | } 39 | }); 40 | } 41 | 42 | on(event: string): Observable { 43 | if (this.events$[event]) { 44 | return this.events$[event]; 45 | } 46 | 47 | this.events$[event] = new Observable(observer => { 48 | this.socket.on(event, observer.next.bind(observer)); 49 | 50 | return () => { 51 | this.socket.off(event); 52 | delete this.events$[event]; 53 | }; 54 | }).pipe(share()); 55 | 56 | return this.events$[event]; 57 | } 58 | 59 | once(event: string): Observable { 60 | return this.on(event).pipe(first()); 61 | } 62 | emit(event: string, data?: T, ack?: false): void; 63 | // This overloading is not working in TS 3.0.1 64 | // emit(event: string, data?: any, ack?: true): Observable; 65 | emit(event: string, data?: T, ack?: true): Observable; 66 | emit( 67 | event: string, 68 | data?: T, 69 | ack?: boolean 70 | ): void | Observable { 71 | if (ack) { 72 | return new Observable(observer => { 73 | this.socket.emit(event, data, observer.next.bind(observer)); 74 | }).pipe(first()); 75 | } else { 76 | this.socket.emit(event, data); 77 | } 78 | } 79 | 80 | open() { 81 | this.socket.connect(); 82 | } 83 | 84 | close() { 85 | this.socket.disconnect(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /server/test/sockets.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { take, tap } from 'rxjs/operators'; 2 | import { observe } from 'rxjs-marbles/jest'; 3 | 4 | import { Test, TestingModule } from '@nestjs/testing'; 5 | 6 | import { AppModule } from '@kb-app'; 7 | 8 | import { SocketService } from './socket.service'; 9 | import { Utils } from './utils'; 10 | 11 | describe('Web Socket Events (e2e)', () => { 12 | let socket: SocketService; 13 | 14 | beforeEach(async () => { 15 | const testingModule: TestingModule = await Test.createTestingModule({ 16 | imports: [AppModule] 17 | }).compile(); 18 | 19 | await Utils.startServer(testingModule); 20 | // each test need a new socket connection 21 | socket = await Utils.createSocket(); 22 | }); 23 | 24 | afterEach(async () => { 25 | // each test need to release the connection for next 26 | await Utils.closeApp(); 27 | }); 28 | 29 | test('socket connection', observe(() => { 30 | return socket.once('connect') 31 | .pipe(tap(() => expect(true).toBeTruthy())); 32 | })); 33 | 34 | test('Events topic', observe(() => { 35 | let counter = 1; 36 | 37 | socket 38 | .once('connect') 39 | .pipe(tap(() => socket.emit('events', { test: 'test' }))) 40 | .subscribe(); 41 | 42 | return socket.on('events') 43 | .pipe( 44 | take(3), 45 | tap(data => expect(data).toBe(counter++)) 46 | ); 47 | })); 48 | 49 | test('Message topic', observe(() => { 50 | const messageData = { test: 'test' }; 51 | socket 52 | .once('connect') 53 | .pipe(tap(() => socket.emit('message', messageData))) 54 | .subscribe(); 55 | 56 | return socket.once('message-response') 57 | .pipe(tap(data => expect(data).toEqual(messageData))); 58 | })); 59 | }); 60 | -------------------------------------------------------------------------------- /server/test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import express from 'express'; 3 | 4 | import { INestApplication } from '@nestjs/common/interfaces'; 5 | import { TestingModule } from '@nestjs/testing/testing-module'; 6 | 7 | import { SocketService } from './socket.service'; 8 | 9 | export class Utils { 10 | public static socket: SocketService; 11 | private static server: express.Express; 12 | private static app: INestApplication; 13 | private static module: TestingModule; 14 | 15 | public static async startServer(testingModule: TestingModule) { 16 | this.module = testingModule; 17 | this.server = express(); 18 | this.server.use(bodyParser.json()); 19 | this.app = await testingModule.createNestApplication(); 20 | await this.app.init(); 21 | } 22 | 23 | public static async createSocket(defer = false) { 24 | await this.app.listen(10109); 25 | this.socket = new SocketService(defer); 26 | 27 | return this.socket; 28 | } 29 | public static async closeApp() { 30 | this.socket.close(); 31 | await this.app.close(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "esModuleInterop": true, 12 | "outDir": "../dist/server", 13 | "baseUrl": "./", 14 | "paths": { 15 | "@kb-models": [ "src/models/index" ], 16 | "@kb-abstracts": [ "src/abstracts/index" ], 17 | "@kb-decorators": [ "src/decorators/index" ], 18 | "@kb-filters": [ "src/filters/index" ], 19 | "@kb-events": [ "src/events/index" ], 20 | "@kb-tasks": [ "src/tasks/index" ], 21 | "@kb-app": [ "src/app/index" ], 22 | "@kb-api": [ "src/api/index" ], 23 | "@kb-dev-tools": [ "src/dev-tools/index" ], 24 | "@kb-interfaces": [ "src/interfaces/index" ], 25 | "@kb-engines": [ "src/engines/index" ], 26 | "@kb-errors": [ "src/errors/index" ], 27 | "@kb-config": [ "src/config/index" ] 28 | 29 | }, 30 | "incremental": true, 31 | "skipLibCheck": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./env.schema.json", 3 | "dbUrl": "mongodb://localhost:27017/test", 4 | "port": 10108 5 | } -------------------------------------------------------------------------------- /tools/initialize.js: -------------------------------------------------------------------------------- 1 | const { terminalConsoleLogo } = require('@kibibit/consologo'); 2 | const inquirer = require('inquirer'); 3 | const Foswig = require('foswig').default; 4 | const Sentencer = require('sentencer'); 5 | const Names = require('./data/names-dictionary'); 6 | const { kebabCase, toLower } = require('lodash'); 7 | const packageNameRegex = require('package-name-regex'); 8 | // const imageToAscii = require('image-to-ascii'); 9 | 10 | //load the words into the markov chain 11 | const chain = new Foswig(3, Names.dictionary); 12 | 13 | // imageToAscii("https://octodex.github.com/images/octofez.png", (err, converted) => { 14 | // console.log(err || converted); 15 | // }); 16 | 17 | const questions = [ 18 | { 19 | type: 'input', 20 | name: 'projectName', 21 | message: 'What is the name of this new project?', 22 | transformer(input) { return `@kibibit/${ input }`; }, 23 | validate(input) { 24 | return packageNameRegex.test(input) || `@kibibit/${ input } is not a valid NPM package name`; 25 | }, 26 | default: '@kibibit/' + toLower(chain.generate({ 27 | minLength: 5, 28 | maxLength: 10, 29 | allowDuplicates: false 30 | })) 31 | }, 32 | { 33 | type: 'input', 34 | name: 'projectDescription', 35 | message: 'Please write a short description for this project', 36 | default: Sentencer.make('This is {{ an_adjective }} {{ noun }}.') 37 | }, 38 | { 39 | type: 'input', 40 | name: 'author', 41 | message: 'Project owner\\author', 42 | default: 'githubusername ' 43 | }, 44 | { 45 | type: 'input', 46 | name: 'githubRepo', 47 | message: 'Paste a url to a github repo or a user/repo pair', 48 | default: 'kibibit/achievibit' 49 | } 50 | ]; 51 | 52 | (async () => { 53 | terminalConsoleLogo('Start a new project', 'Answer these questions to start a new project'); 54 | const answers = await inquirer.prompt(questions); 55 | 56 | answers.projectName = answers.projectName.startsWith('@kibibit/') ? 57 | answers.projectName : `@kibibit/${ answers.projectName }`; 58 | 59 | answers.projectNameSafe = kebabCase(answers.projectName); 60 | 61 | console.log(answers); 62 | })(); -------------------------------------------------------------------------------- /tools/readme.md: -------------------------------------------------------------------------------- 1 | ## APPLICATION TOOLS 2 | 3 | This folder contains tools that can be used while developing. 4 | 5 | The most obvious one is the script to initialize this template. 6 | But you also have things like `prune-untrackted-branches` to delete branches 7 | that already appeared in the cloud and got deleted. -------------------------------------------------------------------------------- /tools/scripts/get-all-contributors.js: -------------------------------------------------------------------------------- 1 | const gitlog = require('gitlog').default; 2 | const { join } = require('path'); 3 | const githubUsername = require('github-username'); 4 | const shell = require('shelljs'); 5 | const { forEach, chain } = require('lodash'); 6 | const { readJson } = require('fs-extra'); 7 | 8 | 9 | (async () => { 10 | const allContributorsConfig = await readJson(join(__dirname, '..', '/.all-contributorsrc')); 11 | const data = gitlog({ 12 | repo: join(__dirname, '..'), 13 | fields: ["authorName", "authorEmail", "authorDate"] 14 | }); 15 | 16 | let result = {}; 17 | 18 | for (const commit of data) { 19 | const isCode = commit.files.find((item) => item.startsWith('server/src') || item.startsWith('client/src')); 20 | const isInfra = commit.files.find((item) => item.startsWith('.github/') || item.startsWith('tools/')); 21 | const isTests = commit.files.find((item) => item.endsWith('.spec.ts')); 22 | let types = []; 23 | if (isCode) { types.push('code'); } 24 | if (isInfra) { types.push('infra'); } 25 | if (isTests) { types.push('test'); } 26 | 27 | if (!result[commit.authorEmail]) { 28 | const githubLogin = await githubUsername(commit.authorEmail); 29 | result[commit.authorEmail] = { 30 | githubLogin, 31 | types 32 | }; 33 | } else { 34 | result[commit.authorEmail].types = result[commit.authorEmail].types.concat(types); 35 | } 36 | } 37 | 38 | forEach(result, (person, email) => { 39 | const existing = chain(allContributorsConfig.contributors) 40 | .find((item) => item.login === person.githubLogin) 41 | .get('contributions', []) 42 | .value(); 43 | const types = chain(person.types.concat(existing)).uniq().sortBy().value(); 44 | const command = `npm run contributors:add ${ person.githubLogin } ${ types.join(',') }`; 45 | console.log(command); 46 | shell.exec(command, { cwd: __dirname }); 47 | }) 48 | })(); -------------------------------------------------------------------------------- /tools/scripts/monorepo-commit-analyze.js: -------------------------------------------------------------------------------- 1 | const sgf = require('staged-git-files'); 2 | 3 | const myArgs = process.argv.slice(2); 4 | const checkPart = myArgs[0]; 5 | 6 | sgf((err, changedFiles) => { 7 | if (err) { 8 | throw err; 9 | } 10 | 11 | const changedFilenames = changedFiles.map((item) => item.filename); 12 | const isServerChanged = changedFilenames 13 | .find((filename) => filename.startsWith('server/')); 14 | const isClientChanged = changedFilenames 15 | .find((filename) => filename.startsWith('client/')); 16 | const isAchChanged = changedFilenames 17 | .find((filename) => filename.startsWith('achievements/')); 18 | const isToolsChanged = changedFilenames 19 | .find((filename) => filename.startsWith('tools/')); 20 | 21 | if (checkPart === 'server' && isServerChanged) { 22 | process.exit(0); 23 | } 24 | 25 | if (checkPart === 'client' && isClientChanged) { 26 | process.exit(0); 27 | } 28 | 29 | if (checkPart === 'ach' && isAchChanged) { 30 | process.exit(0); 31 | } 32 | 33 | if (checkPart === 'tools' && isToolsChanged) { 34 | process.exit(0); 35 | } 36 | 37 | process.exit(1); 38 | }); -------------------------------------------------------------------------------- /tools/scripts/replace-template-string.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const replace = require('replace-in-file'); 3 | 4 | const projectNameArgs = process.argv.slice(2); 5 | const projectName = projectNameArgs[projectNameArgs.length - 1]; 6 | 7 | if (!projectName) { 8 | throw new Error('must pass a project name'); 9 | } else { 10 | console.log(projectName); 11 | // return; 12 | } 13 | 14 | const readmeFile = { 15 | files: './README.md', 16 | from: /kb-server-client-template/g, 17 | to: projectName, 18 | }; 19 | 20 | const devcontainerFile = { 21 | files: './.devcontainer/devcontainer.json', 22 | from: /kb-server-client-template/g, 23 | to: projectName, 24 | }; 25 | 26 | const contributorsFile = { 27 | files: './.all-contributorsrc', 28 | from: /kb-server-client-template/g, 29 | to: projectName, 30 | }; 31 | 32 | const packageFile = { 33 | files: './package.json', 34 | from: /kb-server-client-template/g, 35 | to: projectName, 36 | }; 37 | 38 | const packageLockFile = { 39 | files: './package-lock.json', 40 | from: /kb-server-client-template/g, 41 | to: projectName, 42 | }; 43 | 44 | 45 | 46 | (async () => { 47 | try { 48 | let results = []; 49 | results.push(await replace(readmeFile)); 50 | results.push(await replace(packageFile)); 51 | results.push(await replace(packageLockFile)); 52 | results.push(await replace(contributorsFile)); 53 | results.push(await replace(devcontainerFile)); 54 | console.log('Replacement results:', results); 55 | } 56 | catch (error) { 57 | console.error('Error occurred:', error); 58 | } 59 | })(); 60 | --------------------------------------------------------------------------------