Skip to content

Commit d9512a7

Browse files
committed
[orgs] Persist slug
1 parent ed32312 commit d9512a7

File tree

7 files changed

+53
-14
lines changed

7 files changed

+53
-14
lines changed

components/gitpod-db/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@jmondi/oauth2-server": "^2.6.1",
2828
"mysql": "^2.18.1",
2929
"reflect-metadata": "^0.1.13",
30+
"slugify": "^1.6.5",
3031
"the-big-username-blacklist": "^1.5.2",
3132
"typeorm": "0.2.38"
3233
},

components/gitpod-db/src/typeorm/entity/db-team.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import { Team } from "@gitpod/gitpod-protocol";
88
import { Entity, Column, PrimaryColumn } from "typeorm";
9-
import { Transformer } from "../transformer";
109
import { TypeORM } from "../typeorm";
1110

1211
@Entity()
@@ -18,12 +17,8 @@ export class DBTeam implements Team {
1817
@Column("varchar")
1918
name: string;
2019

21-
// Deprecated.
22-
@Column({
23-
type: "varchar",
24-
transformer: Transformer.ALWAYS_EMPTY_STRING,
25-
})
26-
slug: string = "";
20+
@Column("varchar")
21+
slug: string;
2722

2823
@Column("varchar")
2924
creationTime: string;

components/gitpod-db/src/typeorm/team-db-impl.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import { inject, injectable } from "inversify";
99
import { TypeORM } from "./typeorm";
1010
import { Repository } from "typeorm";
1111
import { v4 as uuidv4 } from "uuid";
12+
import { randomBytes } from "crypto";
1213
import { TeamDB } from "../team-db";
1314
import { DBTeam } from "./entity/db-team";
1415
import { DBTeamMembership } from "./entity/db-team-membership";
1516
import { DBUser } from "./entity/db-user";
1617
import { DBTeamMembershipInvite } from "./entity/db-team-membership-invite";
1718
import { ResponseError } from "vscode-jsonrpc";
1819
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
20+
import slugify from "slugify";
1921

2022
@injectable()
2123
export class TeamDBImpl implements TeamDB {
@@ -121,17 +123,44 @@ export class TeamDBImpl implements TeamDB {
121123
return soleOwnedTeams;
122124
}
123125

124-
public async updateTeam(teamId: string, team: Pick<Team, "name">): Promise<Team> {
126+
public async updateTeam(teamId: string, team: Pick<Team, "name" | "slug">): Promise<Team> {
127+
const name = team.name && team.name.trim();
128+
const slug = team.slug && team.slug.trim();
129+
if (!name && !slug) {
130+
throw new ResponseError(ErrorCodes.BAD_REQUEST, "No update provided");
131+
}
132+
125133
const teamRepo = await this.getTeamRepo();
126134
const existingTeam = await teamRepo.findOne({ id: teamId, deleted: false, markedDeleted: false });
127135
if (!existingTeam) {
128136
throw new ResponseError(ErrorCodes.NOT_FOUND, "Organization not found");
129137
}
130-
const name = team.name && team.name.trim();
131-
if (!name || name.length === 0 || name.length > 32) {
132-
throw new ResponseError(ErrorCodes.INVALID_VALUE, "The name must be between 1 and 32 characters long");
138+
139+
// no changes
140+
if (existingTeam.name === name && existingTeam.slug === slug) {
141+
return existingTeam;
142+
}
143+
144+
if (!!name) {
145+
if (name.length > 32) {
146+
throw new ResponseError(ErrorCodes.INVALID_VALUE, "The name must be between 1 and 32 characters long");
147+
}
148+
existingTeam.name = name;
133149
}
134-
existingTeam.name = name;
150+
if (!!slug && existingTeam.slug != slug) {
151+
if (slug.length > 100) {
152+
throw new ResponseError(ErrorCodes.INVALID_VALUE, "Slug must be between 1 and 100 characters long");
153+
}
154+
if (!/^[A-Za-z0-9-]+$/.test(slug)) {
155+
throw new ResponseError(ErrorCodes.BAD_REQUEST, "Slug must contain only letters, or numbers");
156+
}
157+
const anotherTeamWithThatSlug = await teamRepo.findOne({ slug, deleted: false, markedDeleted: false });
158+
if (anotherTeamWithThatSlug) {
159+
throw new ResponseError(ErrorCodes.INVALID_VALUE, "Slug must be unique");
160+
}
161+
existingTeam.slug = slug;
162+
}
163+
135164
return teamRepo.save(existingTeam);
136165
}
137166

@@ -146,10 +175,18 @@ export class TeamDBImpl implements TeamDB {
146175
);
147176
}
148177

178+
let slug = slugify(name, { lower: true });
149179
const teamRepo = await this.getTeamRepo();
180+
181+
const existingTeam = await teamRepo.findOne({ slug, deleted: false, markedDeleted: false });
182+
if (!!existingTeam) {
183+
slug = slug + "-" + randomBytes(4).toString("hex");
184+
}
185+
150186
const team: Team = {
151187
id: uuidv4(),
152188
name,
189+
slug,
153190
creationTime: new Date().toISOString(),
154191
};
155192
await teamRepo.save(team);

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
174174

175175
// Teams
176176
getTeam(teamId: string): Promise<Team>;
177-
updateTeam(teamId: string, team: Pick<Team, "name">): Promise<Team>;
177+
updateTeam(teamId: string, team: Pick<Team, "name" | "slug">): Promise<Team>;
178178
getTeams(): Promise<Team[]>;
179179
getTeamMembers(teamId: string): Promise<TeamMemberInfo[]>;
180180
createTeam(name: string): Promise<Team>;

components/gitpod-protocol/src/teams-projects-protocol.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export type Team = Organization;
135135
export interface Organization {
136136
id: string;
137137
name: string;
138+
slug?: string;
138139
creationTime: string;
139140
markedDeleted?: boolean;
140141
/** This is a flag that triggers the HARD DELETION of this entity */

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2247,7 +2247,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22472247
return team;
22482248
}
22492249

2250-
public async updateTeam(ctx: TraceContext, teamId: string, team: Pick<Team, "name">): Promise<Team> {
2250+
public async updateTeam(ctx: TraceContext, teamId: string, team: Pick<Team, "name" | "slug">): Promise<Team> {
22512251
traceAPIParams(ctx, { teamId });
22522252
this.checkUser("updateTeam");
22532253

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16359,6 +16359,11 @@ slice-ansi@^4.0.0:
1635916359
astral-regex "^2.0.0"
1636016360
is-fullwidth-code-point "^3.0.0"
1636116361

16362+
slugify@^1.6.5:
16363+
version "1.6.5"
16364+
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.5.tgz#c8f5c072bf2135b80703589b39a3d41451fbe8c8"
16365+
integrity sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ==
16366+
1636216367
smart-buffer@^4.1.0:
1636316368
version "4.2.0"
1636416369
resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz"

0 commit comments

Comments
 (0)