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}