From b8874bb7e93f11b599ccea83e06ab5c969b70765 Mon Sep 17 00:00:00 2001 From: kykyk Date: Mon, 9 Sep 2019 12:10:41 +0300 Subject: [PATCH 01/17] feat(repositories)#1411 new UID repository --- functions/package.json | 3 +- functions/src/client/firebase-admin.ts | 8 ++ functions/src/index.ts | 25 ++--- .../src/mappers/github/repository.mapper.ts | 37 ++++--- functions/src/models/index.model.ts | 1 + functions/src/models/project.model.ts | 1 + functions/src/models/repository.model.ts | 45 +++++++++ functions/src/project/delete-project.ts | 90 ++++++++--------- functions/src/project/update-project.ts | 75 +++++++++++++++ functions/src/project/update-repositories.ts | 70 -------------- .../create-git-webhook-repository.ts | 36 +++++-- functions/src/repository/create-repository.ts | 71 ++++++++++++-- .../delete-git-webhook-repository.ts | 34 +++++-- .../repository/find-git-webhook-repository.ts | 8 +- functions/src/repository/info.ts | 35 ++++--- functions/src/repository/update-repository.ts | 6 +- functions/src/user/create-user.ts | 48 ++++++++++ functions/src/user/events.ts | 15 +-- functions/src/user/repos.ts | 15 +-- functions/src/user/stats.ts | 7 +- functions/src/user/update-user.ts | 96 +++++++++++++++++++ web/package.json | 3 +- web/src/app/core/services/project.service.ts | 18 ++-- .../app/core/services/repository.service.ts | 8 +- .../repository/repository.component.html | 2 +- .../repository/repository.component.ts | 6 +- web/src/app/projects/view/view.component.ts | 13 +-- .../dialog/list/dialog-list.component.html | 2 +- web/src/app/shared/models/repository.model.ts | 11 +-- web/src/typings.d.ts | 6 -- 30 files changed, 550 insertions(+), 245 deletions(-) create mode 100644 functions/src/models/repository.model.ts create mode 100644 functions/src/project/update-project.ts delete mode 100644 functions/src/project/update-repositories.ts create mode 100644 functions/src/user/create-user.ts create mode 100644 functions/src/user/update-user.ts delete mode 100644 web/src/typings.d.ts diff --git a/functions/package.json b/functions/package.json index bd3040747..9f6db9e0f 100644 --- a/functions/package.json +++ b/functions/package.json @@ -20,12 +20,13 @@ "firebase-functions": "^3.1.0", "request": "^2.88.0", "request-promise-native": "^1.0.7", - "uuid": "^3.3.2", + "uuid": "^3.3.3", "winston": "^3.2.1" }, "devDependencies": { "@types/cors": "^2.8.5", "@types/request-promise-native": "^1.0.16", + "@types/uuid": "3.4.3", "firebase-functions-test": "^0.1.6", "tslint": "~5.16.0", "typescript": "^3.4.5" diff --git a/functions/src/client/firebase-admin.ts b/functions/src/client/firebase-admin.ts index 657d0994c..1276acae6 100644 --- a/functions/src/client/firebase-admin.ts +++ b/functions/src/client/firebase-admin.ts @@ -8,6 +8,14 @@ export declare type WriteResult = admin.firestore.WriteResult; export declare type QuerySnapshot = admin.firestore.QuerySnapshot; export declare type QueryDocumentSnapshot = admin.firestore.QueryDocumentSnapshot; export declare type DocumentReference = admin.firestore.DocumentReference; +export declare type Transaction = admin.firestore.Transaction; +export declare type WriteBatch = admin.firestore.WriteBatch; export declare type FieldValue = admin.firestore.FieldValue; +export declare type CollectionReference = admin.firestore.CollectionReference; +export declare type Query = admin.firestore.Query; + +// tslint:disable-next-line: typedef +export const FieldPath = admin.firestore.FieldPath; export const IncrementFieldValue: FieldValue = admin.firestore.FieldValue.increment(1); + diff --git a/functions/src/index.ts b/functions/src/index.ts index 9a49e4467..716b03970 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -13,17 +13,19 @@ import { onResponseGitWebhookRepository } from './repository/response-git-webhoo import { onUpdateRepository } from './repository/update-repository'; // Dashboard users +import { onCreateUser } from './user/create-user'; import { getUserEvents, EventsInput } from './user/events'; import { getUserRepos, ReposInput } from './user/repos'; import { onUpdateUserStats } from './user/stats'; +import { onUpdateUser } from './user/update-user'; // Dashboard projects import { deleteMonitorPings, ping, MonitorInfoInput } from './monitor/monitor'; -import { onDeleteProject, onDeleteProjectRepositories } from './project/delete-project'; -import { onUpdateProjectRepositories } from './project/update-repositories'; +import { onDeleteProject } from './project/delete-project'; +import { onUpdateProject } from './project/update-project'; import { onDeleteGitWebhookRepository, DeleteGitWebhookRepositoryInput } from './repository/delete-git-webhook-repository'; -import { onCreatePings, onCreateProject, onCreateUser } from './application/stats'; +import { onCreatePings as onCreatePingsStats, onCreateProject as onCreateProjectStats, onCreateUser as onCreateUserStats } from './application/stats'; import { updateViews, ProjectInput } from './project/project'; import { deletePingsAfter30days, runAllMonitors60Mins } from './scheduler/schedule'; @@ -34,23 +36,24 @@ declare type Change = functions.Change; export const findAllUserRepositories: HttpsFunction = functions.https.onCall((input: ReposInput, context: CallableContext) => getUserRepos(input.token, context.auth.uid)); export const findAllUserEvents: HttpsFunction = functions.https.onCall((input: EventsInput, context: CallableContext) => getUserEvents(input.token, context.auth.uid, input.username)); -export const findRepositoryInfo: HttpsFunction = functions.https.onCall((input: RepositoryInfoInput, context: CallableContext) => getRepositoryInfo(input.token, input.fullName)); +export const findRepositoryInfo: HttpsFunction = functions.https.onCall((input: RepositoryInfoInput, context: CallableContext) => getRepositoryInfo(input.token, input.repository)); export const createGitWebhookRepository: HttpsFunction = functions.https.onCall((input: CreateGitWebhookRepositoryInput, context: CallableContext) => onCreateGitWebhookRepository(input.token, input.repositoryUid)); -export const deleteGitWebhookRepository: HttpsFunction = functions.https.onCall((input: DeleteGitWebhookRepositoryInput, context: CallableContext) => onDeleteGitWebhookRepository(input.token, input.repositoryUid)); +export const deleteGitWebhookRepository: HttpsFunction = functions.https.onCall((input: DeleteGitWebhookRepositoryInput, context: CallableContext) => onDeleteGitWebhookRepository(input.token, input.data)); export const responseGitWebhookRepository: HttpsFunction = onResponseGitWebhookRepository; export const pingMonitor: HttpsFunction = functions.https.onCall((input: MonitorInfoInput, context: CallableContext) => ping(input.projectUid, input.monitorUid, input.type)); export const deletePingsByMonitor: HttpsFunction = functions.https.onCall((input: MonitorInfoInput, context: CallableContext) => deleteMonitorPings(input.projectUid, input.monitorUid)); export const updateProjectViews: HttpsFunction = functions.https.onCall((input: ProjectInput, context: CallableContext) => updateViews(input.projectUid)); -export const deletePingsByProject: CloudFunction = onDeleteProject; -export const deleteProjectRepositories: CloudFunction = onDeleteProjectRepositories; -export const updateProjectRepositories: CloudFunction = onUpdateProjectRepositories; +export const deleteProject: CloudFunction = onDeleteProject; +export const updateProject: CloudFunction = onUpdateProject; export const updateRepository: CloudFunction> = onUpdateRepository; export const createRepository: CloudFunction = onCreateRepository; export const updateUserStats: CloudFunction = onUpdateUserStats; export const delete30DaysPings: CloudFunction = deletePingsAfter30days; export const runPings60Mins: CloudFunction = runAllMonitors60Mins; - -export const createProject: CloudFunction = onCreateProject; -export const createPing: CloudFunction = onCreatePings; export const createUser: CloudFunction = onCreateUser; +export const updateUser: CloudFunction> = onUpdateUser; + +export const createProjectStat: CloudFunction = onCreateProjectStats; +export const createPingsStats: CloudFunction = onCreatePingsStats; +export const createUserStats: CloudFunction = onCreateUserStats; diff --git a/functions/src/mappers/github/repository.mapper.ts b/functions/src/mappers/github/repository.mapper.ts index 5373a112a..50bb5eb4e 100644 --- a/functions/src/mappers/github/repository.mapper.ts +++ b/functions/src/mappers/github/repository.mapper.ts @@ -11,24 +11,23 @@ import { GitHubReleaseModel } from './release.mapper'; import { GitHubRepositoryWebhookModel } from './webhook.mapper'; export interface GitHubRepositoryInput { - id: string; - uid: string; - name?: string; - full_name?: string; + id: number; + name: string; + full_name: string; description?: string; url: string; private: boolean; - fork: string; + fork: boolean; } export interface GitHubRepositoryModel { - id: string; - uid: string; - fullName?: string; + id: number; + uid?: string; + fullName: string; description?: string; url: string; private: boolean; - fork: string; + fork: boolean; pullRequests?: GitHubPullRequestModel[]; events?: GitHubEventModel[]; releases?: GitHubReleaseModel[]; @@ -40,28 +39,24 @@ export interface GitHubRepositoryModel { } export class GitHubRepositoryMapper { - static fullNameToUid(fullName: string) { - return fullName.replace('/', '+'); - } - static import(input: GitHubRepositoryInput, type: 'minimum' | 'all' | 'event' = 'minimum'): GitHubRepositoryModel { const output: any = {}; if (type === 'all') { - output.fork = input.fork; + output.fork = input.fork; } if (type === 'event' || type === 'all') { - output.id = input.id; - output.fullName = input.name; - output.url = input.url; + output.id = input.id; + output.fullName = input.name; + output.url = input.url; } if (type === 'minimum' || type === 'all') { - output.uid = GitHubRepositoryMapper.fullNameToUid(input.full_name); - output.fullName = input.full_name; - output.description = input.description; - output.private = input.private; + output.id = input.id; + output.fullName = input.full_name; + output.description = input.description; + output.private = input.private; } return output; diff --git a/functions/src/models/index.model.ts b/functions/src/models/index.model.ts index dd0bbd61d..8e123b6f6 100644 --- a/functions/src/models/index.model.ts +++ b/functions/src/models/index.model.ts @@ -1,3 +1,4 @@ export * from './monitor.model'; export * from './ping.model'; export * from './project.model'; +export * from './repository.model'; diff --git a/functions/src/models/project.model.ts b/functions/src/models/project.model.ts index eab1ea7f7..86bb52f41 100644 --- a/functions/src/models/project.model.ts +++ b/functions/src/models/project.model.ts @@ -8,6 +8,7 @@ export class ProjectModel { monitors?: MonitorModel[] = []; uid?: string = ''; url?: string = ''; + repositories?: string[] = []; constructor(uid: string = '') { this.uid = uid; diff --git a/functions/src/models/repository.model.ts b/functions/src/models/repository.model.ts new file mode 100644 index 000000000..5d85d3b39 --- /dev/null +++ b/functions/src/models/repository.model.ts @@ -0,0 +1,45 @@ +// Dashboard hub firebase functions models/mappers +import { DocumentData, DocumentReference, FirebaseAdmin, QuerySnapshot, WriteResult } from "../client/firebase-admin"; + +export interface IRepository { + id: number; + fullName: string; + uid?: string; +} + +export class RepositoryModel implements IRepository { + id: number; + fullName: string; + uid?: string; + + public static getRepositoryReference(uid: string): DocumentReference { + return FirebaseAdmin.firestore().collection('repositories').doc(uid); + } + + public static async getRepositoryById(id: number): Promise { + const repositoriesSnapshot: QuerySnapshot = await FirebaseAdmin.firestore().collection('repositories').where('id', '==', id).limit(1).get(); + if (repositoriesSnapshot.empty) { + return null; + } else { + return repositoriesSnapshot.docs.shift().data(); + } + } + + public static async getRepositoryUidById(id: number): Promise { + const repositoriesSnapshot: QuerySnapshot = await FirebaseAdmin.firestore().collection('repositories').where('id', '==', id).limit(1).get(); + if (repositoriesSnapshot.empty) { + return null; + } else { + return repositoriesSnapshot.docs.shift().id; + } + } + + public static async saveRepository(repository: DocumentData): Promise { + return await FirebaseAdmin + .firestore() + .collection('repositories') + .doc(repository.uid) + .set(repository, { merge: true }); + } + +} \ No newline at end of file diff --git a/functions/src/project/delete-project.ts b/functions/src/project/delete-project.ts index 263da7869..99ddc79e5 100644 --- a/functions/src/project/delete-project.ts +++ b/functions/src/project/delete-project.ts @@ -3,67 +3,67 @@ import { firestore, CloudFunction, EventContext } from 'firebase-functions'; // Dashboard hub firebase functions models/mappers import { Logger } from '../client/logger'; -import { ProjectModel } from '../models/index.model'; +import { ProjectModel, RepositoryModel } from '../models/index.model'; import { deleteMonitorPings } from '../monitor/monitor'; -import { DocumentData, DocumentSnapshot, FirebaseAdmin, WriteResult } from './../client/firebase-admin'; +import { DocumentData, DocumentReference, DocumentSnapshot, FirebaseAdmin, Transaction, WriteResult } from './../client/firebase-admin'; -export const onDeleteProjectRepositories: CloudFunction = firestore - .document('projects/{projectUid}') - .onDelete(async (projectSnapshot: DocumentSnapshot, context: EventContext) => { - - try { - const project: DocumentData = projectSnapshot.data(); - - if (Array.isArray(project.repositories) && project.repositories.length > 0) { - - const promiseList: Promise[] = []; - - for (const repositoryUid of project.repositories) { - const repoData: DocumentData = (await FirebaseAdmin.firestore().collection('repositories').doc(repositoryUid).get()).data(); - - if (Array.isArray(repoData.projects)) { - repoData.projects = repoData.projects.filter((element: string) => element !== context.params.projectUid); - } else { - repoData.projects = []; - } - - const repo: Promise = FirebaseAdmin.firestore().collection('repositories').doc(repositoryUid).update(repoData); - promiseList.push(repo); +async function deleteProjectRepositories(projectUid: string, project: ProjectModel): Promise { + Logger.info('deleteProjectRepositories'); + try { + if (Array.isArray(project.repositories) && project.repositories.length > 0) { + for (const repositoryUid of project.repositories) { + if (!repositoryUid) { + continue; } + const repoRef: DocumentReference = RepositoryModel.getRepositoryReference(repositoryUid); - await Promise.all(promiseList); - } + await FirebaseAdmin.firestore().runTransaction((t: Transaction) => { + return t.get(repoRef) + .then((repo: DocumentSnapshot) => { + const repoData: DocumentData = repo.data(); - return; + if (Array.isArray(repoData.projects)) { + repoData.projects = repoData.projects.filter((element: string) => element !== projectUid); + } else { + repoData.projects = []; + } - } catch (err) { - Logger.error(err); - throw new Error(err); + t.update(repoRef, { projects: repoData.projects }); + }); + }); + } } - }); + } catch (err) { + Logger.error(err); + throw new Error(err); + } + +}; export const deleteProjectPings: any = async (project: ProjectModel, isExpiredFlag: boolean): Promise => { - try { - if (Array.isArray(project.monitors) && project.monitors.length > 0) { - const promises: Promise[] = []; - - for (const monitor of project.monitors) { - promises.push(deleteMonitorPings(project.uid, monitor.uid, isExpiredFlag)) - } - return Promise.all(promises); + Logger.info('deleteProjectPings'); + try { + if (Array.isArray(project.monitors) && project.monitors.length > 0) { + const promises: Promise[] = []; + + for (const monitor of project.monitors) { + promises.push(deleteMonitorPings(project.uid, monitor.uid, isExpiredFlag)) } - return Promise.all([]); - - } catch (err) { - Logger.error(err); - throw new Error(err); + return Promise.all(promises); } - }; + return Promise.all([]); + + } catch (err) { + Logger.error(err); + throw new Error(err); + } +}; export const onDeleteProject: CloudFunction = firestore .document('projects/{projectUid}') .onDelete(async (projectSnapshot: DocumentSnapshot, context: EventContext) => { const project: ProjectModel = projectSnapshot.data(); deleteProjectPings(project); + await deleteProjectRepositories(context.params.projectUid, project); }); diff --git a/functions/src/project/update-project.ts b/functions/src/project/update-project.ts new file mode 100644 index 000000000..797737524 --- /dev/null +++ b/functions/src/project/update-project.ts @@ -0,0 +1,75 @@ +// Third party modules +import { firestore, Change, EventContext } from 'firebase-functions'; + +// Dashboard hub firebase functions models/mappers +import { Logger } from '../client/logger'; +import { RepositoryModel } from '../models/index.model'; +import { DocumentData, DocumentSnapshot, WriteResult } from './../client/firebase-admin'; + +async function updateRepositories(projectUid: string, newData: DocumentData, previousData: DocumentData): Promise { + const isArrayNewDataRepositories: boolean = Array.isArray(newData.repositories) && newData.repositories.length > 0; + const isArrayPreviousDataRepositories: boolean = Array.isArray(previousData.repositories) && previousData.repositories.length > 0; + + let add: string[], remove: string[]; + const promiseList: Promise[] = []; + + if (isArrayNewDataRepositories) { + if (isArrayPreviousDataRepositories) { + add = newData.repositories.filter((element: string) => previousData.repositories.findIndex((item: string) => element === item) === -1); + remove = previousData.repositories.filter((element: string) => newData.repositories.findIndex((item: string) => element === item) === -1); + } else { + add = newData.repositories; + remove = []; + } + } else { + add = []; + remove = isArrayPreviousDataRepositories ? previousData.repositories : []; + } + + for (const item of add) { + const repoData: DocumentData = (await RepositoryModel.getRepositoryReference(item).get()).data(); + + if (Array.isArray(repoData.projects)) { + const foundIndex: number = repoData.projects.findIndex((element: string) => element === projectUid); + if (foundIndex === -1) { + repoData.projects.push(projectUid); + } + } else { + repoData.projects = [projectUid]; + } + + const repo: Promise = RepositoryModel.getRepositoryReference(repoData.uid).update({projects: repoData.projects}); + promiseList.push(repo); + } + + for (const item of remove) { + const repoData: DocumentData = (await RepositoryModel.getRepositoryReference(item).get()).data(); + + if (Array.isArray(repoData.projects)) { + repoData.projects = repoData.projects.filter((element: string) => element !== projectUid); + } else { + repoData.projects = []; + } + + const repo: Promise = RepositoryModel.getRepositoryReference(repoData.uid).update({projects: repoData.projects}); + promiseList.push(repo); + } + + return Promise.all(promiseList); +} + +export const onUpdateProject: any = firestore + .document('projects/{projectUid}') + .onUpdate(async (change: Change, context: EventContext) => { + + try { + const newData: DocumentData = change.after.data(); + const previousData: DocumentData = change.before.data(); + + return updateRepositories(context.params.projectUid, newData, previousData); + } catch (err) { + Logger.error(err); + throw new Error(err); + } + + }); diff --git a/functions/src/project/update-repositories.ts b/functions/src/project/update-repositories.ts deleted file mode 100644 index 877ab4365..000000000 --- a/functions/src/project/update-repositories.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Third party modules -import { firestore, Change, EventContext } from 'firebase-functions'; - -// Dashboard hub firebase functions models/mappers -import { Logger } from '../client/logger'; -import { DocumentData, DocumentSnapshot, FirebaseAdmin, WriteResult } from './../client/firebase-admin'; - -export const onUpdateProjectRepositories: any = firestore - .document('projects/{projectUid}') - .onUpdate(async (change: Change, context: EventContext) => { - - try { - const newData: DocumentData = change.after.data(); - const previousData: DocumentData = change.before.data(); - const isArrayNewDataRepositories: boolean = Array.isArray(newData.repositories) && newData.repositories.length > 0; - const isArrayPreviousDataRepositories: boolean = Array.isArray(previousData.repositories) && previousData.repositories.length > 0; - - let add: string[], remove: string[]; - const promiseList: Promise[] = []; - - if (isArrayNewDataRepositories) { - if (isArrayPreviousDataRepositories) { - add = newData.repositories.filter((element: string) => previousData.repositories.findIndex((item: string) => element === item) === -1); - remove = previousData.repositories.filter((element: string) => newData.repositories.findIndex((item: string) => element === item) === -1); - } else { - add = newData.repositories; - remove = []; - } - } else { - add = []; - remove = isArrayPreviousDataRepositories ? previousData.repositories : []; - } - - for (const repositoryUid of add) { - const repoData: DocumentData = (await FirebaseAdmin.firestore().collection('repositories').doc(repositoryUid).get()).data(); - - if (Array.isArray(repoData.projects)) { - const foundIndex: number = repoData.projects.findIndex((element: string) => element === context.params.projectUid); - if (foundIndex === -1) { - repoData.projects.push(context.params.projectUid); - } - } else { - repoData.projects = [context.params.projectUid]; - } - - const repo: Promise = FirebaseAdmin.firestore().collection('repositories').doc(repositoryUid).update(repoData); - promiseList.push(repo); - } - - for (const repositoryUid of remove) { - const repoData: DocumentData = (await FirebaseAdmin.firestore().collection('repositories').doc(repositoryUid).get()).data(); - - if (Array.isArray(repoData.projects)) { - repoData.projects = repoData.projects.filter((element: string) => element !== context.params.projectUid); - } else { - repoData.projects = []; - } - - const repo: Promise = FirebaseAdmin.firestore().collection('repositories').doc(repositoryUid).set(repoData, { merge: true }); - promiseList.push(repo); - } - - return Promise.all(promiseList); - - } catch (err) { - Logger.error(err); - throw new Error(err); - } - - }); diff --git a/functions/src/repository/create-git-webhook-repository.ts b/functions/src/repository/create-git-webhook-repository.ts index 4687d563f..d48af7ef7 100644 --- a/functions/src/repository/create-git-webhook-repository.ts +++ b/functions/src/repository/create-git-webhook-repository.ts @@ -1,8 +1,10 @@ import { enviroment } from '../environments/environment'; import { GitHubRepositoryWebhookMapper, GitHubRepositoryWebhookModel, GitHubRepositoryWebhookRequestCreate, GitHubRepositoryWebhookResponse } from '../mappers/github/webhook.mapper'; -import { DocumentData, DocumentReference, FirebaseAdmin } from './../client/firebase-admin'; +import { RepositoryModel } from '../models/index.model'; +import { DocumentData, DocumentReference } from './../client/firebase-admin'; import { GitHubClientPost } from './../client/github'; import { Logger } from './../client/logger'; +import { deleteWebhook } from './delete-git-webhook-repository'; import { findWebhook } from './find-git-webhook-repository'; export interface CreateGitWebhookRepositoryInput { @@ -12,7 +14,7 @@ export interface CreateGitWebhookRepositoryInput { export const onCreateGitWebhookRepository: any = async (token: string, repositoryUid: string) => { try { - const repositorySnapshot: DocumentReference = FirebaseAdmin.firestore().collection('repositories').doc(repositoryUid); + const repositorySnapshot: DocumentReference = RepositoryModel.getRepositoryReference(repositoryUid); const repository: DocumentData = (await repositorySnapshot.get()).data(); const webhook: GitHubRepositoryWebhookModel = await getWebhook(repository.fullName, token); @@ -20,7 +22,7 @@ export const onCreateGitWebhookRepository: any = async (token: string, repositor repository.webhook = webhook; await repositorySnapshot.update(repository); - Logger.info({ webhook }); + Logger.info(webhook ? 'Webhook created' : 'Webhook empty'); return repository; } catch (error) { @@ -30,7 +32,7 @@ export const onCreateGitWebhookRepository: any = async (token: string, repositor }; -export function createWebhook(repositoryUid: string, token: string): Promise { +export function createWebhook(repositoryFullName: string, token: string): Promise { const body: GitHubRepositoryWebhookRequestCreate = { // name: enviroment.githubWebhook.name, name: 'web', @@ -42,16 +44,32 @@ export function createWebhook(repositoryUid: string, token: string): Promise(`/repos/${repositoryUid}/hooks`, token, body); + return GitHubClientPost(`/repos/${repositoryFullName}/hooks`, token, body); } -export async function getWebhook(repositoryUid: string, token: string): Promise { +export async function getWebhook(repositoryFullName: string, token: string): Promise { - const exist: GitHubRepositoryWebhookResponse = await findWebhook(repositoryUid, token); + const exist: GitHubRepositoryWebhookResponse = await findWebhook(repositoryFullName, token); if (exist) { - return GitHubRepositoryWebhookMapper.import(exist); + let isEqual: boolean = exist.events.length === enviroment.githubWebhook.events.length; + + if (isEqual) { + + for (const existItem of exist.events) { + if (enviroment.githubWebhook.events.findIndex((envItem: string) => existItem === envItem) === -1) { + isEqual = false; + break; + } + } + + if (isEqual) { + return GitHubRepositoryWebhookMapper.import(exist); + } + } + await deleteWebhook(repositoryFullName, exist.id, token); } - return GitHubRepositoryWebhookMapper.import(await createWebhook(repositoryUid, token)); + + return GitHubRepositoryWebhookMapper.import(await createWebhook(repositoryFullName, token)); } diff --git a/functions/src/repository/create-repository.ts b/functions/src/repository/create-repository.ts index 147da16c4..353a367bc 100644 --- a/functions/src/repository/create-repository.ts +++ b/functions/src/repository/create-repository.ts @@ -3,11 +3,11 @@ import { firestore, CloudFunction, EventContext } from 'firebase-functions'; // Dashboard hub firebase functions models/mappers import { Logger } from '../client/logger'; -import { DocumentData, DocumentSnapshot, FirebaseAdmin, QueryDocumentSnapshot } from './../client/firebase-admin'; +import { GitHubRepositoryModel } from '../mappers/github/index.mapper'; +import { DocumentData, DocumentReference, DocumentSnapshot, FieldPath, FirebaseAdmin, Query, QueryDocumentSnapshot, QuerySnapshot, Transaction } from './../client/firebase-admin'; async function addProjects(repositoryUid: string, repo: DocumentData): Promise { const projects: QueryDocumentSnapshot[] = (await FirebaseAdmin.firestore().collection('projects').where('repositories', 'array-contains', repositoryUid).get()).docs; - if (!Array.isArray(repo.projects)) { repo.projects = []; } @@ -16,23 +16,82 @@ async function addProjects(repositoryUid: string, repo: DocumentData): Promise 0) { + if (repo.projects.length > 0) { return true; } return false; } +async function removeDublicateRepository(repositoryUid: string, repo: DocumentData, repoRef: DocumentReference): Promise { + const repoQuerySnap: QuerySnapshot = (await FirebaseAdmin.firestore().collection('repositories').where('id', '==', repo.id).where('uid', '<', repositoryUid).where('uid', '>', repositoryUid).get()); + if (repoQuerySnap.empty) { + return true; + } else { + + const existRepoUid: string = repoQuerySnap.docs.shift().id; + + // update repo data in user + const usersRef: Query = FirebaseAdmin.firestore().collection('users').where(new FieldPath('repositories', 'uids'), 'array-contains', repositoryUid); + await FirebaseAdmin.firestore().runTransaction((t: Transaction) => { + return t.get(usersRef) + .then((snap: QuerySnapshot) => { + snap.forEach((element: QueryDocumentSnapshot) => { + const userData: DocumentData = element.data(); + if (userData.repositories && Array.isArray(userData.repositories.data) && userData.repositories.data.length > 0) { + const repos: GitHubRepositoryModel[] = userData.repositories.data; + const uids: string[] = userData.repositories.uids; + const foundIndexObj: number = repos.findIndex((item: GitHubRepositoryModel) => item.uid && item.uid === repositoryUid); + const foundIndexUids: number = uids.findIndex((uid: string) => uid === repositoryUid); + + if (foundIndexObj > -1 && foundIndexUids > -1) { + repos[foundIndexObj].uid = existRepoUid; + uids[foundIndexUids] = existRepoUid; + t.update(element.ref, { repositories: { ...userData.repositories, data: repos, uids } }); + } + } + }); + + }); + }); + + // update repo data in project + const projectsRef: Query = FirebaseAdmin.firestore().collection('projects').where('repositories', 'array-contains', repositoryUid); + await FirebaseAdmin.firestore().runTransaction((t: Transaction) => { + return t.get(projectsRef) + .then((snap: QuerySnapshot) => { + snap.forEach((element: QueryDocumentSnapshot) => { + const projectData: DocumentData = element.data(); + if (Array.isArray(projectData.repositories) && projectData.repositories.length > 0) { + const uids: string[] = projectData.repositories; + const foundIndex: number = uids.findIndex((uid: string) => uid === repositoryUid); + + if (foundIndex > -1) { + uids[foundIndex] = existRepoUid; + t.update(element.ref, { repositories: uids }); + } + } + }); + + }); + }); + + await repoRef.delete(); + return false; + } +} + export const onCreateRepository: CloudFunction = firestore .document('repositories/{repositoryUid}') .onCreate(async (repositorySnapshot: DocumentSnapshot, context: EventContext) => { try { const newData: DocumentData = repositorySnapshot.data(); - const isNeedUpdate: boolean = await addProjects(context.params.repositoryUid, newData); + let isNeedUpdate: boolean = await removeDublicateRepository(context.params.repositoryUid, newData, repositorySnapshot.ref); + isNeedUpdate = isNeedUpdate && await addProjects(context.params.repositoryUid, newData); + if (isNeedUpdate) { - return repositorySnapshot.ref.update(newData); + await repositorySnapshot.ref.update(newData); } - return newData; } catch (err) { Logger.error(err); throw new Error(err); diff --git a/functions/src/repository/delete-git-webhook-repository.ts b/functions/src/repository/delete-git-webhook-repository.ts index a7e86029a..3bc0ecc58 100644 --- a/functions/src/repository/delete-git-webhook-repository.ts +++ b/functions/src/repository/delete-git-webhook-repository.ts @@ -1,21 +1,35 @@ -import { DocumentData, DocumentReference, FirebaseAdmin } from './../client/firebase-admin'; +import { RepositoryModel } from '../models/index.model'; +import { DocumentData, DocumentReference } from './../client/firebase-admin'; import { GitHubClientDelete } from './../client/github'; import { Logger } from './../client/logger'; export interface DeleteGitWebhookRepositoryInput { token: string; - repositoryUid: string; + data: { uid?: string, id?: number }; } -export const onDeleteGitWebhookRepository: any = async (token: string, repositoryUid: string) => { - const repositorySnapshot: DocumentReference = FirebaseAdmin.firestore().collection('repositories').doc(repositoryUid); - const repository: DocumentData = (await repositorySnapshot.get()).data(); +export const onDeleteGitWebhookRepository: any = async (token: string, data: { uid?: string, id?: number }) => { + let repository: DocumentData; + let repositoryRef: DocumentReference; + + if (!(data && (data.uid || data.id))) { + Logger.error('Invalid input data!'); + return null; + } + + if (data.uid) { + repositoryRef = RepositoryModel.getRepositoryReference(data.uid); + repository = (await repositoryRef.get()).data(); + } else if (data.id) { + repository = await RepositoryModel.getRepositoryById(data.id); + repositoryRef = RepositoryModel.getRepositoryReference(repository.uid); + } try { - if (repository.projects && repository.projects.length === 1 && repository.webhook) { - await deleteWebhook(repository, token); + if (repository && repository.projects && repository.projects.length === 1 && repository.webhook) { + await deleteWebhook(repository.fullName, repository.webhook.id, token); repository.webhook = null; - await repositorySnapshot.update(repository); + await repositoryRef.update(repository); } Logger.info(`Webhook removed for ${repository.fullName}`); @@ -29,6 +43,6 @@ export const onDeleteGitWebhookRepository: any = async (token: string, repositor }; -function deleteWebhook(repository: DocumentData, token: string): Promise { - return GitHubClientDelete(`/repos/${repository.fullName}/hooks/${repository.webhook.id}`, token); +export function deleteWebhook(repositoryFullName: string, webhookId: number, token: string): Promise { + return GitHubClientDelete(`/repos/${repositoryFullName}/hooks/${webhookId}`, token); } diff --git a/functions/src/repository/find-git-webhook-repository.ts b/functions/src/repository/find-git-webhook-repository.ts index cc788b924..a06c4c0d8 100644 --- a/functions/src/repository/find-git-webhook-repository.ts +++ b/functions/src/repository/find-git-webhook-repository.ts @@ -3,13 +3,13 @@ import { GitHubRepositoryWebhookResponse } from '../mappers/github/webhook.mappe import { GitHubClient } from './../client/github'; -export function listWebhook(repositoryUid: string, token: string): Promise { - return GitHubClient(`/repos/${repositoryUid}/hooks`, token); +export function listWebhook(repositoryFullName: string, token: string): Promise { + return GitHubClient(`/repos/${repositoryFullName}/hooks`, token); } -export async function findWebhook(repositoryUid: string, token: string): Promise { - const list: GitHubRepositoryWebhookResponse[] = await listWebhook(repositoryUid, token); +export async function findWebhook(repositoryFullName: string, token: string): Promise { + const list: GitHubRepositoryWebhookResponse[] = await listWebhook(repositoryFullName, token); return list.find((elem: GitHubRepositoryWebhookResponse) => elem && elem.config && elem.config.url === enviroment.githubWebhook.url) } diff --git a/functions/src/repository/info.ts b/functions/src/repository/info.ts index 94840f5a5..dbd740da9 100644 --- a/functions/src/repository/info.ts +++ b/functions/src/repository/info.ts @@ -1,6 +1,7 @@ // Third party modules import { firestore } from 'firebase-admin'; +// Dashboard hub firebase functions models/mappers import { GitHubContributorInput, GitHubContributorMapper, GitHubEventInput, GitHubEventMapper, @@ -19,10 +20,14 @@ import { getWebhook } from './create-git-webhook-repository'; export interface RepositoryInfoInput { token: string; - fullName: string; + repository: { + uid: string; + id: number; + fullName: string; + } } -export const getRepositoryInfo: any = async (token: string, fullName: string) => { +export const getRepositoryInfo: any = async (token: string, repository: { uid: string; id: number; fullName: string; }) => { let data: [ GitHubRepositoryInput, GitHubPullRequestInput[], @@ -37,13 +42,13 @@ export const getRepositoryInfo: any = async (token: string, fullName: string) => try { data = await Promise.all([ - GitHubClient(`/repos/${fullName}`, token), - GitHubClient(`/repos/${fullName}/pulls?state=open`, token), - GitHubClient(`/repos/${fullName}/events`, token), - GitHubClient(`/repos/${fullName}/releases`, token), - GitHubClient(`/repos/${fullName}/issues`, token), - GitHubClient(`/repos/${fullName}/stats/contributors`, token), - GitHubClient(`/repos/${fullName}/milestones`, token), + GitHubClient(`/repos/${repository.fullName}`, token), + GitHubClient(`/repos/${repository.fullName}/pulls?state=open`, token), + GitHubClient(`/repos/${repository.fullName}/events`, token), + GitHubClient(`/repos/${repository.fullName}/releases`, token), + GitHubClient(`/repos/${repository.fullName}/issues`, token), + GitHubClient(`/repos/${repository.fullName}/stats/contributors`, token), + GitHubClient(`/repos/${repository.fullName}/milestones`, token), ]); mappedData = { ...GitHubRepositoryMapper.import(data[0], 'all'), @@ -56,18 +61,18 @@ export const getRepositoryInfo: any = async (token: string, fullName: string) => updatedAt: firestore.Timestamp.fromDate(new Date()), }; } catch (error) { - Logger.error(error, [`Repository Path: ${fullName}`]); + Logger.error(error, [`Repository Path: ${repository.fullName}`]); throw new Error(error); } try { - mappedData.webhook = await getWebhook(fullName, token); + mappedData.webhook = await getWebhook(repository.fullName, token); } catch (error) { - Logger.error(error, ['Webhook failed', `Repository Path: ${fullName}`]); + Logger.error(error, ['Webhook failed', `Repository Path: ${repository.fullName}`]); } Logger.info({ - repository: fullName, + repository: repository.fullName, imported: { pullRequests: mappedData && mappedData.pullRequests.length || 0, events: mappedData && mappedData.events.length || 0, @@ -79,10 +84,12 @@ export const getRepositoryInfo: any = async (token: string, fullName: string) => }, }); + mappedData.uid = repository.uid; + await FirebaseAdmin .firestore() .collection('repositories') - .doc(GitHubRepositoryMapper.fullNameToUid(fullName)) + .doc(mappedData.uid) .set(mappedData, { merge: true }); return mappedData; diff --git a/functions/src/repository/update-repository.ts b/functions/src/repository/update-repository.ts index d3e4ce069..5b4ca81e2 100644 --- a/functions/src/repository/update-repository.ts +++ b/functions/src/repository/update-repository.ts @@ -3,7 +3,8 @@ import { firestore, Change, CloudFunction, EventContext } from 'firebase-functio // Dashboard hub firebase functions models/mappers import { Logger } from '../client/logger'; -import { DocumentData, DocumentSnapshot, FirebaseAdmin } from './../client/firebase-admin'; +import { RepositoryModel } from '../models/index.model'; +import { DocumentData, DocumentSnapshot } from './../client/firebase-admin'; export const onUpdateRepository: CloudFunction> = firestore .document('repositories/{repositoryUid}') @@ -13,7 +14,8 @@ export const onUpdateRepository: CloudFunction> = fires const newData: DocumentData = change.after.data(); if (!newData.projects || Array.isArray(newData.projects) && newData.projects.length === 0) { - return FirebaseAdmin.firestore().collection('repositories').doc(context.params.repositoryUid).delete(); + Logger.info(`Delete repository ${context.params.repositoryUid}`); + return RepositoryModel.getRepositoryReference(context.params.repositoryUid).delete(); } return newData; diff --git a/functions/src/user/create-user.ts b/functions/src/user/create-user.ts new file mode 100644 index 000000000..ba11928b3 --- /dev/null +++ b/functions/src/user/create-user.ts @@ -0,0 +1,48 @@ +// Third party modules +import { firestore, CloudFunction, EventContext } from 'firebase-functions'; +import { v4 as uuid } from 'uuid'; + +// Dashboard hub firebase functions models/mappers +import { Logger } from '../client/logger'; +import { GitHubRepositoryModel } from '../mappers/github/index.mapper'; +import { DocumentData, DocumentSnapshot } from './../client/firebase-admin'; + +// Dashboard models +import { RepositoryModel } from '../models/index.model'; + +async function addUidToRepositories(user: DocumentData): Promise { + if (user.repositories && Array.isArray(user.repositories.data) && user.repositories.data.length > 0) { + const uids: string[] = []; + user.repositories.data = user.repositories.data + .filter((item: GitHubRepositoryModel) => item && item.id !== null && item.id !== undefined); + + for (const item of user.repositories.data) { + const exitsRepoUid: string = await RepositoryModel.getRepositoryUidById(item.id); + if (exitsRepoUid) { + item.uid = exitsRepoUid; + } else { + item.uid = uuid(); + } + uids.push(item.uid); + } + user.repositories.data.uids = uids + return true; + } + + return false; +} + +export const onCreateUser: CloudFunction = firestore + .document('users/{userUid}') + .onCreate(async (userSnapshot: DocumentSnapshot, context: EventContext) => { + try { + const newData: DocumentData = userSnapshot.data(); + const isNeedUpdate: boolean = await addUidToRepositories(newData); + if (isNeedUpdate) { + await userSnapshot.ref.update(newData); + } + } catch (err) { + Logger.error(err); + throw new Error(err); + } + }); diff --git a/functions/src/user/events.ts b/functions/src/user/events.ts index 8195426f1..d79a8251e 100644 --- a/functions/src/user/events.ts +++ b/functions/src/user/events.ts @@ -1,4 +1,4 @@ -import { FirebaseAdmin } from './../client/firebase-admin'; +import { DocumentReference, FirebaseAdmin, WriteBatch } from './../client/firebase-admin'; import { GitHubEventInput, GitHubEventMapper, GitHubEventModel } from '../mappers/github/index.mapper'; import { GitHubClient } from './../client/github'; @@ -10,6 +10,8 @@ export interface EventsInput { } export const getUserEvents: any = async (token: string, uid: string, username: string) => { + const batch: WriteBatch = FirebaseAdmin.firestore().batch(); + const userRef: DocumentReference = FirebaseAdmin.firestore().collection('users').doc(uid); let events: GitHubEventInput[] = []; try { events = await GitHubClient(`/users/${username}/events`, token); @@ -19,13 +21,12 @@ export const getUserEvents: any = async (token: string, uid: string, username: s } const mappedEvents: GitHubEventModel[] = events.map((event: GitHubEventInput) => GitHubEventMapper.import(event)); - await FirebaseAdmin - .firestore() - .collection('users') - .doc(uid) - .set({ + await batch + .update(userRef, { activity: mappedEvents, - }, { merge: true }); + }); + + await batch.commit(); return mappedEvents; }; diff --git a/functions/src/user/repos.ts b/functions/src/user/repos.ts index 43d629251..3a32898d1 100644 --- a/functions/src/user/repos.ts +++ b/functions/src/user/repos.ts @@ -2,7 +2,7 @@ import * as firebase from 'firebase-admin'; // Dashboard hub firebase functions mappers -import { FirebaseAdmin } from './../client/firebase-admin'; +import { DocumentReference, FirebaseAdmin, WriteBatch } from './../client/firebase-admin'; import { GitHubClient } from './../client/github'; import { Logger } from './../client/logger'; import { GitHubRepositoryInput, GitHubRepositoryMapper, GitHubRepositoryModel } from './../mappers/github/repository.mapper'; @@ -12,6 +12,8 @@ export interface ReposInput { } export const getUserRepos: any = async (token: string, uid: string) => { + const batch: WriteBatch = FirebaseAdmin.firestore().batch(); + const userRef: DocumentReference = FirebaseAdmin.firestore().collection('users').doc(uid); let repositories: GitHubRepositoryInput[] = []; try { repositories = await GitHubClient('/user/repos?visibility=public&affiliation=owner', token); @@ -29,16 +31,15 @@ export const getUserRepos: any = async (token: string, uid: string) => { }, }); - await FirebaseAdmin - .firestore() - .collection('users') - .doc(uid) - .set({ + await batch + .update(userRef, { repositories: { lastUpdated: firebase.firestore.Timestamp.fromDate(new Date()), data: mappedRepos, }, - }, { merge: true }); + }); + + await batch.commit(); return mappedRepos; }; diff --git a/functions/src/user/stats.ts b/functions/src/user/stats.ts index 6b134f542..73211b82c 100644 --- a/functions/src/user/stats.ts +++ b/functions/src/user/stats.ts @@ -12,13 +12,18 @@ export const onUpdateUserStats: any = firestore .onWrite((change: Change, context: EventContext) => { const user: DocumentData = change.after.data(); + if (!user) { + // TODO delete + return null; + } + const data: GitHubUserStatsModel = { name: user.name, username: user.username, avatarUrl: user.avatarUrl, github: { repository: { - total: user.repositories.data.length || 0, + total: user.repositories && user.repositories.data ? user.repositories.data.length : 0, }, activity: { latest: user.activity ? user.activity[0] : {}, diff --git a/functions/src/user/update-user.ts b/functions/src/user/update-user.ts new file mode 100644 index 000000000..b9be161ea --- /dev/null +++ b/functions/src/user/update-user.ts @@ -0,0 +1,96 @@ +// Third party modules +import { firestore, Change, EventContext } from 'firebase-functions'; +import { v4 as uuid } from 'uuid'; + +// Dashboard hub firebase functions models/mappers +import { Logger } from '../client/logger'; +import { GitHubRepositoryModel } from '../mappers/github/index.mapper'; +import { DocumentData, DocumentSnapshot } from './../client/firebase-admin'; + +// Dashboard models +import { RepositoryModel } from '../models/index.model'; + +async function updateRepositories(newData: DocumentData, previousData: DocumentData): Promise { + const isArrayNewDataRepositories: boolean = !!(newData && newData.repositories && Array.isArray(newData.repositories.data) && newData.repositories.data.length > 0); + + if (!isArrayNewDataRepositories) { + return null; + } + + const newRepos: GitHubRepositoryModel[] = newData.repositories.data; + const isArrayPreviousDataRepositories: boolean = !!(previousData && previousData.repositories && Array.isArray(previousData.repositories.data) && previousData.repositories.data.length > 0); + const oldRepos: GitHubRepositoryModel[] = isArrayPreviousDataRepositories ? previousData.repositories.data : []; + let isExitFn: boolean = true; + + if (isArrayPreviousDataRepositories && newRepos.length === oldRepos.length) { + for (const item1 of newRepos) { + if ( + (!item1) + || (!item1.uid) + || item1.id === null + || item1.id === undefined + || oldRepos.findIndex((item2: GitHubRepositoryModel) => item1.id === item2.id) === -1 + ) { + isExitFn = false; + break; + } + } + } else { + isExitFn = false; + } + + if (isExitFn) { + return null; + } + + const result: GitHubRepositoryModel[] = newRepos.filter((item: GitHubRepositoryModel) => item && item.id !== null && item.id !== undefined); + const uids: string[] = []; + + if (isArrayPreviousDataRepositories) { + result.forEach((item1: GitHubRepositoryModel) => { + const found: GitHubRepositoryModel = oldRepos.find((item2: GitHubRepositoryModel) => item1.id === item2.id); + if (found && found.uid) { + item1.uid = found.uid; + } + }); + } + + for (const item of result) { + if (!item.uid) { + const exitsRepoUid: string = await RepositoryModel.getRepositoryUidById(item.id); + if (exitsRepoUid) { + item.uid = exitsRepoUid; + } else { + item.uid = uuid(); + } + } + uids.push(item.uid); + } + + return { + repositories: { + ...newData.repositories, + data: result, + uids, + }, + }; +} + +export const onUpdateUser: any = firestore + .document('users/{userUid}') + .onUpdate(async (change: Change, context: EventContext) => { + + try { + const newData: DocumentData = change.after.data(); + const previousData: DocumentData = change.before.data(); + + const result: any = await updateRepositories(newData, previousData); + + // Then return a promise of a set operation to update the count + return result ? change.after.ref.update(result) : null; + } catch (err) { + Logger.error(err); + throw new Error(err); + } + + }); diff --git a/web/package.json b/web/package.json index 9848137d6..643499644 100644 --- a/web/package.json +++ b/web/package.json @@ -58,7 +58,7 @@ "rxjs": "6.5.2", "tslib": "^1.9.0", "tslint-etc": "^1.6.0", - "uuid": "^3.3.2", + "uuid": "^3.3.3", "web-animations-js": "2.3.1", "zone.js": "0.8.29" }, @@ -69,6 +69,7 @@ "@types/hammerjs": "2.0.36", "@types/jasmine": "2.8.9", "@types/node": "10.12.0", + "@types/uuid": "3.4.3", "codelyzer": "4.5.0", "concurrently": "^4.1.0", "cypress": "^3.4.1", diff --git a/web/src/app/core/services/project.service.ts b/web/src/app/core/services/project.service.ts index 9463c1bd8..8094b8be8 100644 --- a/web/src/app/core/services/project.service.ts +++ b/web/src/app/core/services/project.service.ts @@ -117,14 +117,14 @@ export class ProjectService { // This function add the repository in any project // @TODO: move to repository service - public saveRepositories(project: ProjectModel, repositories: string[]): Observable { + public saveRepositories(project: ProjectModel, repositories: RepositoryModel[]): Observable { // remove webhook from unselected repo if (project.repositories && project.repositories.length > 0) { const remove: string[] = project.repositories - .filter((uid: string) => repositories.findIndex((name: string) => uid === RepositoryModel.getUid(name)) === -1); + .filter((uid: string) => repositories.findIndex((repo: RepositoryModel) => uid === repo.uid) === -1); const removeWebhooks: Observable[] = []; - remove.forEach((element: string) => { - const tmp: Observable = this.repositoryService.deleteGitWebhook(element).pipe(take(1)); + remove.forEach((uid: string) => { + const tmp: Observable = this.repositoryService.deleteGitWebhook({ uid }).pipe(take(1)); removeWebhooks.push(tmp); }); forkJoin(removeWebhooks).subscribe(); @@ -150,16 +150,20 @@ export class ProjectService { return this.activityService .start() .pipe( - mergeMap(() => forkJoin(...repositories.map((repository: string) => this.repositoryService.loadRepository(repository)))), + mergeMap(() => forkJoin( + ...repositories.map((repository: RepositoryModel) => this.repositoryService.loadRepository(repository)) + )), switchMap(() => this.afs .collection('projects') .doc(project.uid) .set( { - repositories: repositories.map((repoUid: string) => new RepositoryModel(repoUid).uid), + repositories: repositories.map((repo: RepositoryModel) => repo.uid), updatedOn: firebase.firestore.Timestamp.fromDate(new Date()), }, - { merge: true })), + { merge: true } + ) + ), take(1) ); } diff --git a/web/src/app/core/services/repository.service.ts b/web/src/app/core/services/repository.service.ts index 4ed5336e0..62ca02b48 100644 --- a/web/src/app/core/services/repository.service.ts +++ b/web/src/app/core/services/repository.service.ts @@ -37,9 +37,9 @@ export class RepositoryService { } // This function loads all the available repositories - public loadRepository(fullName: string): Observable { + public loadRepository(repo: RepositoryModel): Observable { const callable: any = this.fns.httpsCallable('findRepositoryInfo'); - return callable({ fullName, token: this.authService.profile.oauth.githubToken }); + return callable({ repository: repo, token: this.authService.profile.oauth.githubToken }); } public createGitWebhook(repo: RepositoryModel): Observable { @@ -47,8 +47,8 @@ export class RepositoryService { return callable({ repositoryUid: repo.uid, token: this.authService.profile.oauth.githubToken }); } - public deleteGitWebhook(repositoryUid: string): Observable { + public deleteGitWebhook(repo: { uid?: string, id?: number }): Observable { const callable: any = this.fns.httpsCallable('deleteGitWebhookRepository'); - return callable({ repositoryUid, token: this.authService.profile.oauth.githubToken }); + return callable({ data: { uid: repo.uid, id: repo.id }, token: this.authService.profile.oauth.githubToken }); } } diff --git a/web/src/app/projects/repository/repository.component.html b/web/src/app/projects/repository/repository.component.html index 3cb518838..918015178 100644 --- a/web/src/app/projects/repository/repository.component.html +++ b/web/src/app/projects/repository/repository.component.html @@ -1,7 +1,7 @@ {{ repository.fullName }} - diff --git a/web/src/app/projects/repository/repository.component.ts b/web/src/app/projects/repository/repository.component.ts index d67ec4276..235d458eb 100644 --- a/web/src/app/projects/repository/repository.component.ts +++ b/web/src/app/projects/repository/repository.component.ts @@ -14,7 +14,7 @@ import { ContributorModel, MilestoneModel, PullRequestModel, ReleaseModel, Repos export class RepositoryComponent implements OnInit, OnDestroy { private repositorySubscription: Subscription; - public isAlertEnabled: Boolean = false; + public isAlertEnabled: boolean = false; @Input() public uid: string; @@ -60,9 +60,9 @@ export class RepositoryComponent implements OnInit, OnDestroy { .subscribe(); } - public reloadRepository(repositoryName: string): void { + public reloadRepository(repository: RepositoryModel): void { this.manualReload = true; - this.repositoryService.loadRepository(repositoryName) + this.repositoryService.loadRepository(repository) .subscribe(() => setTimeout(() => this.manualReload = false, 60000)); // disable the ping button for 60 seconds; } diff --git a/web/src/app/projects/view/view.component.ts b/web/src/app/projects/view/view.component.ts index cd4bec77a..e2ee52d2e 100644 --- a/web/src/app/projects/view/view.component.ts +++ b/web/src/app/projects/view/view.component.ts @@ -4,13 +4,13 @@ import { ActivatedRoute, Router } from '@angular/router'; // Thid party modules import { Subscription } from 'rxjs'; -import { filter, switchMap } from 'rxjs/operators'; +import { filter, switchMap, take } from 'rxjs/operators'; // DashboardHub import { AuthenticationService, ProjectService } from '@core/services/index.service'; import { DialogConfirmationComponent } from '@shared/dialog/confirmation/dialog-confirmation.component'; import { DialogListComponent } from '@shared/dialog/list/dialog-list.component'; -import { ProjectModel } from '@shared/models/index.model'; +import { ProjectModel, RepositoryModel } from '@shared/models/index.model'; @Component({ selector: 'dashboard-projects-view', @@ -53,13 +53,14 @@ export class ViewProjectComponent implements OnInit, OnDestroy { }) .afterClosed() .pipe( - filter((selectedRepositories: { value: string }[]) => !!selectedRepositories), - switchMap((selectedRepositories: { value: string }[]) => this.projectService.saveRepositories( + take(1), + filter((selectedRepositories: { value: RepositoryModel }[]) => !!selectedRepositories), + switchMap((selectedRepositories: { value: RepositoryModel }[]) => this.projectService.saveRepositories( this.project, - selectedRepositories.map((fullName: { value: string }) => fullName.value) + selectedRepositories.map((item: { value: RepositoryModel }) => item.value).filter((value: RepositoryModel) => value.uid) )) ) - .subscribe(() => true); + .subscribe(); } // This function delete the project diff --git a/web/src/app/shared/dialog/list/dialog-list.component.html b/web/src/app/shared/dialog/list/dialog-list.component.html index 692c2666f..6b46d85a8 100644 --- a/web/src/app/shared/dialog/list/dialog-list.component.html +++ b/web/src/app/shared/dialog/list/dialog-list.component.html @@ -6,7 +6,7 @@

