Skip to content
85 linesCodeBlameRaw
1/**
2 * A repository moved: transferred to another workspace, or renamed within
3 * its own. Its projects go with it.
4 *
5 * A project lives in the workspace its repository is in, so each of the
6 * repository's projects under a stale path's workspace moves to the
7 * workspace the repository is in now, keeping its id (deployments,
8 * secrets and the rest are keyed by it) and its slug. Projects elsewhere
9 * that build from the repository only learn its new path.
10 *
11 * Renamed, the repository's own project (its primary one, named after it)
12 * takes the new name as its slug, so its pages at `/<workspace>/<name>`
13 * follow, unless the workspace already has a project of that slug; it
14 * keeps its old slug then. Its display name follows when it was still the
15 * repository's old name.
16 *
17 * Each statement changes nothing once the rows have moved, so a repeated
18 * or late delivery is harmless.
19 */
20
21import type { Statement } from "./rename";
22
23/** A name as an address: lowercase letters, digits, `-`, `_` and `.`. */
24export function slugOf(name: string): string {
25 return name
26 .trim()
27 .toLowerCase()
28 .replace(/[^a-z0-9._-]+/g, "-")
29 .replace(/^[-.]+|[-.]+$/g, "")
30 .slice(0, 100);
31}
32
33/** `stale` and `current` are `namespace/name` paths. */
34export function moveStatements(stale: string[], current: string, repoId: string): Statement[] {
35 const [workspace, name] = current.split("/") as [string, string];
36 const moves = [...new Set(stale.map((path) => path.split("/")[0]!))]
37 .filter((from) => from !== workspace)
38 .map((from) => ({
39 // A project of the same slug already in the destination keeps it; the
40 // moving one is given a free slug afterwards (see `strandedQuery`).
41 sql: "UPDATE OR IGNORE projects SET workspace = ?, repo_namespace = ? WHERE repo_id = ? AND workspace = ?",
42 params: [workspace, workspace, repoId, from],
43 }));
44 const renames = [...new Set(stale.map((path) => path.split("/")[1]!))]
45 .filter((old) => old !== name)
46 .flatMap((old) => {
47 const statements: Statement[] = [];
48 const [from, to] = [slugOf(old), slugOf(name)];
49 if (from && to && from !== to) {
50 // Ignored when the workspace already has a project called `to`.
51 statements.push({
52 sql: "UPDATE OR IGNORE projects SET slug = ? WHERE repo_id = ? AND is_primary = 1 AND workspace = ? AND slug = ?",
53 params: [to, repoId, workspace, from],
54 });
55 }
56 statements.push({
57 sql: "UPDATE projects SET name = ? WHERE repo_id = ? AND is_primary = 1 AND name = ?",
58 params: [name, repoId, old],
59 });
60 return statements;
61 });
62 return [
63 ...moves,
64 ...renames,
65 {
66 sql: "UPDATE projects SET repo_namespace = ?, repo_name = ? WHERE repo_id = ?",
67 params: [workspace, name, repoId],
68 },
69 ];
70}
71
72/**
73 * The repository's projects still under a stale workspace after the moves:
74 * those whose slug the destination already had. Null for a rename within
75 * the workspace, which moves no project between workspaces.
76 */
77export function strandedQuery(stale: string[], current: string, repoId: string): Statement | null {
78 const workspace = current.split("/")[0]!;
79 const from = [...new Set(stale.map((path) => path.split("/")[0]!))].filter((ns) => ns !== workspace);
80 if (from.length === 0) return null;
81 return {
82 sql: `SELECT * FROM projects WHERE repo_id = ? AND workspace IN (${from.map(() => "?").join(", ")})`,
83 params: [repoId, ...from],
84 };
85}