Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contribution page #78

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2b33b7e
express session pacakge
txtonylee Oct 29, 2021
d96df77
Merge branch 'AmericanAirlines:main' into main
txtonylee Oct 29, 2021
fbfcdbb
Merge branch 'AmericanAirlines:main' into main
txtonylee Nov 1, 2021
78c9012
Create contribution table and add contributionsLastCheckedAt column t…
Nov 8, 2021
62c2baa
prettier and lint
Nov 8, 2021
3c4fd05
Merge remote-tracking branch 'upstream/main'
txtonylee Nov 9, 2021
0a03023
initial ideas
Nov 10, 2021
bdf353c
update to contribution cron job, currently returns single repo PRs
Nov 12, 2021
6a82561
working data fetch and db save
Nov 14, 2021
86de1af
front page init
txtonylee Nov 17, 2021
a2a2a92
front end done
txtonylee Nov 17, 2021
2c5106a
contribution test added
txtonylee Nov 17, 2021
ef50b0a
contributionsbox test added
txtonylee Nov 17, 2021
b8adc1a
yarn prettier:fix
txtonylee Nov 17, 2021
8194886
naming fixed
txtonylee Nov 17, 2021
4a3ddce
more work on the cron job
Nov 17, 2021
69da6c5
working cron job
Nov 18, 2021
1eed590
working cron job
Nov 18, 2021
d2bb998
working happy path test
Nov 23, 2021
b036c6a
Merge branch 'main' into contribution-cron-job
Nov 23, 2021
b08a9a7
added additional tests to bring things up to 100% coverage
Nov 24, 2021
61ba45d
Merge remote-tracking branch 'upstream/main' into contribution_page
txtonylee Nov 26, 2021
53b2ad8
requested changes fix
txtonylee Nov 26, 2021
09993bb
requested changes fix
txtonylee Nov 26, 2021
005f85a
add video search bar
ayushagarwal0316 Dec 2, 2021
6d95cd6
Merge branch 'final-demo' into contribution_page
txtonylee Dec 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@

- Generate a Client Secret ID from GitHub and use it to replace the value of `GITHUB_SECRET` within your `.env.local`

## Setting up the Github Access Token

- Login to github, then at the top right click on the icon and go to settings.

- Find the developer settings, then click on Personal access tokens, and create a new access token.

- Name the token something descriptive, no additional options need to be selected.

- Click the Generate token button.

- Next, copy the token and replace the value of `GITHUB_TOKEN` within your `.env.local`

## Setting up Discord OAuth

- Go to the [Discord Develop Portal](https://discord.com/developers/applications) and log in.
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/__mocks__/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const env = {
port: '3000',
githubId: 'mock-client-id',
githubSecretId: 'Secrete_secret',
githubToken: 'mock-token',
discordClientId: 'mock-discord-id',
discordSecretId: 'discord-secret',
};
233 changes: 233 additions & 0 deletions packages/api/src/api/contributions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import express, { Router } from 'express';
import { env } from '../env';
import { Contribution } from '../entities/Contribution';
import { User } from '../entities/User';
import logger from '../logger';
import { Project } from '../entities/Project';

export const contributions = Router();
contributions.use(express.json());

export interface PullRequestContribution {
id: string;
title: string;
permalink: string;
mergedAt: string;
author: {
login: string;
};
}

export interface Repository {
id: string;
nameWithOwner: string;
}

interface ContributionResponse {
data: {
search: {
pageInfo: {
hasNextPage: boolean;
endCursor: string | null;
};
nodes: PullRequestContribution[];
};
};
}

interface RepositoryResponse {
data: {
nodes: Repository[];
};
}

const buildProjectsQuery = async (projects: Project[]) => {
let idString = '';

projects.forEach((p) => {
idString += `"${p.nodeID}",`;
});

idString = idString.substring(0, idString.length - 1);

const repoQueryString = `
query {
nodes(ids: [${idString}]){
... on Repository {
id
nameWithOwner
}
}
}
`;

const fetchRes = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `bearer ${env.githubToken}`,
},
body: JSON.stringify({
query: repoQueryString,
}),
});

const { data: responseData }: RepositoryResponse = await fetchRes.json();

let queryString = '';

responseData.nodes.forEach((repo) => {
queryString += `repo:${repo.nameWithOwner} `;
});

return queryString;
};

const buildUsersQuery = (users: User[]) => {
let queryString = '';

users.forEach((u) => {
queryString += `author:${u.githubUsername} `;
});

return queryString;
};

const buildDateQuery = (startDate: Date, endDate: Date) => {
const locale = 'en-US';
const queryString = `${startDate.getFullYear()}-${(startDate.getMonth() + 1).toLocaleString(
locale,
{ minimumIntegerDigits: 2 },
)}-${startDate
.getDate()
.toLocaleString(locale, { minimumIntegerDigits: 2 })}..${endDate.getFullYear()}-${(
endDate.getMonth() + 1
).toLocaleString(locale, { minimumIntegerDigits: 2 })}-${endDate
.getDate()
.toLocaleString(locale, { minimumIntegerDigits: 2 })}`;
return queryString;
};

const buildQueryString = (
projectsString: string,
dateString: string,
userString: string,
cursor: string | null,
) => {
const afterString = cursor ? `after:"${cursor}",` : '';

const queryString = `
{
search (first: 2, ${afterString} query: "${projectsString} is:pr is:merged merged:${dateString} ${userString}", type: ISSUE) {
pageInfo {
hasNextPage
endCursor
}
nodes {
... on PullRequest {
id
title
permalink
mergedAt
author {
login
}
}
}
}
}
`;

return queryString;
};

