Skip to content

Commit 42bfc6f

Browse files
ddmukhDmytro DmukhFelixTJDietrich
authored
feat(application-server): add GitHub team synchronization (#342)
Co-authored-by: Dmytro Dmukh <[email protected]> Co-authored-by: Felix T.J. Dietrich <[email protected]>
1 parent daed626 commit 42bfc6f

22 files changed

+876
-99
lines changed

docs/dev/database/schema.mmd

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,31 @@ erDiagram
228228
BIGINT repository_id PK,FK
229229
}
230230

231+
TeamV2 {
232+
BIGINT id PK
233+
TIMESTAMPTZ created_at
234+
TIMESTAMPTZ updated_at
235+
TEXT description
236+
TEXT html_url
237+
TIMESTAMPTZ last_synced_at
238+
VARCHAR(255) name
239+
VARCHAR(255) organization
240+
BIGINT parent_id
241+
VARCHAR(32) privacy
242+
}
243+
244+
TeamV2Membership {
245+
VARCHAR(32) role
246+
BIGINT user_id PK,FK
247+
BIGINT team_id PK,FK
248+
}
249+
250+
TeamV2RepositoryPermission {
251+
VARCHAR(32) permission "NOT NULL"
252+
BIGINT repository_id PK,FK
253+
BIGINT team_id PK,FK
254+
}
255+
231256
User {
232257
BIGINT id PK
233258
TIMESTAMPTZ created_at
@@ -290,6 +315,10 @@ erDiagram
290315
User }o--o{ TeamMember : belongs_to
291316
Repository }o--o{ TeamRepository : has
292317
Team }o--o{ TeamRepository : has
318+
TeamV2 }o--o{ TeamV2Membership : belongs_to
319+
User }o--o{ TeamV2Membership : belongs_to
320+
Repository }o--o{ TeamV2RepositoryPermission : has
321+
TeamV2 }o--o{ TeamV2RepositoryPermission : has
293322

294323
%% Styling
295324
classDef primaryEntity fill:#e1f5fe,stroke:#01579b,stroke-width:2px

server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandler.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,8 @@ public void onMessage(Message msg) {
5757
* @return The GHEvent.
5858
*/
5959
protected abstract GHEvent getHandlerEvent();
60+
61+
protected boolean isOrganizationEvent() {
62+
return false;
63+
}
6064
}

server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/common/github/GitHubMessageHandlerRegistry.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,28 @@
1010
@Component
1111
public class GitHubMessageHandlerRegistry {
1212

13-
private final Map<GHEvent, GitHubMessageHandler<?>> handlerMap = new HashMap<>();
13+
private final Map<GHEvent, GitHubMessageHandler<?>> repositoryHandlerMap = new HashMap<>();
14+
private final Map<GHEvent, GitHubMessageHandler<?>> organizationHandlerMap = new HashMap<>();
1415

1516
public GitHubMessageHandlerRegistry(GitHubMessageHandler<?>[] handlers) {
1617
for (GitHubMessageHandler<?> handler : handlers) {
17-
handlerMap.put(handler.getHandlerEvent(), handler);
18+
if (handler.isOrganizationEvent()) {
19+
organizationHandlerMap.put(handler.getHandlerEvent(), handler);
20+
} else {
21+
repositoryHandlerMap.put(handler.getHandlerEvent(), handler);
22+
}
1823
}
1924
}
2025

2126
public GitHubMessageHandler<?> getHandler(GHEvent eventType) {
22-
return handlerMap.get(eventType);
27+
return repositoryHandlerMap.getOrDefault(eventType, organizationHandlerMap.get(eventType));
2328
}
2429

25-
public List<GHEvent> getSupportedEvents() {
26-
return new ArrayList<>(handlerMap.keySet());
30+
public List<GHEvent> getSupportedRepositoryEvents() {
31+
return new ArrayList<>(repositoryHandlerMap.keySet());
32+
}
33+
34+
public List<GHEvent> getSupportedOrganizationEvents() {
35+
return new ArrayList<>(organizationHandlerMap.keySet());
2736
}
2837
}

server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/repository/Repository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
import de.tum.in.www1.hephaestus.gitprovider.label.Label;
66
import de.tum.in.www1.hephaestus.gitprovider.milestone.Milestone;
77
import de.tum.in.www1.hephaestus.gitprovider.team.Team;
8+
import de.tum.in.www1.hephaestus.gitprovider.teamV2.TeamV2;
89
import jakarta.persistence.CascadeType;
910
import jakarta.persistence.Entity;
1011
import jakarta.persistence.EnumType;
1112
import jakarta.persistence.Enumerated;
13+
import jakarta.persistence.JoinColumn;
14+
import jakarta.persistence.JoinTable;
1215
import jakarta.persistence.ManyToMany;
1316
import jakarta.persistence.OneToMany;
1417
import jakarta.persistence.Table;
@@ -86,6 +89,15 @@ public class Repository extends BaseGitServiceEntity {
8689
@ToString.Exclude
8790
private Set<Team> teams = new HashSet<>();
8891

92+
@ManyToMany
93+
@JoinTable(
94+
name = "team_v2_repository_permission",
95+
joinColumns = @JoinColumn(name = "repository_id"),
96+
inverseJoinColumns = @JoinColumn(name = "team_id")
97+
)
98+
@ToString.Exclude
99+
private Set<TeamV2> teamsV2 = new HashSet<>();
100+
89101
public enum Visibility {
90102
PUBLIC,
91103
PRIVATE,

server/application-server/src/main/java/de/tum/in/www1/hephaestus/gitprovider/team/TeamService.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ public class TeamService {
2525
private LabelRepository labelRepository;
2626

2727
public List<TeamInfoDTO> getAllTeams() {
28-
List<TeamInfoDTO> teams = teamRepository.findAll().stream().map(TeamInfoDTO::fromTeam).toList();
29-
return teams;
28+
return teamRepository.findAll().stream().map(TeamInfoDTO::fromTeam).toList();
3029
}
3130

3231
public TeamInfoDTO hideTeam(Long id, Boolean hidden) {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package de.tum.in.www1.hephaestus.gitprovider.teamV2;
2+
3+
import de.tum.in.www1.hephaestus.gitprovider.common.BaseGitServiceEntity;
4+
import de.tum.in.www1.hephaestus.gitprovider.teamV2.membership.TeamMembership;
5+
import de.tum.in.www1.hephaestus.gitprovider.teamV2.permission.TeamRepositoryPermission;
6+
import jakarta.persistence.*;
7+
import java.time.OffsetDateTime;
8+
import java.util.HashSet;
9+
import java.util.Set;
10+
import lombok.Getter;
11+
import lombok.NoArgsConstructor;
12+
import lombok.Setter;
13+
import lombok.ToString;
14+
15+
@Entity
16+
@Table(name = "team_v2")
17+
@Getter
18+
@Setter
19+
@NoArgsConstructor
20+
@ToString(callSuper = true)
21+
public class TeamV2 extends BaseGitServiceEntity {
22+
23+
private String name;
24+
25+
@Column(columnDefinition = "TEXT")
26+
private String description;
27+
28+
@Column(length = 32)
29+
@Enumerated(EnumType.STRING)
30+
private Privacy privacy;
31+
32+
private String organization;
33+
34+
@Column(columnDefinition = "TEXT")
35+
private String htmlUrl;
36+
37+
private Long parentId;
38+
39+
private OffsetDateTime lastSyncedAt;
40+
41+
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
42+
@ToString.Exclude
43+
private Set<TeamRepositoryPermission> repoPermissions = new HashSet<>();
44+
45+
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
46+
@ToString.Exclude
47+
private Set<TeamMembership> memberships = new HashSet<>();
48+
49+
public void addMembership(TeamMembership membership) {
50+
memberships.add(membership);
51+
membership.setTeam(this);
52+
}
53+
54+
public void removeMembership(TeamMembership membership) {
55+
memberships.remove(membership);
56+
membership.setTeam(null);
57+
}
58+
59+
public void addRepoPermission(TeamRepositoryPermission permission) {
60+
repoPermissions.add(permission);
61+
permission.setTeam(this);
62+
}
63+
64+
public void clearAndAddRepoPermissions(Set<TeamRepositoryPermission> fresh) {
65+
repoPermissions.clear();
66+
fresh.forEach(this::addRepoPermission);
67+
}
68+
69+
public enum Privacy {
70+
/** Only organization members can view or request access. */
71+
SECRET,
72+
/** Visible to all members of the organization. */
73+
CLOSED,
74+
}
75+
// Ignored GitHub properties:
76+
// - nodeId
77+
// - slug
78+
// - apiUrl (API URL for this team)
79+
// - members_url (templated URL for member listing)
80+
// - repositories_url (templated URL for repos listing)
81+
// - parent (if this team has a parent team)
82+
// - permissions (maps to our repoPermissions, but scoped to the OAuth user)
83+
// - members_count (cached count; we page through listMembers())
84+
// - repos_count (cached count; we page through listRepositories())
85+
// - privacy_level (older name for privacy)
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package de.tum.in.www1.hephaestus.gitprovider.teamV2;
2+
3+
import java.util.List;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.PathVariable;
7+
import org.springframework.web.bind.annotation.RequestMapping;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
@RestController
11+
@RequestMapping("/api/teamsV2")
12+
public class TeamV2Controller {
13+
14+
private final TeamV2Repository teamRepo;
15+
private final TeamV2InfoDTOConverter converter;
16+
17+
public TeamV2Controller(TeamV2Repository teamRepo, TeamV2InfoDTOConverter converter) {
18+
this.teamRepo = teamRepo;
19+
this.converter = converter;
20+
}
21+
22+
@GetMapping
23+
public ResponseEntity<List<TeamV2InfoDTO>> getAll() {
24+
return ResponseEntity.ok(teamRepo.findAll().stream().map(converter::convert).toList());
25+
}
26+
27+
@GetMapping("/{id}")
28+
public ResponseEntity<TeamV2InfoDTO> getById(@PathVariable Long id) {
29+
return teamRepo
30+
.findById(id)
31+
.map(converter::convert)
32+
.map(ResponseEntity::ok)
33+
.orElse(ResponseEntity.notFound().build());
34+
}
35+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package de.tum.in.www1.hephaestus.gitprovider.teamV2;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import de.tum.in.www1.hephaestus.gitprovider.teamV2.TeamV2.Privacy;
5+
import java.time.OffsetDateTime;
6+
import org.springframework.lang.NonNull;
7+
8+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
9+
public record TeamV2InfoDTO(
10+
@NonNull Long id,
11+
@NonNull String name,
12+
Long parentId,
13+
String description,
14+
Privacy privacy,
15+
String organization,
16+
String htmlUrl,
17+
OffsetDateTime lastSyncedAt,
18+
int membershipCount,
19+
int repoPermissionCount
20+
) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package de.tum.in.www1.hephaestus.gitprovider.teamV2;
2+
3+
import org.springframework.core.convert.converter.Converter;
4+
import org.springframework.lang.NonNull;
5+
import org.springframework.stereotype.Component;
6+
7+
@Component
8+
public class TeamV2InfoDTOConverter implements Converter<TeamV2, TeamV2InfoDTO> {
9+
10+
@Override
11+
public TeamV2InfoDTO convert(@NonNull TeamV2 source) {
12+
return new TeamV2InfoDTO(
13+
source.getId(),
14+
source.getName(),
15+
source.getParentId(),
16+
source.getDescription(),
17+
source.getPrivacy(),
18+
source.getOrganization(),
19+
source.getHtmlUrl(),
20+
source.getLastSyncedAt(),
21+
source.getMemberships().size(),
22+
source.getRepoPermissions().size()
23+
);
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package de.tum.in.www1.hephaestus.gitprovider.teamV2;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.stereotype.Repository;
5+
6+
@Repository
7+
public interface TeamV2Repository extends JpaRepository<TeamV2, Long> {}

0 commit comments

Comments
 (0)