1use crate::Result;
2use fjall::{Keyspace, PartitionHandle, PersistMode};
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5use tracing::{debug, info};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub enum MappingStatus {
9 Pending, Synced, Failed, Stale, }
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct TaskMapping {
17 pub linear_issue_id: String,
18 pub motion_task_id: Option<String>, pub sync_source: String,
20 pub status: MappingStatus,
21 pub linear_issue_data: serde_json::Value, pub created_at: chrono::DateTime<chrono::Utc>,
23 pub updated_at: chrono::DateTime<chrono::Utc>,
24 pub last_sync_attempt: Option<chrono::DateTime<chrono::Utc>>,
25 pub sync_error: Option<String>,
26}
27
28pub struct MappingStore {
29 keyspace: Keyspace,
30 mappings: PartitionHandle,
31}
32
33impl MappingStore {
34 pub async fn new<P: AsRef<Path>>(db_path: P) -> Result<Self> {
35 let keyspace = fjall::Config::new(db_path).open()?;
36 let mappings =
37 keyspace.open_partition("task_mappings", fjall::PartitionCreateOptions::default())?;
38
39 debug!("task mapping store initialized");
40
41 Ok(Self { keyspace, mappings })
42 }
43
44 pub async fn store_mapping(&self, mapping: TaskMapping) -> Result<()> {
45 let key = format!("{}:{}", mapping.sync_source, mapping.linear_issue_id);
46 let value = serde_json::to_vec(&mapping)?;
47
48 self.mappings.insert(&key, &value)?;
49 debug!(
50 "Stored mapping: {} -> {:?} (status: {:?})",
51 mapping.linear_issue_id, mapping.motion_task_id, mapping.status
52 );
53
54 Ok(())
55 }
56
57 pub async fn get_mapping_by_linear_id(
58 &self,
59 sync_source: &str,
60 linear_issue_id: &str,
61 ) -> Result<Option<TaskMapping>> {
62 let key = format!("{}:{}", sync_source, linear_issue_id);
63
64 match self.mappings.get(&key)? {
65 Some(value) => {
66 let mapping: TaskMapping = serde_json::from_slice(&value)?;
67 debug!(
68 "Found mapping: {} -> {:?}",
69 linear_issue_id, mapping.motion_task_id
70 );
71 Ok(Some(mapping))
72 }
73 None => {
74 debug!("No mapping found for Linear issue: {}", linear_issue_id);
75 Ok(None)
76 }
77 }
78 }
79
80 pub async fn get_mapping_by_motion_id(
81 &self,
82 motion_task_id: &str,
83 ) -> Result<Option<TaskMapping>> {
84 for item in self.mappings.iter() {
87 let (_, value) = item?;
88 let mapping: TaskMapping = serde_json::from_slice(&value)?;
89
90 if mapping.motion_task_id.as_deref() == Some(motion_task_id) {
91 debug!(
92 "Found mapping by Motion ID: {} -> {}",
93 motion_task_id, mapping.linear_issue_id
94 );
95 return Ok(Some(mapping));
96 }
97 }
98
99 debug!("No mapping found for Motion task: {}", motion_task_id);
100 Ok(None)
101 }
102
103 pub async fn remove_mapping(
104 &self,
105 sync_source: &str,
106 linear_issue_id: &str,
107 ) -> Result<Option<TaskMapping>> {
108 let key = format!("{}:{}", sync_source, linear_issue_id);
109
110 let existing = match self.mappings.get(&key)? {
111 Some(value) => Some(serde_json::from_slice(&value)?),
112 None => None,
113 };
114
115 if existing.is_some() {
116 self.mappings.remove(&key)?;
117 debug!("Removed mapping for Linear issue: {}", linear_issue_id);
118 }
119
120 Ok(existing)
121 }
122
123 pub async fn list_mappings_by_source(&self, sync_source: &str) -> Result<Vec<TaskMapping>> {
124 let prefix = format!("{}:", sync_source);
125 let mut mappings = Vec::new();
126
127 for item in self.mappings.iter() {
128 let (key, value) = item?;
129 let key_str = String::from_utf8_lossy(&key);
130
131 if key_str.starts_with(&prefix) {
132 let mapping: TaskMapping = serde_json::from_slice(&value)?;
133 mappings.push(mapping);
134 }
135 }
136
137 debug!(
138 "Found {} mappings for sync source: {}",
139 mappings.len(),
140 sync_source
141 );
142 Ok(mappings)
143 }
144
145 pub async fn list_all_mappings(&self) -> Result<Vec<TaskMapping>> {
146 let mut mappings = Vec::new();
147
148 for item in self.mappings.iter() {
149 let (_, value) = item?;
150 let mapping: TaskMapping = serde_json::from_slice(&value)?;
151 mappings.push(mapping);
152 }
153
154 debug!("Found {} total mappings", mappings.len());
155 Ok(mappings)
156 }
157
158 pub async fn update_mapping(&self, mapping: TaskMapping) -> Result<()> {
159 let updated_mapping = TaskMapping {
160 updated_at: chrono::Utc::now(),
161 ..mapping
162 };
163
164 self.store_mapping(updated_mapping).await
165 }
166
167 pub async fn create_pending_mapping(
169 &self,
170 sync_source: &str,
171 issue: &crate::clients::linear::LinearIssue,
172 ) -> Result<TaskMapping> {
173 let mapping = TaskMapping {
174 linear_issue_id: issue.id.clone(),
175 motion_task_id: None,
176 sync_source: sync_source.to_string(),
177 status: MappingStatus::Pending,
178 linear_issue_data: serde_json::to_value(issue)?,
179 created_at: chrono::Utc::now(),
180 updated_at: chrono::Utc::now(),
181 last_sync_attempt: None,
182 sync_error: None,
183 };
184
185 self.store_mapping(mapping.clone()).await?;
186 debug!("Created pending mapping for Linear issue: {}", issue.id);
187 Ok(mapping)
188 }
189
190 pub async fn mark_synced(
192 &self,
193 sync_source: &str,
194 linear_issue_id: &str,
195 motion_task_id: String,
196 ) -> Result<()> {
197 if let Some(mut mapping) = self
198 .get_mapping_by_linear_id(sync_source, linear_issue_id)
199 .await?
200 {
201 mapping.motion_task_id = Some(motion_task_id);
202 mapping.status = MappingStatus::Synced;
203 mapping.updated_at = chrono::Utc::now();
204 mapping.sync_error = None;
205
206 self.store_mapping(mapping).await?;
207 debug!("Marked mapping as synced: {}", linear_issue_id);
208 }
209 Ok(())
210 }
211
212 pub async fn mark_failed(
214 &self,
215 sync_source: &str,
216 linear_issue_id: &str,
217 error: String,
218 ) -> Result<()> {
219 if let Some(mut mapping) = self
220 .get_mapping_by_linear_id(sync_source, linear_issue_id)
221 .await?
222 {
223 mapping.status = MappingStatus::Failed;
224 mapping.updated_at = chrono::Utc::now();
225 mapping.last_sync_attempt = Some(chrono::Utc::now());
226 mapping.sync_error = Some(error);
227
228 self.store_mapping(mapping).await?;
229 debug!("Marked mapping as failed: {}", linear_issue_id);
230 }
231 Ok(())
232 }
233
234 pub async fn update_issue_data(
236 &self,
237 sync_source: &str,
238 linear_issue_id: &str,
239 issue: &crate::clients::linear::LinearIssue,
240 ) -> Result<()> {
241 if let Some(mut mapping) = self
242 .get_mapping_by_linear_id(sync_source, linear_issue_id)
243 .await?
244 {
245 mapping.linear_issue_data = serde_json::to_value(issue)?;
246 mapping.updated_at = chrono::Utc::now();
247
248 self.store_mapping(mapping).await?;
249 debug!("Updated issue data for mapping: {}", linear_issue_id);
250 }
251 Ok(())
252 }
253
254 pub async fn list_mappings_by_status(&self, status: MappingStatus) -> Result<Vec<TaskMapping>> {
256 let mut mappings = Vec::new();
257
258 for item in self.mappings.iter() {
259 let (_, value) = item?;
260 let mapping: TaskMapping = serde_json::from_slice(&value)?;
261
262 if std::mem::discriminant(&mapping.status) == std::mem::discriminant(&status) {
263 mappings.push(mapping);
264 }
265 }
266
267 debug!(
268 "Found {} mappings with status: {:?}",
269 mappings.len(),
270 status
271 );
272 Ok(mappings)
273 }
274
275 pub async fn flush(&self) -> Result<()> {
276 self.keyspace.persist(PersistMode::SyncAll)?;
277 Ok(())
278 }
279}
280
281impl TaskMapping {
282 pub fn new(linear_issue_id: String, motion_task_id: String, sync_source: String) -> Self {
283 let now = chrono::Utc::now();
284
285 Self {
286 linear_issue_id,
287 motion_task_id: Some(motion_task_id),
288 sync_source,
289 status: MappingStatus::Synced,
290 linear_issue_data: serde_json::Value::Null, created_at: now,
292 updated_at: now,
293 last_sync_attempt: Some(now),
294 sync_error: None,
295 }
296 }
297}