Skip to content

Commit 4f4bbe8

Browse files
committed
[orgs] Persist slug
1 parent b465d06 commit 4f4bbe8

File tree

7 files changed

+79
-26
lines changed

7 files changed

+79
-26
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: 68 additions & 17 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,18 +123,53 @@ export class TeamDBImpl implements TeamDB {
121123
return soleOwnedTeams;
122124
}
123125

124-
public async updateTeam(teamId: string, team: Pick<Team, "name">): Promise<Team> {
125-
const teamRepo = await this.getTeamRepo();
126-
const existingTeam = await teamRepo.findOne({ id: teamId, deleted: false, markedDeleted: false });
127-
if (!existingTeam) {
128-
throw new ResponseError(ErrorCodes.NOT_FOUND, "Organization not found");
129-
}
126+
public async updateTeam(teamId: string, team: Pick<Team, "name" | "slug">): Promise<Team> {
130127
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");
128+
const slug = team.slug && team.slug.trim();
129+
if (!name && !slug) {
130+
throw new ResponseError(ErrorCodes.BAD_REQUEST, "No update provided");
133131
}
134-
existingTeam.name = name;
135-
return teamRepo.save(existingTeam);
132+
133+
// Storing entry in a TX to avoid potential slug dupes caused by racing requests.
134+
const em = await this.getEntityManager();
135+
return await em.transaction<DBTeam>(async (em) => {
136+
const teamRepo = em.getRepository<DBTeam>(DBTeam);
137+
138+
const existingTeam = await teamRepo.findOne({ id: teamId, deleted: false, markedDeleted: false });
139+
if (!existingTeam) {
140+
throw new ResponseError(ErrorCodes.NOT_FOUND, "Organization not found");
141+
}
142+
143+
// no changes
144+
if (existingTeam.name === name && existingTeam.slug === slug) {
145+
return existingTeam;
146+
}
147+
148+
if (!!name) {
149+
if (name.length > 32) {
150+
throw new ResponseError(
151+
ErrorCodes.INVALID_VALUE,
152+
"The name must be between 1 and 32 characters long",
153+
);
154+
}
155+
existingTeam.name = name;
156+
}
157+
if (!!slug && existingTeam.slug != slug) {
158+
if (slug.length > 100) {
159+
throw new ResponseError(ErrorCodes.INVALID_VALUE, "Slug must be between 1 and 100 characters long");
160+
}
161+
if (!/^[A-Za-z0-9-]+$/.test(slug)) {
162+
throw new ResponseError(ErrorCodes.BAD_REQUEST, "Slug must contain only letters, or numbers");
163+
}
164+
const anotherTeamWithThatSlug = await teamRepo.findOne({ slug, deleted: false, markedDeleted: false });
165+
if (anotherTeamWithThatSlug) {
166+
throw new ResponseError(ErrorCodes.INVALID_VALUE, "Slug must be unique");
167+
}
168+
existingTeam.slug = slug;
169+
}
170+
171+
return teamRepo.save(existingTeam);
172+
});
136173
}
137174

138175
public async createTeam(userId: string, name: string): Promise<Team> {
@@ -146,13 +183,27 @@ export class TeamDBImpl implements TeamDB {
146183
);
147184
}
148185

149-
const teamRepo = await this.getTeamRepo();
150-
const team: Team = {
151-
id: uuidv4(),
152-
name,
153-
creationTime: new Date().toISOString(),
154-
};
155-
await teamRepo.save(team);
186+
let slug = slugify(name, { lower: true });
187+
188+
// Storing new entry in a TX to avoid potential dupes caused by racing requests.
189+
const em = await this.getEntityManager();
190+
const team = await em.transaction<DBTeam>(async (em) => {
191+
const teamRepo = em.getRepository<DBTeam>(DBTeam);
192+
193+
const existingTeam = await teamRepo.findOne({ slug, deleted: false, markedDeleted: false });
194+
if (!!existingTeam) {
195+
slug = slug + "-" + randomBytes(4).toString("hex");
196+
}
197+
198+
const team: Team = {
199+
id: uuidv4(),
200+
name,
201+
slug,
202+
creationTime: new Date().toISOString(),
203+
};
204+
return await teamRepo.save(team);
205+
});
206+
156207
const membershipRepo = await this.getMembershipRepo();
157208
await membershipRepo.save({
158209
id: uuidv4(),

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
@@ -2243,7 +2243,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
22432243
return team;
22442244
}
22452245

2246-
public async updateTeam(ctx: TraceContext, teamId: string, team: Pick<Team, "name">): Promise<Team> {
2246+
public async updateTeam(ctx: TraceContext, teamId: string, team: Pick<Team, "name" | "slug">): Promise<Team> {
22472247
traceAPIParams(ctx, { teamId });
22482248
this.checkUser("updateTeam");
22492249

yarn.lock

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

16260+
slugify@^1.6.5:
16261+
version "1.6.5"
16262+
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.5.tgz#c8f5c072bf2135b80703589b39a3d41451fbe8c8"
16263+
integrity sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ==
16264+
1626016265
snakeize@^0.1.0:
1626116266
version "0.1.0"
1626216267
resolved "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz"

0 commit comments

Comments
 (0)