/
team.ts
581 lines (518 loc) Β· 18.6 KB
/
team.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
//
// Copyright (c) Microsoft.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
import * as common from './common';
import { wrapError } from '../utils';
import _ from 'lodash';
import { Organization, OrganizationMembershipState } from './organization';
import { Operations } from './operations';
import { ICacheOptions, ICacheOptionsPageLimiter, IPagedCacheOptions, IGetAuthorizationHeader, IPurposefulGetAuthorizationHeader, IPagedCrossOrganizationCacheOptions } from '../transitional';
import { TeamMember } from './teamMember';
import { TeamRepositoryPermission } from './teamRepositoryPermission';
import { IApprovalProvider } from '../entities/teamJoinApproval/approvalProvider';
import { TeamJoinApprovalEntity } from '../entities/teamJoinApproval/teamJoinApproval';
import { Repository } from './repository';
import { AppPurpose } from '../github';
const teamPrimaryProperties = [
'id',
'name',
'slug',
'description',
'members_count',
'repos_count',
'created_at',
'updated_at',
];
const teamSecondaryProperties = [
'privacy',
'permission',
'organization',
'url',
'members_url',
'repositories_url',
];
export enum GitHubRepositoryType {
Sources = 'sources',
}
export interface ICheckRepositoryPermissionOptions extends ICacheOptions {
organizationName?: string;
}
export interface IGetTeamRepositoriesOptions extends ICacheOptionsPageLimiter {
type?: GitHubRepositoryType;
}
export interface ITeamMembershipRoleState {
role?: GitHubTeamRole;
state?: OrganizationMembershipState;
}
export interface IIsMemberOptions extends ICacheOptions {
role?: GitHubTeamRole;
}
export interface IGetMembersOptions extends ICacheOptionsPageLimiter {
role?: GitHubTeamRole;
}
export enum GitHubTeamRole {
Member = 'member',
Maintainer = 'maintainer',
}
export interface ICrossOrganizationTeamMembership extends IPagedCrossOrganizationCacheOptions {
role?: GitHubTeamRole;
}
export interface ITeamMembershipOptions {
role?: GitHubTeamRole;
}
export interface IUpdateTeamMembershipOptions extends ICacheOptions {
role?: GitHubTeamRole;
}
interface IGetMembersParameters {
team_slug: string;
org: string;
per_page: number;
role?: string;
pageLimit?: any;
}
interface IGetRepositoriesParameters {
org: string;
team_slug: string;
per_page: number;
pageLimit?: any;
}
export class Team {
public static PrimaryProperties = teamPrimaryProperties;
private _organization: Organization;
private _operations: Operations;
private _getAuthorizationHeader: IPurposefulGetAuthorizationHeader;
private _id: number;
private _slug?: string;
private _name?: string;
private _created_at?: any;
private _updated_at?: any;
private _description: string;
private _repos_count: any;
private _members_count: any;
private _detailsEntity?: any;
get id(): number {
return this._id;
}
get name(): string {
return this._name;
}
get slug(): string {
return this._slug;
}
get description(): string {
return this._description;
}
get repos_count(): any {
return this._repos_count;
}
get members_count(): any {
return this._members_count;
}
get created_at(): any {
return this._created_at;
}
get updated_at(): any {
return this._updated_at;
}
get organization(): Organization {
return this._organization;
}
constructor(organization: Organization, entity, getAuthorizationHeader: IPurposefulGetAuthorizationHeader, operations: Operations) {
if (!entity || !entity.id) {
throw new Error('Team instantiation requires an incoming entity, or minimum-set entity containing an id property.');
}
if (typeof(entity.id) !== 'number') {
throw new Error('Team constructor entity.id must be a Number');
}
this._organization = organization;
// TODO: remove assignKnownFieldsPrefixed concept, use newer field definitions instead?
common.assignKnownFieldsPrefixed(this, entity, 'team', teamPrimaryProperties, teamSecondaryProperties);
this._getAuthorizationHeader = getAuthorizationHeader;
this._operations = operations;
}
get baseUrl() {
if (this._organization && (this._slug || this._name)) {
return this._organization.baseUrl + 'teams/' + (this._slug || this._name) + '/';
}
const operations = this._operations;
return operations.baseUrl + 'teams?q=' + this._id;
}
get absoluteBaseUrl(): string {
return `${this._organization.absoluteBaseUrl}teams/${this._slug || this._name}/`;
}
get nativeUrl() {
if (this._organization && this._slug) {
return this._organization.nativeManagementUrl + `teams/${this._slug}/`;
}
// Less ideal fallback
return this._organization.nativeManagementUrl + `teams/`;
}
async ensureName(): Promise<void> {
if (this._name && this._slug) {
return;
}
return await this.getDetails();
}
async isDeleted(options?: ICacheOptions): Promise<boolean> {
try {
await this.getDetails(options);
} catch (maybeDeletedError) {
if (maybeDeletedError && maybeDeletedError.status && maybeDeletedError.status === 404) {
return true;
}
}
return false;
}
async getDetails(options?: ICacheOptions): Promise<any> {
options = options || {};
const operations = this._operations;
const cacheOptions = {
maxAgeSeconds: options.maxAgeSeconds || operations.defaults.orgTeamDetailsStaleSeconds,
backgroundRefresh: false,
};
if (options.backgroundRefresh !== undefined) {
cacheOptions.backgroundRefresh = options.backgroundRefresh;
}
const id = this._id;
if (!id) {
throw new Error('team.id required to retrieve team details');
}
// If the details already have been loaded, move along without refreshing
// CONSIDER: Either a time-based cache or ability to override the local cached behavior
if (this._detailsEntity) {
return this._detailsEntity;
}
const parameters = {
org_id: this.organization.id,
team_id: id,
};
try {
const entity = await operations.github.request(
this.authorize(AppPurpose.Data),
'GET /organizations/:org_id/team/:team_id', parameters, cacheOptions);
this._detailsEntity = entity;
// TODO: move beyond setting with this approach
common.assignKnownFieldsPrefixed(this, entity, 'team', teamPrimaryProperties, teamSecondaryProperties);
return entity;
} catch (error) {
if (error.status && error.status === 404) {
error = new Error(`The GitHub team ID ${id} could not be found`);
error.status = 404;
throw error;
}
throw wrapError(error, `Could not get details about team ID ${this._id} in the GitHub organization ${this.organization.name}: ${error.message}`);
}
}
get isBroadAccessTeam(): boolean {
const teams = this._organization.broadAccessTeams;
// TODO: validating typing here - number or int?
if (typeof(this._id) !== 'number') {
throw new Error('Team.id must be a number');
}
const res = teams.indexOf(this._id);
return res >= 0;
}
get isSystemTeam(): boolean {
const systemTeams = this._organization.systemTeamIds;
const res = systemTeams.indexOf(this._id);
return res >= 0;
}
delete(): Promise<void> {
const operations = this._operations;
const github = operations.github;
const parameters = {
org_id: this.organization.id,
team_id: this._id,
};
// alternate of teams.deleteInOrg
return github.requestAsPost(this.authorize(AppPurpose.Operations), 'DELETE /organizations/:org_id/team/:team_id', parameters);
}
edit(patch: unknown): Promise<void> {
const operations = this._operations;
const github = operations.github;
const parameters = {
org_id: this.organization.id,
team_id: this._id,
};
Object.assign({}, patch, parameters);
// alternate of teams.editInOrg
return github.requestAsPost(this.authorize(AppPurpose.Operations), 'PATCH /organizations/:org_id/team/:team_id', parameters);
}
removeMembership(username: string): Promise<void> {
const operations = this._operations;
const github = operations.github;
const parameters = {
org_id: this.organization.id,
team_id: this._id,
username: username,
};
return github.requestAsPost(this.authorize(AppPurpose.Operations), 'DELETE /organizations/:org_id/team/:team_id/memberships/:username', parameters);
}
async addMembership(username: string, options?: IUpdateTeamMembershipOptions): Promise<any> {
const operations = this._operations;
const github = operations.github;
options = options || {};
const role = options.role || GitHubTeamRole.Member;
if (!this.slug) {
await this.getDetails();
}
const parameters = {
org: this.organization.name,
team_slug: this.slug,
username,
role,
};
const ok = await github.post(this.authorize(AppPurpose.CustomerFacing), 'teams.addOrUpdateMembershipInOrg', parameters);
return ok;
}
addMaintainer(username: string): Promise<void> {
return this.addMembership(username, { role: GitHubTeamRole.Maintainer });
}
async getMembership(username: string, options: ICacheOptions): Promise<ITeamMembershipRoleState | boolean> {
const operations = this._operations;
options = options || {};
if (!options.maxAgeSeconds) {
options.maxAgeSeconds = operations.defaults.orgMembershipDirectStaleSeconds;
}
// If a background refresh setting is not present, perform a live
// lookup with this call. This is the opposite of most of the library's
// general behavior.
if (options.backgroundRefresh === undefined) {
options.backgroundRefresh = false;
}
const parameters = {
org_id: this.organization.id,
team_id: this._id,
username,
};
try {
const result = await operations.github.request(
this.authorize(AppPurpose.CustomerFacing),
'GET /organizations/:org_id/team/:team_id/memberships/:username',
parameters,
options);
return result;
} catch (error) {
if (error.status == /* loose */ 404) {
return false;
}
let reason = error.message;
if (error.status) {
reason += ' ' + error.status;
}
const wrappedError = wrapError(error, `Trouble retrieving the membership for ${username} in team ${this._id}. ${reason}`);
if (error.status) {
wrappedError['status'] = error.status;
}
throw wrappedError;
}
}
async getMembershipEfficiently(username: string, options?: IIsMemberOptions): Promise<ITeamMembershipRoleState | boolean> {
// Hybrid calls are used to check for membership. Since there is
// often a relatively fresh cache available of all of the members
// of a team, that data source is used first to avoid a unique
// GitHub API call.
const operations = this._operations;
// A background cache is used that is slightly more aggressive
// than the standard org members list to at least frontload a
// refresh of the data.
options = options || {};
if (!options.maxAgeSeconds) {
options.maxAgeSeconds = operations.defaults.orgMembershipStaleSeconds;
}
const isMaintainer = await this.isMaintainer(username, options);
if (isMaintainer) {
return {
role: GitHubTeamRole.Maintainer,
state: OrganizationMembershipState.Active,
};
}
const isMember = await this.isMember(username);
if (isMember) {
return {
role: GitHubTeamRole.Member,
state: OrganizationMembershipState.Active,
};
}
// Fallback to the standard membership lookup
const membershipOptions = {
maxAgeSeconds: operations.defaults.orgMembershipDirectStaleSeconds,
};
const result = await this.getMembership(username, membershipOptions);
if (result === false || (result as ITeamMembershipRoleState).role) {
return false;
}
return result;
}
async isMaintainer(username: string, options?: ICacheOptions): Promise<boolean> {
const isOptions: IIsMemberOptions = Object.assign({}, options);
isOptions.role = GitHubTeamRole.Maintainer;
const maintainer = await this.isMember(username, isOptions) as GitHubTeamRole;
return maintainer === GitHubTeamRole.Maintainer ? true : false;
}
async isMember(username: string, options?: IIsMemberOptions): Promise<GitHubTeamRole | boolean> {
const operations = this._operations;
options = options || {};
if (!options.maxAgeSeconds) {
options.maxAgeSeconds = operations.defaults.orgMembershipStaleSeconds;
}
const getMembersOptions: IGetMembersOptions = Object.assign({}, options);
if (!options.role) {
getMembersOptions.role = GitHubTeamRole.Member;
}
const members = await this.getMembers(getMembersOptions);
const expected = username.toLowerCase();
for (let i = 0; i < members.length; i++) {
const member = members[i];
if (member.login.toLowerCase() === expected) {
return getMembersOptions.role;
}
}
return false;
}
getMaintainers(options?: ICacheOptionsPageLimiter): Promise<TeamMember[]> {
options = options || {};
if (!options.maxAgeSeconds) {
options.maxAgeSeconds = this._operations.defaults.teamMaintainersStaleSeconds;
}
const getMemberOptions: IGetMembersOptions = Object.assign({}, options || {});
getMemberOptions.role = GitHubTeamRole.Maintainer;
return this.getMembers(getMemberOptions);
}
async getMembers(options?: IGetMembersOptions): Promise<TeamMember[]> {
options = options || {};
const operations = this._operations;
const github = operations.github;
if (!this.slug) {
console.log('WARN: team.getMembers had to slowly retrieve a slug to perform the call');
await this.getDetails(); // octokit rest v17 requires slug or custom endpoint requests
}
const parameters: IGetMembersParameters = {
team_slug: this.slug,
org: this.organization.name,
per_page: operations.defaultPageSize,
};
const caching: IPagedCacheOptions = {
maxAgeSeconds: options.maxAgeSeconds || operations.defaults.orgMembersStaleSeconds,
backgroundRefresh: true,
};
if (options && options.backgroundRefresh === false) {
caching.backgroundRefresh = false;
}
if (options.role) {
parameters.role = options.role;
}
if (options.pageLimit) {
parameters.pageLimit = options.pageLimit;
}
// CONSIDER: Check the error object, if present, for error.status == /* loose */ 404 to alert/store telemetry on deleted teams
const teamMembersEntities = await github.collections.getTeamMembers(this.authorize(AppPurpose.Data), parameters, caching);
const teamMembers = common.createInstances<TeamMember>(this, this.memberFromEntity, teamMembersEntities);
return teamMembers;
}
async getRepositories(options?: IGetTeamRepositoriesOptions): Promise<Repository[]> {
options = options || {};
const operations = this._operations;
const github = operations.github;
// GitHub does not have a concept of filtering this out so we add it
const customTypeFilteringParameter = options.type;
if (customTypeFilteringParameter && customTypeFilteringParameter !== GitHubRepositoryType.Sources) {
throw new Error(`Custom \'type\' parameter is specified, but at this time only \'sources\' is a valid enum value. Value: ${customTypeFilteringParameter}`);
}
if (!this.slug) {
console.log('WARN: had to request team.slug slowly');
await this.getDetails();
}
const parameters: IGetRepositoriesParameters = {
org: this.organization.name,
team_slug: this.slug,
per_page: operations.defaultPageSize,
};
const caching: IPagedCacheOptions = {
maxAgeSeconds: options.maxAgeSeconds || operations.defaults.orgMembersStaleSeconds,
backgroundRefresh: true,
};
if (options && options.backgroundRefresh === false) {
caching.backgroundRefresh = false;
}
if (options.pageLimit) {
parameters.pageLimit = options.pageLimit;
}
const entities = await github.collections.getTeamRepos(this.authorize(AppPurpose.Data), parameters, caching);
if (customTypeFilteringParameter === 'sources') {
// Remove forks (non-sources)
_.remove(entities, (repo: any) => { return repo.fork; });
}
return common.createInstances<Repository>(this, repositoryFromEntity, entities);
}
async getOfficialMaintainers(): Promise<TeamMember[]> {
await this.getDetails();
const maintainers = await this.getMaintainers();
if (maintainers.length > 0) {
return resolveDirectLinks(maintainers);
}
const members = await this.organization.sudoersTeam.getMembers();
return resolveDirectLinks(members);
}
member(id, optionalEntity?) {
let entity = optionalEntity || {};
if (!optionalEntity) {
entity.id = id;
}
const member = new TeamMember(
this,
entity,
this._operations);
// CONSIDER: Cache any members in the local instance
return member;
}
memberFromEntity(entity) {
return this.member(entity.id, entity);
}
async getApprovals(): Promise<TeamJoinApprovalEntity[]> {
const operations = this._operations;
const approvalProvider = operations.providers.approvalProvider as IApprovalProvider;
if (!approvalProvider) {
throw new Error('No approval provider instance available');
}
let pendingApprovals: TeamJoinApprovalEntity[] = null;
try {
pendingApprovals = await approvalProvider.queryPendingApprovalsForTeam(this.id.toString());
} catch(error) {
throw wrapError(error, 'We were unable to retrieve the pending approvals list for this team. There may be a data store problem or temporary outage.');
}
return pendingApprovals;
}
toSimpleJsonObject() {
return {
id: typeof(this.id) === 'number' ? this.id : parseInt(this.id, 10),
name: this.name,
slug: this.slug,
description: this.description,
repos_count: this.repos_count,
members_count: this.members_count,
created_at: this.created_at,
updated_at: this.updated_at,
};
}
private authorize(purpose: AppPurpose): IGetAuthorizationHeader | string {
const getAuthorizationHeader = this._getAuthorizationHeader.bind(this, purpose) as IGetAuthorizationHeader;
return getAuthorizationHeader;
}
}
async function resolveDirectLinks(people: TeamMember[]): Promise<TeamMember[]> {
for (let i = 0; i < people.length; i++) {
const member = people[i];
await member.getMailAddress();
}
return people;
}
function repositoryFromEntity(entity) {
// private, remapped "this"
const instance = new TeamRepositoryPermission(
this,
entity,
this._operations);
return instance;
}