diff --git a/README.md b/README.md index 66a3cab04..6cfc55390 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Please get in touch via [@DashboardHub](https://twitter.com/DashboardHub) and le 2. Enter the 2 OAuth private keys from GitHub into the Firebase Authentication 3. Click **Databases** and create an empty `firestore` database (indexes, security, collections and rules will all be automatically created later on as part of the deployment) 4. Update `{{ FIREBASE_FUNCTIONS_URL }}` in file `functions/src/environments/environment.ts` with your function subdomain, for example `us-central1-pipelinedashboard-test` +4. Update `{{ GITHUB_WEBHOOK_SECRET }}` in file `functions/src/environments/environment.ts` with your private secret key (random string), this is used to protect your webhook function, for example `pipelinedashboard-test-123` #### Angular diff --git a/functions/package-lock.json b/functions/package-lock.json index a1c661851..1ce79f4be 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -892,6 +892,15 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.4.tgz", "integrity": "sha512-Set5ZdrAaKI/qHdFlVMgm/GsAv/wkXhSTuZFkJ+JI7HK+wIkIlOaUXSXieIvJ0+OvGIqtREFoE+NHJtEq0gtEw==" }, + "@types/uuid": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", + "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1423,7 +1432,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -1437,7 +1446,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" @@ -2592,7 +2601,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -3240,9 +3249,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "vary": { "version": "1.1.2", 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/environments/environment.ts b/functions/src/environments/environment.ts index 0057c661e..92079ed85 100644 --- a/functions/src/environments/environment.ts +++ b/functions/src/environments/environment.ts @@ -2,9 +2,60 @@ export const enviroment: Config = { githubWebhook: { url: 'https://{{ FIREBASE_FUNCTIONS_URL }}.cloudfunctions.net/responseGitWebhookRepository', + secret: '{{ GITHUB_WEBHOOK_SECRET }}', + content_type: 'json', + insecure_ssl: '0', 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', ], }, @@ -13,6 +64,9 @@ export const enviroment: Config = { interface Config { githubWebhook: { url: string, + secret: string, + content_type: 'json' | 'form', + insecure_ssl: '0' | '1', events: string[], } } 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/event.mapper.ts b/functions/src/mappers/github/event.mapper.ts index 99e04defe..beb6c83ab 100644 --- a/functions/src/mappers/github/event.mapper.ts +++ b/functions/src/mappers/github/event.mapper.ts @@ -1,7 +1,6 @@ -// Third party modules import { firestore } from 'firebase-admin'; -// Dashboard hub firebase functions mappers/modesl +// Dashboard mappers/models import { GitHubEventType } from './event.mapper'; import { GitHubOrganisationtInput, GitHubOrganisationMapper, GitHubOrganisationModel } from './organisation.mapper'; import { GitHubPayloadInput, GitHubPayloadMapper, GitHubPayloadModel } from './payload.mapper'; @@ -13,18 +12,18 @@ 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; @@ -41,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 1b19a7e93..4aa61da5f 100644 --- a/functions/src/mappers/github/issue.mapper.ts +++ b/functions/src/mappers/github/issue.mapper.ts @@ -1,11 +1,10 @@ -// Third party modules import { firestore } from 'firebase-admin'; -// Dashboard hub firebase functions mappers/models +// Dashboard 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 +12,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; @@ -41,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 490998d6d..f64ed293d 100644 --- a/functions/src/mappers/github/milestone.mapper.ts +++ b/functions/src/mappers/github/milestone.mapper.ts @@ -1,6 +1,10 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models import { GitHubUserInput, GitHubUserMapper, GitHubUserModel } from './index.mapper'; export interface GitHubMilestoneInput { + id: number; title: string; creator: GitHubUserInput; state: string; @@ -12,6 +16,7 @@ export interface GitHubMilestoneInput { } export interface GitHubMilestoneModel { + uid: number; title: string; creator: GitHubUserModel; state: string; @@ -19,12 +24,13 @@ export interface GitHubMilestoneModel { closeIssues: number; htmlUrl: string; description: string; - updatedAt: string; + updatedAt: firestore.Timestamp; } export class GitHubMilestoneMapper { static import(input: GitHubMilestoneInput): GitHubMilestoneModel { return { + uid: input.id, title: input.title, creator: GitHubUserMapper.import(input.creator), state: input.state, @@ -32,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/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..00d460916 100644 --- a/functions/src/mappers/github/pullRequest.mapper.ts +++ b/functions/src/mappers/github/pullRequest.mapper.ts @@ -1,11 +1,10 @@ -// Third party modules import { firestore } from 'firebase-admin'; -// Dashboard hub firebase functions mappers/models +// Dashboard 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 +13,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; @@ -44,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 aed8562a5..c2ca99eb3 100644 --- a/functions/src/mappers/github/release.mapper.ts +++ b/functions/src/mappers/github/release.mapper.ts @@ -1,25 +1,26 @@ -// Third party modules import { firestore } from 'firebase-admin'; -// Dashboard hub firebase functions mappers/models +// Dashboard 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; + isPrerelease: boolean; } export class GitHubReleaseMapper { @@ -30,7 +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[]): 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/repository.mapper.ts b/functions/src/mappers/github/repository.mapper.ts index 5373a112a..c585005f1 100644 --- a/functions/src/mappers/github/repository.mapper.ts +++ b/functions/src/mappers/github/repository.mapper.ts @@ -11,24 +11,26 @@ 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; + forks_count: number; + stargazers_count: number; + watchers_count: number; } 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[]; @@ -37,31 +39,32 @@ export interface GitHubRepositoryModel { milestones?: GitHubMilestoneModel[]; updatedAt: firestore.Timestamp; webhook?: GitHubRepositoryWebhookModel; + forksCount: number; + stargazersCount: number; + watchersCount: number; } 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; + output.forksCount = input.forks_count; + output.stargazersCount = input.stargazers_count; + output.watchersCount = input.watchers_count; } 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/mappers/github/webhook-event-response/create.ts b/functions/src/mappers/github/webhook-event-response/create.ts new file mode 100644 index 000000000..322960a34 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/create.ts @@ -0,0 +1,58 @@ +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'; +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: firestore.Timestamp.now(), + }; + + 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..2af930c4b --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/issue-comment.ts @@ -0,0 +1,69 @@ +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'; +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: string; + updated_at: string; + 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: 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 new file mode 100644 index 000000000..56b735590 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/issues.ts @@ -0,0 +1,141 @@ +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'; +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: firestore.Timestamp.now(), + }; + + 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: firestore.Timestamp.fromDate(new Date(this.issue.created_at)), + updatedOn: firestore.Timestamp.fromDate(new Date(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..8a6ac61e3 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/milestone.ts @@ -0,0 +1,114 @@ +import { firestore } from 'firebase-admin'; + +// Dashboard mappers/models +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: firestore.Timestamp.fromDate(new Date(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..98363b332 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/pull-request.ts @@ -0,0 +1,207 @@ +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'; +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: firestore.Timestamp.now(), + }; + + 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: firestore.Timestamp.fromDate(new Date(this.pull_request.created_at)), + updatedOn: firestore.Timestamp.fromDate(new Date(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..24f1cec10 --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/push.ts @@ -0,0 +1,74 @@ +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'; +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: 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 new file mode 100644 index 000000000..a8101ef8a --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/release.ts @@ -0,0 +1,170 @@ +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'; +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: firestore.Timestamp.now(), + }; + + 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: firestore.Timestamp.fromDate(new Date(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..e6e90cbbe --- /dev/null +++ b/functions/src/mappers/github/webhook-event-response/watch.ts @@ -0,0 +1,48 @@ +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'; +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: 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, 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..9b90fcadc 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', @@ -38,20 +40,43 @@ 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 + && exist.config.content_type === enviroment.githubWebhook.content_type + && exist.config.insecure_ssl === enviroment.githubWebhook.insecure_ssl + && ((!exist.config.secret && !enviroment.githubWebhook.secret) || (!!exist.config.secret && !!enviroment.githubWebhook.secret)); + + if (isEqual) { + + for (const existItem of exist.events) { + if (enviroment.githubWebhook.events.findIndex((envItem: string) => existItem === envItem) === -1) { + isEqual = false; + break; + } + } + + if (isEqual) { + Logger.info('Webhook is exist'); + return GitHubRepositoryWebhookMapper.import(exist); + } + } + Logger.info('Webhook is deleting'); + await deleteWebhook(repositoryFullName, exist.id, token); } - return GitHubRepositoryWebhookMapper.import(await createWebhook(repositoryUid, token)); + + Logger.info('Webhook is creating'); + 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..c70e63938 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,37 +42,37 @@ 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'), 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)) : [], 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/response-git-webhook-repository.ts b/functions/src/repository/response-git-webhook-repository.ts index 9eb32fdba..de3f466a8 100644 --- a/functions/src/repository/response-git-webhook-repository.ts +++ b/functions/src/repository/response-git-webhook-repository.ts @@ -1,8 +1,30 @@ // Third party modules import * as CORS from 'cors'; +import * as crypto from "crypto"; import { https, HttpsFunction, Response } from 'firebase-functions'; +import { enviroment } from '../environments/environment'; + +// Dashboard hub firebase functions models/mappers +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({ @@ -16,7 +38,205 @@ 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 sig: string = 'sha1=' + crypto.createHmac('sha1', enviroment.githubWebhook.secret).update(req.rawBody).digest('hex'); + + if (sig !== req.headers['x-hub-signature']) { + res.status(401).send('Error secret token!'); + return; + } + + 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/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/scripts/deployment/dev.sh b/scripts/deployment/dev.sh index f43759447..00f86638e 100644 --- a/scripts/deployment/dev.sh +++ b/scripts/deployment/dev.sh @@ -2,6 +2,7 @@ # FUNCTIONS (cd functions; npm install) +(cd functions/src/environments; sed -i 's/{{ GITHUB_WEBHOOK_SECRET }}/'$GITHUB_WEBHOOK_SECRET'/g' environment.ts) (cd functions/src/environments; sed -i 's/{{ FIREBASE_FUNCTIONS_URL }}/us-central1-pipelinedashboard-dev/g' environment.ts) # WEB diff --git a/scripts/deployment/eddie.sh b/scripts/deployment/eddie.sh index a76ac5959..2e398e192 100644 --- a/scripts/deployment/eddie.sh +++ b/scripts/deployment/eddie.sh @@ -2,6 +2,7 @@ # FUNCTIONS (cd functions; npm install) +(cd functions/src/environments; sed -i 's/{{ GITHUB_WEBHOOK_SECRET }}/'$GITHUB_WEBHOOK_SECRET'/g' environment.ts) (cd functions/src/environments; sed -i 's/{{ FIREBASE_FUNCTIONS_URL }}/us-central1-pipelinedashboard-eddie/g' environment.ts) # WEB diff --git a/scripts/deployment/khush.sh b/scripts/deployment/khush.sh index 4f1fa23e4..4a38f388b 100644 --- a/scripts/deployment/khush.sh +++ b/scripts/deployment/khush.sh @@ -2,6 +2,7 @@ # FUNCTIONS (cd functions; npm install) +(cd functions/src/environments; sed -i 's/{{ GITHUB_WEBHOOK_SECRET }}/'$GITHUB_WEBHOOK_SECRET'/g' environment.ts) (cd functions/src/environments; sed -i 's/{{ FIREBASE_FUNCTIONS_URL }}/us-central1-pipelinedashboard-khush/g' environment.ts) # WEB diff --git a/scripts/deployment/prod.sh b/scripts/deployment/prod.sh index 16f869933..0afc9b8ae 100644 --- a/scripts/deployment/prod.sh +++ b/scripts/deployment/prod.sh @@ -2,10 +2,11 @@ # FUNCTIONS (cd functions; npm install) +(cd functions/src/environments; sed -i 's/{{ GITHUB_WEBHOOK_SECRET }}/'$GITHUB_WEBHOOK_SECRET'/g' environment.ts) (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) 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 cadbefed1..1121e55c0 100644 --- a/web/src/app/core/services/repository.service.ts +++ b/web/src/app/core/services/repository.service.ts @@ -37,10 +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 { @@ -49,19 +48,24 @@ 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 }); } 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; - rating = (issuePoints + releasesPoints) / 2; - - return rating; + const checks: number[] = []; + + checks.push(repo.issues.length > 0 ? this.getPoints(repo.issues[0].createdOn.toDate()) : 0); + checks.push(repo.releases.length > 0 ? this.getPoints(repo.releases[0].createdOn.toDate()) : 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); + 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); + + return checks.reduce((total: number, current: number) => total + current, 0) / checks.length; } public getPoints(date: Date): number { @@ -78,4 +82,20 @@ 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/projects/repository/repository.component.html b/web/src/app/projects/repository/repository.component.html index 979ed5025..235ae3750 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

@@ -304,7 +304,7 @@

no data -