Undisclosed project number 1234
1use jacquard_common::types::collection::Collection;
2use jacquard_lexicon::schema::LexiconSchema;
3use serde::{Deserialize, Serialize};
4use serde_json::Value as Json;
5use superjam_core::{AtUri, Cid, Did, Nsid, Rkey};
6
7use crate::client::Pds;
8use crate::error::{Error, Result};
9use crate::newtype::WriteOpCount;
10
11pub const APPLY_WRITES_MAX: WriteOpCount = WriteOpCount::new(200);
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct RecordRef {
15 pub uri: AtUri,
16 pub cid: Cid,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum WriteResult {
21 Created(RecordRef),
22 Updated(RecordRef),
23 Deleted,
24}
25
26#[derive(Debug, Clone, Serialize)]
27#[serde(transparent)]
28pub struct Write(WriteInner);
29
30#[derive(Debug, Clone, Serialize)]
31#[serde(tag = "$type")]
32enum WriteInner {
33 #[serde(rename = "com.atproto.repo.applyWrites#create")]
34 Create {
35 collection: Nsid,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 rkey: Option<Rkey>,
38 value: Json,
39 },
40 #[serde(rename = "com.atproto.repo.applyWrites#update")]
41 Update {
42 collection: Nsid,
43 rkey: Rkey,
44 value: Json,
45 },
46 #[serde(rename = "com.atproto.repo.applyWrites#delete")]
47 Delete { collection: Nsid, rkey: Rkey },
48}
49
50impl Write {
51 pub fn create<R>(rkey: Option<Rkey>, record: &R) -> Result<Self>
52 where
53 R: Collection + Serialize + LexiconSchema,
54 {
55 record
56 .validate()
57 .map_err(|e| Error::LexiconValidation(e.to_string()))?;
58 Ok(Self(WriteInner::Create {
59 collection: collection_of::<R>(),
60 rkey,
61 value: serde_json::to_value(record)?,
62 }))
63 }
64
65 pub fn update<R>(rkey: Rkey, record: &R) -> Result<Self>
66 where
67 R: Collection + Serialize + LexiconSchema,
68 {
69 record
70 .validate()
71 .map_err(|e| Error::LexiconValidation(e.to_string()))?;
72 Ok(Self(WriteInner::Update {
73 collection: collection_of::<R>(),
74 rkey,
75 value: serde_json::to_value(record)?,
76 }))
77 }
78
79 pub fn delete<R: Collection>(rkey: Rkey) -> Self {
80 Self(WriteInner::Delete {
81 collection: collection_of::<R>(),
82 rkey,
83 })
84 }
85}
86
87fn collection_of<R: Collection>() -> Nsid {
88 Nsid::new_static(R::NSID).expect("codegen-generated NSID is well-formed")
89}
90
91impl<S> Pds<S>
92where
93 S: jacquard_common::session::SessionStore<Did, superjam_oauth::Session>
94 + Clone
95 + Send
96 + Sync
97 + 'static,
98{
99 pub async fn create_record<R>(&mut self, rkey: Option<&Rkey>, record: &R) -> Result<RecordRef>
100 where
101 R: Collection + Serialize + LexiconSchema,
102 {
103 self.validate_record(record)?;
104 let nsid =
105 Nsid::new_static("com.atproto.repo.createRecord").expect("static NSID well-formed");
106 let url = self.xrpc_url(&nsid)?;
107 let body = RecordRequest {
108 repo: self.did().clone(),
109 collection: collection_of::<R>(),
110 rkey,
111 record,
112 validate: None,
113 };
114 let resp: RecordResponse = self.post_json(url, &body).await?;
115 Ok(RecordRef {
116 uri: resp.uri,
117 cid: resp.cid,
118 })
119 }
120
121 pub async fn apply_writes(&mut self, writes: Vec<Write>) -> Result<Vec<WriteResult>> {
122 let count = WriteOpCount::new(writes.len());
123 if writes.is_empty() {
124 return Err(Error::ApplyWritesEmpty);
125 }
126 if count > APPLY_WRITES_MAX {
127 return Err(Error::ApplyWritesLimit {
128 got: count,
129 max: APPLY_WRITES_MAX,
130 });
131 }
132 let nsid =
133 Nsid::new_static("com.atproto.repo.applyWrites").expect("static NSID well-formed");
134 let url = self.xrpc_url(&nsid)?;
135 let body = ApplyWritesRequest {
136 repo: self.did().clone(),
137 validate: None,
138 writes: &writes,
139 };
140 let resp: ApplyWritesResponse = self.post_json(url, &body).await?;
141 Ok(resp.results.into_iter().map(WriteResult::from).collect())
142 }
143}
144
145impl From<ApplyWritesRawResult> for WriteResult {
146 fn from(r: ApplyWritesRawResult) -> Self {
147 match r {
148 ApplyWritesRawResult::Create { uri, cid } => {
149 WriteResult::Created(RecordRef { uri, cid })
150 }
151 ApplyWritesRawResult::Update { uri, cid } => {
152 WriteResult::Updated(RecordRef { uri, cid })
153 }
154 ApplyWritesRawResult::Delete {} => WriteResult::Deleted,
155 }
156 }
157}
158
159#[derive(Serialize)]
160#[serde(rename_all = "camelCase")]
161struct RecordRequest<'a, R: Serialize + ?Sized> {
162 repo: Did,
163 collection: Nsid,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 rkey: Option<&'a Rkey>,
166 record: &'a R,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 validate: Option<bool>,
169}
170
171#[derive(Serialize)]
172#[serde(rename_all = "camelCase")]
173struct ApplyWritesRequest<'a> {
174 repo: Did,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 validate: Option<bool>,
177 writes: &'a [Write],
178}
179
180#[derive(Deserialize)]
181#[serde(rename_all = "camelCase")]
182struct RecordResponse {
183 uri: AtUri,
184 cid: Cid,
185}
186
187#[derive(Deserialize)]
188struct ApplyWritesResponse {
189 #[serde(default)]
190 results: Vec<ApplyWritesRawResult>,
191}
192
193#[derive(Deserialize)]
194#[serde(tag = "$type")]
195enum ApplyWritesRawResult {
196 #[serde(rename = "com.atproto.repo.applyWrites#createResult")]
197 Create { uri: AtUri, cid: Cid },
198 #[serde(rename = "com.atproto.repo.applyWrites#updateResult")]
199 Update { uri: AtUri, cid: Cid },
200 #[serde(rename = "com.atproto.repo.applyWrites#deleteResult")]
201 Delete {},
202}