1use anyhow::{anyhow, Result};
2use serde::Deserialize;
3use std::io::BufRead;
4use std::path::Path;
5
6use crate::db;
7
8#[derive(Deserialize)]
9struct ImportTask {
10 id: String,
11 title: String,
12 #[serde(default)]
13 description: String,
14 #[serde(rename = "type", default = "default_type")]
15 task_type: String,
16 #[serde(default = "default_priority")]
17 priority: String,
18 #[serde(default = "default_status")]
19 status: String,
20 #[serde(default = "default_effort")]
21 effort: String,
22 #[serde(default)]
23 parent: Option<String>,
24 created_at: String,
25 updated_at: String,
26 #[serde(default)]
27 deleted_at: Option<String>,
28 #[serde(default)]
29 labels: Vec<String>,
30 #[serde(default)]
31 blockers: Vec<String>,
32 #[serde(default)]
33 logs: Vec<ImportLog>,
34}
35
36#[derive(Deserialize)]
37struct ImportLog {
38 id: String,
39 timestamp: String,
40 message: String,
41}
42
43fn default_type() -> String {
44 "task".into()
45}
46fn default_priority() -> String {
47 "medium".into()
48}
49fn default_status() -> String {
50 "open".into()
51}
52fn default_effort() -> String {
53 "medium".into()
54}
55
56pub fn run(root: &Path, file: &str) -> Result<()> {
57 let store = db::open(root)?;
58
59 let reader: Box<dyn BufRead> = if file == "-" {
60 Box::new(std::io::stdin().lock())
61 } else {
62 Box::new(std::io::BufReader::new(std::fs::File::open(file)?))
63 };
64
65 for line in reader.lines() {
66 let line = line?;
67 if line.trim().is_empty() {
68 continue;
69 }
70 let t: ImportTask = serde_json::from_str(&line)?;
71 let id = db::TaskId::parse(&t.id)?;
72 db::parse_priority(&t.priority)?;
73 db::parse_status(&t.status)?;
74 db::parse_effort(&t.effort)?;
75
76 store.apply_and_persist(|doc| {
77 let tasks = doc.get_map("tasks");
78 let task = if let Some(existing) = db::get_task_map(&tasks, &id)? {
79 existing
80 } else {
81 db::insert_task_map(&tasks, &id)?
82 };
83
84 task.insert("title", t.title.clone())?;
85 task.insert("description", t.description.clone())?;
86 task.insert("type", t.task_type.clone())?;
87 task.insert("priority", t.priority.clone())?;
88 task.insert("status", t.status.clone())?;
89 task.insert("effort", t.effort.clone())?;
90 task.insert("parent", t.parent.as_deref().unwrap_or(""))?;
91 task.insert("created_at", t.created_at.clone())?;
92 task.insert("updated_at", t.updated_at.clone())?;
93 task.insert("deleted_at", t.deleted_at.as_deref().unwrap_or(""))?;
94
95 let labels = db::get_or_create_child_map(&task, "labels")?;
96 for lbl in &t.labels {
97 labels.insert(lbl, true)?;
98 }
99 let blockers = db::get_or_create_child_map(&task, "blockers")?;
100 for blk in &t.blockers {
101 let parsed =
102 db::TaskId::parse(blk).map_err(|_| anyhow!("invalid blocker id '{blk}'"))?;
103 blockers.insert(parsed.as_str(), true)?;
104 }
105 let logs = db::get_or_create_child_map(&task, "logs")?;
106 for entry in &t.logs {
107 let log_id = db::TaskId::parse(&entry.id)
108 .map_err(|_| anyhow!("invalid log id '{}'", entry.id))?;
109 let record = logs.get_or_create_container(log_id.as_str(), loro::LoroMap::new())?;
110 record.insert("timestamp", entry.timestamp.clone())?;
111 record.insert("message", entry.message.clone())?;
112 }
113 Ok(())
114 })?;
115 }
116
117 Ok(())
118}