зеркало из https://github.com/microsoft/ghcrawler.git
768 строки
30 KiB
JavaScript
768 строки
30 KiB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License.
|
|
|
|
const moment = require('moment');
|
|
const parse = require('parse-link-header');
|
|
const Request = require('./request');
|
|
const Q = require('q');
|
|
const qlimit = require('qlimit');
|
|
const TraversalPolicy = require('./traversalPolicy');
|
|
const URL = require('url');
|
|
const uuid = require('node-uuid');
|
|
|
|
class GitHubProcessor {
|
|
constructor(store) {
|
|
this.store = store;
|
|
this.version = 8;
|
|
}
|
|
|
|
process(request) {
|
|
const handler = this._getHandler(request);
|
|
if (!handler) {
|
|
request.markSkip('Skip', `No handler found for request type: ${request.type}`);
|
|
return request.document;
|
|
}
|
|
|
|
const oldVersion = request.document._metadata.version;
|
|
if (request.policy.shouldProcess(request, this.version)) {
|
|
request.processMode = 'process';
|
|
} else {
|
|
// We are not going to process but may still need to traverse the doc to get to referenced docs that
|
|
// do need proecessing. If so, mark the request for no saving (already have good content) and carry on.
|
|
// Otherwise, skip the doc altogether.
|
|
if (request.policy.shouldTraverse(request)) {
|
|
request.processMode = 'traverse';
|
|
request.markNoSave();
|
|
} else {
|
|
request.markSkip('Excluded', `Traversal policy excluded this resource`);
|
|
return request.document;
|
|
}
|
|
}
|
|
|
|
const result = handler.call(this, request);
|
|
if (result) {
|
|
result._metadata.version = this.version;
|
|
result._metadata.processedAt = moment.utc().toISOString();
|
|
if (result._metadata.version !== oldVersion) {
|
|
request.markSave();
|
|
}
|
|
}
|
|
if (!request.shouldSave()) {
|
|
request.outcome = request.outcome || 'Traversed';
|
|
}
|
|
return result;
|
|
}
|
|
|
|
collection(request) {
|
|
// if there are additional pages, queue them up to be processed. Note that these go
|
|
// on the 'soon' queue so they are loaded before they change much.
|
|
const linkHeader = (request.response && request.response.headers) ? request.response.headers.link : null;
|
|
if (linkHeader) {
|
|
const links = parse(linkHeader);
|
|
const requests = [];
|
|
for (let i = 2; i <= links.last.page; i++) {
|
|
const separator = request.url.includes('?') ? '&' : '?';
|
|
const url = request.url + `${separator}page=${i}&per_page=100`;
|
|
const newRequest = new Request(request.type, url, request.context);
|
|
// Carry this request's transitivity forward to the other pages.
|
|
newRequest.policy = request.policy;
|
|
requests.push(newRequest);
|
|
}
|
|
request.queueRequests(requests, 'soon');
|
|
}
|
|
|
|
// Process the rest of the request as a page.
|
|
return this.page(1, request);
|
|
}
|
|
|
|
page(page, request) {
|
|
const document = request.document;
|
|
const qualifier = request.context.qualifier;
|
|
request.linkResource('self', `${qualifier}:${request.type}:page:${page}`);
|
|
// If the context defines a relation, create a link in this page. This can be used to
|
|
// track that a page defines a relation between an entity and a set of entities. For example,
|
|
// a repo and its teams. The teams are not exclusively "part of" the repo, they are just related.
|
|
const relation = request.context.relation;
|
|
let elementType = request.context.elementType;
|
|
if (relation) {
|
|
this._processRelation(request, relation);
|
|
elementType = relation.type;
|
|
}
|
|
// Use the current request's policy as that is assumed to be precomputed for this page's elements. Don't bother queuing
|
|
// if we are just going for the pages themselves (e.g., [] map step).
|
|
const step = request.policy.getCurrentStep();
|
|
if (Array.isArray(step) && step.length === 0) {
|
|
return document;
|
|
}
|
|
// Queue up the page elements. Use the same policy as this request as the page itself is more of an implementation
|
|
// detail and should not be part of the user model of traversal.
|
|
document.elements.forEach(item => {
|
|
if (elementType) {
|
|
const elementQualifier = this.isRootType(elementType) ? 'urn:' : qualifier;
|
|
const newContext = { qualifier: elementQualifier, history: request.context.history };
|
|
request.queue(elementType, item.url, request.policy, newContext);
|
|
} else {
|
|
// TODO if there is no elementType on a collection then assume it is events. Need to fix this up and
|
|
// formalize the model of collections where the request carries the payload.
|
|
const baseUrl = request.url.split("?")[0];
|
|
const newContext = { history: request.context.history };
|
|
const newRequest = new Request(item.type, `${baseUrl}/${item.id}`, newContext);
|
|
newRequest.payload = { etag: 1, body: item };
|
|
newRequest.policy = request.policy;
|
|
request.queueRequests(newRequest);
|
|
}
|
|
});
|
|
return document;
|
|
}
|
|
|
|
org(request) {
|
|
const document = request.document;
|
|
request.addRootSelfLink();
|
|
request.linkSiblings('urn:orgs');
|
|
// TODO look at new API for membership differentiation
|
|
// * hooks
|
|
//
|
|
this._addRoot(request, 'user', 'user', document.url.replace('/orgs/', '/users/'), `urn:user:${document.id}`);
|
|
this._addCollection(request, 'repos', 'repo', null, `urn:user:${document.id}:repos`);
|
|
if (document.members_url) {
|
|
this._addRelation(request, 'members', 'user', document.members_url.replace('{/member}', ''), `${this._getQualifier(request)}:org_members`);
|
|
}
|
|
const url = `${document.url.replace('/users/', '/orgs/')}/teams`;
|
|
this._addRelation(request, 'teams', 'team', url, `${this._getQualifier(request)}:org_teams`);
|
|
|
|
return document;
|
|
}
|
|
|
|
user(request) {
|
|
// TODO links to consider
|
|
// * followers
|
|
// * following
|
|
// * starred
|
|
// * subscriptions
|
|
// * orgs???
|
|
const document = request.document;
|
|
request.addRootSelfLink();
|
|
request.linkSiblings('urn:users');
|
|
|
|
this._addCollection(request, 'repos', "repo");
|
|
return document;
|
|
}
|
|
|
|
// reactions
|
|
// commit comment, issue, issue comment, pull request review comment
|
|
|
|
repo(request) {
|
|
// TODO links to consider
|
|
// * forks *** not yet
|
|
// * deployments
|
|
// * labels
|
|
// * languages
|
|
// * milestone
|
|
// * hooks
|
|
// * releases
|
|
// * invitations
|
|
// * stargazers
|
|
const document = request.document;
|
|
if (document.private) {
|
|
request.context.repoType = 'private';
|
|
}
|
|
request.addRootSelfLink();
|
|
request.linkSiblings(`urn:user:${document.owner.id}:repos`);
|
|
// Pull requests are issues but with some additional bits. Here just link to the collection and let the harvesting
|
|
// of issues pull in (pun!) the pull request view of the world.
|
|
request.linkCollection('pull_requests', `${document._metadata.links.self.href}:pull_requests`);
|
|
|
|
if (document.organization) {
|
|
this._addRoot(request, 'owner', 'user');
|
|
this._addRoot(request, 'organization', 'org');
|
|
} else {
|
|
this._addRoot(request, 'owner', 'user');
|
|
}
|
|
|
|
this._addRelation(request, 'teams', 'team');
|
|
// this._addRelation(request, 'collaborators', 'user', document.collaborators_url.replace('{/collaborator}', ''));
|
|
this._addRelation(request, 'collaborators', 'user', document.collaborators_url.replace('{/collaborator}', '?affiliation=outside'));
|
|
this._addRelation(request, 'contributors', 'user');
|
|
if (document.subscribers_count) {
|
|
this._addRelation(request, 'subscribers', 'user');
|
|
}
|
|
if (document.stargazers_count) {
|
|
this._addRelation(request, 'stargazers', 'user');
|
|
}
|
|
this._addCollection(request, 'issues', 'issue', document.issues_url.replace('{/number}', '?state=all'));
|
|
this._addCollection(request, 'commits', 'commit', document.commits_url.replace('{/sha}', ''));
|
|
this._addCollection(request, 'events', null);
|
|
|
|
return document;
|
|
}
|
|
|
|
commit(request) {
|
|
const document = request.document;
|
|
const context = request.context;
|
|
request.addSelfLink('sha');
|
|
request.linkSiblings(`${context.qualifier}:commits`);
|
|
|
|
// Most often there actually are no comments. Get the comments if we think there will be some and this resource is being processed (vs. traversed).
|
|
// Note that if we are doing event processing, new comments will be added to the list dynamically so the only reason we need to refetch the
|
|
// comment list in general is if we think we missed some events.
|
|
const commentsUrn = `${document._metadata.links.self.href}:commit_comments`;
|
|
if (document.comments_url && (document.commit.comment_count > 0 && request.processMode === 'process')) {
|
|
this._addCollection(request, 'commit_comments', 'commit_comment', document.comments_url, commentsUrn);
|
|
} else {
|
|
// even if there are no comments to process, add a link to the comment collection for future use
|
|
request.linkCollection('commit_comments', commentsUrn);
|
|
}
|
|
this._addRoot(request, 'repo', 'repo', document.url.replace(/\/commits\/.*/, ''), `${context.qualifier}`);
|
|
// TODO some commits have author and committer properties, others have email info in a "commit" property
|
|
// For the former, this code works. For the latter, consider queuing an email lookup and storing a
|
|
// email key here for the author/committer.
|
|
this._addRoot(request, 'author', 'user');
|
|
this._addRoot(request, 'committer', 'user');
|
|
|
|
if (document.files) {
|
|
document.files.forEach(file => {
|
|
delete file.patch;
|
|
});
|
|
}
|
|
return document;
|
|
}
|
|
|
|
commit_comment(request) {
|
|
// TODO links to consider
|
|
// * reactions -- get this by using the following Accept header: application/vnd.github.squirrel-girl-preview
|
|
const document = request.document;
|
|
const context = request.context;
|
|
request.addSelfLink();
|
|
request.linkResource('commit', context.qualifier);
|
|
request.linkSiblings(`${context.qualifier}:commit_comments`);
|
|
|
|
this._addRoot(request, 'user', 'user');
|
|
return document;
|
|
}
|
|
|
|
pull_request(request) {
|
|
const document = request.document;
|
|
const context = request.context;
|
|
request.addSelfLink();
|
|
request.linkResource('repo', `urn:repo:${document.base.repo.id}`);
|
|
request.linkSiblings(`${context.qualifier}:pull_requests`);
|
|
|
|
this._addRoot(request, 'user', 'user');
|
|
this._addRoot(request, 'merged_by', 'user');
|
|
this._addRoot(request, 'assignee', 'user');
|
|
if (document.head.repo) {
|
|
this._addRoot(request, 'head', 'repo', document.head.repo.url, `urn:repo:${document.head.repo.id}`);
|
|
}
|
|
this._addRoot(request, 'base', 'repo', document.base.repo.url, `urn:repo:${document.base.repo.id}`);
|
|
|
|
this._addCollection(request, 'reviews', 'review', `${document._links.self.href}/reviews`);
|
|
if (document._links.review_comments && document.comments) {
|
|
this._addCollection(request, 'review_comments', 'review_comment', document._links.review_comments.href);
|
|
}
|
|
if (document._links.statuses) {
|
|
const sha = document._links.statuses.href.split('/').slice(-1)[0];
|
|
this._addCollection(request, 'statuses', 'status', document._links.statuses.href, `${context.qualifier}:commit:${sha}:statuses`);
|
|
// this._addResource(request, 'status', 'status', null, statusUrl, `${context.qualifier}:commits:${sha}:status`);
|
|
}
|
|
|
|
if (document._links.commits && document.commits) {
|
|
// TODO. look at PR commit to see if it should be shared with the repo commit (use a relation if shared)
|
|
this._addCollection(request, 'commits', 'commit', document._links.commits.href);
|
|
}
|
|
|
|
// link and queue the related issue. Getting the issue will bring in the comments for this PR
|
|
if (document._links.issue) {
|
|
// link to the relate issue and its comments.
|
|
request.linkResource('issue', `${context.qualifier}:issue:${document.id}`);
|
|
request.linkCollection('issue_comments', `${context.qualifier}:issue:${document.id}:issue_comments`);
|
|
}
|
|
return document;
|
|
}
|
|
|
|
review(request) {
|
|
const document = request.document;
|
|
const context = request.context;
|
|
request.addSelfLink();
|
|
request.linkResource('pull_request', context.qualifier);
|
|
request.linkSiblings(`${context.qualifier}:reviews`);
|
|
|
|
this._addRoot(request, 'user', 'user');
|
|
return document;
|
|
}
|
|
|
|
review_comment(request) {
|
|
// TODO links to consider
|
|
// * reactions -- get this by using the following Accept header: application/vnd.github.squirrel-girl-preview
|
|
const document = request.document;
|
|
const context = request.context;
|
|
request.addSelfLink();
|
|
request.linkResource('pull_request', context.qualifier);
|
|
request.linkSiblings(`${context.qualifier}:review_comments`);
|
|
|
|
this._addRoot(request, 'user', 'user');
|
|
return document;
|
|
}
|
|
|
|
issue(request) {
|
|
// TODO links to consider
|
|
// * milestone
|
|
// * reactions -- get this by using the following Accept header: application/vnd.github.squirrel-girl-preview
|
|
const document = request.document;
|
|
const context = request.context;
|
|
request.addSelfLink();
|
|
request.linkSiblings(`${context.qualifier}:issues`);
|
|
|
|
const assignees = document.assignees.map(assignee => { return `urn:user:${assignee.id}`; });
|
|
if (assignees.length > 0) {
|
|
request.linkResource('assignees', assignees);
|
|
}
|
|
|
|
this._addRoot(request, 'user', 'user');
|
|
this._addRoot(request, 'repo', 'repo', document.repository_url, context.qualifier);
|
|
this._addRoot(request, 'assignee', 'user');
|
|
this._addRoot(request, 'closed_by', 'user');
|
|
if (document.comments_url && document.comments) {
|
|
this._addCollection(request, 'issue_comments', 'issue_comment', document.comments_url);
|
|
}
|
|
if (document.pull_request) {
|
|
this._addResource(request, 'pull_request', 'pull_request', document.id, document.pull_request.url, null, context.qualifier);
|
|
}
|
|
// Add hrefs for all the labels but do not queue up the labels themselves. Chances of there being more than a
|
|
// page of labels on an issue are near 0. So we don't need a separate collection of label pages so inline that here.
|
|
if (document.labels) {
|
|
const labelUrns = document.labels.map(label => `${context.qualifier}:label:${label.id}`);
|
|
request.linkResource('labels', labelUrns);
|
|
}
|
|
return document;
|
|
}
|
|
|
|
issue_comment(request) {
|
|
// TODO links to consider
|
|
// * reactions -- get this by using the following Accept header: application/vnd.github.squirrel-girl-preview
|
|
const document = request.document;
|
|
const context = request.context;
|
|
request.addSelfLink();
|
|
request.linkResource('issue', context.qualifier);
|
|
request.linkSiblings(`${context.qualifier}:issue_comments`);
|
|
|
|
this._addRoot(request, 'user', 'user');
|
|
return document;
|
|
}
|
|
|
|
team(request) {
|
|
const document = request.document;
|
|
request.addRootSelfLink();
|
|
request.linkSiblings(`urn:org:${document.organization.id}:teams`);
|
|
|
|
this._addRoot(request, 'organization', 'org');
|
|
this._addRelation(request, 'members', 'user', document.members_url.replace('{/member}', ''), `${this._getQualifier(request)}:team_members`);
|
|
this._addRelation(request, 'repos', 'repo', document.repositories_url);
|
|
return document;
|
|
}
|
|
|
|
deployment(request) {
|
|
const document = request.document;
|
|
const context = request.context;
|
|
request.addSelfLink();
|
|
request.linkSiblings(`${context.qualifier}:deployments`);
|
|
request.linkResource('commit', `${context.qualifier}:commit:${document.sha}`);
|
|
this._addRoot(request, 'creator', 'user');
|
|
return document;
|
|
}
|
|
|
|
traffic(request) {
|
|
const document = request.document;
|
|
request.queueChild('referrers', `${request.url}/traffic/popular/referrers`, `urn:repo:${document.id}`);
|
|
request.queueChild('paths', `${request.url}/traffic/popular/paths`, `urn:repo:${document.id}`);
|
|
request.queueChild('views', `${request.url}/traffic/views`, `urn:repo:${document.id}`);
|
|
request.queueChild('clones', `${request.url}/traffic/clones`, `urn:repo:${document.id}`);
|
|
return null;
|
|
}
|
|
|
|
referrers(request) {
|
|
return this._trafficChild(request);
|
|
}
|
|
|
|
views(request) {
|
|
return this._trafficChild(request);
|
|
}
|
|
|
|
clones(request) {
|
|
return this._trafficChild(request);
|
|
}
|
|
|
|
paths(request) {
|
|
return this._trafficChild(request);
|
|
}
|
|
|
|
_trafficChild(request) {
|
|
request.document.id = moment.utc(request.document._metadata.fetchedAt).format('YYYY_MM_DD');
|
|
request.addSelfLink();
|
|
request.linkResource('repo', request.context.qualifier);
|
|
return request.document;
|
|
}
|
|
|
|
// =============== Event Processors ============
|
|
|
|
// The events in a repo or org have changed. Go get the latest events, discover any new
|
|
// ones and queue them for processing.
|
|
update_events(request) {
|
|
const events = request.document.elements;
|
|
// create a promise and track it right away so that this request does not finish
|
|
// processing and exit before the event discovery and queueing completes.
|
|
const processingPromise = this._findNew(events).then(newEvents => {
|
|
// build a new request for each discovered event and include the event itself in the request
|
|
const newRequests = newEvents.map(event => {
|
|
// make sure the URL here is unique. Even though it will not actually be fetched (the content
|
|
// is in the payload), it will need to be unique for the queue tagging/optimization
|
|
// Events are immutable (and we can't fetch them later) so set the etag to a constant
|
|
const baseUrl = request.url.split("?")[0];
|
|
const newRequest = new Request(event.type, `${baseUrl}/${event.id}`);
|
|
newRequest.policy = TraversalPolicy.events();
|
|
newRequest.payload = { etag: 1, body: event };
|
|
return newRequest;
|
|
});
|
|
request.queueRequests(newRequests);
|
|
});
|
|
request.track(processingPromise);
|
|
return null;
|
|
}
|
|
|
|
_findNew(events) {
|
|
const self = this;
|
|
return Q.all(events.map(qlimit(10)(event => {
|
|
const url = event.repo ? `${event.repo.url}/events/${event.id}` : `${event.org.url}/events/${event.id}`;
|
|
return self.store.etag('event', url).then(etag => {
|
|
return etag ? null : event;
|
|
});
|
|
}))).then(events => {
|
|
return events.filter(event => event);
|
|
});
|
|
}
|
|
|
|
CommitCommentEvent(request) {
|
|
let [document, repo, payload] = this._addEventBasics(request);
|
|
let url = `${document.repo.url}/commits/${payload.comment.commit_id}`;
|
|
let urn = `urn:repo:${repo}:commit:${payload.comment.commit_id}`;
|
|
this._addResource(request, 'commit', 'commit', null, url, urn, `urn:repo:${repo}`);
|
|
|
|
url = payload.comment.url;
|
|
const commentUrn = `${urn}:commit_comment:${payload.comment.id}`;
|
|
this._addResource(request, 'commit_comment', 'commit_comment', null, url, commentUrn, urn);
|
|
return document;
|
|
}
|
|
|
|
CreateEvent(request) {
|
|
let [document] = this._addEventBasics(request);
|
|
request.linkResource('repository', document._metadata.links.repo.href);
|
|
return document;
|
|
}
|
|
|
|
DeleteEvent(request) {
|
|
// TODO do something for interesting deletions e.g., where ref-type === 'repository'
|
|
let [document] = this._addEventBasics(request);
|
|
return document;
|
|
}
|
|
|
|
DeploymentEvent(request) {
|
|
let [, repo] = this._addEventBasics(request);
|
|
return this._addEventResourceContains(request, repo, 'deployment');
|
|
}
|
|
|
|
DeploymentStatusEvent(request) {
|
|
let [, repo] = this._addEventBasics(request);
|
|
// TODO figure out how to do this more deeply nested structure
|
|
// request.linkResource('deployment_status', `urn:repo:${repo}:deployment:${payload.deployment.id}:status:${payload.deployment_status.id}`);
|
|
return this._addEventResourceContains(request, repo, 'deployment');
|
|
}
|
|
|
|
ForkEvent(request) {
|
|
// TODO figure out what else to do
|
|
let [document] = this._addEventBasics(request);
|
|
return document;
|
|
}
|
|
|
|
GollumEvent(request) {
|
|
let [document] = this._addEventBasics(request);
|
|
return document;
|
|
}
|
|
|
|
IssueCommentEvent(request) {
|
|
let [, repo, payload] = this._addEventBasics(request);
|
|
const qualifier = `urn:repo:${repo}:issue:${payload.issue.id}`;
|
|
this._addEventResourceContains(request, repo, 'comment', 'issue_comment', qualifier);
|
|
return this._addEventResourceContains(request, repo, 'issue');
|
|
}
|
|
|
|
IssuesEvent(request) {
|
|
let [document, repo, payload] = this._addEventBasics(request);
|
|
this._addEventResourceContains(request, repo, 'issue');
|
|
if (payload.assignee) {
|
|
this._addEventResourceReference(request, null, 'assignee', 'user');
|
|
}
|
|
if (payload.label) {
|
|
this._addEventResourceContains(request, repo, 'label');
|
|
}
|
|
return document;
|
|
}
|
|
|
|
LabelEvent(request) {
|
|
let [document] = this._addEventBasics(request);
|
|
return document;
|
|
}
|
|
|
|
MemberEvent(request) {
|
|
this._addEventBasics(request);
|
|
return this._addEventResourceReference(request, null, 'member', 'user');
|
|
}
|
|
|
|
MembershipEvent(request) {
|
|
this._addEventBasics(request);
|
|
this._addEventResourceReference(request, null, 'member', 'user');
|
|
return this._addEventResourceReference(request, null, 'team');
|
|
}
|
|
|
|
MilestoneEvent(request) {
|
|
// TODO complete implementation and add Milestone handler
|
|
// let [, repo] = this._addEventBasics(request);
|
|
// return this._addEventResource(request, repo, 'milestone');
|
|
let [document] = this._addEventBasics(request);
|
|
return document;
|
|
}
|
|
|
|
PageBuildEvent(request) {
|
|
// TODO complete implementation and add page_build handler
|
|
// let [document, repo, payload] = this._addEventBasics(request);
|
|
// // This does not fit in to the standard model. In particular, the payload is less structured
|
|
// request.linkResource('page_build', `urn:repo:${repo}:page_builds:${payload.id}`);
|
|
// request.queue('page_build', payload.build.url);
|
|
// return document;
|
|
let [document] = this._addEventBasics(request);
|
|
return document;
|
|
}
|
|
|
|
PublicEvent(request) {
|
|
let [document] = this._addEventBasics(request);
|
|
return document;
|
|
}
|
|
|
|
PullRequestEvent(request) {
|
|
let [, repo] = this._addEventBasics(request);
|
|
return this._addEventResourceContains(request, repo, 'pull_request');
|
|
}
|
|
|
|
PullRequestReviewEvent(request) {
|
|
let [, repo] = this._addEventBasics(request);
|
|
return this._addEventResourceContains(request, repo, 'pull_request');
|
|
}
|
|
|
|
PullRequestReviewCommentEvent(request) {
|
|
let [, repo, payload] = this._addEventBasics(request);
|
|
const qualifier = `urn:repo:${repo}:pull_request:${payload.pull_request.id}`;
|
|
this._addEventResourceContains(request, repo, 'comment', 'review_comment', qualifier);
|
|
return this._addEventResourceContains(request, repo, 'pull_request');
|
|
}
|
|
|
|
PushEvent(request) {
|
|
let [document] = this._addEventBasics(request);
|
|
// TODO figure out what to do with the commits
|
|
return document;
|
|
}
|
|
|
|
ReleaseEvent(request) {
|
|
// TODO complet implementation and add a Release handler
|
|
// let [, repo] = this._addEventBasics(request);
|
|
// return this._addEventResource(request, repo, 'release');
|
|
let [document] = this._addEventBasics(request);
|
|
return document;
|
|
}
|
|
|
|
RepositoryEvent(request) {
|
|
this._addEventBasics(request);
|
|
return this._addEventResourceReference(request, null, 'repository', 'repo');
|
|
}
|
|
|
|
StatusEvent(request) {
|
|
// TODO complete this by adding a status handler and linking/queuing the status here.
|
|
let [document, , payload] = this._addEventBasics(request);
|
|
request.linkResource('commit', `${document._metadata.links.repo.href}:commit:${payload.sha}`);
|
|
return document;
|
|
}
|
|
|
|
TeamEvent(request) {
|
|
let [, , payload] = this._addEventBasics(request, `urn:team:${request.document.payload.team.id}`);
|
|
if (payload.repository) {
|
|
this._addEventResourceReference(request, null, 'repository', 'repo');
|
|
}
|
|
return this._addEventResourceReference(request, null, 'team');
|
|
}
|
|
|
|
TeamAddEvent(request) {
|
|
this._addEventBasics(request, `urn:team:${request.document.payload.team.id}`);
|
|
this._addEventResourceReference(request, null, 'repository', 'repo');
|
|
return this._addEventResourceReference(request, null, 'team');
|
|
}
|
|
|
|
WatchEvent(request) {
|
|
let [document] = this._addEventBasics(request);
|
|
request.linkResource('repository', document._metadata.links.repo.href);
|
|
return document;
|
|
}
|
|
|
|
// ================ HELPERS ========================
|
|
|
|
isCollectionType(request) {
|
|
const collections = new Set([
|
|
'collaborators', 'commit_comments', 'commits', 'contributors', 'events', 'issues', 'issue_comments', 'members', 'orgs', 'repos', 'reviews', 'review_comments', 'subscribers', 'statuses', 'teams'
|
|
]);
|
|
return collections.has(request.type);
|
|
}
|
|
|
|
isRootType(type) {
|
|
const roots = new Set(['orgs', 'org', 'repos', 'repo', 'teams', 'team', 'user', 'members']);
|
|
return roots.has(type);
|
|
}
|
|
|
|
_getQualifier(request) {
|
|
return this.isRootType(request.type) ? request.getRootQualifier() : request.getChildQualifier();
|
|
}
|
|
|
|
_getHandler(request, type = request.type) {
|
|
const parsed = URL.parse(request.url, true);
|
|
const page = parsed.query.page;
|
|
// TODO / check is a temporary measure to work around a queuing bug. Remove once queue is cleared
|
|
if (page && !parsed.query.per_page.includes('/')) {
|
|
return this.page.bind(this, page);
|
|
}
|
|
if (this.isCollectionType(request)) {
|
|
return this.collection;
|
|
}
|
|
return (this[type]);
|
|
}
|
|
|
|
_addEventBasics(request, qualifier = null) {
|
|
// TODO handle org event cases (no repo etc)
|
|
const document = request.document;
|
|
const repo = document.repo ? document.repo.id : null;
|
|
qualifier = qualifier || (repo ? `urn:repo:${repo}` : `urn:org:${document.org.id}`);
|
|
request.linkResource('self', `${qualifier}:${request.type}:${document.id}`);
|
|
request.linkSiblings(`${qualifier}:${request.type}s`);
|
|
|
|
// TODO understand if the actor is typically the same as the creator or pusher in the payload
|
|
this._addRoot(request, 'actor', 'user');
|
|
if (repo) {
|
|
this._addRoot(request, 'repo', 'repo');
|
|
}
|
|
this._addRoot(request, 'org', 'org');
|
|
|
|
return [document, repo, document.payload];
|
|
}
|
|
|
|
_addEventResourceReference(request, repo, name, type = name, qualifier = null) {
|
|
return this._addEventResource(request, repo, name, type, qualifier);
|
|
}
|
|
|
|
_addEventResourceContains(request, repo, name, type = name, qualifier = null) {
|
|
return this._addEventResource(request, repo, name, type, qualifier);
|
|
}
|
|
|
|
_addEventResource(request, repo, name, type = name, qualifier = null) {
|
|
const payload = request.document.payload;
|
|
const target = payload[name];
|
|
if (!target) {
|
|
throw new Error(`payload[${name}] missing in ${request.toString()}`);
|
|
}
|
|
// if the repo is given then use it. Otherwise, assume the type is a root and construct a urn
|
|
qualifier = qualifier || (repo ? `urn:repo:${repo}` : 'urn:');
|
|
const separator = qualifier.endsWith(':') ? '' : ':';
|
|
request.linkResource(name, `${qualifier}${separator}${type}:${payload[name].id}`);
|
|
const newRequest = new Request(type, payload[name].url, { qualifier: qualifier });
|
|
newRequest.policy = request.getNextPolicy(name);
|
|
if (newRequest.policy) {
|
|
request.queueRequests(newRequest);
|
|
}
|
|
return request.document;
|
|
}
|
|
|
|
_addResource(request, name, type, id, url = null, urn = null, qualifier = null) {
|
|
qualifier = qualifier || this._getQualifier(request);
|
|
urn = urn || `${qualifier}:${name}:${id}`;
|
|
url = url || request.document[`${name}_url`];
|
|
|
|
request.linkResource(name, urn);
|
|
const newPolicy = request.getNextPolicy(name);
|
|
request.queue(type, url, newPolicy, { qualifier: qualifier });
|
|
}
|
|
|
|
_addCollection(request, name, type, url = null, urn = null) {
|
|
const qualifier = this._getQualifier(request);
|
|
urn = urn || `${qualifier}:${name}`;
|
|
url = url || request.document[`${name}_url`];
|
|
|
|
request.linkCollection(name, urn);
|
|
const newPolicy = request.getNextPolicy(name);
|
|
const newContext = { qualifier: request.document._metadata.links.self.href, elementType: type };
|
|
request.queue(name, url, newPolicy, newContext);
|
|
}
|
|
|
|
_addRoot(request, name, type, url = null, urn = null) {
|
|
const element = request.document[name];
|
|
// If there is no element then we must have both the url and urn as otherwise we don't know how to compute them
|
|
if (!element && !(urn && url)) {
|
|
return;
|
|
}
|
|
|
|
urn = urn || `urn:${type}:${element.id}`;
|
|
url = url || element.url;
|
|
request.linkResource(name, urn);
|
|
const newPolicy = request.getNextPolicy(name);
|
|
request.queue(type, url, newPolicy);
|
|
}
|
|
|
|
/**
|
|
* Relate this document to a collection of other documents of the given type. For example,
|
|
* a repo to its collaborators which are users.
|
|
*
|
|
* This creates a relation between the current document being processed and the named
|
|
* target resource of the given type. This results in a siblings link with the given name
|
|
* and urn being added to this document and a relation request queued for the given url.
|
|
* The document produced by processing that url will have matching siblings links (called 'siblings')
|
|
*
|
|
* Relations are always references.
|
|
*/
|
|
_addRelation(request, name, type, url = null, urn = null) {
|
|
const qualifier = this._getQualifier(request);
|
|
urn = urn || `${qualifier}:${name}`;
|
|
url = url || request.document[`${name}_url`];
|
|
|
|
// For relations we want to have a guid that uniquely identifies all of the pages for this
|
|
// particular state of the relation. Add the guid here for the relation link and brand
|
|
// each page with it in its siblings link to get a coherent state.
|
|
const guid = uuid.v4();
|
|
request.linkRelation(name, `${urn}:pages:${guid}`);
|
|
// Also add an 'knownPages' link to help deal with page clean up.
|
|
// request.linkCollection('knownPages', `${urn}:pages`);
|
|
const context = { qualifier: qualifier, relation: { origin: request.type, qualifier: urn, type: type, guid: guid } };
|
|
const newPolicy = request.getNextPolicy(name);
|
|
request.queue(name, url, newPolicy, context, false);
|
|
}
|
|
|
|
/**
|
|
* Process a page resource for a relation. Add links identifying this page as part of a
|
|
* relation with the given info and enumerate links for the resources referenced from this page.
|
|
* Note that currently relations can only point to root resources.
|
|
*/
|
|
_processRelation(request, relation) {
|
|
const document = request.document;
|
|
const qualifier = request.context.qualifier;
|
|
request.linkResource('origin', `${qualifier}`);
|
|
request.linkResource(relation.origin, `${qualifier}`);
|
|
request.linkSiblings(`${relation.qualifier}:pages`);
|
|
request.linkCollection('unique', `${relation.qualifier}:pages:${relation.guid}`);
|
|
const urns = document.elements.map(element => `urn:${relation.type}:${element.id}`);
|
|
request.linkResource('resources', urns);
|
|
return document;
|
|
}
|
|
}
|
|
|
|
module.exports = GitHubProcessor; |