Last updated {{ data.repositories.lastUpdated.toDate() | timeAgo }}

- + lock lock_open

{{ repository.fullName }}

diff --git a/web/src/app/shared/models/repository.model.ts b/web/src/app/shared/models/repository.model.ts index c717d12c7..3a45bbe33 100644 --- a/web/src/app/shared/models/repository.model.ts +++ b/web/src/app/shared/models/repository.model.ts @@ -10,7 +10,7 @@ import { WebhookModel } from './webhook.model'; */ export class RepositoryModel { uid?: string; - id?: string; + id: number; name?: string; description?: string; fullName: string; @@ -35,12 +35,7 @@ export class RepositoryModel { milestones: MilestoneModel[]; webhook: WebhookModel; - constructor(fullName: string) { - this.fullName = fullName; - this.uid = RepositoryModel.getUid(fullName); - } - - public static getUid(fullName: string): string { - return fullName.replace('/', '+'); + constructor(uid?: string) { + this.uid = uid; } } diff --git a/web/src/typings.d.ts b/web/src/typings.d.ts deleted file mode 100644 index 10872d8c1..000000000 --- a/web/src/typings.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Typings reference file, see links for more information -// https://github.com/typings/typings -// https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html - -declare var System: any; -declare var require: any; \ No newline at end of file From 899736bd7f51cf2d5b78249aa356a56fd2376e22 Mon Sep 17 00:00:00 2001 From: kykyk Date: Mon, 9 Sep 2019 14:33:49 +0300 Subject: [PATCH 02/17] feat(repositories)#1123 add parse webhook events --- functions/src/environments/environment.ts | 50 +++- functions/src/mappers/github/event.mapper.ts | 13 +- functions/src/mappers/github/issue.mapper.ts | 15 +- .../src/mappers/github/milestone.mapper.ts | 3 + .../src/mappers/github/payload.mapper.ts | 8 +- .../src/mappers/github/pullRequest.mapper.ts | 15 +- .../src/mappers/github/release.mapper.ts | 24 +- .../github/webhook-event-response/create.ts | 55 +++++ .../github/webhook-event-response/index.ts | 11 + .../webhook-event-response/issue-comment.ts | 66 ++++++ .../github/webhook-event-response/issues.ts | 138 +++++++++++ .../github/webhook-event-response/member.ts | 56 +++++ .../webhook-event-response/milestone.ts | 111 +++++++++ .../webhook-event-response/pull-request.ts | 204 +++++++++++++++++ .../github/webhook-event-response/push.ts | 71 ++++++ .../github/webhook-event-response/release.ts | 167 ++++++++++++++ .../webhook-event-response/repository.ts | 118 ++++++++++ .../shared/functions.ts | 24 ++ .../webhook-event-response/shared/index.ts | 6 + .../shared/interfaces.ts | 7 + .../webhook-event-response/shared/issue.ts | 38 ++++ .../shared/milestone.ts | 20 ++ .../shared/repository.ts | 77 +++++++ .../webhook-event-response/shared/user.ts | 20 ++ .../github/webhook-event-response/status.ts | 92 ++++++++ .../github/webhook-event-response/watch.ts | 45 ++++ functions/src/repository/info.ts | 2 +- .../response-git-webhook-repository.ts | 214 +++++++++++++++++- scripts/deployment/prod.sh | 2 +- 29 files changed, 1631 insertions(+), 41 deletions(-) create mode 100644 functions/src/mappers/github/webhook-event-response/create.ts create mode 100644 functions/src/mappers/github/webhook-event-response/index.ts create mode 100644 functions/src/mappers/github/webhook-event-response/issue-comment.ts create mode 100644 functions/src/mappers/github/webhook-event-response/issues.ts create mode 100644 functions/src/mappers/github/webhook-event-response/member.ts create mode 100644 functions/src/mappers/github/webhook-event-response/milestone.ts create mode 100644 functions/src/mappers/github/webhook-event-response/pull-request.ts create mode 100644 functions/src/mappers/github/webhook-event-response/push.ts create mode 100644 functions/src/mappers/github/webhook-event-response/release.ts create mode 100644 functions/src/mappers/github/webhook-event-response/repository.ts create mode 100644 functions/src/mappers/github/webhook-event-response/shared/functions.ts create mode 100644 functions/src/mappers/github/webhook-event-response/shared/index.ts create mode 100644 functions/src/mappers/github/webhook-event-response/shared/interfaces.ts create mode 100644 functions/src/mappers/github/webhook-event-response/shared/issue.ts create mode 100644 functions/src/mappers/github/webhook-event-response/shared/milestone.ts create mode 100644 functions/src/mappers/github/webhook-event-response/shared/repository.ts create mode 100644 functions/src/mappers/github/webhook-event-response/shared/user.ts create mode 100644 functions/src/mappers/github/webhook-event-response/status.ts create mode 100644 functions/src/mappers/github/webhook-event-response/watch.ts diff --git a/functions/src/environments/environment.ts b/functions/src/environments/environment.ts index 0057c661e..f3e3419e8 100644 --- a/functions/src/environments/environment.ts +++ b/functions/src/environments/environment.ts @@ -3,8 +3,56 @@ export const enviroment: Config = { githubWebhook: { url: 'https://{{ FIREBASE_FUNCTIONS_URL }}.cloudfunctions.net/responseGitWebhookRepository', events: [ - 'push', + // IMPLEMENTED + 'create', + 'issue_comment', + 'issues', + 'member', + 'milestone', 'pull_request', + 'push', + 'release', + 'repository', + 'status', + 'watch', + + // NOT IMPLEMENTED + 'check_run', + 'check_suite', + 'commit_comment', + 'delete', + 'deploy_key', + 'deployment', + 'deployment_status', + 'fork', + 'gollum', + 'label', + 'meta', + 'page_build', + 'project_card', + 'project_column', + 'project', + 'public', + 'pull_request_review', + 'pull_request_review_comment', + 'registry_package', + 'repository_import', + 'repository_vulnerability_alert', + 'star', + 'team_add', + + // NOT ALLOW for this hook + // 'content_reference', + // 'github_app_authorization', + // 'installation', + // 'installation_repositories', + // 'marketplace_purchase', + // 'membership', + // 'organization', + // 'org_block', + // 'repository_dispatch', + // 'security_advisory', + // 'team', ], }, diff --git a/functions/src/mappers/github/event.mapper.ts b/functions/src/mappers/github/event.mapper.ts index 99e04defe..eafa6870e 100644 --- a/functions/src/mappers/github/event.mapper.ts +++ b/functions/src/mappers/github/event.mapper.ts @@ -1,6 +1,3 @@ -// Third party modules -import { firestore } from 'firebase-admin'; - // Dashboard hub firebase functions mappers/modesl import { GitHubEventType } from './event.mapper'; import { GitHubOrganisationtInput, GitHubOrganisationMapper, GitHubOrganisationModel } from './organisation.mapper'; @@ -13,23 +10,23 @@ export type GitHubEventType = 'PullRequestEvent' | 'IssueCommentEvent' | 'Create export interface GitHubEventInput { id: string; type: GitHubEventType; - public: string; + public: boolean; actor: GitHubUserInput; repo: GitHubRepositoryInput; org: GitHubOrganisationtInput; payload: GitHubPayloadInput; - created_at: firestore.Timestamp; + created_at: string; } export interface GitHubEventModel { - uid: string; + uid?: string; type: GitHubEventType; - public: string; + public: boolean; actor: GitHubUserModel; repository: GitHubRepositoryModel; organisation?: GitHubOrganisationModel; payload: GitHubPayloadModel; - createdOn: firestore.Timestamp; + createdOn: string; } export class GitHubEventMapper { diff --git a/functions/src/mappers/github/issue.mapper.ts b/functions/src/mappers/github/issue.mapper.ts index 1b19a7e93..618c9760b 100644 --- a/functions/src/mappers/github/issue.mapper.ts +++ b/functions/src/mappers/github/issue.mapper.ts @@ -1,11 +1,8 @@ -// Third party modules -import { firestore } from 'firebase-admin'; - // Dashboard hub firebase functions mappers/models import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './user.mapper'; export interface GitHubIssueInput { - id: string; + id: number; html_url: string; state: string; title: string; @@ -13,12 +10,12 @@ export interface GitHubIssueInput { body: string; user: GitHubUserInput; assignees: GitHubUserInput[]; - created_at: firestore.Timestamp; - updated_at: firestore.Timestamp; + created_at: string; + updated_at: string; } export interface GitHubIssueModel { - uid: string; + uid: number; url: string; state: string; title: string; @@ -26,8 +23,8 @@ export interface GitHubIssueModel { description: string; owner: GitHubUserModel; assignees: GitHubUserModel[]; - createdOn: firestore.Timestamp; - updatedOn: firestore.Timestamp; + createdOn: string; + updatedOn: string; } export class GitHubIssueMapper { diff --git a/functions/src/mappers/github/milestone.mapper.ts b/functions/src/mappers/github/milestone.mapper.ts index 490998d6d..b82eec808 100644 --- a/functions/src/mappers/github/milestone.mapper.ts +++ b/functions/src/mappers/github/milestone.mapper.ts @@ -1,6 +1,7 @@ import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './index.mapper'; export interface GitHubMilestoneInput { + id: number; title: string; creator: GitHubUserInput; state: string; @@ -12,6 +13,7 @@ export interface GitHubMilestoneInput { } export interface GitHubMilestoneModel { + uid: number; title: string; creator: GitHubUserModel; state: string; @@ -25,6 +27,7 @@ export interface GitHubMilestoneModel { export class GitHubMilestoneMapper { static import(input: GitHubMilestoneInput): GitHubMilestoneModel { return { + uid: input.id, title: input.title, creator: GitHubUserMapper.import(input.creator), state: input.state, diff --git a/functions/src/mappers/github/payload.mapper.ts b/functions/src/mappers/github/payload.mapper.ts index 8e7bb479e..1992567aa 100644 --- a/functions/src/mappers/github/payload.mapper.ts +++ b/functions/src/mappers/github/payload.mapper.ts @@ -1,7 +1,7 @@ import { GitHubEventType } from './event.mapper'; export interface GitHubPayloadInput { - title: string; + title?: string; action?: string; ref?: string; ref_type?: string; @@ -15,7 +15,9 @@ export interface GitHubPayloadInput { body: string; }; release?: { - name: string; + tag_name: string; + target_commitish: string; + name?: string; }; } @@ -47,7 +49,7 @@ export class GitHubPayloadMapper { output.title = `${input.ref_type}: ${input.ref}`; break; case 'ReleaseEvent': - output.title = `${input.action}: ${input.release.name}`; + output.title = `${input.action}: ${input.release.tag_name}@${input.release.target_commitish}` + (input.release.name ? ` - ${input.release.name}` : ''); break; case 'WatchEvent': output.title = `${input.action} watching`; diff --git a/functions/src/mappers/github/pullRequest.mapper.ts b/functions/src/mappers/github/pullRequest.mapper.ts index 5bda33669..0aa4a1545 100644 --- a/functions/src/mappers/github/pullRequest.mapper.ts +++ b/functions/src/mappers/github/pullRequest.mapper.ts @@ -1,11 +1,8 @@ -// Third party modules -import { firestore } from 'firebase-admin'; - // Dashboard hub firebase functions mappers/models import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './user.mapper'; export interface GitHubPullRequestInput { - id: string; + id: number; html_url: string; state: string; title: string; @@ -14,12 +11,12 @@ export interface GitHubPullRequestInput { user: GitHubUserInput assignees: GitHubUserInput[]; requested_reviewers: GitHubUserInput[]; - created_at: firestore.Timestamp; - updated_at: firestore.Timestamp; + created_at: string; + updated_at: string; } export interface GitHubPullRequestModel { - uid: string; + uid: number; url: string; state: string; title: string; @@ -28,8 +25,8 @@ export interface GitHubPullRequestModel { owner: GitHubUserModel; assignees: GitHubUserModel[]; reviewers: GitHubUserModel[]; - createdOn: firestore.Timestamp; - updatedOn: firestore.Timestamp; + createdOn: string; + updatedOn: string; } export class GitHubPullRequestMapper { diff --git a/functions/src/mappers/github/release.mapper.ts b/functions/src/mappers/github/release.mapper.ts index aed8562a5..028cc374b 100644 --- a/functions/src/mappers/github/release.mapper.ts +++ b/functions/src/mappers/github/release.mapper.ts @@ -1,25 +1,24 @@ -// Third party modules -import { firestore } from 'firebase-admin'; - // Dashboard hub firebase functions mappers/models import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './user.mapper'; export interface GitHubReleaseInput { - id: string; + id: number; name: string; body: string; author: GitHubUserInput; html_url: string; - published_at: firestore.Timestamp; + published_at: string; + prerelease: boolean; } export interface GitHubReleaseModel { - uid: string; + uid: number; title: string; description: string; owner: GitHubUserModel; htmlUrl: string; - createdOn: firestore.Timestamp; + createdOn: string; + isPrerelease: boolean; } export class GitHubReleaseMapper { @@ -31,6 +30,17 @@ export class GitHubReleaseMapper { owner: GitHubUserMapper.import(input.author), htmlUrl: input.html_url, createdOn: input.published_at, + isPrerelease: input.prerelease, }; } + + public static sortReleaseList(releases: GitHubReleaseModel[]) { + return releases.sort( + (a: GitHubReleaseModel, b: GitHubReleaseModel): number => { + const date1: Date = new Date(a.createdOn); + const date2: Date = new Date(b.createdOn); + return date2.getTime() - date1.getTime(); + } + ); + } } diff --git a/functions/src/mappers/github/webhook-event-response/create.ts b/functions/src/mappers/github/webhook-event-response/create.ts new file mode 100644 index 000000000..08b60a0e7 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/create.ts @@ -0,0 +1,55 @@ +import { GitHubEventModel, GitHubEventType } from '../event.mapper'; +import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; +import { GitHubRepositoryMapper } from '../repository.mapper'; +import { GitHubUserMapper } from '../user.mapper'; +import { isExistProperties, HubEventActions, Repository, User } from './shared'; + +export interface CreateEventInput { + ref: string; + ref_type: 'branch' | 'tag'; + master_branch: string; + description?: any; + pusher_type: string; + repository: Repository; + sender: User; +} + +export class CreateEventModel implements CreateEventInput, HubEventActions { + ref: string; + ref_type: 'branch' | 'tag'; + master_branch: string; + description?: any; + pusher_type: string; + repository: Repository; + sender: User; + + constructor(input: CreateEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['ref', 'ref_type', 'master_branch', 'pusher_type', 'repository', 'sender']; + return isExistProperties(input, requireKeys); + } + + convertToHubEvent(): GitHubEventModel { + const eventType: GitHubEventType = 'CreateEvent'; + const payload: GitHubPayloadInput = { + // title: `Created ${this.ref_type} '${this.ref}'.` + this.description ? ` ${this.description}` : '', + ref: this.ref, + ref_type: this.ref_type, + } + + const data: GitHubEventModel = { + type: eventType, + public: true, // TODO where get + actor: GitHubUserMapper.import(this.sender), + repository: GitHubRepositoryMapper.import(this.repository, 'event'), + payload: GitHubPayloadMapper.import(eventType, payload), + createdOn: new Date().toISOString(), + }; + + return data; + } + +} diff --git a/functions/src/mappers/github/webhook-event-response/index.ts b/functions/src/mappers/github/webhook-event-response/index.ts new file mode 100644 index 000000000..fd9f0e056 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/index.ts @@ -0,0 +1,11 @@ +export * from './issues'; +export * from './milestone'; +export * from './pull-request'; +export * from './release'; +export * from './repository'; +export * from './watch'; +export * from './create'; +export * from './push'; +export * from './issue-comment'; +export * from './member'; +export * from './status'; diff --git a/functions/src/mappers/github/webhook-event-response/issue-comment.ts b/functions/src/mappers/github/webhook-event-response/issue-comment.ts new file mode 100644 index 000000000..9edbf7253 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/issue-comment.ts @@ -0,0 +1,66 @@ +import { GitHubEventModel, GitHubEventType } from '../event.mapper'; +import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; +import { GitHubRepositoryMapper } from '../repository.mapper'; +import { GitHubUserMapper } from '../user.mapper'; +import { isExistProperties, HubEventActions, Issue, Repository, User } from './shared'; + +interface Comment { + url: string; + html_url: string; + issue_url: string; + id: number; + node_id: string; + user: User; + created_at: Date; + updated_at: Date; + author_association: string; + body: string; +} + +type Action = 'created' | 'edited' | 'deleted'; + +export interface IssueCommentEventInput { + action: Action; + issue: Issue; + comment: Comment; + repository: Repository; + sender: User; +} + +export class IssueCommentEventModel implements IssueCommentEventInput, HubEventActions { + action: Action; + issue: Issue; + comment: Comment; + repository: Repository; + sender: User; + + constructor(input: IssueCommentEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['action', 'issue', 'comment', 'repository', 'sender']; + return isExistProperties(input, requireKeys); + } + + convertToHubEvent(): GitHubEventModel { + const eventType: GitHubEventType = 'IssueCommentEvent'; + const payload: GitHubPayloadInput = { + action: this.action, + comment: this.comment, + }; + + const data: GitHubEventModel = { + type: eventType, + public: true, // TODO where get + actor: GitHubUserMapper.import(this.sender), + repository: GitHubRepositoryMapper.import(this.repository, 'event'), + payload: GitHubPayloadMapper.import(eventType, payload), + createdOn: new Date().toISOString(), + }; + + return data; + } + + +} diff --git a/functions/src/mappers/github/webhook-event-response/issues.ts b/functions/src/mappers/github/webhook-event-response/issues.ts new file mode 100644 index 000000000..abbcd9c74 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/issues.ts @@ -0,0 +1,138 @@ +import { DocumentData } from '../../../client/firebase-admin'; +import { GitHubEventModel, GitHubEventType } from '../event.mapper'; +import { GitHubIssueModel } from '../issue.mapper'; +import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; +import { GitHubRepositoryMapper } from '../repository.mapper'; +import { GitHubUserMapper } from '../user.mapper'; +import { isExistProperties, HubEventActions, Issue, Repository, User } from './shared'; + +type Action = 'opened' | 'edited' | 'deleted' | 'transferred' | 'pinned' | 'unpinned' | 'closed' | 'reopened' | 'assigned' | 'unassigned' | 'labeled' | 'unlabeled' | 'locked' | 'unlocked' | 'milestoned' | 'demilestoned'; + +export interface IssuesEventInput { + action: Action; + issue: Issue; + changes?: any; + repository: Repository; + sender: User; +} + +export class IssuesEventModel implements IssuesEventInput, HubEventActions { + action: Action; + issue: Issue; + changes?: any; + repository: Repository; + sender: User; + + constructor(input: IssuesEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['action', 'issue', 'repository', 'sender']; + return isExistProperties(input, requireKeys); + } + + convertToHubEvent(): GitHubEventModel { + const eventType: GitHubEventType = 'IssuesEvent'; + const payload: GitHubPayloadInput = { + action: this.action, + issue: this.issue, + }; + + const data: GitHubEventModel = { + type: eventType, + public: true, // TODO where get + actor: GitHubUserMapper.import(this.sender), + repository: GitHubRepositoryMapper.import(this.repository, 'event'), + payload: GitHubPayloadMapper.import(eventType, payload), + createdOn: new Date().toISOString(), + }; + + return data; + } + + + updateData(repository: DocumentData): void { + + if (!Array.isArray(repository.issues)) { + repository.issues = []; + } + + switch (this.action) { + case 'opened': { + this.opened(repository); + break; + } + + case 'pinned': + case 'unpinned': + case 'reopened': + case 'assigned': + case 'unassigned': + case 'labeled': + case 'unlabeled': + case 'locked': + case 'unlocked': + case 'milestoned': + case 'demilestoned': + case 'transferred': + case 'edited': { + this.edited(repository); + break; + } + + case 'closed': + case 'deleted': { + this.deleted(repository); + break; + } + + default: { + throw new Error('Not found action'); + } + } + + } + + private getModel(): GitHubIssueModel { + return { + uid: this.issue.id, + url: this.issue.html_url, + state: this.issue.state, + title: this.issue.title, + number: this.issue.number, + description: this.issue.body, + owner: GitHubUserMapper.import(this.issue.user), + assignees: this.issue.assignees.map((assignee: User) => GitHubUserMapper.import(assignee)), + createdOn: this.issue.created_at, + updatedOn: this.issue.updated_at, + } + } + + private opened(repository: DocumentData): void { + + const issue: GitHubIssueModel = this.getModel(); + + repository.issues.unshift(issue); + } + + + private edited(repository: DocumentData): void { + const foundIndex: number = repository.issues.findIndex((elem: GitHubIssueModel) => elem.uid === this.issue.id); + if (foundIndex > -1) { + repository.issues[foundIndex] = this.getModel(); + } else { + this.opened(repository); + } + } + + + private deleted(repository: DocumentData): void { + if (!Array.isArray(repository.issues)) { + return; + } + + repository.issues = repository.issues.filter((item: GitHubIssueModel) => item.uid !== this.issue.id); + } + +} diff --git a/functions/src/mappers/github/webhook-event-response/member.ts b/functions/src/mappers/github/webhook-event-response/member.ts new file mode 100644 index 000000000..32c8bdde5 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/member.ts @@ -0,0 +1,56 @@ +import { isExistProperties, Repository, User } from './shared'; + +interface Member { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; +} + +export interface MemberEventInput { + action: 'added' | 'deleted' | 'edited'; + member: Member; + repository: Repository; + sender: User; + changes?: { old_permission: { from: string } }; +} + +export class MemberEventModel implements MemberEventInput { + action: 'added' | 'deleted' | 'edited'; + member: Member; + repository: Repository; + sender: User; + changes?: { old_permission: { from: string } }; + + constructor(input: MemberEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['action', 'member', 'repository', 'sender']; + + const objKeys: string[] = Object.keys(input); + let length: number = requireKeys.length; + + if (objKeys.find((elem: string) => elem === 'changes')) { + ++length; + } + + return objKeys.length === length && isExistProperties(input, requireKeys); + } + +} diff --git a/functions/src/mappers/github/webhook-event-response/milestone.ts b/functions/src/mappers/github/webhook-event-response/milestone.ts new file mode 100644 index 000000000..81b49b884 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/milestone.ts @@ -0,0 +1,111 @@ +import { DocumentData } from '../../../client/firebase-admin'; +import { GitHubMilestoneModel } from '../milestone.mapper'; +import { GitHubUserMapper } from '../user.mapper'; +import { isExistProperties, Milestone, Repository, User } from './shared'; + +type Action = 'created' | 'closed' | 'opened' | 'edited' | 'deleted'; + +export interface MilestoneEventInput { + action: Action; + milestone: Milestone; + repository: Repository; + sender: User; +} + +export class MilestoneEventModel implements MilestoneEventInput { + action: Action; + milestone: Milestone; + repository: Repository; + sender: User; + + constructor(input: MilestoneEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['action', 'milestone', 'repository', 'sender']; + return isExistProperties(input, requireKeys); + } + + updateData(repository: DocumentData): void { + + if (!Array.isArray(repository.milestones)) { + repository.milestones = []; + } + + switch (this.action) { + case 'created': { + this.created(repository); + break; + } + case 'opened': { + this.opened(repository); + break; + } + case 'closed': { + this.closed(repository); + break; + } + case 'edited': { + this.edited(repository); + break; + } + case 'deleted': { + this.deleted(repository); + break; + } + + default: { + throw new Error('Not found action'); + } + } + + } + + private getModel(): GitHubMilestoneModel { + return { + uid: this.milestone.id, + title: this.milestone.title, + creator: GitHubUserMapper.import(this.milestone.creator), + state: this.milestone.state, + openIssues: this.milestone.open_issues, + closeIssues: this.milestone.closed_issues, + htmlUrl: this.milestone.html_url, + description: this.milestone.description, + updatedAt: this.milestone.updated_at, + }; + } + + private created(repository: DocumentData): void { + + const milestone: GitHubMilestoneModel = this.getModel(); + + repository.milestones.unshift(milestone); + } + + private opened(repository: DocumentData): void { + this.edited(repository); + } + + private closed(repository: DocumentData): void { + this.edited(repository); + } + + private edited(repository: DocumentData): void { + const foundIndex: number = repository.milestones.findIndex((elem: GitHubMilestoneModel) => elem.uid === this.milestone.id); + if (foundIndex > -1) { + repository.milestones[foundIndex] = this.getModel(); + } else { + this.created(repository); + } + } + + + private deleted(repository: DocumentData): void { + if (!Array.isArray(repository.milestones)) { + return; + } + repository.milestones = repository.milestones.filter((item: GitHubMilestoneModel) => item.uid !== this.milestone.id); + } + +} diff --git a/functions/src/mappers/github/webhook-event-response/pull-request.ts b/functions/src/mappers/github/webhook-event-response/pull-request.ts new file mode 100644 index 000000000..9d43d00c3 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/pull-request.ts @@ -0,0 +1,204 @@ +import { DocumentData } from '../../../client/firebase-admin'; +import { GitHubEventModel, GitHubEventType } from '../event.mapper'; +import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; +import { GitHubPullRequestModel } from '../pullRequest.mapper'; +import { GitHubRepositoryMapper } from '../repository.mapper'; +import { GitHubUserMapper } from '../user.mapper'; +import { isExistProperties, HubEventActions, Repository, User } from './shared'; + + +interface InfoRepoObj { + label: string; + ref: string; + sha: string; + user: User; + repo: Repository; +} + +interface LinkObj { + href: string; +} + +interface Links { + self: LinkObj; + html: LinkObj; + issue: LinkObj; + comments: LinkObj; + review_comments: LinkObj; + review_comment: LinkObj; + commits: LinkObj; + statuses: LinkObj; +} + +interface PullRequest { + url: string; + id: number; + node_id: string; + html_url: string; + diff_url: string; + patch_url: string; + issue_url: string; + number: number; + state: string; + locked: boolean; + title: string; + user: User; + body: string; + created_at: string; + updated_at: string; + closed_at?: any; + merged_at?: any; + merge_commit_sha?: any; + assignee?: any; + assignees: any[]; + requested_reviewers: any[]; + requested_teams: any[]; + labels: any[]; + milestone?: any; + commits_url: string; + review_comments_url: string; + review_comment_url: string; + comments_url: string; + statuses_url: string; + head: InfoRepoObj; + base: InfoRepoObj; + _links: Links; + author_association: string; + draft: boolean; + merged: boolean; + mergeable?: any; + rebaseable?: any; + mergeable_state: string; + merged_by?: any; + comments: number; + review_comments: number; + maintainer_can_modify: boolean; + commits: number; + additions: number; + deletions: number; + changed_files: number; +} + +type Action = 'assigned' | 'unassigned' | 'review_requested' | 'review_request_removed' | 'labeled' | 'unlabeled' | 'opened' | 'edited' | 'closed' | 'ready_for_review' | 'locked' | 'unlocked' | 'reopened'; + +export interface PullRequestEventInput { + action: Action; + number: number; + pull_request: PullRequest; + repository: Repository; + sender: User; +} + +export class PullRequestEventModel implements PullRequestEventInput, HubEventActions { + action: Action; + number: number; + pull_request: PullRequest; + repository: Repository; + sender: User; + + constructor(input: PullRequestEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['action', 'number', 'pull_request', 'repository', 'sender']; + return isExistProperties(input, requireKeys); + } + + convertToHubEvent(): GitHubEventModel { + const eventType: GitHubEventType = 'PullRequestEvent'; + const payload: GitHubPayloadInput = { + action: this.action, + pull_request: this.pull_request, + }; + + const data: GitHubEventModel = { + type: eventType, + public: true, // TODO where get + actor: GitHubUserMapper.import(this.sender), + repository: GitHubRepositoryMapper.import(this.repository, 'event'), + payload: GitHubPayloadMapper.import(eventType, payload), + createdOn: new Date().toISOString(), + }; + + return data; + } + + updateData(repository: DocumentData): void { + + if (!Array.isArray(repository.pullRequests)) { + repository.pullRequests = []; + } + + switch (this.action) { + case 'opened': { + this.opened(repository); + break; + } + case 'closed': { + this.closed(repository); + break; + } + case 'assigned': + case 'unassigned': + case 'review_requested': + case 'review_request_removed': + case 'labeled': + case 'unlabeled': + case 'ready_for_review': + case 'locked': + case 'unlocked': + case 'reopened': + case 'edited': { + this.edited(repository); + break; + } + default: { + throw new Error('Not found action'); + } + } + + } + + private getModel(): GitHubPullRequestModel { + return { + uid: this.pull_request.id, + url: this.pull_request.html_url, + state: this.pull_request.state, + title: this.pull_request.title, + description: this.pull_request.body, + id: this.pull_request.number, + owner: GitHubUserMapper.import(this.pull_request.user), + assignees: this.pull_request.assignees.map((assignee: User) => GitHubUserMapper.import(assignee)), + reviewers: this.pull_request.requested_reviewers.map((reviewer: User) => GitHubUserMapper.import(reviewer)), + createdOn: this.pull_request.created_at, + updatedOn: this.pull_request.updated_at, + } + } + + private opened(repository: DocumentData): void { + + const pull_request: GitHubPullRequestModel = this.getModel(); + + repository.pullRequests.unshift(pull_request); + } + + + private edited(repository: DocumentData): void { + const foundIndex: number = repository.pullRequests.findIndex((elem: GitHubPullRequestModel) => elem.uid === this.pull_request.id); + if (foundIndex > -1) { + repository.pullRequests[foundIndex] = this.getModel(); + } else { + this.opened(repository); + } + } + + + private closed(repository: DocumentData): void { + if (!Array.isArray(repository.pullRequests)) { + return; + } + repository.pullRequests = repository.pullRequests.filter((item: GitHubPullRequestModel) => item.uid !== this.pull_request.id); + } + +} diff --git a/functions/src/mappers/github/webhook-event-response/push.ts b/functions/src/mappers/github/webhook-event-response/push.ts new file mode 100644 index 000000000..b9ac7dec2 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/push.ts @@ -0,0 +1,71 @@ +import { GitHubEventModel, GitHubEventType } from '../event.mapper'; +import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; +import { GitHubRepositoryMapper } from '../repository.mapper'; +import { GitHubUserMapper } from '../user.mapper'; +import { isExistProperties, HubEventActions, Repository, User } from './shared'; + +interface Pusher { + name: string; + email: string; +} + +export interface PushEventInput { + ref: string; + before: string; + after: string; + created: boolean; + deleted: boolean; + forced: boolean; + base_ref?: any; + compare: string; + commits: any[]; + head_commit?: any; + repository: Repository; + pusher: Pusher; + sender: User; +} + + +export class PushEventModel implements PushEventInput, HubEventActions { + ref: string; + before: string; + after: string; + created: boolean; + deleted: boolean; + forced: boolean; + base_ref?: any; + compare: string; + commits: any[]; + head_commit?: any; + repository: Repository; + pusher: Pusher; + sender: User; + + constructor(input: PushEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['ref', 'before', 'after', 'created', 'deleted', 'forced', 'base_ref', 'compare', 'commits', 'head_commit', 'repository', 'pusher', 'sender']; + return isExistProperties(input, requireKeys); + } + + convertToHubEvent(): GitHubEventModel { + const eventType: GitHubEventType = 'PushEvent'; + const payload: GitHubPayloadInput = { + ref: this.ref, + } + + const data: GitHubEventModel = { + type: eventType, + public: true, // TODO where get + actor: GitHubUserMapper.import(this.sender), + repository: GitHubRepositoryMapper.import(this.repository, 'event'), + payload: GitHubPayloadMapper.import(eventType, payload), + createdOn: new Date().toISOString(), + }; + + return data; + } + +} diff --git a/functions/src/mappers/github/webhook-event-response/release.ts b/functions/src/mappers/github/webhook-event-response/release.ts new file mode 100644 index 000000000..486df7199 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/release.ts @@ -0,0 +1,167 @@ +import { DocumentData } from '../../../client/firebase-admin'; +import { GitHubEventModel, GitHubEventType } from '../event.mapper'; +import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; +import { GitHubReleaseMapper, GitHubReleaseModel } from '../release.mapper'; +import { GitHubRepositoryMapper } from '../repository.mapper'; +import { GitHubUserMapper } from '../user.mapper'; +import { isExistProperties, HubEventActions, Repository, User } from './shared'; + +interface Release { + url: string; + assets_url: string; + upload_url: string; + html_url: string; + id: number; + node_id: string; + tag_name: string; + target_commitish: string; + name?: string; + draft: boolean; + author: User; + prerelease: boolean; + created_at: string; + published_at: string; + assets: any[]; + tarball_url: string; + zipball_url: string; + body?: any; +} + +type Action = 'published' | 'unpublished' | 'created' | 'edited' | 'deleted' | 'prereleased'; + +export interface ReleaseEventInput { + action: Action; + release: Release; + repository: Repository; + sender: User; +} + +export class ReleaseEventModel implements ReleaseEventInput, HubEventActions { + action: Action; + changes?: { + body: { from: string }, + name: { from: string }, + }; + release: Release; + repository: Repository; + sender: User; + + constructor(input: ReleaseEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['action', 'release', 'repository', 'sender']; + return isExistProperties(input, requireKeys); + } + + convertToHubEvent(): GitHubEventModel { + const eventType: GitHubEventType = 'ReleaseEvent'; + const payload: GitHubPayloadInput = { + action: this.action, + release: this.release, + } + + const data: GitHubEventModel = { + type: eventType, + public: true, // TODO where get + actor: GitHubUserMapper.import(this.sender), + repository: GitHubRepositoryMapper.import(this.repository, 'event'), + payload: GitHubPayloadMapper.import(eventType, payload), + createdOn: new Date().toISOString(), + }; + + return data; + } + + updateData(repository: DocumentData): void { + + if (!Array.isArray(repository.releases)) { + repository.releases = []; + } + + switch (this.action) { + case 'created': { + this.created(repository); + } + case 'published': { + this.published(repository); + break; + } + + case 'unpublished': { + this.unpublished(repository); + break; + } + + case 'edited': { + this.edited(repository); + break; + } + + case 'deleted': { + this.deleted(repository); + break; + } + + case 'prereleased': { + this.prereleased(repository); + break; + } + + default: { + throw new Error('Not found action'); + } + } + + GitHubReleaseMapper.sortReleaseList(repository.releases); + } + + private getModel(): GitHubReleaseModel { + return { + uid: this.release.id, + title: this.release.name, + description: this.release.body, + owner: GitHubUserMapper.import(this.release.author), + htmlUrl: this.release.html_url, + createdOn: this.release.published_at, + isPrerelease: this.release.prerelease, + } + } + + private created(repository: DocumentData): void { + + const release: GitHubReleaseModel = this.getModel(); + + repository.releases.unshift(release); + } + + private published(repository: DocumentData): void { + const foundIndex: number = repository.releases.findIndex((elem: GitHubReleaseModel) => elem.uid === this.release.id); + if (foundIndex > -1) { + repository.releases[foundIndex] = this.getModel(); + } else { + this.created(repository); + } + } + + private unpublished(repository: DocumentData): void { + this.published(repository); + } + + private edited(repository: DocumentData): void { + this.published(repository); + } + + private prereleased(repository: DocumentData): void { + this.published(repository); + } + + private deleted(repository: DocumentData): void { + if (!Array.isArray(repository.releases)) { + return; + } + + repository.releases = repository.releases.filter((item: GitHubReleaseModel) => item.uid !== this.release.id); + } +} diff --git a/functions/src/mappers/github/webhook-event-response/repository.ts b/functions/src/mappers/github/webhook-event-response/repository.ts new file mode 100644 index 000000000..f9ae82320 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/repository.ts @@ -0,0 +1,118 @@ +import { DocumentData, FieldPath, FirebaseAdmin, Query, QueryDocumentSnapshot, QuerySnapshot, Transaction } from '../../../client/firebase-admin'; +import { RepositoryModel } from '../../../models/index.model'; +import { GitHubRepositoryMapper, GitHubRepositoryModel } from '../repository.mapper'; +import { isExistProperties, Repository, User } from './shared'; + +type Action = 'created' | 'deleted' | 'archived' | 'unarchived' | 'edited' | 'renamed' | 'transferred' | 'publicized' | 'privatized'; + +export interface RepositoryEventInput { + action: Action; + changes?: { + repository: { + name: { from: string } + } + }; + repository: Repository; + sender: User; +} + +export class RepositoryEventModel implements RepositoryEventInput { + action: Action; + changes?: { + repository: { + name: { from: string } + } + }; + repository: Repository; + sender: User; + + constructor(input: RepositoryEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['action', 'repository', 'sender']; + const objKeys: string[] = Object.keys(input); + let length: number = requireKeys.length; + + if (objKeys.find((elem: string) => elem === 'changes')) { + ++length; + } + + return objKeys.length === length && isExistProperties(input, requireKeys); + } + + public async updateData(): Promise { + + switch (this.action) { + case 'edited': { + await this.edited(); + break; + } + case 'renamed': { + await this.renamed(); + break; + } + case 'transferred': { + await this.transferred(); + break; + } + case 'publicized': { + await this.publicized(); + break; + } + case 'privatized': { + await this.privatized(); + break; + } + } + + + } + + private async edited(): Promise { + const repository: DocumentData = await RepositoryModel.getRepositoryById(this.repository.id); + // update repo in users + const usersRef: Query = FirebaseAdmin.firestore().collection('users').where(new FieldPath('repositories', 'uids'), 'array-contains', repository.uid); + const newMinDataRepo: GitHubRepositoryModel = GitHubRepositoryMapper.import(this.repository); + await FirebaseAdmin.firestore().runTransaction((t: Transaction) => { + return t.get(usersRef) + .then((snap: QuerySnapshot) => { + snap.forEach((element: QueryDocumentSnapshot) => { + const userData: DocumentData = element.data(); + if (userData.repositories && Array.isArray(userData.repositories.data) && userData.repositories.data.length > 0) { + const repos: GitHubRepositoryModel[] = userData.repositories.data; + const foundIndex: number = repos.findIndex((item: GitHubRepositoryModel) => item.uid && item.uid === repository.uid) + + if (foundIndex > -1) { + Object.assign(repos[foundIndex], newMinDataRepo); + t.update(element.ref, { repositories: { ...userData.repositories, data: repos } }); + } + } + }); + + }); + }); + + const newDataRepo: GitHubRepositoryModel = GitHubRepositoryMapper.import(this.repository, 'all'); + Object.assign(repository, newDataRepo); + await RepositoryModel.saveRepository(repository); + } + + private async renamed(): Promise { + await this.edited(); + } + + private async transferred(): Promise { + await this.edited(); + } + + private async publicized(): Promise { + await this.edited(); + } + + private async privatized(): Promise { + await this.edited(); + } + +} diff --git a/functions/src/mappers/github/webhook-event-response/shared/functions.ts b/functions/src/mappers/github/webhook-event-response/shared/functions.ts new file mode 100644 index 000000000..190625285 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/shared/functions.ts @@ -0,0 +1,24 @@ +import { DocumentData } from "../../../../client/firebase-admin"; +import { HubEventActions } from "./interfaces"; + +export function isExistProperties(obj: any, keys: string[]){ + if (!obj) { + return false; + } + + for (const key of keys) { + if (!obj.hasOwnProperty(key)) { + return false; + } + } + + return true; +} + +export function addHubEventToCollection(repository: DocumentData, event: HubEventActions) { + if (!Array.isArray(repository.events)) { + repository.events = []; + } + + repository.events.unshift(event.convertToHubEvent()); +} diff --git a/functions/src/mappers/github/webhook-event-response/shared/index.ts b/functions/src/mappers/github/webhook-event-response/shared/index.ts new file mode 100644 index 000000000..8f06f1cd0 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/shared/index.ts @@ -0,0 +1,6 @@ +export * from './repository'; +export * from './user'; +export * from './milestone'; +export * from './functions'; +export * from './issue'; +export * from './interfaces'; diff --git a/functions/src/mappers/github/webhook-event-response/shared/interfaces.ts b/functions/src/mappers/github/webhook-event-response/shared/interfaces.ts new file mode 100644 index 000000000..c0b90fe4c --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/shared/interfaces.ts @@ -0,0 +1,7 @@ +import { GitHubEventModel } from "../../event.mapper"; +import { Repository } from "./repository"; + +export interface HubEventActions { + convertToHubEvent(): GitHubEventModel; + repository: Repository; +} diff --git a/functions/src/mappers/github/webhook-event-response/shared/issue.ts b/functions/src/mappers/github/webhook-event-response/shared/issue.ts new file mode 100644 index 000000000..e758132c7 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/shared/issue.ts @@ -0,0 +1,38 @@ +import { Milestone } from './milestone'; +import { User } from './user'; + +interface Label { + id: number; + node_id: string; + url: string; + name: string; + color: string; + default: boolean; +} + + +export interface Issue { + url: string; + repository_url: string; + labels_url: string; + comments_url: string; + events_url: string; + html_url: string; + id: number; + node_id: string; + number: number; + title: string; + user: User; + labels: Label[]; + state: string; + locked: boolean; + assignee: User; + assignees: User[]; + milestone: Milestone; + comments: number; + created_at: string; + updated_at: string; + closed_at?: any; + author_association: string; + body: string; +} diff --git a/functions/src/mappers/github/webhook-event-response/shared/milestone.ts b/functions/src/mappers/github/webhook-event-response/shared/milestone.ts new file mode 100644 index 000000000..87d202321 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/shared/milestone.ts @@ -0,0 +1,20 @@ +import { User } from './user'; + +export interface Milestone { + url: string; + html_url: string; + labels_url: string; + id: number; + node_id: string; + number: number; + title: string; + description: string; + creator: User; + open_issues: number; + closed_issues: number; + state: string; + created_at: string; + updated_at: string; + due_on: string; + closed_at?: string; +} diff --git a/functions/src/mappers/github/webhook-event-response/shared/repository.ts b/functions/src/mappers/github/webhook-event-response/shared/repository.ts new file mode 100644 index 000000000..a8a8a04f1 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/shared/repository.ts @@ -0,0 +1,77 @@ +import { User } from './user'; + +export interface Repository { + id: number; + node_id: string; + name: string; + full_name: string; + private: boolean; + owner: User; + html_url: string; + description?: any; + fork: boolean; + url: string; + forks_url: string; + keys_url: string; + collaborators_url: string; + teams_url: string; + hooks_url: string; + issue_events_url: string; + events_url: string; + assignees_url: string; + branches_url: string; + tags_url: string; + blobs_url: string; + git_tags_url: string; + git_refs_url: string; + trees_url: string; + statuses_url: string; + languages_url: string; + stargazers_url: string; + contributors_url: string; + subscribers_url: string; + subscription_url: string; + commits_url: string; + git_commits_url: string; + comments_url: string; + issue_comment_url: string; + contents_url: string; + compare_url: string; + merges_url: string; + archive_url: string; + downloads_url: string; + issues_url: string; + pulls_url: string; + milestones_url: string; + notifications_url: string; + labels_url: string; + releases_url: string; + deployments_url: string; + created_at: Date; + updated_at: Date; + pushed_at: Date; + git_url: string; + ssh_url: string; + clone_url: string; + svn_url: string; + homepage?: any; + size: number; + stargazers_count: number; + watchers_count: number; + language?: string; + has_issues: boolean; + has_projects: boolean; + has_downloads: boolean; + has_wiki: boolean; + has_pages: boolean; + forks_count: number; + mirror_url?: any; + archived: boolean; + disabled: boolean; + open_issues_count: number; + license?: any; + forks: number; + open_issues: number; + watchers: number; + default_branch: string; +} diff --git a/functions/src/mappers/github/webhook-event-response/shared/user.ts b/functions/src/mappers/github/webhook-event-response/shared/user.ts new file mode 100644 index 000000000..d212ae1b0 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/shared/user.ts @@ -0,0 +1,20 @@ +export interface User { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; +} diff --git a/functions/src/mappers/github/webhook-event-response/status.ts b/functions/src/mappers/github/webhook-event-response/status.ts new file mode 100644 index 000000000..03db737b5 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/status.ts @@ -0,0 +1,92 @@ +import { isExistProperties, Repository, User } from './shared'; + +interface Author { + name: string; + email: string; + date: string; +} + +interface Verification { + verified: boolean; + reason: string; + signature: string; + payload: string; +} + +interface Commit2 { + author: Author; + committer: Author; + message: string; + tree: { + sha: string; + url: string; + }; + url: string; + comment_count: number; + verification: Verification; +} + +interface Commit { + sha: string; + node_id: string; + commit: Commit2; + url: string; + html_url: string; + comments_url: string; + author: User; + committer: User; + parents: any[]; +} + + +interface Branch { + name: string; + commit: { + sha: string; + url: string; + }; + protected: boolean; +} + +export interface StatusEventInput { + id: number; + sha: string; + name: string; + target_url: string; + context: string; + description: string; + state: 'pending' | 'success' | 'failure' | 'error'; + commit: Commit; + branches: Branch[]; + created_at: string; + updated_at: string; + repository: Repository; + sender: User; +} + +export class StatusEventModel implements StatusEventInput { + id: number; + sha: string; + name: string; + target_url: string; + context: string; + description: string; + state: 'error' | 'pending' | 'success' | 'failure'; + commit: Commit; + branches: Branch[]; + created_at: string; + updated_at: string; + repository: Repository; + sender: User; + + constructor(input: StatusEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['id', 'sha', 'name', 'target_url', 'context', 'description', + 'state', 'commit', 'branches', 'created_at', 'updated_at', 'repository', 'sender']; + return isExistProperties(input, requireKeys); + } + +} diff --git a/functions/src/mappers/github/webhook-event-response/watch.ts b/functions/src/mappers/github/webhook-event-response/watch.ts new file mode 100644 index 000000000..d5c10637d --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/watch.ts @@ -0,0 +1,45 @@ +import { GitHubEventModel, GitHubEventType } from '../event.mapper'; +import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; +import { GitHubRepositoryMapper } from '../repository.mapper'; +import { GitHubUserMapper } from '../user.mapper'; +import { isExistProperties, HubEventActions, Repository, User } from './shared'; + +export interface WatchEventInput { + action: 'started'; + repository: Repository; + sender: User; +} + +export class WatchEventModel implements WatchEventInput, HubEventActions { + action: 'started'; + repository: Repository; + sender: User; + + constructor(input: WatchEventInput) { + Object.assign(this, input); + } + + public static isCurrentModel(input: any): boolean { + const requireKeys: string[] = ['action', 'repository', 'sender']; + return Object.keys(input).length === requireKeys.length && isExistProperties(input, requireKeys) && input.action === 'started'; + } + + convertToHubEvent(): GitHubEventModel { + const eventType: GitHubEventType = 'WatchEvent'; + const payload: GitHubPayloadInput = { + action: this.action, + } + + const data: GitHubEventModel = { + type: eventType, + public: true, // TODO where get + actor: GitHubUserMapper.import(this.sender), + repository: GitHubRepositoryMapper.import(this.repository, 'event'), + payload: GitHubPayloadMapper.import(eventType, payload), + createdOn: new Date().toISOString(), + }; + + return data; + } + +} diff --git a/functions/src/repository/info.ts b/functions/src/repository/info.ts index dbd740da9..c70e63938 100644 --- a/functions/src/repository/info.ts +++ b/functions/src/repository/info.ts @@ -54,7 +54,7 @@ export const getRepositoryInfo: any = async (token: string, repository: { uid: s ...GitHubRepositoryMapper.import(data[0], 'all'), pullRequests: data[1] ? data[1].map((pullrequest: GitHubPullRequestInput) => GitHubPullRequestMapper.import(pullrequest)) : [], events: data[2] ? data[2].map((event: GitHubEventInput) => GitHubEventMapper.import(event)) : [], - releases: data[3] ? data[3].map((release: GitHubReleaseInput) => GitHubReleaseMapper.import(release)) : [], + releases: data[3] ? GitHubReleaseMapper.sortReleaseList(data[3].map((release: GitHubReleaseInput) => GitHubReleaseMapper.import(release))) : [], issues: Array.isArray(data[4]) ? data[4].map((issue: GitHubIssueInput) => GitHubIssueMapper.import(issue)) : [], contributors: Array.isArray(data[5]) ? data[5].map((contributor: GitHubContributorInput) => GitHubContributorMapper.import(contributor)) : [], milestones: Array.isArray(data[6]) ? data[6].map((milestone: GitHubMilestoneInput) => GitHubMilestoneMapper.import(milestone)) : [], diff --git a/functions/src/repository/response-git-webhook-repository.ts b/functions/src/repository/response-git-webhook-repository.ts index 9eb32fdba..171340f95 100644 --- a/functions/src/repository/response-git-webhook-repository.ts +++ b/functions/src/repository/response-git-webhook-repository.ts @@ -1,8 +1,25 @@ // Third party modules import * as CORS from 'cors'; import { https, HttpsFunction, Response } from 'firebase-functions'; - +import { GitHubClient } from '../client/github'; import { Logger } from '../client/logger'; +import { GitHubContributorInput, GitHubContributorMapper } from '../mappers/github/index.mapper'; +import { + CreateEventModel, + IssuesEventModel, + IssueCommentEventModel, + MemberEventModel, + MilestoneEventModel, + PullRequestEventModel, + PushEventModel, + ReleaseEventModel, + RepositoryEventModel, + StatusEventModel, + WatchEventModel, +} from '../mappers/github/webhook-event-response'; +import { addHubEventToCollection, HubEventActions } from '../mappers/github/webhook-event-response/shared'; +import { RepositoryModel } from '../models/index.model'; +import { DocumentData, FieldPath, FirebaseAdmin, QuerySnapshot } from './../client/firebase-admin'; // tslint:disable-next-line: typedef const cors = CORS({ @@ -17,6 +34,199 @@ export interface ResponseGitWebhookRepositoryInput { export const onResponseGitWebhookRepository: HttpsFunction = https.onRequest((req: https.Request, res: Response) => { return cors(req, res, () => { Logger.info(`${req.protocol}://${req.hostname} ; onResponseGitWebhookRepository: success!`); - res.status(200).send(); + + const inputData: any = req.body; + let result: Promise; + + Logger.info(Object.keys(inputData)); + Logger.info(inputData); + + if (IssueCommentEventModel.isCurrentModel(inputData)) { + + result = issueCommentEvent(new IssueCommentEventModel(inputData)); + + } else if (IssuesEventModel.isCurrentModel(inputData)) { + + result = issuesEvent(new IssuesEventModel(inputData)); + + } else if (CreateEventModel.isCurrentModel(inputData)) { + + result = createEvent(new CreateEventModel(inputData)); + + } else if (PushEventModel.isCurrentModel(inputData)) { + + result = pushEvent(new PushEventModel(inputData)); + + } else if (PullRequestEventModel.isCurrentModel(inputData)) { + + result = pullRequestEvent(new PullRequestEventModel(inputData)); + + } else if (ReleaseEventModel.isCurrentModel(inputData)) { + + result = releaseEvent(new ReleaseEventModel(inputData)); + + } else if (MilestoneEventModel.isCurrentModel(inputData)) { + + result = milestoneEvent(new MilestoneEventModel(inputData)); + + } else if (WatchEventModel.isCurrentModel(inputData)) { + + result = watchEvent(new WatchEventModel(inputData)); + + } else if (RepositoryEventModel.isCurrentModel(inputData)) { + + result = repositoryEvent(new RepositoryEventModel(inputData)); + + } else if (MemberEventModel.isCurrentModel(inputData)) { + + result = memberEvent(new MemberEventModel(inputData)); + + } else if (StatusEventModel.isCurrentModel(inputData)) { + + result = statusEvent(new StatusEventModel(inputData)); + + } else { + Logger.error('Not found parser for event'); + } + + if (result) { + result + .then(() => { + Logger.info('Parsing done!'); + res.status(200).send(); + }) + .catch((err: any) => { + Logger.error('Parser error!'); + Logger.error(err); + res.status(500).send('Parser error!'); + }); + } else { + // res.status(200).send(); + res.status(200).send('Not found parser for event'); + } + }); }); + +async function simpleHubEvent(data: HubEventActions): Promise { + const repository: DocumentData = await RepositoryModel.getRepositoryById(data.repository.id); + + addHubEventToCollection(repository, data); + await RepositoryModel.saveRepository(repository); +} + +async function issuesEvent(data: IssuesEventModel): Promise { + Logger.info('issuesEvent'); + const repository: DocumentData = await RepositoryModel.getRepositoryById(data.repository.id); + + data.updateData(repository); + + addHubEventToCollection(repository, data); + await RepositoryModel.saveRepository(repository); +} + +async function repositoryEvent(data: RepositoryEventModel): Promise { + Logger.info('repositoryEvent'); + await data.updateData(); +} + +async function pullRequestEvent(data: PullRequestEventModel): Promise { + Logger.info('pullRequestEvent'); + const repository: DocumentData = await RepositoryModel.getRepositoryById(data.repository.id); + + data.updateData(repository); + + addHubEventToCollection(repository, data); + await RepositoryModel.saveRepository(repository); +} + +async function releaseEvent(data: ReleaseEventModel): Promise { + Logger.info('releaseEvent'); + const repository: DocumentData = await RepositoryModel.getRepositoryById(data.repository.id); + + data.updateData(repository); + + addHubEventToCollection(repository, data); + await RepositoryModel.saveRepository(repository); +} + +async function milestoneEvent(data: MilestoneEventModel): Promise { + Logger.info('milestoneEvent'); + const repository: DocumentData = await RepositoryModel.getRepositoryById(data.repository.id); + + data.updateData(repository); + + await RepositoryModel.saveRepository(repository); +} + +async function watchEvent(data: WatchEventModel): Promise { + Logger.info('watchEvent'); + await simpleHubEvent(data); +} + +async function pushEvent(data: PushEventModel): Promise { + Logger.info('pushEvent'); + const repository: DocumentData = await RepositoryModel.getRepositoryById(data.repository.id); + + addHubEventToCollection(repository, data); + await updateContributors(repository); + + await RepositoryModel.saveRepository(repository); +} + +async function issueCommentEvent(data: IssueCommentEventModel): Promise { + Logger.info('issueCommentEvent'); + await simpleHubEvent(data); +} + +async function createEvent(data: CreateEventModel): Promise { + Logger.info('createEvent'); + await simpleHubEvent(data); +} + +async function memberEvent(data: MemberEventModel): Promise { + Logger.info('memberEvent'); + const repository: DocumentData = await RepositoryModel.getRepositoryById(data.repository.id); + + await updateContributors(repository); + + await RepositoryModel.saveRepository(repository); +} + +async function statusEvent(data: StatusEventModel): Promise { + Logger.info('statusEvent'); + const repository: DocumentData = await RepositoryModel.getRepositoryById(data.repository.id); + + await updateContributors(repository); + + await RepositoryModel.saveRepository(repository); +} + +async function updateContributors(repository: DocumentData): Promise { + const usersRef: QuerySnapshot = await (FirebaseAdmin.firestore().collection('users').where(new FieldPath('repositories', 'uids'), 'array-contains', repository.uid).get()); + + if (!usersRef.empty) { + for (const element of usersRef.docs) { + const userData: DocumentData = element.data(); + const githubToken: string = userData && userData.oauth ? userData.oauth.githubToken : null; + if (githubToken) { + try { + // TODO return empty object + // const delay: any = (ms: number) => new Promise((_: any) => setTimeout(_, ms)); + // await delay(30000); + const response: GitHubContributorInput[] = await GitHubClient(`/repos/${repository.fullName}/stats/contributors`, githubToken); + if (Array.isArray(response) && response.length > 0) { + repository.contributors = response.map((contributor: GitHubContributorInput) => GitHubContributorMapper.import(contributor)); + Logger.info('Repository contributors updated'); + break; + } else { + Logger.info('Repository contributors empty'); + } + } catch (err) { + Logger.error(err); + } + } + } + } + +} diff --git a/scripts/deployment/prod.sh b/scripts/deployment/prod.sh index 16f869933..9b305f9b3 100644 --- a/scripts/deployment/prod.sh +++ b/scripts/deployment/prod.sh @@ -5,7 +5,7 @@ (cd functions/src/environments; sed -i 's/{{ FIREBASE_FUNCTIONS_URL }}/us-central1-pipelinedashboard/g' environment.ts) # WEB -(cd web/src/environments; sed -i 's/x\.x\.x/v0.11.prod-'$TRAVIS_BUILD_NUMBER'-ALPHA/g' environment.prod.ts) +(cd web/src/environments; sed -i 's/x\.x\.x/v0.11-'$TRAVIS_BUILD_NUMBER'-ALPHA/g' environment.prod.ts) (cd web/src/environments; sed -i 's/{{ FIREBASE_API_KEY }}/'$FIREBASE_API_KEY_PROD'/g' environment.prod.ts) (cd web/src/environments; sed -i 's/{{ FIREBASE_AUTH_DOMAIN }}/'$FIREBASE_AUTH_DOMAIN_PROD'/g' environment.prod.ts) (cd web/src/environments; sed -i 's/{{ FIREBASE_DATABASE_URL }}/'$FIREBASE_DATABASE_URL_PROD'/g' environment.prod.ts) From 0212c7d433b52329428cf4c6aa722cf4d8337153 Mon Sep 17 00:00:00 2001 From: Khushboo Date: Tue, 10 Sep 2019 22:13:13 +0530 Subject: [PATCH 03/17] feat(projects): #1478 added url , description and milestones in repo rating calculation --- web/src/app/core/services/repository.service.ts | 5 ++++- web/src/app/shared/models/repository.model.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web/src/app/core/services/repository.service.ts b/web/src/app/core/services/repository.service.ts index cadbefed1..f17511520 100644 --- a/web/src/app/core/services/repository.service.ts +++ b/web/src/app/core/services/repository.service.ts @@ -59,7 +59,10 @@ export class RepositoryService { let rating: number = 0; const issuePoints: number = repo.issues.length > 0 ? this.getPoints(repo.issues[0].createdOn) : 0; const releasesPoints: number = repo.releases.length > 0 ? this.getPoints(repo.releases[0].createdOn) : 0; - rating = (issuePoints + releasesPoints) / 2; + const milestonesPoints: number = repo.milestones.length > 0 ? this.getPoints(new Date(repo.milestones[0].updatedAt)) : 0; + const urlPoints: number = repo.url ? 100 : 0; + const descriptionPoints: number = repo.description ? 100 : 0; + rating = (issuePoints + releasesPoints + milestonesPoints + urlPoints + descriptionPoints) / 5; return rating; } diff --git a/web/src/app/shared/models/repository.model.ts b/web/src/app/shared/models/repository.model.ts index c717d12c7..82d376a0e 100644 --- a/web/src/app/shared/models/repository.model.ts +++ b/web/src/app/shared/models/repository.model.ts @@ -34,6 +34,7 @@ export class RepositoryModel { contributors: ContributorModel[]; milestones: MilestoneModel[]; webhook: WebhookModel; + url?: string; constructor(fullName: string) { this.fullName = fullName; From da4a92fcea2dff6ac777e8c248c833cf8760f0df Mon Sep 17 00:00:00 2001 From: Khushboo Date: Tue, 10 Sep 2019 22:48:25 +0530 Subject: [PATCH 04/17] fix(projects): #1478 refactored getRating method using loop --- web/src/app/core/services/repository.service.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/app/core/services/repository.service.ts b/web/src/app/core/services/repository.service.ts index f17511520..377ef36df 100644 --- a/web/src/app/core/services/repository.service.ts +++ b/web/src/app/core/services/repository.service.ts @@ -57,13 +57,13 @@ export class RepositoryService { public getRating(repo: RepositoryModel): number { let rating: number = 0; - const issuePoints: number = repo.issues.length > 0 ? this.getPoints(repo.issues[0].createdOn) : 0; - const releasesPoints: number = repo.releases.length > 0 ? this.getPoints(repo.releases[0].createdOn) : 0; - const milestonesPoints: number = repo.milestones.length > 0 ? this.getPoints(new Date(repo.milestones[0].updatedAt)) : 0; - const urlPoints: number = repo.url ? 100 : 0; - const descriptionPoints: number = repo.description ? 100 : 0; - rating = (issuePoints + releasesPoints + milestonesPoints + urlPoints + descriptionPoints) / 5; - + const checks: number[] = []; + checks.push(repo.issues.length > 0 ? this.getPoints(repo.issues[0].createdOn) : 0); + checks.push(repo.releases.length > 0 ? this.getPoints(repo.releases[0].createdOn) : 0); + checks.push(repo.milestones.length > 0 ? this.getPoints(new Date(repo.milestones[0].updatedAt)) : 0); + checks.push(repo.url ? 100 : 0); + checks.push(repo.description ? 100 : 0); + rating = checks.reduce((total: number, current: number) => total + current, 0) / checks.length; return rating; } From 0b3f9a38a6ab5e169da01cf7355ed8def62c7c6b Mon Sep 17 00:00:00 2001 From: kykyk Date: Wed, 11 Sep 2019 19:05:41 +0300 Subject: [PATCH 05/17] feat(repositories)#1123 add timestamp to functions --- functions/src/mappers/github/event.mapper.ts | 8 +++-- functions/src/mappers/github/issue.mapper.ts | 12 ++++--- .../src/mappers/github/milestone.mapper.ts | 7 ++-- .../src/mappers/github/pullRequest.mapper.ts | 12 ++++--- .../src/mappers/github/release.mapper.ts | 36 +++++++++++++------ .../github/webhook-event-response/create.ts | 5 ++- .../webhook-event-response/issue-comment.ts | 9 +++-- .../github/webhook-event-response/issues.ts | 9 +++-- .../webhook-event-response/milestone.ts | 5 ++- .../webhook-event-response/pull-request.ts | 9 +++-- .../github/webhook-event-response/push.ts | 5 ++- .../github/webhook-event-response/release.ts | 7 ++-- .../github/webhook-event-response/watch.ts | 5 ++- .../src/mappers/github/webhook.mapper.ts | 14 ++++---- 14 files changed, 96 insertions(+), 47 deletions(-) diff --git a/functions/src/mappers/github/event.mapper.ts b/functions/src/mappers/github/event.mapper.ts index eafa6870e..beb6c83ab 100644 --- a/functions/src/mappers/github/event.mapper.ts +++ b/functions/src/mappers/github/event.mapper.ts @@ -1,4 +1,6 @@ -// Dashboard hub firebase functions mappers/modesl +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubEventType } from './event.mapper'; import { GitHubOrganisationtInput, GitHubOrganisationMapper, GitHubOrganisationModel } from './organisation.mapper'; import { GitHubPayloadInput, GitHubPayloadMapper, GitHubPayloadModel } from './payload.mapper'; @@ -26,7 +28,7 @@ export interface GitHubEventModel { repository: GitHubRepositoryModel; organisation?: GitHubOrganisationModel; payload: GitHubPayloadModel; - createdOn: string; + createdOn: firestore.Timestamp; } export class GitHubEventMapper { @@ -38,7 +40,7 @@ export class GitHubEventMapper { actor: GitHubUserMapper.import(input.actor), repository: GitHubRepositoryMapper.import(input.repo, 'event'), payload: GitHubPayloadMapper.import(input.type, input.payload), - createdOn: input.created_at, + createdOn: firestore.Timestamp.fromDate(new Date(input.created_at)), }; if (input.org) { diff --git a/functions/src/mappers/github/issue.mapper.ts b/functions/src/mappers/github/issue.mapper.ts index 618c9760b..4aa61da5f 100644 --- a/functions/src/mappers/github/issue.mapper.ts +++ b/functions/src/mappers/github/issue.mapper.ts @@ -1,4 +1,6 @@ -// Dashboard hub firebase functions mappers/models +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './user.mapper'; export interface GitHubIssueInput { @@ -23,8 +25,8 @@ export interface GitHubIssueModel { description: string; owner: GitHubUserModel; assignees: GitHubUserModel[]; - createdOn: string; - updatedOn: string; + createdOn: firestore.Timestamp; + updatedOn: firestore.Timestamp; } export class GitHubIssueMapper { @@ -38,8 +40,8 @@ export class GitHubIssueMapper { description: input.body, owner: GitHubUserMapper.import(input.user), assignees: input.assignees.map((assignee: GitHubUserInput) => GitHubUserMapper.import(assignee)), - createdOn: input.created_at, - updatedOn: input.updated_at, + createdOn: firestore.Timestamp.fromDate(new Date(input.created_at)), + updatedOn: firestore.Timestamp.fromDate(new Date(input.updated_at)), }; } } diff --git a/functions/src/mappers/github/milestone.mapper.ts b/functions/src/mappers/github/milestone.mapper.ts index b82eec808..f64ed293d 100644 --- a/functions/src/mappers/github/milestone.mapper.ts +++ b/functions/src/mappers/github/milestone.mapper.ts @@ -1,3 +1,6 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './index.mapper'; export interface GitHubMilestoneInput { @@ -21,7 +24,7 @@ export interface GitHubMilestoneModel { closeIssues: number; htmlUrl: string; description: string; - updatedAt: string; + updatedAt: firestore.Timestamp; } export class GitHubMilestoneMapper { @@ -35,7 +38,7 @@ export class GitHubMilestoneMapper { closeIssues: input.closed_issues, htmlUrl: input.html_url, description: input.description, - updatedAt: input.updated_at, + updatedAt: firestore.Timestamp.fromDate(new Date(input.updated_at)), }; } } diff --git a/functions/src/mappers/github/pullRequest.mapper.ts b/functions/src/mappers/github/pullRequest.mapper.ts index 0aa4a1545..00d460916 100644 --- a/functions/src/mappers/github/pullRequest.mapper.ts +++ b/functions/src/mappers/github/pullRequest.mapper.ts @@ -1,4 +1,6 @@ -// Dashboard hub firebase functions mappers/models +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './user.mapper'; export interface GitHubPullRequestInput { @@ -25,8 +27,8 @@ export interface GitHubPullRequestModel { owner: GitHubUserModel; assignees: GitHubUserModel[]; reviewers: GitHubUserModel[]; - createdOn: string; - updatedOn: string; + createdOn: firestore.Timestamp; + updatedOn: firestore.Timestamp; } export class GitHubPullRequestMapper { @@ -41,8 +43,8 @@ export class GitHubPullRequestMapper { owner: GitHubUserMapper.import(input.user), assignees: input.assignees.map((assignee: GitHubUserInput) => GitHubUserMapper.import(assignee)), reviewers: input.requested_reviewers.map((reviewer: GitHubUserInput) => GitHubUserMapper.import(reviewer)), - createdOn: input.created_at, - updatedOn: input.updated_at, + createdOn: firestore.Timestamp.fromDate(new Date(input.created_at)), + updatedOn: firestore.Timestamp.fromDate(new Date(input.updated_at)), }; } } diff --git a/functions/src/mappers/github/release.mapper.ts b/functions/src/mappers/github/release.mapper.ts index 028cc374b..c2ca99eb3 100644 --- a/functions/src/mappers/github/release.mapper.ts +++ b/functions/src/mappers/github/release.mapper.ts @@ -1,4 +1,6 @@ -// Dashboard hub firebase functions mappers/models +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './user.mapper'; export interface GitHubReleaseInput { @@ -17,7 +19,7 @@ export interface GitHubReleaseModel { description: string; owner: GitHubUserModel; htmlUrl: string; - createdOn: string; + createdOn: firestore.Timestamp; isPrerelease: boolean; } @@ -29,18 +31,30 @@ export class GitHubReleaseMapper { description: input.body, owner: GitHubUserMapper.import(input.author), htmlUrl: input.html_url, - createdOn: input.published_at, + createdOn: firestore.Timestamp.fromDate(new Date(input.published_at)), isPrerelease: input.prerelease, }; } - public static sortReleaseList(releases: GitHubReleaseModel[]) { - return releases.sort( - (a: GitHubReleaseModel, b: GitHubReleaseModel): number => { - const date1: Date = new Date(a.createdOn); - const date2: Date = new Date(b.createdOn); - return date2.getTime() - date1.getTime(); - } - ); + public static sortReleaseList(releases: GitHubReleaseModel[]): GitHubReleaseModel[] { + return releases + .sort( + (a: GitHubReleaseModel, b: GitHubReleaseModel): number => { + // tslint:disable-next-line: triple-equals + if (a.createdOn == null && b.createdOn == null) { + return 0; + } + // tslint:disable-next-line: triple-equals + if (a.createdOn == null) { + return 1; + } + // tslint:disable-next-line: triple-equals + if (b.createdOn == null) { + return -1; + } + return b.createdOn.toMillis() - a.createdOn.toMillis(); + } + ) + ; } } diff --git a/functions/src/mappers/github/webhook-event-response/create.ts b/functions/src/mappers/github/webhook-event-response/create.ts index 08b60a0e7..322960a34 100644 --- a/functions/src/mappers/github/webhook-event-response/create.ts +++ b/functions/src/mappers/github/webhook-event-response/create.ts @@ -1,3 +1,6 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubEventModel, GitHubEventType } from '../event.mapper'; import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; import { GitHubRepositoryMapper } from '../repository.mapper'; @@ -46,7 +49,7 @@ export class CreateEventModel implements CreateEventInput, HubEventActions { actor: GitHubUserMapper.import(this.sender), repository: GitHubRepositoryMapper.import(this.repository, 'event'), payload: GitHubPayloadMapper.import(eventType, payload), - createdOn: new Date().toISOString(), + createdOn: firestore.Timestamp.now(), }; return data; diff --git a/functions/src/mappers/github/webhook-event-response/issue-comment.ts b/functions/src/mappers/github/webhook-event-response/issue-comment.ts index 9edbf7253..2af930c4b 100644 --- a/functions/src/mappers/github/webhook-event-response/issue-comment.ts +++ b/functions/src/mappers/github/webhook-event-response/issue-comment.ts @@ -1,3 +1,6 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubEventModel, GitHubEventType } from '../event.mapper'; import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; import { GitHubRepositoryMapper } from '../repository.mapper'; @@ -11,8 +14,8 @@ interface Comment { id: number; node_id: string; user: User; - created_at: Date; - updated_at: Date; + created_at: string; + updated_at: string; author_association: string; body: string; } @@ -56,7 +59,7 @@ export class IssueCommentEventModel implements IssueCommentEventInput, HubEventA actor: GitHubUserMapper.import(this.sender), repository: GitHubRepositoryMapper.import(this.repository, 'event'), payload: GitHubPayloadMapper.import(eventType, payload), - createdOn: new Date().toISOString(), + createdOn: firestore.Timestamp.now(), }; return data; diff --git a/functions/src/mappers/github/webhook-event-response/issues.ts b/functions/src/mappers/github/webhook-event-response/issues.ts index abbcd9c74..56b735590 100644 --- a/functions/src/mappers/github/webhook-event-response/issues.ts +++ b/functions/src/mappers/github/webhook-event-response/issues.ts @@ -1,3 +1,6 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { DocumentData } from '../../../client/firebase-admin'; import { GitHubEventModel, GitHubEventType } from '../event.mapper'; import { GitHubIssueModel } from '../issue.mapper'; @@ -45,7 +48,7 @@ export class IssuesEventModel implements IssuesEventInput, HubEventActions { actor: GitHubUserMapper.import(this.sender), repository: GitHubRepositoryMapper.import(this.repository, 'event'), payload: GitHubPayloadMapper.import(eventType, payload), - createdOn: new Date().toISOString(), + createdOn: firestore.Timestamp.now(), }; return data; @@ -104,8 +107,8 @@ export class IssuesEventModel implements IssuesEventInput, HubEventActions { description: this.issue.body, owner: GitHubUserMapper.import(this.issue.user), assignees: this.issue.assignees.map((assignee: User) => GitHubUserMapper.import(assignee)), - createdOn: this.issue.created_at, - updatedOn: this.issue.updated_at, + createdOn: firestore.Timestamp.fromDate(new Date(this.issue.created_at)), + updatedOn: firestore.Timestamp.fromDate(new Date(this.issue.updated_at)), } } diff --git a/functions/src/mappers/github/webhook-event-response/milestone.ts b/functions/src/mappers/github/webhook-event-response/milestone.ts index 81b49b884..8a6ac61e3 100644 --- a/functions/src/mappers/github/webhook-event-response/milestone.ts +++ b/functions/src/mappers/github/webhook-event-response/milestone.ts @@ -1,3 +1,6 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { DocumentData } from '../../../client/firebase-admin'; import { GitHubMilestoneModel } from '../milestone.mapper'; import { GitHubUserMapper } from '../user.mapper'; @@ -72,7 +75,7 @@ export class MilestoneEventModel implements MilestoneEventInput { closeIssues: this.milestone.closed_issues, htmlUrl: this.milestone.html_url, description: this.milestone.description, - updatedAt: this.milestone.updated_at, + updatedAt: firestore.Timestamp.fromDate(new Date(this.milestone.updated_at)), }; } diff --git a/functions/src/mappers/github/webhook-event-response/pull-request.ts b/functions/src/mappers/github/webhook-event-response/pull-request.ts index 9d43d00c3..98363b332 100644 --- a/functions/src/mappers/github/webhook-event-response/pull-request.ts +++ b/functions/src/mappers/github/webhook-event-response/pull-request.ts @@ -1,3 +1,6 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { DocumentData } from '../../../client/firebase-admin'; import { GitHubEventModel, GitHubEventType } from '../event.mapper'; import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; @@ -118,7 +121,7 @@ export class PullRequestEventModel implements PullRequestEventInput, HubEventAct actor: GitHubUserMapper.import(this.sender), repository: GitHubRepositoryMapper.import(this.repository, 'event'), payload: GitHubPayloadMapper.import(eventType, payload), - createdOn: new Date().toISOString(), + createdOn: firestore.Timestamp.now(), }; return data; @@ -171,8 +174,8 @@ export class PullRequestEventModel implements PullRequestEventInput, HubEventAct owner: GitHubUserMapper.import(this.pull_request.user), assignees: this.pull_request.assignees.map((assignee: User) => GitHubUserMapper.import(assignee)), reviewers: this.pull_request.requested_reviewers.map((reviewer: User) => GitHubUserMapper.import(reviewer)), - createdOn: this.pull_request.created_at, - updatedOn: this.pull_request.updated_at, + createdOn: firestore.Timestamp.fromDate(new Date(this.pull_request.created_at)), + updatedOn: firestore.Timestamp.fromDate(new Date(this.pull_request.updated_at)), } } diff --git a/functions/src/mappers/github/webhook-event-response/push.ts b/functions/src/mappers/github/webhook-event-response/push.ts index b9ac7dec2..24f1cec10 100644 --- a/functions/src/mappers/github/webhook-event-response/push.ts +++ b/functions/src/mappers/github/webhook-event-response/push.ts @@ -1,3 +1,6 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubEventModel, GitHubEventType } from '../event.mapper'; import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; import { GitHubRepositoryMapper } from '../repository.mapper'; @@ -62,7 +65,7 @@ export class PushEventModel implements PushEventInput, HubEventActions { actor: GitHubUserMapper.import(this.sender), repository: GitHubRepositoryMapper.import(this.repository, 'event'), payload: GitHubPayloadMapper.import(eventType, payload), - createdOn: new Date().toISOString(), + createdOn: firestore.Timestamp.now(), }; return data; diff --git a/functions/src/mappers/github/webhook-event-response/release.ts b/functions/src/mappers/github/webhook-event-response/release.ts index 486df7199..a8101ef8a 100644 --- a/functions/src/mappers/github/webhook-event-response/release.ts +++ b/functions/src/mappers/github/webhook-event-response/release.ts @@ -1,3 +1,6 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { DocumentData } from '../../../client/firebase-admin'; import { GitHubEventModel, GitHubEventType } from '../event.mapper'; import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; @@ -68,7 +71,7 @@ export class ReleaseEventModel implements ReleaseEventInput, HubEventActions { actor: GitHubUserMapper.import(this.sender), repository: GitHubRepositoryMapper.import(this.repository, 'event'), payload: GitHubPayloadMapper.import(eventType, payload), - createdOn: new Date().toISOString(), + createdOn: firestore.Timestamp.now(), }; return data; @@ -124,7 +127,7 @@ export class ReleaseEventModel implements ReleaseEventInput, HubEventActions { description: this.release.body, owner: GitHubUserMapper.import(this.release.author), htmlUrl: this.release.html_url, - createdOn: this.release.published_at, + createdOn: firestore.Timestamp.fromDate(new Date(this.release.published_at)), isPrerelease: this.release.prerelease, } } diff --git a/functions/src/mappers/github/webhook-event-response/watch.ts b/functions/src/mappers/github/webhook-event-response/watch.ts index d5c10637d..e6e90cbbe 100644 --- a/functions/src/mappers/github/webhook-event-response/watch.ts +++ b/functions/src/mappers/github/webhook-event-response/watch.ts @@ -1,3 +1,6 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubEventModel, GitHubEventType } from '../event.mapper'; import { GitHubPayloadInput, GitHubPayloadMapper } from '../payload.mapper'; import { GitHubRepositoryMapper } from '../repository.mapper'; @@ -36,7 +39,7 @@ export class WatchEventModel implements WatchEventInput, HubEventActions { actor: GitHubUserMapper.import(this.sender), repository: GitHubRepositoryMapper.import(this.repository, 'event'), payload: GitHubPayloadMapper.import(eventType, payload), - createdOn: new Date().toISOString(), + createdOn: firestore.Timestamp.now(), }; return data; diff --git a/functions/src/mappers/github/webhook.mapper.ts b/functions/src/mappers/github/webhook.mapper.ts index a56b5263b..bc9909e70 100644 --- a/functions/src/mappers/github/webhook.mapper.ts +++ b/functions/src/mappers/github/webhook.mapper.ts @@ -1,3 +1,5 @@ +import { firestore } from "firebase-admin"; + export interface GitHubRepositoryWebhookResponse { type: string; id: number; @@ -10,8 +12,8 @@ export interface GitHubRepositoryWebhookResponse { secret?: string; insecure_ssl?: '0' | '1'; }; - updated_at: Date; - created_at: Date; + updated_at: string; + created_at: string; url: string; test_url: string; ping_url: string; @@ -47,8 +49,8 @@ export interface GitHubRepositoryWebhookModel { secret?: string; insecureSsl?: '0' | '1'; }; - updatedAt: Date; - createdAt: Date; + updatedOn: firestore.Timestamp; + createdOn: firestore.Timestamp; url: string; testUrl: string; pingUrl: string; @@ -101,8 +103,8 @@ export class GitHubRepositoryWebhookMapper { active: input.active, events: input.events, config: config, - updatedAt: input.updated_at, - createdAt: input.created_at, + updatedOn: firestore.Timestamp.fromDate(new Date(input.updated_at)), + createdOn: firestore.Timestamp.fromDate(new Date(input.created_at)), url: input.url, testUrl: input.test_url, pingUrl: input.ping_url, From c5c85f5f312cbb5ece81e911229c04642f97256f Mon Sep 17 00:00:00 2001 From: kykyk Date: Wed, 11 Sep 2019 19:21:08 +0300 Subject: [PATCH 06/17] feat(repositories)#1123 fix timestamp to web --- web/src/app/core/services/repository.service.ts | 4 ++-- web/src/app/projects/repository/repository.component.html | 8 ++++---- web/src/app/shared/models/event.model.ts | 3 ++- web/src/app/shared/models/issue.model.ts | 5 +++-- web/src/app/shared/models/pullRequest.model.ts | 5 +++-- web/src/app/shared/models/release.model.ts | 3 ++- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/web/src/app/core/services/repository.service.ts b/web/src/app/core/services/repository.service.ts index 16fa7077e..7cab34f6d 100644 --- a/web/src/app/core/services/repository.service.ts +++ b/web/src/app/core/services/repository.service.ts @@ -55,8 +55,8 @@ export class RepositoryService { public getRating(repo: RepositoryModel): number { let rating: number = 0; - const issuePoints: number = repo.issues.length > 0 ? this.getPoints(repo.issues[0].createdOn) : 0; - const releasesPoints: number = repo.releases.length > 0 ? this.getPoints(repo.releases[0].createdOn) : 0; + const issuePoints: number = repo.issues.length > 0 ? this.getPoints(repo.issues[0].createdOn.toDate()) : 0; + const releasesPoints: number = repo.releases.length > 0 ? this.getPoints(repo.releases[0].createdOn.toDate()) : 0; rating = (issuePoints + releasesPoints) / 2; return rating; diff --git a/web/src/app/projects/repository/repository.component.html b/web/src/app/projects/repository/repository.component.html index 979ed5025..b87688066 100644 --- a/web/src/app/projects/repository/repository.component.html +++ b/web/src/app/projects/repository/repository.component.html @@ -95,7 +95,7 @@

#{{ issue.number }} {{ issue.title }}

-

{{ issue.updatedOn | timeAgo }}

+

{{ issue.updatedOn.toDate() | timeAgo }}

@@ -127,7 +127,7 @@

{{ pullRequest.title

{{ pullRequest.owner.username }}

-

{{ pullRequest.createdOn | timeAgo }}

+

{{ pullRequest.createdOn.toDate() | timeAgo }}

@@ -160,7 +160,7 @@

{{ event.actor.username }}

-

{{ event.createdOn | timeAgo }}

+

{{ event.createdOn.toDate() | timeAgo }}

@@ -192,7 +192,7 @@

href="{{ repository.releases[0].htmlUrl }}">{{ repository.releases[0].title }}

- Published: {{ repository.releases[0].createdOn | timeAgo }}

+ Published: {{ repository.releases[0].createdOn.toDate() | timeAgo }}

Published: N/A

diff --git a/web/src/app/shared/models/event.model.ts b/web/src/app/shared/models/event.model.ts index f843173cc..743d2b1c9 100644 --- a/web/src/app/shared/models/event.model.ts +++ b/web/src/app/shared/models/event.model.ts @@ -1,3 +1,4 @@ +import { firestore } from 'firebase'; import { RepositoryModel } from './repository.model'; import { UserModel } from './user.model'; @@ -9,5 +10,5 @@ export class EventModel { repository: RepositoryModel; // organisation: input.org ? GitHubOrganisationMapper.import(input.org) : {}, // payload: GitHubPayloadMapper.import(input.type, input.payload), - createdOn: Date; + createdOn: firestore.Timestamp; } diff --git a/web/src/app/shared/models/issue.model.ts b/web/src/app/shared/models/issue.model.ts index 5dd462ff4..4d7f5725c 100644 --- a/web/src/app/shared/models/issue.model.ts +++ b/web/src/app/shared/models/issue.model.ts @@ -1,3 +1,4 @@ +import { firestore } from 'firebase'; import { UserModel } from './user.model'; export class IssueModel { @@ -9,6 +10,6 @@ export class IssueModel { owner: UserModel; assigned: UserModel; description: string = ''; - createdOn: Date; - updatedOn: Date; + createdOn: firestore.Timestamp; + updatedOn: firestore.Timestamp; } diff --git a/web/src/app/shared/models/pullRequest.model.ts b/web/src/app/shared/models/pullRequest.model.ts index c51999865..6955b7382 100644 --- a/web/src/app/shared/models/pullRequest.model.ts +++ b/web/src/app/shared/models/pullRequest.model.ts @@ -1,3 +1,4 @@ +import { firestore } from 'firebase'; import { UserModel } from './user.model'; export class PullRequestModel { @@ -10,6 +11,6 @@ export class PullRequestModel { assigned: UserModel; requestedReviewers: UserModel; description: string = ''; - createdOn: Date; - updatedOn: Date; + createdOn: firestore.Timestamp; + updatedOn: firestore.Timestamp; } diff --git a/web/src/app/shared/models/release.model.ts b/web/src/app/shared/models/release.model.ts index 90d74dc24..b7be85313 100644 --- a/web/src/app/shared/models/release.model.ts +++ b/web/src/app/shared/models/release.model.ts @@ -1,3 +1,4 @@ +import { firestore } from 'firebase'; import { UserModel } from './user.model'; export class ReleaseModel { @@ -6,5 +7,5 @@ export class ReleaseModel { description: string; owner: UserModel; htmlUrl: string; - createdOn: Date; + createdOn: firestore.Timestamp; } From b6f038d1aa04ac52b3f353c9efb04f41bb826330 Mon Sep 17 00:00:00 2001 From: Khushboo Date: Wed, 11 Sep 2019 23:19:01 +0530 Subject: [PATCH 07/17] feat(projects): #1488 added forks_count , stargazersCount and watchersCount in repo rating --- .../src/mappers/github/repository.mapper.ts | 10 +++++++++- .../app/core/services/repository.service.ts | 18 ++++++++++++++++++ web/src/app/shared/models/repository.model.ts | 3 +++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/functions/src/mappers/github/repository.mapper.ts b/functions/src/mappers/github/repository.mapper.ts index 50bb5eb4e..4a92dfa28 100644 --- a/functions/src/mappers/github/repository.mapper.ts +++ b/functions/src/mappers/github/repository.mapper.ts @@ -18,6 +18,9 @@ export interface GitHubRepositoryInput { url: string; private: boolean; fork: boolean; + forks_count: number, + stargazers_count: number, + watchers_count: number } export interface GitHubRepositoryModel { @@ -36,14 +39,19 @@ export interface GitHubRepositoryModel { milestones?: GitHubMilestoneModel[]; updatedAt: firestore.Timestamp; webhook?: GitHubRepositoryWebhookModel; + forksCount: 9, + stargazersCount: 80, + watchersCount: 80, } export class GitHubRepositoryMapper { static import(input: GitHubRepositoryInput, type: 'minimum' | 'all' | 'event' = 'minimum'): GitHubRepositoryModel { const output: any = {}; - if (type === 'all') { output.fork = input.fork; + output.forksCount = input.forks_count; + output.stargazersCount = input.stargazers_count; + output.watchersCount = input.watchers_count; } if (type === 'event' || type === 'all') { diff --git a/web/src/app/core/services/repository.service.ts b/web/src/app/core/services/repository.service.ts index abeebc000..e70a482ad 100644 --- a/web/src/app/core/services/repository.service.ts +++ b/web/src/app/core/services/repository.service.ts @@ -61,6 +61,9 @@ export class RepositoryService { checks.push(repo.milestones.length > 0 ? this.getPoints(new Date(repo.milestones[0].updatedAt)) : 0); checks.push(repo.url ? 100 : 0); checks.push(repo.description ? 100 : 0); + checks.push(repo.forksCount ? this.getPointsByCount(repo.forksCount, 50) : 0); + checks.push(repo.stargazersCount ? this.getPointsByCount(repo.stargazersCount, 100) : 0); + checks.push(repo.watchersCount ? this.getPointsByCount(repo.watchersCount, 25) : 0); rating = checks.reduce((total: number, current: number) => total + current, 0) / checks.length; return rating; } @@ -79,4 +82,19 @@ export class RepositoryService { return ((boundary - duration) / 30) * 100; // percentage } + + public getPointsByCount(count: number, limit: number): number { + let points: number; + switch (true) { + case (count >= 1 && count <= limit): + points = 50; + break; + case (count > limit): + points = 100; + break; + default: + points = 0; + } + return points; + } } diff --git a/web/src/app/shared/models/repository.model.ts b/web/src/app/shared/models/repository.model.ts index 5ca3ceb10..02d7eae0f 100644 --- a/web/src/app/shared/models/repository.model.ts +++ b/web/src/app/shared/models/repository.model.ts @@ -35,6 +35,9 @@ export class RepositoryModel { milestones: MilestoneModel[]; webhook: WebhookModel; url?: string; + forksCount: number; + stargazersCount: number; + watchersCount: number; constructor(uid?: string) { this.uid = uid; From 2aa699be025247917c7a021a0b20895958b2bd41 Mon Sep 17 00:00:00 2001 From: Khushboo Date: Wed, 11 Sep 2019 23:31:34 +0530 Subject: [PATCH 08/17] fix(projects): #1488 fixed refresh repo issue on console --- web/src/app/projects/repository/repository.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/app/projects/repository/repository.component.html b/web/src/app/projects/repository/repository.component.html index 979ed5025..2b50778e9 100644 --- a/web/src/app/projects/repository/repository.component.html +++ b/web/src/app/projects/repository/repository.component.html @@ -304,7 +304,7 @@

no data -