Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | 15x 4x 11x 33x 33x 10x 23x 6x 6x 4x 2x 17x 7x 7x 2x 5x 5x 3x 2x 10x 10x 1x 9x 9x 1x 8x 8x 3x 5x 5x 1x 4x 4x | // Slug redirect helpers: record old→new slug history and resolve a redirect path.
//
// Used by:
// 1. Service layer — hook into project.service.update (title-change slug regen)
// to record the old slug before overwriting it.
// 2. Marketplace route miss paths — when a live-slug lookup returns nothing,
// check slug_redirects to see if the slug was renamed. If so, return a
// redirect path so the Astro SSR page can issue a 301.
//
// Design rationale (from the implementation plan):
// - Store entity_id (not new_slug) as the redirect target so chained renames
// resolve automatically: A→B→C yields two rows, both pointing at the entity,
// and both resolve to the current slug C in one hop each.
// - Resolve at read time: look up the entity by ID, check published status, then
// compute the canonical path. Unpublished/deleted targets return null → 404.
// - The resolver is called ONLY on a live-lookup miss, so the happy path pays
// nothing extra.
import { getRoomCategorySlug } from "@interioring/utils/constants/room-categories";
import type { Dal } from "../dal";
import type { SlugRedirectEntityType } from "../dal/slug-redirects.dal";
export type { SlugRedirectEntityType };
/**
* Record that oldSlug of entityType/entityId has been superseded by newSlug.
*
* No-op when:
* - oldSlug is falsy (entity never had a slug)
* - oldSlug === newSlug (no actual change)
*
* Idempotent: uses INSERT OR IGNORE on the (entity_type, old_slug) unique index.
* The caller must invoke this BEFORE the slug UPDATE so that on partial failure
* the redirect row points at a still-live slug (harmless), not the other way
* around (redirect row exists but the old slug is now the live one — broken).
*/
export async function recordSlugChange(
dal: Dal,
entityType: SlugRedirectEntityType,
oldSlug: string | null | undefined,
entityId: string,
newSlug: string,
): Promise<void> {
if (!oldSlug || oldSlug === newSlug) {
return;
}
await dal.slugRedirects.record({
entityType,
oldSlug,
entityId,
newSlug,
});
}
/**
* Resolve a slug that missed the live-lookup to its current canonical path.
*
* Steps:
* 1. Look up slug_redirects for (entityType, oldSlug). Miss → return null.
* 2. Resolve the entity by its stored entityId. Missing → return null.
* 3. Check published status (entity-type-specific gating). Not published → null.
* 4. Compute and return the canonical path.
*
* Returns null in all failure cases so the calling route returns 404 as normal.
*
* Publish gating matches each route's own checks:
* - pro: pro.status === "published"
* - project: project.status === "published" AND parent pro.status === "published"
* - room: parent project.status === "published" AND parent pro.status === "published"
*/
export async function resolveSlugRedirect(
dal: Dal,
entityType: SlugRedirectEntityType,
oldSlug: string,
): Promise<string | null> {
// Step 1: history lookup
const row = await dal.slugRedirects.findByOldSlug(entityType, oldSlug);
if (!row) {
return null;
}
if (entityType === "pro") {
const pro = await dal.pros.findById(row.entityId);
if (!pro || pro.status !== "published") {
return null;
}
return `/pros/${pro.slug}`;
}
if (entityType === "project") {
const project = await dal.projects.findById(row.entityId);
if (!project || project.status !== "published") {
return null;
}
// Also check parent pro is published
const pro = await dal.pros.findById(project.proId);
if (!pro || pro.status !== "published") {
return null;
}
return `/projects/${project.slug}`;
}
// entityType === "room"
const roomId = Number.parseInt(row.entityId, 10);
if (!Number.isFinite(roomId) || roomId <= 0) {
return null;
}
const room = await dal.rooms.findById(roomId);
if (!room) {
return null;
}
// Check parent project is published
const project = await dal.projects.findById(room.projectId);
if (!project || project.status !== "published") {
return null;
}
// Check parent pro is published
const pro = await dal.pros.findById(project.proId);
if (!pro || pro.status !== "published") {
return null;
}
const categorySlug = getRoomCategorySlug(room.roomType);
return `/rooms/${categorySlug}/${room.slug}`;
}
|