69 lines
2.2 KiB
TypeScript
69 lines
2.2 KiB
TypeScript
/**
|
|
* Checks which slugs a category may take. A category's feed lives at the URL
|
|
* root (`/music`), so its slug must not shadow another top-level page. The
|
|
* category service runs this check when a category is created or renamed, and
|
|
* the migrations that reserve more words rename the categories that had one
|
|
* with {@link renameReservedCategories}.
|
|
*
|
|
* Dump and playlist slugs reserve nothing: they live under their owner's
|
|
* `dumps/` and `playlists/`, where nothing else is named by a user. A user
|
|
* can't clash with a category either, since `@` is never in a slug.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import type { DatabaseSync } from "node:sqlite";
|
|
import { FEED_TABS } from "../../shared/paths.ts";
|
|
|
|
/**
|
|
* Lists the top-level path segments a category slug may not take: the feed
|
|
* tabs (`/hot`), the SPA's own pages (`src/App.tsx`), and `api`, `ws`,
|
|
* `assets` and `emojibase`, which the server answers before the SPA. Keep it
|
|
* in step with the route table and the build's top-level folders.
|
|
*/
|
|
export const RESERVED_SLUGS: readonly string[] = [
|
|
...FEED_TABS,
|
|
"search",
|
|
"login",
|
|
"register",
|
|
"notifications",
|
|
"reset-password",
|
|
"unsubscribe",
|
|
"settings",
|
|
"manage",
|
|
"community",
|
|
"api",
|
|
"ws",
|
|
"assets",
|
|
"emojibase",
|
|
];
|
|
|
|
/** Checks whether `slug` is one of the {@link RESERVED_SLUGS}. */
|
|
export function isReservedSlug(slug: string): boolean {
|
|
return RESERVED_SLUGS.includes(slug);
|
|
}
|
|
|
|
/**
|
|
* Renames each category whose slug is now reserved, such as `hot`, to the
|
|
* first of `<slug>-2`, `<slug>-3`… that is free, and logs it. Migration 0021
|
|
* runs it for v4's words, and 0022 for `community`.
|
|
*/
|
|
export function renameReservedCategories(db: DatabaseSync): void {
|
|
const rows = db.prepare(`SELECT id, slug FROM categories;`).all() as {
|
|
id: string;
|
|
slug: string;
|
|
}[];
|
|
const taken = new Set(rows.map((r) => r.slug));
|
|
for (const { id, slug } of rows) {
|
|
if (!isReservedSlug(slug)) continue;
|
|
let n = 2;
|
|
while (taken.has(`${slug}-${n}`) || isReservedSlug(`${slug}-${n}`)) n++;
|
|
const renamed = `${slug}-${n}`;
|
|
taken.add(renamed);
|
|
db.prepare(`UPDATE categories SET slug = ? WHERE id = ?;`).run(renamed, id);
|
|
console.log(
|
|
`[migrate] category "${slug}" is now "${renamed}": its slug is reserved`,
|
|
);
|
|
}
|
|
}
|