refct(events,catalog/github): change membership and team.edit handlers

Signed-off-by: Rogerio Angeliski <angeliski@hotmail.com>
This commit is contained in:
Rogerio Angeliski
2023-01-02 20:00:59 +00:00
parent 7418d09737
commit 8c758fed63
6 changed files with 671 additions and 112 deletions
@@ -246,7 +246,6 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
// @public
export type GithubTeam = {
databaseId: number;
slug: string;
combinedSlug: string;
name?: string;
@@ -90,10 +90,6 @@ export const defaultOrganizationTeamTransformer: TeamTransformer =
'github.com/team-slug': team.combinedSlug,
};
if (team.databaseId) {
annotations['github.com/team-id'] = `${team.databaseId}`;
}
if (team.editTeamUrl) {
annotations['backstage.io/edit-url'] = team.editTeamUrl;
}
@@ -31,6 +31,7 @@ import {
GithubTeam,
createAddEntitiesOperation,
createRemoveEntitiesOperation,
createReplaceEntitiesOperation,
} from './github';
import fetch from 'node-fetch';
@@ -206,7 +207,6 @@ describe('github', () => {
pageInfo: { hasNextPage: false },
nodes: [
{
databaseId: 1,
slug: 'team',
combinedSlug: 'blah/team',
name: 'Team',
@@ -214,7 +214,6 @@ describe('github', () => {
avatarUrl: 'http://example.com/team.jpeg',
editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit',
parentTeam: {
databaseId: 2,
slug: 'parent',
combinedSlug: '',
members: [],
@@ -239,7 +238,6 @@ describe('github', () => {
description: 'The one and only team',
annotations: {
'github.com/team-slug': 'blah/team',
'github.com/team-id': '1',
'backstage.io/edit-url':
'http://example.com/orgs/blah/teams/team/edit',
},
@@ -309,7 +307,6 @@ describe('github', () => {
pageInfo: { hasNextPage: false },
nodes: [
{
databaseId: 1,
slug: 'team',
combinedSlug: 'blah/team',
name: 'Team',
@@ -317,7 +314,6 @@ describe('github', () => {
avatarUrl: 'http://example.com/team.jpeg',
editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit',
parentTeam: {
databaseId: 3,
slug: 'parent',
combinedSlug: '',
members: [],
@@ -376,7 +372,6 @@ describe('github', () => {
pageInfo: { hasNextPage: false },
nodes: [
{
databaseId: 1,
slug: 'team',
combinedSlug: 'blah/team',
name: 'Team',
@@ -384,7 +379,6 @@ describe('github', () => {
avatarUrl: 'http://example.com/team.jpeg',
editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit',
parentTeam: {
databaseId: 3,
slug: 'parent',
combinedSlug: '',
members: [],
@@ -395,7 +389,6 @@ describe('github', () => {
},
},
{
databaseId: 2,
slug: 'team',
combinedSlug: 'blah/team',
name: 'aa',
@@ -403,7 +396,6 @@ describe('github', () => {
avatarUrl: 'http://example.com/team.jpeg',
editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit',
parentTeam: {
databaseId: 3,
slug: 'parent',
combinedSlug: '',
members: [],
@@ -464,7 +456,6 @@ describe('github', () => {
const input: QueryResponse = {
organization: {
team: {
databaseId: 1,
slug: '',
combinedSlug: '',
members: {
@@ -635,4 +626,41 @@ describe('github', () => {
});
});
});
describe('createReplaceEntitiesOperation', () => {
it('create a function to replace deferred entities to a delta operation', () => {
const operation = createReplaceEntitiesOperation('my-id', 'host');
const userEntity: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
name: 'githubuser',
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/githubuser',
'backstage.io/managed-by-origin-location':
'url:https://github.com/githubuser',
'github.com/user-login': 'githubuser',
},
},
spec: {
memberOf: ['new-team'],
},
};
expect(operation('org', [userEntity])).toEqual({
removed: [
{
locationKey: 'github-org-provider:my-id',
entity: userEntity,
},
],
added: [
{
locationKey: 'github-org-provider:my-id',
entity: userEntity,
},
],
});
});
});
});
@@ -71,7 +71,6 @@ export type GithubUser = {
* @public
*/
export type GithubTeam = {
databaseId: number;
slug: string;
combinedSlug: string;
name?: string;
@@ -186,7 +185,6 @@ export async function getOrganizationTeams(
teams(first: 100, after: $cursor) {
pageInfo { hasNextPage, endCursor }
nodes {
databaseId
slug
combinedSlug
name
@@ -196,7 +194,14 @@ export async function getOrganizationTeams(
parentTeam { slug }
members(first: 100, membership: IMMEDIATE) {
pageInfo { hasNextPage }
nodes { login }
nodes {
avatarUrl,
bio,
email,
login,
name,
organizationVerifiedDomainEmails(login: $org)
}
}
}
}
@@ -243,6 +248,164 @@ export async function getOrganizationTeams(
return { groups };
}
export async function getOrganizationTeamsFromUsers(
client: typeof graphql,
org: string,
userLogins: string[],
teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer,
): Promise<{
groups: GroupEntity[];
}> {
const query = `
query teams($org: String!, $cursor: String, $userLogins: [String!] = "") {
organization(login: $org) {
teams(first: 100, after: $cursor, userLogins: $userLogins) {
pageInfo {
hasNextPage
endCursor
}
nodes {
slug
combinedSlug
name
description
avatarUrl
editTeamUrl
parentTeam {
slug
}
members(first: 100, membership: IMMEDIATE) {
pageInfo {
hasNextPage
}
nodes {
avatarUrl,
bio,
email,
login,
name,
organizationVerifiedDomainEmails(login: $org)
}
}
}
}
}
}`;
const materialisedTeams = async (
item: GithubTeamResponse,
ctx: TransformerContext,
): Promise<GroupEntity | undefined> => {
const memberNames: GithubUser[] = [];
if (!item.members.pageInfo.hasNextPage) {
// We got all the members in one go, run the fast path
for (const user of item.members.nodes) {
memberNames.push(user);
}
} else {
// There were more than a hundred immediate members - run the slow
// path of fetching them explicitly
const { members } = await getTeamMembers(ctx.client, ctx.org, item.slug);
for (const userLogin of members) {
memberNames.push(userLogin);
}
}
const team: GithubTeam = {
...item,
members: memberNames,
};
return await teamTransformer(team, ctx);
};
const groups = await queryWithPaging(
client,
query,
org,
r => r.organization?.teams,
materialisedTeams,
{ org, userLogins },
);
return { groups };
}
export async function getOrganizationTeam(
client: typeof graphql,
org: string,
teamSlug: string,
teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer,
): Promise<{
group: GroupEntity;
}> {
const query = `
query teams($org: String!, $teamSlug: String!) {
organization(login: $org) {
team(slug:$teamSlug) {
slug
combinedSlug
name
description
avatarUrl
editTeamUrl
parentTeam { slug }
members(first: 100, membership: IMMEDIATE) {
pageInfo { hasNextPage }
nodes { login }
}
}
}
}`;
const materialisedTeam = async (
item: GithubTeamResponse,
ctx: TransformerContext,
): Promise<GroupEntity | undefined> => {
const memberNames: GithubUser[] = [];
if (!item.members.pageInfo.hasNextPage) {
// We got all the members in one go, run the fast path
for (const user of item.members.nodes) {
memberNames.push(user);
}
} else {
// There were more than a hundred immediate members - run the slow
// path of fetching them explicitly
const { members } = await getTeamMembers(ctx.client, ctx.org, item.slug);
for (const userLogin of members) {
memberNames.push(userLogin);
}
}
const team: GithubTeam = {
...item,
members: memberNames,
};
return await teamTransformer(team, ctx);
};
const response: QueryResponse = await client(query, {
org,
teamSlug,
});
if (!response.organization?.team)
throw new Error(`Found no match for group ${teamSlug}`);
const group = await materialisedTeam(response.organization?.team, {
query,
client,
org,
});
if (!group) throw new Error(`Can't transform for group ${teamSlug}`);
return { group };
}
export async function getOrganizationRepositories(
client: typeof graphql,
org: string,
@@ -435,3 +598,16 @@ export const createRemoveEntitiesOperation =
entity: withLocations(`https://${host}`, org, entity),
})),
});
export const createReplaceEntitiesOperation =
(id: string, host: string) => (org: string, entities: Entity[]) => {
const entitiesToReplace = entities.map(entity => ({
locationKey: `github-org-provider:${id}`,
entity: withLocations(`https://${host}`, org, entity),
}));
return {
removed: entitiesToReplace,
added: entitiesToReplace,
};
};
@@ -628,6 +628,26 @@ describe('GithubOrgEntityProvider', () => {
const mockClient = jest.fn();
mockClient
.mockResolvedValueOnce({
organization: {
team: {
slug: 'team',
combinedSlug: 'blah/team',
name: 'Team',
description: 'The one and only team',
avatarUrl: 'http://example.com/team.jpeg',
parentTeam: {
slug: 'parent',
combinedSlug: '',
members: { pageInfo: { hasNextPage: false }, nodes: [] },
},
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'a' }, { login: 'githubuser' }],
},
},
},
})
.mockResolvedValueOnce({
organization: {
membersWithRole: {
@@ -640,6 +660,13 @@ describe('GithubOrgEntityProvider', () => {
email: 'd',
avatarUrl: 'e',
},
{
login: 'githubuser',
name: 'githubuser',
bio: 'githubuser',
email: 'd',
avatarUrl: 'e',
},
],
},
},
@@ -662,7 +689,7 @@ describe('GithubOrgEntityProvider', () => {
},
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'a' }],
nodes: [{ login: 'a' }, { login: 'githubuser' }],
},
},
],
@@ -680,7 +707,7 @@ describe('GithubOrgEntityProvider', () => {
action: 'edited',
changes: {
name: {
from: 'mygroup',
from: 'mygroup with spaces',
},
},
team: {
@@ -704,11 +731,10 @@ describe('GithubOrgEntityProvider', () => {
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
entities: [
added: [
{
locationKey: 'github-org-provider:my-id',
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
annotations: {
'backstage.io/managed-by-location':
@@ -717,24 +743,50 @@ describe('GithubOrgEntityProvider', () => {
'url:https://github.com/a',
'github.com/user-login': 'a',
},
description: 'c',
name: 'a',
description: 'c',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
spec: {
memberOf: ['team'],
profile: {
displayName: 'b',
email: 'd',
picture: 'e',
},
memberOf: ['team'],
},
},
locationKey: 'github-org-provider:my-id',
},
{
locationKey: 'github-org-provider:my-id',
entity: {
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/githubuser',
'backstage.io/managed-by-origin-location':
'url:https://github.com/githubuser',
'github.com/user-login': 'githubuser',
},
name: 'githubuser',
description: 'githubuser',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
kind: 'User',
spec: {
profile: {
displayName: 'githubuser',
email: 'd',
picture: 'e',
},
memberOf: ['team'],
},
},
},
{
locationKey: 'github-org-provider:my-id',
entity: {
metadata: {
annotations: {
'backstage.io/managed-by-location':
@@ -746,21 +798,50 @@ describe('GithubOrgEntityProvider', () => {
name: 'team',
description: 'The one and only team',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
spec: {
children: [],
parent: 'parent',
type: 'team',
profile: {
displayName: 'Team',
picture: 'http://example.com/team.jpeg',
},
type: 'team',
members: ['a'],
children: [],
parent: 'parent',
members: ['a', 'githubuser'],
},
},
locationKey: 'github-org-provider:my-id',
},
],
type: 'full',
removed: [
{
locationKey: 'github-org-provider:my-id',
entity: {
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/orgs/backstage/teams/mygroup-with-spaces',
'backstage.io/managed-by-origin-location':
'url:https://github.com/orgs/backstage/teams/mygroup-with-spaces',
},
name: 'mygroup-with-spaces',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
spec: {
type: 'team',
profile: {
displayName: 'Team',
picture: 'http://example.com/team.jpeg',
},
children: [],
parent: 'parent',
members: ['a', 'githubuser'],
},
},
},
],
type: 'delta',
});
});
@@ -795,6 +876,26 @@ describe('GithubOrgEntityProvider', () => {
const mockClient = jest.fn();
mockClient
.mockResolvedValueOnce({
organization: {
team: {
slug: 'team',
combinedSlug: 'blah/team',
name: 'Team',
description: 'The one and only team',
avatarUrl: 'http://example.com/team.jpeg',
parentTeam: {
slug: 'parent',
combinedSlug: '',
members: { pageInfo: { hasNextPage: false }, nodes: [] },
},
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'a' }, { login: 'githubuser' }],
},
},
},
})
.mockResolvedValueOnce({
organization: {
membersWithRole: {
@@ -807,6 +908,13 @@ describe('GithubOrgEntityProvider', () => {
email: 'd',
avatarUrl: 'e',
},
{
login: 'githubuser',
name: 'githubuser',
bio: 'githubuser',
email: 'd',
avatarUrl: 'e',
},
],
},
},
@@ -829,7 +937,7 @@ describe('GithubOrgEntityProvider', () => {
},
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'a' }],
nodes: [{ login: 'a' }, { login: 'githubuser' }],
},
},
],
@@ -867,37 +975,36 @@ describe('GithubOrgEntityProvider', () => {
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
entities: [
added: [
{
locationKey: 'github-org-provider:my-id',
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/a',
'url:https://github.com/githubuser',
'backstage.io/managed-by-origin-location':
'url:https://github.com/a',
'github.com/user-login': 'a',
'url:https://github.com/githubuser',
'github.com/user-login': 'githubuser',
},
description: 'c',
name: 'a',
name: 'githubuser',
description: 'githubuser',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
spec: {
memberOf: ['team'],
profile: {
displayName: 'b',
displayName: 'githubuser',
email: 'd',
picture: 'e',
},
memberOf: ['team'],
},
},
locationKey: 'github-org-provider:my-id',
},
{
locationKey: 'github-org-provider:my-id',
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
annotations: {
'backstage.io/managed-by-location':
@@ -909,21 +1016,78 @@ describe('GithubOrgEntityProvider', () => {
name: 'team',
description: 'The one and only team',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
spec: {
children: [],
parent: 'parent',
type: 'team',
profile: {
displayName: 'Team',
picture: 'http://example.com/team.jpeg',
},
type: 'team',
members: ['a'],
children: [],
parent: 'parent',
members: ['a', 'githubuser'],
},
},
locationKey: 'github-org-provider:my-id',
},
],
type: 'full',
removed: [
{
locationKey: 'github-org-provider:my-id',
entity: {
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/githubuser',
'backstage.io/managed-by-origin-location':
'url:https://github.com/githubuser',
'github.com/user-login': 'githubuser',
},
name: 'githubuser',
description: 'githubuser',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
spec: {
profile: {
displayName: 'githubuser',
email: 'd',
picture: 'e',
},
memberOf: ['team'],
},
},
},
{
locationKey: 'github-org-provider:my-id',
entity: {
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/orgs/backstage/teams/team',
'backstage.io/managed-by-origin-location':
'url:https://github.com/orgs/backstage/teams/team',
'github.com/team-slug': 'blah/team',
},
name: 'team',
description: 'The one and only team',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
spec: {
type: 'team',
profile: {
displayName: 'Team',
picture: 'http://example.com/team.jpeg',
},
children: [],
parent: 'parent',
members: ['a', 'githubuser'],
},
},
},
],
type: 'delta',
});
});
@@ -958,15 +1122,35 @@ describe('GithubOrgEntityProvider', () => {
const mockClient = jest.fn();
mockClient
.mockResolvedValueOnce({
organization: {
team: {
slug: 'team',
combinedSlug: 'blah/team',
name: 'Team',
description: 'The one and only team',
avatarUrl: 'http://example.com/team.jpeg',
parentTeam: {
slug: 'parent',
combinedSlug: '',
members: { pageInfo: { hasNextPage: false }, nodes: [] },
},
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'a' }],
},
},
},
})
.mockResolvedValueOnce({
organization: {
membersWithRole: {
pageInfo: { hasNextPage: false },
nodes: [
{
login: 'a',
name: 'b',
bio: 'c',
login: 'githubuser',
name: 'githubuser',
bio: 'githubuser',
email: 'd',
avatarUrl: 'e',
},
@@ -978,24 +1162,7 @@ describe('GithubOrgEntityProvider', () => {
organization: {
teams: {
pageInfo: { hasNextPage: false },
nodes: [
{
slug: 'team',
combinedSlug: 'blah/team',
name: 'Team',
description: 'The one and only team',
avatarUrl: 'http://example.com/team.jpeg',
parentTeam: {
slug: 'parent',
combinedSlug: '',
members: { pageInfo: { hasNextPage: false }, nodes: [] },
},
members: {
pageInfo: { hasNextPage: false },
nodes: [{ login: 'a' }],
},
},
],
nodes: [],
},
},
});
@@ -1030,37 +1197,36 @@ describe('GithubOrgEntityProvider', () => {
expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
entities: [
added: [
{
locationKey: 'github-org-provider:my-id',
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/a',
'url:https://github.com/githubuser',
'backstage.io/managed-by-origin-location':
'url:https://github.com/a',
'github.com/user-login': 'a',
'url:https://github.com/githubuser',
'github.com/user-login': 'githubuser',
},
description: 'c',
name: 'a',
name: 'githubuser',
description: 'githubuser',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
spec: {
memberOf: ['team'],
profile: {
displayName: 'b',
displayName: 'githubuser',
email: 'd',
picture: 'e',
},
memberOf: [],
},
},
locationKey: 'github-org-provider:my-id',
},
{
locationKey: 'github-org-provider:my-id',
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
metadata: {
annotations: {
'backstage.io/managed-by-location':
@@ -1072,21 +1238,78 @@ describe('GithubOrgEntityProvider', () => {
name: 'team',
description: 'The one and only team',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
spec: {
children: [],
parent: 'parent',
type: 'team',
profile: {
displayName: 'Team',
picture: 'http://example.com/team.jpeg',
},
type: 'team',
children: [],
parent: 'parent',
members: ['a'],
},
},
locationKey: 'github-org-provider:my-id',
},
],
type: 'full',
removed: [
{
locationKey: 'github-org-provider:my-id',
entity: {
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/githubuser',
'backstage.io/managed-by-origin-location':
'url:https://github.com/githubuser',
'github.com/user-login': 'githubuser',
},
name: 'githubuser',
description: 'githubuser',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'User',
spec: {
profile: {
displayName: 'githubuser',
email: 'd',
picture: 'e',
},
memberOf: [],
},
},
},
{
locationKey: 'github-org-provider:my-id',
entity: {
metadata: {
annotations: {
'backstage.io/managed-by-location':
'url:https://github.com/orgs/backstage/teams/team',
'backstage.io/managed-by-origin-location':
'url:https://github.com/orgs/backstage/teams/team',
'github.com/team-slug': 'blah/team',
},
name: 'team',
description: 'The one and only team',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
spec: {
type: 'team',
profile: {
displayName: 'Team',
picture: 'http://example.com/team.jpeg',
},
children: [],
parent: 'parent',
members: ['a'],
},
},
},
],
type: 'delta',
});
});
});
@@ -40,6 +40,8 @@ import {
OrganizationMemberAddedEvent,
OrganizationMemberRemovedEvent,
TeamEvent,
TeamEditedEvent,
MembershipEvent,
} from '@octokit/webhooks-types';
import { merge } from 'lodash';
import * as uuid from 'uuid';
@@ -58,7 +60,10 @@ import { TeamTransformer, UserTransformer } from '../lib';
import {
createAddEntitiesOperation,
createRemoveEntitiesOperation,
createReplaceEntitiesOperation,
DeferredEntitiesBuilder,
getOrganizationTeam,
getOrganizationTeamsFromUsers,
} from '../lib/github';
/**
@@ -255,6 +260,11 @@ export class GithubOrgEntityProvider
this.options.gitHubConfig.host,
);
const replaceEntitiesOperation = createReplaceEntitiesOperation(
this.options.id,
this.options.gitHubConfig.host,
);
// handle change users in the org
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#organization
if (params.topic.includes('organization')) {
@@ -283,14 +293,21 @@ export class GithubOrgEntityProvider
: removeEntitiesOperation;
await this.onTeamChangeInOrganization(teamEvent, createDeltaOperation);
} else if (teamEvent.action === 'edited') {
this.onEventToRebuildOrg();
await this.onTeamEditedInOrganization(
teamEvent,
replaceEntitiesOperation,
);
}
}
// handle change membership in the org
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
if (params.topic.includes('membership')) {
this.onEventToRebuildOrg();
const membershipEvent = params.eventPayload as MembershipEvent;
this.onMembershipChangedInOrganization(
membershipEvent,
replaceEntitiesOperation,
);
}
return;
@@ -301,6 +318,143 @@ export class GithubOrgEntityProvider
return ['github.organization', 'github.team', 'github.membership'];
}
private async onTeamEditedInOrganization(
event: TeamEditedEvent,
createDeltaOperation: DeferredEntitiesBuilder,
) {
if (!this.connection) {
throw new Error('Not initialized');
}
const teamSlug = event.team.slug;
const { headers, type: tokenType } =
await this.credentialsProvider.getCredentials({
url: this.options.orgUrl,
});
const client = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers,
});
const { org } = parseGithubOrgUrl(this.options.orgUrl);
const { group } = await getOrganizationTeam(
client,
org,
teamSlug,
this.options.teamTransformer,
);
const { users } = await getOrganizationUsers(
client,
org,
tokenType,
this.options.userTransformer,
);
const usersFromChangedGroup = group.spec.members || [];
const usersToRebuild = users.filter(u =>
usersFromChangedGroup.includes(u.metadata.name),
);
const { groups } = await getOrganizationTeamsFromUsers(
client,
org,
usersToRebuild.map(u => u.metadata.name),
this.options.teamTransformer,
);
assignGroupsToUsers(usersToRebuild, groups);
buildOrgHierarchy(groups);
const oldName = event.changes.name?.from || '';
const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-');
const { removed } = createDeltaOperation(org, [
{
...group,
metadata: {
name: oldSlug,
},
},
]);
const { added } = createDeltaOperation(org, [...usersToRebuild, ...groups]);
await this.connection.applyMutation({
type: 'delta',
removed,
added,
});
}
private async onMembershipChangedInOrganization(
event: MembershipEvent,
createDeltaOperation: DeferredEntitiesBuilder,
) {
if (!this.connection) {
throw new Error('Not initialized');
}
// The docs are saying I will receive the slug for the removed event,
// but the types don't reflect that,
// so I will just check to be sure the slug is there
// https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
if (!('slug' in event.team)) {
return;
}
const teamSlug = event.team.slug;
const userLogin = event.member.login;
const { headers, type: tokenType } =
await this.credentialsProvider.getCredentials({
url: this.options.orgUrl,
});
const client = graphql.defaults({
baseUrl: this.options.gitHubConfig.apiBaseUrl,
headers,
});
const { org } = parseGithubOrgUrl(this.options.orgUrl);
const { group } = await getOrganizationTeam(
client,
org,
teamSlug,
this.options.teamTransformer,
);
const { users } = await getOrganizationUsers(
client,
org,
tokenType,
this.options.userTransformer,
);
const usersToRebuild = users.filter(u => u.metadata.name === userLogin);
const { groups } = await getOrganizationTeamsFromUsers(
client,
org,
[userLogin],
this.options.teamTransformer,
);
// we include group because the removed event need to update the old group too
if (!groups.some(g => g.metadata.name === group.metadata.name)) {
groups.push(group);
}
assignGroupsToUsers(usersToRebuild, groups);
buildOrgHierarchy(groups);
const { added, removed } = createDeltaOperation(org, [
...usersToRebuild,
...groups,
]);
await this.connection.applyMutation({
type: 'delta',
removed,
added,
});
}
private async onTeamChangeInOrganization(
event: TeamEvent,
createDeltaOperation: DeferredEntitiesBuilder,
@@ -311,13 +465,7 @@ export class GithubOrgEntityProvider
const organizationTeamTransformer =
this.options.teamTransformer || defaultOrganizationTeamTransformer;
const {
id: databaseId,
name,
html_url: url,
description,
slug,
} = event.team;
const { name, html_url: url, description, slug } = event.team;
const org = event.organization.login;
const { headers } = await this.credentialsProvider.getCredentials({
url: this.options.orgUrl,
@@ -329,7 +477,6 @@ export class GithubOrgEntityProvider
const group = (await organizationTeamTransformer(
{
databaseId,
name,
slug,
editTeamUrl: `${url}/edit`,
@@ -355,16 +502,6 @@ export class GithubOrgEntityProvider
});
}
private onEventToRebuildOrg() {
try {
this.read(this.options).then(() => {
this.options.logger.debug('Event process finished');
});
} catch (e) {
this.options.logger.debug(`Event process error ${e}`);
}
}
private async onMemberChangeInOrganization(
event: OrganizationMemberAddedEvent | OrganizationMemberRemovedEvent,
createDeltaOperation: DeferredEntitiesBuilder,