flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/identity/src/directory.rs

114 lines3,849 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Search across all of g1t, Explore, and a command palette1//! What other services may know of every account and workspace: the
2//! directory search indexes, and the events that keep it current.
3//!
4//! Only what public pages already show goes out: a username or slug, a
5//! name, a bio or description, an avatar. Never an email address, and
6//! never who belongs to which workspace.
7
8use g1t_contracts::events::{NewEvent, Publish, UserUpdated, WorkspaceUpdated};
9use g1t_contracts::identity::{DirectoryArgs, DirectoryEntry, DirectoryPage};
10use serde::{Deserialize, Serialize};
11use worker::Result;
12
13use crate::Identity;
14
15const SOURCE: &str = "identity";
16/// The most entries on one page of the directory.
17const MAX_PAGE: u32 = 200;
18
19#[derive(Deserialize)]
20struct Row {
21 id: String,
22 slug: String,
23 name: Option<String>,
24 bio: Option<String>,
25 avatar: Option<String>,
26 created_at: String,
27}
28
29impl Identity {
30 pub async fn directory(&self, a: DirectoryArgs) -> Result<DirectoryPage> {
31 let limit = a.limit.clamp(1, MAX_PAGE);
32 let sql = match a.kind.as_str() {
33 "user" => {
34 "SELECT id, username AS slug, display_name AS name, bio, avatar, created_at
35 FROM users WHERE username > ? ORDER BY username LIMIT ?"
36 }
37 "workspace" => {
38 "SELECT id, slug, name, description AS bio, avatar, created_at
39 FROM workspaces WHERE slug > ? ORDER BY slug LIMIT ?"
40 }
41 _ => return Ok(DirectoryPage::default()),
42 };
43 let rows = self
44 .db
45 .prepare(sql)
46 .bind(&[a.after.unwrap_or_default().into(), limit.into()])?
47 .all()
48 .await?
49 .results::<Row>()?;
50 let next = (rows.len() == limit as usize).then(|| rows.last().map(|row| row.slug.clone())).flatten();
51 Ok(DirectoryPage {
52 entries: rows
53 .into_iter()
54 .map(|row| DirectoryEntry {
55 id: row.id,
56 slug: row.slug,
57 name: row.name,
58 bio: row.bio,
59 avatar: row.avatar,
60 created_at: row.created_at,
61 })
62 .collect(),
63 next,
64 })
65 }
66
67 /// Tells other services that what an account's profile shows changed.
68 /// Best effort: the change has happened, so a failure is logged.
69 pub async fn announce_user(&self, username: &str, actor: Option<&str>) {
70 self.announce(
71 "user.updated",
72 actor,
73 UserUpdated {
74 username: username.to_owned(),
75 },
76 )
77 .await;
78 }
79
80 /// Tells other services a workspace was made or changed.
81 pub async fn announce_workspace(&self, workspace_id: &str, slug: &str, actor: Option<&str>) {
82 self.announce(
83 "workspace.updated",
84 actor,
85 WorkspaceUpdated {
86 workspace_id: workspace_id.to_owned(),
87 slug: slug.to_owned(),
88 },
89 )
90 .await;
91 }
92
93 async fn announce<T: Serialize>(&self, kind: &'static str, actor: Option<&str>, data: T) {
94 let events = match self.env.service("EVENTS") {
95 Ok(events) => events,
96 Err(error) => {
97 worker::console_error!("{kind} not published: {error}");
98 return;
99 }
100 };
101 let publish = Publish {
102 events: vec![NewEvent {
103 kind,
104 source: SOURCE,
105 repo_id: None,
106 actor: actor.map(str::to_owned),
107 data,
108 }],
109 };
110 if let Err(error) = g1t_kit::call::<_, serde_json::Value>(&events, "publish", &publish).await {
111 worker::console_error!("{kind} not published: {error}");
112 }
113 }
114}