dynamodb_book_ch21_github/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use aliri_braid::braid;
6use compact_str::{format_compact, CompactString};
7use modyne::{keys, Entity, Table};
8use svix_ksuid::Ksuid;
9use time::format_description::well_known::Rfc3339;
10
11#[derive(Clone, Debug)]
12pub struct App {
13    table_name: std::sync::Arc<str>,
14    client: aws_sdk_dynamodb::Client,
15}
16
17impl App {
18    pub fn new(client: aws_sdk_dynamodb::Client) -> Self {
19        Self::new_with_table(client, "GitHubTable")
20    }
21
22    pub fn new_with_table(client: aws_sdk_dynamodb::Client, table_name: &str) -> Self {
23        Self {
24            table_name: std::sync::Arc::from(table_name),
25            client,
26        }
27    }
28}
29
30impl Table for App {
31    type PrimaryKey = keys::Primary;
32    type IndexKeys = (keys::Gsi1, keys::Gsi2, keys::Gsi3);
33
34    fn table_name(&self) -> &str {
35        &self.table_name
36    }
37
38    fn client(&self) -> &aws_sdk_dynamodb::Client {
39        &self.client
40    }
41}
42
43#[braid(serde)]
44pub struct OwnerName(CompactString);
45
46#[braid(serde)]
47pub struct RepoName(CompactString);
48
49#[derive(Clone, Copy, Debug)]
50pub struct RepositoryId<'a> {
51    pub repo_owner: &'a OwnerNameRef,
52    pub repo_name: &'a RepoNameRef,
53}
54
55#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
56pub struct RepositoryIdentity {
57    pub repo_owner: OwnerName,
58    pub repo_name: RepoName,
59}
60
61impl RepositoryIdentity {
62    fn borrowed(&self) -> RepositoryId {
63        RepositoryId {
64            repo_owner: &self.repo_owner,
65            repo_name: &self.repo_name,
66        }
67    }
68}
69
70#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
71pub struct Repository {
72    #[serde(flatten)]
73    pub id: RepositoryIdentity,
74    pub created_at: time::OffsetDateTime,
75    pub updated_at: time::OffsetDateTime,
76    pub issues_and_pull_request_count: u32,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub fork_source: Option<RepositoryIdentity>,
79    pub fork_count: u32,
80    pub star_count: u32,
81}
82
83impl Entity for Repository {
84    type KeyInput<'a> = RepositoryId<'a>;
85    type Table = App;
86    type IndexKeys = (keys::Gsi1, keys::Gsi2, keys::Gsi3);
87
88    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
89        let common = format!("REPO#{}#{}", input.repo_owner, input.repo_name);
90        keys::Primary {
91            hash: common.clone(),
92            range: common,
93        }
94    }
95
96    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
97        let primary = Self::primary_key(self.id.borrowed());
98        let updated_at = self.updated_at.format(&Rfc3339).unwrap();
99
100        let fork_index = if let Some(source) = &self.fork_source {
101            keys::Gsi2 {
102                hash: format!("REPO#{}#{}", source.repo_owner, source.repo_name),
103                range: format!("FORK#{}", self.id.repo_owner),
104            }
105        } else {
106            keys::Gsi2 {
107                hash: primary.hash.clone(),
108                range: format!("#REPO#{}", self.id.repo_name),
109            }
110        };
111
112        keys::FullKey {
113            indexes: (
114                keys::Gsi1 {
115                    hash: primary.hash.clone(),
116                    range: primary.range.clone(),
117                },
118                fork_index,
119                keys::Gsi3 {
120                    hash: format!("ACCOUNT#{}", self.id.repo_owner),
121                    range: format!("#{}", updated_at),
122                },
123            ),
124            primary,
125        }
126    }
127}
128
129#[derive(Clone, Copy, Debug)]
130pub struct IssueId<'a> {
131    repo: RepositoryId<'a>,
132    issue_number: u32,
133}
134
135#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
136pub struct Issue {
137    #[serde(flatten)]
138    pub repo: RepositoryIdentity,
139    pub issue_number: u32,
140    pub created_at: time::OffsetDateTime,
141    pub status: IssueStatus,
142    pub star_count: u32,
143}
144
145impl Entity for Issue {
146    type KeyInput<'a> = IssueId<'a>;
147    type Table = App;
148    type IndexKeys = ();
149
150    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
151        keys::Primary {
152            hash: format!("REPO#{}#{}", input.repo.repo_owner, input.repo.repo_name),
153            range: format!("ISSUE#{:010}", input.issue_number),
154        }
155    }
156
157    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
158        keys::FullKey {
159            primary: Self::primary_key(IssueId {
160                repo: self.repo.borrowed(),
161                issue_number: self.issue_number,
162            }),
163            indexes: (),
164        }
165    }
166}
167
168#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
169pub enum IssueStatus {
170    Open,
171    Closed,
172}
173
174#[derive(Clone, Copy, Debug)]
175pub struct IssueCommentId<'a> {
176    repo: RepositoryId<'a>,
177    issue_number: u32,
178    comment_id: Ksuid,
179}
180
181#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
182pub struct IssueComment {
183    #[serde(flatten)]
184    pub repo: RepositoryIdentity,
185    pub issue_number: u32,
186    pub comment_id: Ksuid,
187    pub created_at: time::OffsetDateTime,
188    pub comment: String,
189    pub star_count: u32,
190}
191
192impl Entity for IssueComment {
193    type KeyInput<'a> = IssueCommentId<'a>;
194    type Table = App;
195    type IndexKeys = ();
196
197    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
198        keys::Primary {
199            hash: format!(
200                "ISSUECOMMENT#{}#{}#{}",
201                input.repo.repo_owner, input.repo.repo_name, input.issue_number
202            ),
203            range: format!("ISSUECOMMENT#{}", input.comment_id),
204        }
205    }
206
207    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
208        keys::FullKey {
209            primary: Self::primary_key(IssueCommentId {
210                repo: self.repo.borrowed(),
211                issue_number: self.issue_number,
212                comment_id: self.comment_id,
213            }),
214            indexes: (),
215        }
216    }
217}
218
219#[derive(Clone, Copy, Debug)]
220pub struct PullRequestId<'a> {
221    repo: RepositoryId<'a>,
222    pull_request_number: u32,
223}
224
225#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
226pub struct PullRequest {
227    #[serde(flatten)]
228    pub repo: RepositoryIdentity,
229    pub pull_request_number: u32,
230    pub created_at: time::OffsetDateTime,
231    pub star_count: u32,
232}
233
234impl Entity for PullRequest {
235    type KeyInput<'a> = PullRequestId<'a>;
236    type Table = App;
237    type IndexKeys = keys::Gsi1;
238
239    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
240        let common = format!(
241            "PR#{}#{}#{:010}",
242            input.repo.repo_owner, input.repo.repo_name, input.pull_request_number
243        );
244        keys::Primary {
245            hash: common.clone(),
246            range: common,
247        }
248    }
249
250    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
251        keys::FullKey {
252            primary: Self::primary_key(PullRequestId {
253                repo: self.repo.borrowed(),
254                pull_request_number: self.pull_request_number,
255            }),
256            indexes: keys::Gsi1 {
257                hash: format!("REPO#{}#{}", self.repo.repo_owner, self.repo.repo_name),
258                range: format!("PR#{:010}", self.pull_request_number),
259            },
260        }
261    }
262}
263
264#[derive(Clone, Copy, Debug)]
265pub struct PullRequestCommentId<'a> {
266    repo: RepositoryId<'a>,
267    pull_request_number: u32,
268    comment_id: Ksuid,
269}
270
271#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
272pub struct PullRequestComment {
273    #[serde(flatten)]
274    pub repo: RepositoryIdentity,
275    pub pull_request_number: u32,
276    pub comment_id: Ksuid,
277    pub created_at: time::OffsetDateTime,
278    pub comment: String,
279    pub star_count: u32,
280}
281
282impl Entity for PullRequestComment {
283    type KeyInput<'a> = PullRequestCommentId<'a>;
284    type Table = App;
285    type IndexKeys = ();
286
287    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
288        keys::Primary {
289            hash: format!(
290                "PRCOMMENT#{}#{}#{}",
291                input.repo.repo_owner, input.repo.repo_name, input.pull_request_number
292            ),
293            range: format!("PRCOMMENT#{}", input.comment_id),
294        }
295    }
296
297    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
298        keys::FullKey {
299            primary: Self::primary_key(PullRequestCommentId {
300                repo: self.repo.borrowed(),
301                pull_request_number: self.pull_request_number,
302                comment_id: self.comment_id,
303            }),
304            indexes: (),
305        }
306    }
307}
308
309#[derive(Clone, Copy, Debug)]
310pub struct StarId<'a> {
311    pub repo: RepositoryId<'a>,
312    pub staring_user: &'a OwnerNameRef,
313}
314
315#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
316pub struct Star {
317    pub repo: RepositoryIdentity,
318    pub staring_user: OwnerName,
319}
320
321impl Entity for Star {
322    type KeyInput<'a> = StarId<'a>;
323    type Table = App;
324    type IndexKeys = ();
325
326    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
327        keys::Primary {
328            hash: format!("REPO#{}#{}", input.repo.repo_owner, input.repo.repo_name),
329            range: format!("STAR#{}", input.staring_user),
330        }
331    }
332
333    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
334        keys::FullKey {
335            primary: Self::primary_key(StarId {
336                repo: self.repo.borrowed(),
337                staring_user: &self.staring_user,
338            }),
339            indexes: (),
340        }
341    }
342}
343
344pub struct ReactionId<'a> {
345    pub repo: RepositoryId<'a>,
346    pub target_type: ReactionTarget,
347    pub reacting_user: &'a OwnerNameRef,
348}
349
350#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
351#[serde(tag = "target_type", content = "target_id")]
352pub enum ReactionTarget {
353    Issue(u32),
354    IssueComment(Ksuid),
355    PullRequest(u32),
356    PullRequestComment(Ksuid),
357}
358
359impl ReactionTarget {
360    fn fmt_components(&self) -> (&'static str, CompactString) {
361        match self {
362            ReactionTarget::Issue(num) => ("ISSUE", format_compact!("{:010}", num)),
363            ReactionTarget::IssueComment(id) => ("ISSUECOMMENT", format_compact!("{}", id)),
364            ReactionTarget::PullRequest(num) => ("PR", format_compact!("{:010}", num)),
365            ReactionTarget::PullRequestComment(id) => ("PRCOMMENT", format_compact!("{}", id)),
366        }
367    }
368}
369
370#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
371pub struct Reaction {
372    #[serde(flatten)]
373    pub repo: RepositoryIdentity,
374    #[serde(flatten)]
375    pub target_type: ReactionTarget,
376    pub reacting_user: OwnerName,
377    #[serde(
378        default,
379        skip_serializing_if = "BTreeSet::is_empty",
380        with = "serde_dynamo::string_set"
381    )]
382    pub reactions: BTreeSet<CompactString>,
383}
384
385impl Entity for Reaction {
386    type KeyInput<'a> = ReactionId<'a>;
387    type Table = App;
388    type IndexKeys = ();
389
390    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
391        let (target_type, target_id) = input.target_type.fmt_components();
392        let common = format!(
393            "{}REACTION#{}#{}#{}#{}",
394            target_type,
395            input.repo.repo_owner,
396            input.repo.repo_name,
397            target_id,
398            input.reacting_user
399        );
400        keys::Primary {
401            hash: common.clone(),
402            range: common,
403        }
404    }
405
406    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
407        keys::FullKey {
408            primary: Self::primary_key(ReactionId {
409                repo: self.repo.borrowed(),
410                target_type: self.target_type,
411                reacting_user: &self.reacting_user,
412            }),
413            indexes: (),
414        }
415    }
416}
417
418#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
419pub struct User {
420    pub username: OwnerName,
421    pub created_at: time::OffsetDateTime,
422    pub organizations: BTreeMap<OwnerName, Role>,
423    pub payment_plan: PaymentPlan,
424}
425
426impl Entity for User {
427    type KeyInput<'a> = &'a OwnerNameRef;
428    type Table = App;
429    type IndexKeys = keys::Gsi3;
430
431    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
432        let common = format!("ACCOUNT#{}", input);
433        keys::Primary {
434            hash: common.clone(),
435            range: common,
436        }
437    }
438
439    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
440        let primary = Self::primary_key(&self.username);
441        keys::FullKey {
442            indexes: keys::Gsi3 {
443                hash: primary.hash.clone(),
444                range: primary.range.clone(),
445            },
446            primary,
447        }
448    }
449}
450
451#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
452pub enum Role {
453    Owner,
454    Member,
455}
456
457#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
458pub struct Organization {
459    pub organization_name: OwnerName,
460    pub created_at: time::OffsetDateTime,
461    pub payment_plan: PaymentPlan,
462}
463
464impl Entity for Organization {
465    type KeyInput<'a> = &'a OwnerNameRef;
466    type Table = App;
467    type IndexKeys = keys::Gsi3;
468
469    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
470        let common = format!("ACCOUNT#{}", input);
471        keys::Primary {
472            hash: common.clone(),
473            range: common,
474        }
475    }
476
477    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
478        let primary = Self::primary_key(&self.organization_name);
479        keys::FullKey {
480            indexes: keys::Gsi3 {
481                hash: primary.hash.clone(),
482                range: primary.range.clone(),
483            },
484            primary,
485        }
486    }
487}
488
489#[derive(Clone, Copy, Debug)]
490pub struct MembershipId<'a> {
491    pub organization: &'a OwnerNameRef,
492    pub username: &'a OwnerNameRef,
493}
494
495#[derive(Clone, Debug, modyne::EntityDef, serde::Serialize, serde::Deserialize)]
496pub struct Membership {
497    pub organization: OwnerName,
498    pub username: OwnerName,
499    pub created_at: time::OffsetDateTime,
500    pub role: Role,
501}
502
503impl Entity for Membership {
504    type KeyInput<'a> = MembershipId<'a>;
505    type Table = App;
506    type IndexKeys = ();
507
508    fn primary_key(input: Self::KeyInput<'_>) -> keys::Primary {
509        keys::Primary {
510            hash: format!("ACCOUNT#{}", input.organization),
511            range: format!("MEMBERSHIP#{}", input.username),
512        }
513    }
514
515    fn full_key(&self) -> keys::FullKey<keys::Primary, Self::IndexKeys> {
516        keys::FullKey {
517            primary: Self::primary_key(MembershipId {
518                organization: &self.organization,
519                username: &self.username,
520            }),
521            indexes: (),
522        }
523    }
524}
525
526#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
527pub struct PaymentPlan {
528    pub plan_type: PlanType,
529    pub plan_start_date: time::OffsetDateTime,
530}
531
532#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
533pub enum PlanType {
534    Free,
535    Pro,
536    Enterprise,
537}