Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.tsBlame119 lines · 2 contributors
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
29d00eeccantynz-alt13import { and, eq, isNull, sql } from "drizzle-orm";
7437605Claude14import { 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 {
29d00eeccantynz-alt31 // Case-insensitive slug match — /Ccantynz, /ccantynz, /CCANTYNZ all
32 // resolve to the same namespace. The row carries back the CANONICAL slug
33 // (u.slug / o.slug) so downstream disk/URL use stays correct.
7437605Claude34 const [u] = await db
35 .select({ id: users.id, slug: users.username })
36 .from(users)
29d00eeccantynz-alt37 .where(sql`lower(${users.username}) = lower(${slug})`)
7437605Claude38 .limit(1);
39 if (u) return { kind: "user", id: u.id, slug: u.slug };
40
41 const [o] = await db
42 .select({ id: organizations.id, slug: organizations.slug })
43 .from(organizations)
29d00eeccantynz-alt44 .where(sql`lower(${organizations.slug}) = lower(${slug})`)
7437605Claude45 .limit(1);
46 if (o) return { kind: "org", id: o.id, slug: o.slug };
47
48 return null;
49 } catch (err) {
50 console.error("[namespace] resolveNamespace:", err);
51 return null;
52 }
53}
54
55/**
56 * Load a repo by its URL path `:owner/:repo`. Works for both user-owned
57 * and org-owned repos.
58 */
59export async function loadRepoByPath(
60 ownerSlug: string,
61 repoName: string
62): Promise<typeof repositories.$inferSelect | null> {
63 const ns = await resolveNamespace(ownerSlug);
64 if (!ns) return null;
65 try {
66 if (ns.kind === "user") {
67 const [r] = await db
68 .select()
69 .from(repositories)
70 .where(
71 and(
72 eq(repositories.ownerId, ns.id),
29d00eeccantynz-alt73 sql`lower(${repositories.name}) = lower(${repoName})`,
7437605Claude74 isNull(repositories.orgId)
75 )
76 )
77 .limit(1);
78 return r || null;
79 }
80 const [r] = await db
81 .select()
82 .from(repositories)
83 .where(
29d00eeccantynz-alt84 and(
85 eq(repositories.orgId, ns.id),
86 sql`lower(${repositories.name}) = lower(${repoName})`
87 )
7437605Claude88 )
89 .limit(1);
90 return r || null;
91 } catch (err) {
92 console.error("[namespace] loadRepoByPath:", err);
93 return null;
94 }
95}
96
97/**
98 * List all repos (user or org) for a URL slug. Used by the profile page
99 * to render a unified "repos owned by X" list.
100 */
101export async function listReposForNamespace(ns: Namespace) {
102 try {
103 if (ns.kind === "user") {
104 return await db
105 .select()
106 .from(repositories)
107 .where(
108 and(eq(repositories.ownerId, ns.id), isNull(repositories.orgId))
109 );
110 }
111 return await db
112 .select()
113 .from(repositories)
114 .where(eq(repositories.orgId, ns.id));
115 } catch (err) {
116 console.error("[namespace] listReposForNamespace:", err);
117 return [];
118 }
119}