contributions.get('', async (req, res) => {
try {
const timeRange = new Date();
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 100);
timeRange.setHours(timeRange.getHours() - 5);

const userList = await req.entityManager.find(User, {
$or: [
{
contributionsLastCheckedAt: {
$lt: timeRange,
},
},
{
contributionsLastCheckedAt: {
$eq: null,
},
},
],
});

if (userList.length === 0) {
res.sendStatus(200);
return;
}

const projectList = await req.entityManager.find(Project, {});
const projectsString = await buildProjectsQuery(projectList);
const dateString = buildDateQuery(yesterday, today);
const usersString = buildUsersQuery(userList);
let cursor = null;
let hasNextPage = true;

while (hasNextPage) {
const queryString = buildQueryString(projectsString, dateString, usersString, cursor);

const fetchRes = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `bearer ${env.githubToken}`,
},
body: JSON.stringify({
query: queryString,
}),
});

const { data: responseData }: ContributionResponse = await fetchRes.json();
for (const pr of responseData.search.nodes.entries()) {
// Only Add new contributions
if (
(await req.entityManager.find(Contribution, { nodeID: { $eq: pr[1].id } })).length === 0
) {
const user = await req.entityManager.findOne(User, {
githubUsername: { $eq: pr[1].author.login },
});
if (user) {
const newContribution = new Contribution({
nodeID: pr[1].id,
authorGithubId: user.githubId,
type: 'Pull Request',
description: pr[1].title,
contributedAt: new Date(Date.parse(pr[1].mergedAt)),
score: 100,
});
req.entityManager.persist(newContribution);
}
}
}
hasNextPage = responseData.search.pageInfo.hasNextPage;
cursor = responseData.search.pageInfo.endCursor;
}

for (const user of userList) {
user.contributionsLastCheckedAt = new Date();
req.entityManager.persist(user);
}

// Save new info in the db
void req.entityManager.flush();

res.sendStatus(200);
} catch (error) {
res.sendStatus(500);
const errorMessage = 'There was an issue saving contribution data to the database';
logger.error(errorMessage, error);
}
});
2 changes: 2 additions & 0 deletions packages/api/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { health } from './health';
import { users } from './users';
import { videos } from './videos';
import { project } from './project';
import { contributions } from './contributions';

export const api = Router();

Expand All @@ -13,3 +14,4 @@ api.use('/users', users);
api.use('/videos', videos);
api.use('/auth', auth);
api.use('/project', project);
api.use('/contributions', contributions);
13 changes: 11 additions & 2 deletions packages/api/src/api/videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ import logger from '../logger';

export const videos = Router();

videos.get('', async (req, res) => {
videos.get('', async (req, res) => {
const query = req.query.q || '';
if(typeof query !== 'string'){
res.status(400).send('query must be string');
return;
}
try {
const videosList = await req.entityManager.find(Video, {});
const videosList = await req.entityManager.find(Video, { title: {
$ilike: `%${query.trim()}%`,
}
});

res.status(200).send(videosList);
} catch (error) {
Expand All @@ -15,6 +23,7 @@ videos.get('', async (req, res) => {
}
});


videos.get('/:videoId', async (req, res) => {
const { videoId } = req.params;

Expand Down
7 changes: 6 additions & 1 deletion packages/api/src/entities/Contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ export type ContributionConstructorValues = ConstructorValues<Contribution>;

@Entity()
export class Contribution extends Node<Contribution> {
@Property({ columnType: 'text' })
@Property({ columnType: 'text', unique: true })
nodeID: string;

@Property({ columnType: 'text' })
authorGithubId: string;

@Property({ columnType: 'text' })
description: string;

Expand All @@ -24,6 +27,7 @@ export class Contribution extends Node<Contribution> {

constructor({
nodeID,
authorGithubId,
description,
type,
score,
Expand All @@ -32,6 +36,7 @@ export class Contribution extends Node<Contribution> {
}: ContributionConstructorValues) {
super(extraValues);
this.nodeID = nodeID;
this.authorGithubId = authorGithubId;
this.description = description;
this.type = type;
this.score = score;
Expand Down
5 changes: 5 additions & 0 deletions packages/api/src/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export class User extends Node<User> {
@Property({ columnType: 'text' })
githubId: string;

@Property({ columnType: 'text' })
githubUsername: string;

@Property({ columnType: 'text', nullable: true })
discordId?: string;

Expand Down Expand Up @@ -49,6 +52,7 @@ export class User extends Node<User> {
constructor({
name,
githubId,
githubUsername,
hireable,
purpose,
isAdmin,
Expand All @@ -61,6 +65,7 @@ export class User extends Node<User> {
this.hireable = hireable;
this.purpose = purpose;
this.githubId = githubId;
this.githubUsername = githubUsername;
this.isAdmin = isAdmin ?? false;
this.email = email;
}
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const env = setEnv({
discordSecretId: 'DISCORD_SECRET_ID',
githubClientId: 'GITHUB_CLIENT_ID',
githubSecret: 'GITHUB_SECRET',
githubToken: 'GITHUB_TOKEN',
appUrl: 'APP_URL',
},
optional: {
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ app.use(express.json());

const GitHubStrategy = require('passport-github2');

const contributionPollTimer = (milliseconds: number) => {
logger.info(`Starting new contribution poll timer: ${milliseconds}ms`);
setTimeout(async () => {
await fetch(`${env.appUrl}/api/contributions`);
contributionPollTimer(milliseconds);
}, milliseconds);
};

const authRequired: Handler = (req, res, next) => {
if (req.user) {
next();
Expand Down Expand Up @@ -129,6 +137,7 @@ void (async () => {
.then(() => {
app.listen(port, () => {
logger.info(`🚀 Listening at http://localhost:${port}`);
contributionPollTimer(3600000);
});
})
.catch((err) => {
Expand Down
Loading