doido_generators/generators/
storage_install.rs1use crate::generator::{GeneratedFile, Generator};
9use crate::generators::migration_support::{
10 register_migration, render_migration_file, MIGRATION_LIB_BASE, MIGRATION_SRC_DIR,
11};
12use chrono::Utc;
13use doido_core::Result;
14
15pub struct StorageInstallGenerator;
16
17const IMPORTS: &str = "use doido_model::migration::{add_index, create_table, drop_table};";
18
19const UP_BODY: &str = r#" create_table(manager, "storage_blobs", |t| {
20 t.string("key").not_null().unique_key();
21 t.string("filename").not_null();
22 t.string("content_type");
23 t.text("metadata");
24 t.string("service_name").not_null();
25 t.big_integer("byte_size").not_null();
26 t.string("checksum");
27 t.timestamp("created_at").not_null();
28 })
29 .await?;
30 create_table(manager, "storage_attachments", |t| {
31 t.string("name").not_null();
32 t.string("record_type").not_null();
33 t.string("record_id").not_null();
34 t.string("blob_key").not_null();
35 t.timestamp("created_at").not_null();
36 })
37 .await?;
38 create_table(manager, "storage_variant_records", |t| {
39 t.string("blob_key").not_null();
40 t.string("variation_digest").not_null();
41 })
42 .await?;
43 add_index(
44 manager,
45 "storage_attachments",
46 &["record_type", "record_id", "name"],
47 )
48 .await?;
49 add_index(
50 manager,
51 "storage_variant_records",
52 &["blob_key", "variation_digest"],
53 )
54 .await?;
55 Ok(())
56"#;
57
58const DOWN_BODY: &str = r#" drop_table(manager, "storage_variant_records").await?;
59 drop_table(manager, "storage_attachments").await?;
60 drop_table(manager, "storage_blobs").await
61"#;
62
63fn storage_section(active: &str) -> String {
66 format!(
67 "\nstorage:\n service: {active}\n services:\n local:\n type: disk\n root: storage\n test:\n type: memory\n"
68 )
69}
70
71fn config_file(path: &str, active: &str) -> Option<GeneratedFile> {
73 let existing = std::fs::read_to_string(path).ok()?;
74 if existing.contains("storage:") {
75 return None;
76 }
77 Some(GeneratedFile {
78 path: path.to_string(),
79 content: format!("{}{}", existing.trim_end(), storage_section(active)),
80 })
81}
82
83impl Generator for StorageInstallGenerator {
84 fn name(&self) -> &str {
85 "storage:install"
86 }
87
88 fn generate(&self, _args: &[&str]) -> Result<Vec<GeneratedFile>> {
89 let migration = render_migration_file(IMPORTS, UP_BODY, DOWN_BODY);
90
91 let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
92 let migration_module = format!("m{timestamp}_create_storage_tables");
93
94 let lib_path = format!("{MIGRATION_SRC_DIR}/lib.rs");
95 let existing =
96 std::fs::read_to_string(&lib_path).unwrap_or_else(|_| MIGRATION_LIB_BASE.to_string());
97 let lib = register_migration(&existing, &migration_module);
98
99 let mut files = vec![
100 GeneratedFile {
101 path: format!("{MIGRATION_SRC_DIR}/{migration_module}.rs"),
102 content: migration,
103 },
104 GeneratedFile {
105 path: lib_path,
106 content: lib,
107 },
108 ];
109
110 if let Some(f) = config_file("config/development.yml", "local") {
112 files.push(f);
113 }
114 if let Some(f) = config_file("config/test.yml", "test") {
115 files.push(f);
116 }
117
118 Ok(files)
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn emits_migration_and_registers_it() {
128 let files = StorageInstallGenerator.generate(&[]).unwrap();
129 let migration = files
131 .iter()
132 .find(|f| f.path.contains("create_storage_tables") && f.path.ends_with(".rs"))
133 .expect("migration file emitted");
134 assert!(migration.content.contains("storage_blobs"));
135 assert!(migration.content.contains("storage_attachments"));
136 assert!(migration.content.contains("storage_variant_records"));
137
138 let lib = files
139 .iter()
140 .find(|f| f.path.ends_with("lib.rs"))
141 .expect("lib.rs emitted");
142 assert!(lib.content.contains("create_storage_tables"));
143 }
144}