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

schema-extensions.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.

schema-extensions.tsBlame198 lines · 1 contributor
45e31d0Claude1/**
2 * Extended database schema — branch protection, status checks,
3 * notifications, organizations, and teams.
4 *
5 * These extend the base schema to add enterprise-grade features.
6 */
7
8import {
9 pgTable,
10 text,
11 timestamp,
12 uuid,
13 boolean,
14 integer,
15 uniqueIndex,
16 index,
17 serial,
18 jsonb,
19} from "drizzle-orm/pg-core";
20import { users, repositories } from "./schema";
21
22// ─── Branch Protection Rules ────────────────────────────────────────────────
23
24export const branchProtection = pgTable(
25 "branch_protection",
26 {
27 id: uuid("id").primaryKey().defaultRandom(),
28 repositoryId: uuid("repository_id")
29 .notNull()
30 .references(() => repositories.id, { onDelete: "cascade" }),
31 pattern: text("pattern").notNull(), // e.g. "main", "release/*"
32 requireStatusChecks: boolean("require_status_checks").default(false).notNull(),
33 requiredChecks: text("required_checks").default(""), // comma-separated check names
34 requireReviews: boolean("require_reviews").default(false).notNull(),
35 requiredReviewCount: integer("required_review_count").default(1).notNull(),
36 requireUpToDate: boolean("require_up_to_date").default(false).notNull(),
37 dismissStaleReviews: boolean("dismiss_stale_reviews").default(false).notNull(),
38 restrictPush: boolean("restrict_push").default(false).notNull(),
39 allowForcePush: boolean("allow_force_push").default(false).notNull(),
40 allowDeletion: boolean("allow_deletion").default(false).notNull(),
41 isActive: boolean("is_active").default(true).notNull(),
42 createdAt: timestamp("created_at").defaultNow().notNull(),
43 updatedAt: timestamp("updated_at").defaultNow().notNull(),
44 },
45 (table) => [
46 index("branch_protection_repo").on(table.repositoryId),
47 uniqueIndex("branch_protection_repo_pattern").on(
48 table.repositoryId,
49 table.pattern
50 ),
51 ]
52);
53
54// ─── Status Checks (CI Integration) ────────────────────────────────────────
55
56export const statusChecks = pgTable(
57 "status_checks",
58 {
59 id: uuid("id").primaryKey().defaultRandom(),
60 repositoryId: uuid("repository_id")
61 .notNull()
62 .references(() => repositories.id, { onDelete: "cascade" }),
63 commitSha: text("commit_sha").notNull(),
64 context: text("context").notNull(), // e.g. "ci/build", "gatetest/scan"
65 state: text("state").notNull().default("pending"), // pending, success, failure, error
66 description: text("description"),
67 targetUrl: text("target_url"), // link to CI build
68 createdBy: text("created_by"), // token name or integration name
69 createdAt: timestamp("created_at").defaultNow().notNull(),
70 updatedAt: timestamp("updated_at").defaultNow().notNull(),
71 },
72 (table) => [
73 index("status_checks_repo_sha").on(table.repositoryId, table.commitSha),
74 index("status_checks_repo_context").on(table.repositoryId, table.context),
75 ]
76);
77
78// ─── Notifications ──────────────────────────────────────────────────────────
79
80export const notifications = pgTable(
81 "notifications",
82 {
83 id: uuid("id").primaryKey().defaultRandom(),
84 userId: uuid("user_id")
85 .notNull()
86 .references(() => users.id, { onDelete: "cascade" }),
87 type: text("type").notNull(), // issue_comment, pr_review, mention, star, ci_status
88 title: text("title").notNull(),
89 body: text("body"),
90 url: text("url"), // link to the relevant page
91 repositoryId: uuid("repository_id").references(() => repositories.id, {
92 onDelete: "cascade",
93 }),
94 actorId: uuid("actor_id").references(() => users.id),
95 isRead: boolean("is_read").default(false).notNull(),
96 createdAt: timestamp("created_at").defaultNow().notNull(),
97 },
98 (table) => [
99 index("notifications_user_read").on(table.userId, table.isRead),
100 index("notifications_user_created").on(table.userId, table.createdAt),
101 ]
102);
103
104// ─── Organizations ──────────────────────────────────────────────────────────
105
106export const organizations = pgTable("organizations", {
107 id: uuid("id").primaryKey().defaultRandom(),
108 name: text("name").notNull().unique(),
109 displayName: text("display_name"),
110 description: text("description"),
111 avatarUrl: text("avatar_url"),
112 website: text("website"),
113 location: text("location"),
114 isVerified: boolean("is_verified").default(false).notNull(),
115 createdAt: timestamp("created_at").defaultNow().notNull(),
116 updatedAt: timestamp("updated_at").defaultNow().notNull(),
117});
118
119export const orgMembers = pgTable(
120 "org_members",
121 {
122 id: uuid("id").primaryKey().defaultRandom(),
123 orgId: uuid("org_id")
124 .notNull()
125 .references(() => organizations.id, { onDelete: "cascade" }),
126 userId: uuid("user_id")
127 .notNull()
128 .references(() => users.id, { onDelete: "cascade" }),
129 role: text("role").notNull().default("member"), // owner, admin, member
130 createdAt: timestamp("created_at").defaultNow().notNull(),
131 },
132 (table) => [
133 uniqueIndex("org_members_unique").on(table.orgId, table.userId),
134 index("org_members_user").on(table.userId),
135 ]
136);
137
138export const teams = pgTable(
139 "teams",
140 {
141 id: uuid("id").primaryKey().defaultRandom(),
142 orgId: uuid("org_id")
143 .notNull()
144 .references(() => organizations.id, { onDelete: "cascade" }),
145 name: text("name").notNull(),
146 description: text("description"),
147 permission: text("permission").notNull().default("read"), // read, write, admin
148 createdAt: timestamp("created_at").defaultNow().notNull(),
149 },
150 (table) => [
151 uniqueIndex("teams_org_name").on(table.orgId, table.name),
152 ]
153);
154
155export const teamMembers = pgTable(
156 "team_members",
157 {
158 id: uuid("id").primaryKey().defaultRandom(),
159 teamId: uuid("team_id")
160 .notNull()
161 .references(() => teams.id, { onDelete: "cascade" }),
162 userId: uuid("user_id")
163 .notNull()
164 .references(() => users.id, { onDelete: "cascade" }),
165 createdAt: timestamp("created_at").defaultNow().notNull(),
166 },
167 (table) => [
168 uniqueIndex("team_members_unique").on(table.teamId, table.userId),
169 ]
170);
171
172export const teamRepos = pgTable(
173 "team_repos",
174 {
175 id: uuid("id").primaryKey().defaultRandom(),
176 teamId: uuid("team_id")
177 .notNull()
178 .references(() => teams.id, { onDelete: "cascade" }),
179 repositoryId: uuid("repository_id")
180 .notNull()
181 .references(() => repositories.id, { onDelete: "cascade" }),
182 permission: text("permission").notNull().default("read"), // read, write, admin
183 createdAt: timestamp("created_at").defaultNow().notNull(),
184 },
185 (table) => [
186 uniqueIndex("team_repos_unique").on(table.teamId, table.repositoryId),
187 ]
188);
189
190// ─── Type Exports ───────────────────────────────────────────────────────────
191
192export type BranchProtectionRule = typeof branchProtection.$inferSelect;
193export type StatusCheck = typeof statusChecks.$inferSelect;
194export type Notification = typeof notifications.$inferSelect;
195export type Organization = typeof organizations.$inferSelect;
196export type OrgMember = typeof orgMembers.$inferSelect;
197export type Team = typeof teams.$inferSelect;
198export type TeamMember = typeof teamMembers.$inferSelect;