Skip to content

Commit 028aaba

Browse files
add alias urls to serve the profiles and starting point for clarity
1 parent b6da4d8 commit 028aaba

1 file changed

Lines changed: 78 additions & 9 deletions

File tree

backend/src/routes/serve.ts

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,93 @@ import { ApiResponse } from '../types/dctap.js';
2222

2323
const router = Router();
2424

25+
// Normalize a workspace name into a URL-safe slug
26+
function slugifyName(name: string): string {
27+
return name
28+
.toLowerCase()
29+
.trim()
30+
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric runs with a single hyphen
31+
.replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
32+
}
33+
34+
// Build a map of slug -> workspace ID, using only the first workspace for duplicate slugs.
35+
// Returns the map and a set of slugs that have duplicates.
36+
function buildSlugMap(workspaces: Array<{ id: string; name: string }>) {
37+
const slugToId = new Map<string, string>();
38+
const duplicateSlugs = new Set<string>();
39+
40+
for (const ws of workspaces) {
41+
const slug = slugifyName(ws.name);
42+
if (!slug) continue; // skip if name normalizes to empty
43+
if (slugToId.has(slug)) {
44+
duplicateSlugs.add(slug);
45+
} else {
46+
slugToId.set(slug, ws.id);
47+
}
48+
}
49+
50+
return { slugToId, duplicateSlugs };
51+
}
52+
2553
// List all workspaces available for serving
2654
router.get('/workspaces', (_req: Request, res: Response) => {
2755
const workspaces = workspaceService.list();
56+
const { duplicateSlugs } = buildSlugMap(workspaces);
57+
58+
const workspaceList = workspaces.map(ws => {
59+
const slug = slugifyName(ws.name);
60+
const entry: Record<string, any> = {
61+
id: ws.id,
62+
name: ws.name,
63+
updatedAt: ws.updatedAt,
64+
profileUrl: `/api/serve/${ws.id}/profile`,
65+
startingPointsUrl: `/api/serve/${ws.id}/starting-points`,
66+
csvUrl: `/api/serve/${ws.id}/csv`,
67+
tsvUrl: `/api/serve/${ws.id}/tsv`
68+
};
69+
70+
if (slug) {
71+
entry.profileUrlAlias = `/api/serve/${slug}/profile`;
72+
entry.startingPointsUrlAlias = `/api/serve/${slug}/starting-points`;
73+
}
74+
75+
if (duplicateSlugs.has(slug)) {
76+
entry.warning = `Multiple workspaces share the normalized name "${slug}". The alias URLs resolve to the first workspace encountered.`;
77+
}
2878

29-
const workspaceList = workspaces.map(ws => ({
30-
id: ws.id,
31-
name: ws.name,
32-
updatedAt: ws.updatedAt,
33-
profileUrl: `/api/serve/${ws.id}/profile`,
34-
startingPointsUrl: `/api/serve/${ws.id}/starting-points`,
35-
csvUrl: `/api/serve/${ws.id}/csv`,
36-
tsvUrl: `/api/serve/${ws.id}/tsv`
37-
}));
79+
return entry;
80+
});
3881

3982
const response: ApiResponse = { success: true, data: workspaceList };
4083
res.json(response);
4184
});
4285

86+
// Middleware: resolve name-based slugs to workspace IDs.
87+
// If the :workspaceId param is not a UUID, treat it as a slug and look up the matching workspace.
88+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
89+
90+
function resolveWorkspaceSlug(req: Request, _res: Response, next: NextFunction) {
91+
const param = req.params.workspaceId;
92+
if (UUID_RE.test(param)) {
93+
return next(); // Already a UUID, nothing to resolve
94+
}
95+
96+
// Treat param as a slug — find the first workspace whose name matches
97+
const workspaces = workspaceService.list();
98+
const { slugToId } = buildSlugMap(workspaces);
99+
const workspaceId = slugToId.get(param);
100+
101+
if (!workspaceId) {
102+
return next(new AppError(404, 'Workspace not found', 'WORKSPACE_NOT_FOUND'));
103+
}
104+
105+
req.params.workspaceId = workspaceId;
106+
next();
107+
}
108+
109+
router.use('/:workspaceId/profile', resolveWorkspaceSlug);
110+
router.use('/:workspaceId/starting-points', resolveWorkspaceSlug);
111+
43112
// Serve Marva profile JSON for a workspace
44113
router.get('/:workspaceId/profile', (req: Request, res: Response, next: NextFunction) => {
45114
try {

0 commit comments

Comments
 (0)