| 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 | |
| 21 | import type { Statement } from "./rename"; |
| 22 | |
| 23 | /** A name as an address: lowercase letters, digits, `-`, `_` and `.`. */ |
| 24 | export 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. */ |
| 34 | export 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 | */ |
| 77 | export 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 | } |