Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

namespace.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

namespace.tsBlame113 lines · 1 contributor
7437605Claude1/**
2 * Namespace resolution (Block B2).
3 *
4 * The URL path `/:slug` can resolve to either a user or an organization.
5 * Usernames and org slugs occupy the same routing namespace; at creation
6 * time we refuse an org slug that collides with a username (and vice-versa
7 * at register time — see `routes/auth.tsx`).
8 *
9 * Helpers here are read-only and swallow DB errors: they return `null` on
10 * failure so page handlers can fall through to 404 instead of 500.
11 */
12
13import { and, eq, isNull } from "drizzle-orm";
14import { db } from "../db";
15import { users, organizations, repositories } from "../db/schema";
16
17export type Namespace =
18 | { kind: "user"; id: string; slug: string }
19 | { kind: "org"; id: string; slug: string };
20
21/**
22 * Resolve a URL slug to either a user or an org. User lookups win first
23 * (usernames are the legacy, most-used namespace). Returns `null` if neither
24 * exists or the DB is unreachable.
25 */
26export async function resolveNamespace(
27 slug: string
28): Promise<Namespace | null> {
29 if (!slug) return null;
30 try {
31 const [u] = await db
32 .select({ id: users.id, slug: users.username })
33 .from(users)
34 .where(eq(users.username, slug))
35 .limit(1);
36 if (u) return { kind: "user", id: u.id, slug: u.slug };
37
38 const [o] = await db
39 .select({ id: organizations.id, slug: organizations.slug })
40 .from(organizations)
41 .where(eq(organizations.slug, slug))
42 .limit(1);
43 if (o) return { kind: "org", id: o.id, slug: o.slug };
44
45 return null;
46 } catch (err) {
47 console.error("[namespace] resolveNamespace:", err);
48 return null;
49 }
50}
51
52/**
53 * Load a repo by its URL path `:owner/:repo`. Works for both user-owned
54 * and org-owned repos.
55 */
56export async function loadRepoByPath(
57 ownerSlug: string,
58 repoName: string
59): Promise<typeof repositories.$inferSelect | null> {
60 const ns = await resolveNamespace(ownerSlug);
61 if (!ns) return null;
62 try {
63 if (ns.kind === "user") {
64 const [r] = await db
65 .select()
66 .from(repositories)
67 .where(
68 and(
69 eq(repositories.ownerId, ns.id),
70 eq(repositories.name, repoName),
71 isNull(repositories.orgId)
72 )
73 )
74 .limit(1);
75 return r || null;
76 }
77 const [r] = await db
78 .select()
79 .from(repositories)
80 .where(
81 and(eq(repositories.orgId, ns.id), eq(repositories.name, repoName))
82 )
83 .limit(1);
84 return r || null;
85 } catch (err) {
86 console.error("[namespace] loadRepoByPath:", err);
87 return null;
88 }
89}
90
91/**
92 * List all repos (user or org) for a URL slug. Used by the profile page
93 * to render a unified "repos owned by X" list.
94 */
95export async function listReposForNamespace(ns: Namespace) {
96 try {
97 if (ns.kind === "user") {
98 return await db
99 .select()
100 .from(repositories)
101 .where(
102 and(eq(repositories.ownerId, ns.id), isNull(repositories.orgId))
103 );
104 }
105 return await db
106 .select()
107 .from(repositories)
108 .where(eq(repositories.orgId, ns.id));
109 } catch (err) {
110 console.error("[namespace] listReposForNamespace:", err);
111 return [];
112 }
113}