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 |
--------------------------------------------------------------------------------