fabrica

hanna/fabrica

15221 bytes
Raw
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! In-memory representations of the persisted entities.
6//!
7//! These are plain data types with no behaviour and no I/O — [`crate::store`]
8//! (the `store` crate) is responsible for reading and writing them. Timestamps
9//! are Unix milliseconds UTC ([`Timestamp`]); identifiers are ULID strings.
10//!
11//! Storage-only columns (the normalized `*_lower` uniqueness keys) are
12//! deliberately absent: they are an implementation detail of the schema, not
13//! part of the domain.
14
15/// A Unix timestamp in **milliseconds** UTC, matching the `BIGINT` columns in
16/// the schema.
17pub type Timestamp = i64;
18
19/// The kind of a registered public key: an SSH key (used for both push auth and
20/// signature verification) or a GPG key (signature verification only). Stored as
21/// the `keys.kind` text `'ssh'` / `'gpg'`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum KeyKind {
24 /// An `OpenSSH` public key.
25 Ssh,
26 /// An `OpenPGP` (GPG) public key.
27 Gpg,
28}
29
30impl KeyKind {
31 /// The lowercase database token (`"ssh"` / `"gpg"`).
32 #[must_use]
33 pub fn as_str(self) -> &'static str {
34 match self {
35 Self::Ssh => "ssh",
36 Self::Gpg => "gpg",
37 }
38 }
39
40 /// Parse from the database token, case-insensitively.
41 #[must_use]
42 pub fn from_token(s: &str) -> Option<Self> {
43 match s.to_ascii_lowercase().as_str() {
44 "ssh" => Some(Self::Ssh),
45 "gpg" => Some(Self::Gpg),
46 _ => None,
47 }
48 }
49}
50
51impl std::fmt::Display for KeyKind {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.write_str(self.as_str())
54 }
55}
56
57/// A repository's visibility, GitLab-style. Stored as the `repos.visibility`
58/// text token.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum Visibility {
61 /// Visible to everyone, anonymous included (subject to `allow_anonymous`).
62 Public,
63 /// Visible to any signed-in user, but not to anonymous visitors.
64 Internal,
65 /// Visible only to the owner, collaborators, and site admins.
66 #[default]
67 Private,
68}
69
70impl Visibility {
71 /// The lowercase database/wire token.
72 #[must_use]
73 pub fn as_str(self) -> &'static str {
74 match self {
75 Self::Public => "public",
76 Self::Internal => "internal",
77 Self::Private => "private",
78 }
79 }
80
81 /// Parse from the token, case-insensitively.
82 #[must_use]
83 pub fn from_token(s: &str) -> Option<Self> {
84 match s.to_ascii_lowercase().as_str() {
85 "public" => Some(Self::Public),
86 "internal" => Some(Self::Internal),
87 "private" => Some(Self::Private),
88 _ => None,
89 }
90 }
91
92 /// Whether the repository is private (only owner/collaborators/admins).
93 #[must_use]
94 pub fn is_private(self) -> bool {
95 matches!(self, Self::Private)
96 }
97
98 /// A rank where higher is more visible (`private` < `internal` < `public`).
99 /// Used to cap a fork's visibility at its parent's.
100 #[must_use]
101 pub fn rank(self) -> u8 {
102 match self {
103 Self::Private => 0,
104 Self::Internal => 1,
105 Self::Public => 2,
106 }
107 }
108}
109
110impl std::fmt::Display for Visibility {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.write_str(self.as_str())
113 }
114}
115
116/// One of a user's email addresses. Exactly one per user is primary; the primary
117/// is mirrored into [`User::email`].
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct UserEmail {
120 /// ULID primary key.
121 pub id: String,
122 /// Owning user.
123 pub user_id: String,
124 /// The address as entered.
125 pub email: String,
126 /// Whether this is the user's primary address.
127 pub is_primary: bool,
128 /// When the address was verified, or `None` if still unverified.
129 pub verified_at: Option<Timestamp>,
130 /// Creation time.
131 pub created_at: Timestamp,
132}
133
134impl UserEmail {
135 /// Whether the address has been verified.
136 #[must_use]
137 pub fn verified(&self) -> bool {
138 self.verified_at.is_some()
139 }
140}
141
142/// A registered account.
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct User {
145 /// ULID primary key.
146 pub id: String,
147 /// The name as the user typed it (case preserved for display).
148 pub username: String,
149 /// The primary email address (denormalized from [`UserEmail`] for quick
150 /// access and case-insensitive login/uniqueness).
151 pub email: String,
152 /// Whether the primary email is shown on the public profile.
153 pub email_public: bool,
154 /// Optional human-friendly display name.
155 pub display_name: Option<String>,
156 /// Optional pronouns, shown on the profile (e.g. `they/them`).
157 pub pronouns: Option<String>,
158 /// Optional free-text bio shown on the profile.
159 pub bio: Option<String>,
160 /// Optional location string.
161 pub location: Option<String>,
162 /// Up to five arbitrary profile links (their brand icons are inferred from
163 /// the URL host).
164 pub links: Vec<String>,
165 /// Content type of the uploaded avatar, or `None` to use a generated
166 /// identicon. The bytes live on disk, not in this row.
167 pub avatar_mime: Option<String>,
168 /// Argon2id PHC hash, or `None` until an invite is completed.
169 pub password_hash: Option<String>,
170 /// Whether the user must set a new password on next login.
171 pub must_change_password: bool,
172 /// Whether the user is a site administrator.
173 pub is_admin: bool,
174 /// When the account was disabled, if it is.
175 pub disabled_at: Option<Timestamp>,
176 /// Creation time.
177 pub created_at: Timestamp,
178 /// Last modification time.
179 pub updated_at: Timestamp,
180}
181
182/// A pending activation or password-reset invite. The emailed token is never
183/// stored; `token_hash` is its SHA-256, matched on redemption.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct Invite {
186 /// ULID primary key.
187 pub id: String,
188 /// The user this invite activates.
189 pub user_id: String,
190 /// SHA-256 (hex) of the emailed token.
191 pub token_hash: String,
192 /// `"activate"` or `"reset"`.
193 pub purpose: String,
194 /// When the invite expires.
195 pub expires_at: Timestamp,
196 /// When it was redeemed, if it has been (single-use).
197 pub used_at: Option<Timestamp>,
198 /// Creation time.
199 pub created_at: Timestamp,
200}
201
202/// A nested, user-owned group. Groups provide the repository hierarchy; `path` is
203/// the materialized `group/sub/leaf` under the owner.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct Group {
206 /// ULID primary key.
207 pub id: String,
208 /// Owning user.
209 pub owner_id: String,
210 /// Parent group, or `None` for a top-level group.
211 pub parent_id: Option<String>,
212 /// The group's own name (the final path segment).
213 pub name: String,
214 /// Materialized full path within the owner, e.g. `group/sub/leaf`.
215 pub path: String,
216 /// Optional description.
217 pub description: Option<String>,
218 /// Whether the group was created explicitly (persists) rather than
219 /// auto-created to hold a repo (garbage-collected when empty).
220 pub manual: bool,
221 /// The group's visibility (gates its listing page).
222 pub visibility: Visibility,
223 /// Creation time.
224 pub created_at: Timestamp,
225}
226
227/// A registered public key, for SSH push authentication and/or signature
228/// verification.
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct Key {
231 /// ULID primary key.
232 pub id: String,
233 /// Owning user.
234 pub user_id: String,
235 /// SSH or GPG.
236 pub kind: KeyKind,
237 /// Optional label.
238 pub name: Option<String>,
239 /// `SHA256:…` for SSH, full hex for GPG.
240 pub fingerprint: String,
241 /// The `authorized_keys` line (SSH) or ASCII-armored block (GPG).
242 pub public_key: String,
243 /// Stable 1-based index within `(user, kind)`, used by `key del`. Deleting one
244 /// key never renumbers the others.
245 pub ordinal: i64,
246 /// When the key was last used to authenticate, if ever.
247 pub last_used_at: Option<Timestamp>,
248 /// When ownership of the key was verified, or `None` if unverified. SSH keys
249 /// verify on first successful SSH auth; GPG keys via a signed challenge.
250 pub verified_at: Option<Timestamp>,
251 /// Creation time.
252 pub created_at: Timestamp,
253}
254
255impl Key {
256 /// Whether ownership of the key has been verified.
257 #[must_use]
258 pub fn verified(&self) -> bool {
259 self.verified_at.is_some()
260 }
261}
262
263/// An API token record. The JWT itself is never stored; this row exists so a token
264/// can be listed and revoked (`revoked_at`), which the API checks on every request.
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct ApiToken {
267 /// ULID primary key, and the JWT `jti`.
268 pub id: String,
269 /// Owning user.
270 pub user_id: String,
271 /// Human label.
272 pub name: String,
273 /// Comma-separated scope list.
274 pub scopes: String,
275 /// Expiry, or `None` for a non-expiring token.
276 pub expires_at: Option<Timestamp>,
277 /// When the token was revoked, if it has been.
278 pub revoked_at: Option<Timestamp>,
279 /// When the token was last used, if ever.
280 pub last_used_at: Option<Timestamp>,
281 /// Creation time.
282 pub created_at: Timestamp,
283}
284
285/// A per-repo issue/PR label, optionally **scoped** (`kind: value`, e.g.
286/// `type: backend`). Scoped labels are mutually exclusive within their scope.
287#[derive(Debug, Clone, PartialEq, Eq)]
288pub struct Label {
289 /// ULID primary key.
290 pub id: String,
291 /// Owning repository.
292 pub repo_id: String,
293 /// The label name, e.g. `bug` or `type: backend`.
294 pub name: String,
295 /// Hex colour (`#rrggbb`).
296 pub color: String,
297 /// Optional description.
298 pub description: Option<String>,
299 /// Creation time.
300 pub created_at: Timestamp,
301}
302
303impl Label {
304 /// The label's scope (`kind`) when it is `kind: value`, else `None`.
305 #[must_use]
306 pub fn scope(&self) -> Option<&str> {
307 self.name.split_once(": ").map(|(scope, _)| scope.trim())
308 }
309}
310
311/// Whether an issue/PR is open or closed.
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum IssueState {
314 /// Open.
315 Open,
316 /// Closed.
317 Closed,
318}
319
320impl IssueState {
321 /// The database token (`open` / `closed`).
322 #[must_use]
323 pub fn as_str(self) -> &'static str {
324 match self {
325 Self::Open => "open",
326 Self::Closed => "closed",
327 }
328 }
329
330 /// Parse from the database token.
331 #[must_use]
332 pub fn from_token(s: &str) -> Self {
333 if s == "closed" {
334 Self::Closed
335 } else {
336 Self::Open
337 }
338 }
339}
340
341/// An issue or (when `is_pull`) a pull request. Both share the `issues` table and
342/// the per-repo number counter, so `#N` is unique across both.
343#[derive(Debug, Clone, PartialEq, Eq)]
344pub struct Issue {
345 /// ULID primary key.
346 pub id: String,
347 /// Owning repository.
348 pub repo_id: String,
349 /// The per-repo number shown as `#N`.
350 pub number: Timestamp,
351 /// The author.
352 pub author_id: String,
353 /// Title.
354 pub title: String,
355 /// Markdown body.
356 pub body: String,
357 /// Open or closed.
358 pub state: IssueState,
359 /// Whether this row is a pull request.
360 pub is_pull: bool,
361 /// The assignee, if any.
362 pub assignee_id: Option<String>,
363 /// Creation time.
364 pub created_at: Timestamp,
365 /// Last update time.
366 pub updated_at: Timestamp,
367 /// When it was closed, if it is.
368 pub closed_at: Option<Timestamp>,
369 /// When the conversation was locked, or `None` if unlocked.
370 pub locked_at: Option<Timestamp>,
371}
372
373impl Issue {
374 /// Whether the conversation is locked (only writers may comment).
375 #[must_use]
376 pub fn locked(&self) -> bool {
377 self.locked_at.is_some()
378 }
379}
380
381/// A comment on an issue or pull request.
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct Comment {
384 /// ULID primary key.
385 pub id: String,
386 /// The issue/PR this comment belongs to.
387 pub issue_id: String,
388 /// The author.
389 pub author_id: String,
390 /// Markdown body.
391 pub body: String,
392 /// Creation time.
393 pub created_at: Timestamp,
394 /// Last edit time.
395 pub updated_at: Timestamp,
396}
397
398/// The pull-request side table for an `issues` row with `is_pull = 1`.
399#[derive(Debug, Clone, PartialEq, Eq)]
400pub struct PullRequest {
401 /// The `issues` row id.
402 pub issue_id: String,
403 /// The base ref (merged into).
404 pub base_ref: String,
405 /// The head ref (source of the changes).
406 pub head_ref: String,
407 /// When it was merged, if it has been.
408 pub merged_at: Option<Timestamp>,
409 /// Who merged it.
410 pub merged_by: Option<String>,
411 /// The resulting merge commit sha.
412 pub merge_sha: Option<String>,
413 /// The strategy used (`merge` | `squash` | `rebase`).
414 pub merge_strategy: Option<String>,
415}
416
417/// A git repository. On disk it lives at `{repo_dir}/{id[0..2]}/{id}.git`; the
418/// database is the sole authority mapping `(owner, path)` to this id.
419// Several independent feature toggles (issues/pulls/releases/mirror) are flags,
420// not a state machine — a struct of bools is the clearest representation.
421#[allow(clippy::struct_excessive_bools)]
422#[derive(Debug, Clone, PartialEq, Eq)]
423pub struct Repo {
424 /// ULID primary key, and the on-disk directory stem.
425 pub id: String,
426 /// Owning user.
427 pub owner_id: String,
428 /// Containing group, or `None` when the repo hangs directly off the owner.
429 pub group_id: Option<String>,
430 /// The repo's own name (the final path segment).
431 pub name: String,
432 /// Materialized full path within the owner, e.g. `group/sub/repo`; equal to
433 /// `name` when ungrouped.
434 pub path: String,
435 /// Optional one-line description.
436 pub description: Option<String>,
437 /// Who may see the repository (public / internal / private).
438 pub visibility: Visibility,
439 /// The branch shown by default and used for HEAD.
440 pub default_branch: String,
441 /// The object hash algorithm (`sha1` or `sha256`), fixed at creation.
442 pub object_format: String,
443 /// Whether issues are enabled for this repository.
444 pub issues_enabled: bool,
445 /// Whether pull requests are enabled for this repository.
446 pub pulls_enabled: bool,
447 /// Whether releases are enabled for this repository.
448 pub releases_enabled: bool,
449 /// Whether this repository is a mirror (kept in sync from a remote).
450 pub is_mirror: bool,
451 /// The repository this one was forked from, if any.
452 pub fork_parent_id: Option<String>,
453 /// The next issue/PR number to allocate (shared counter).
454 pub next_iid: Timestamp,
455 /// When the repo was archived (read-only), if it is.
456 pub archived_at: Option<Timestamp>,
457 /// Approximate on-disk size in bytes, refreshed by maintenance.
458 pub size_bytes: i64,
459 /// Time of the last push, if any.
460 pub pushed_at: Option<Timestamp>,
461 /// Creation time.
462 pub created_at: Timestamp,
463 /// Last modification time.
464 pub updated_at: Timestamp,
465}