Skip to main content

doido_generators/generators/
storage_install.rs

1//! `doido generate storage:install` — the `active_storage:install` analogue.
2//!
3//! Emits a migration creating the `storage_blobs`, `storage_attachments` and
4//! `storage_variant_records` tables (via the `doido_model::migration` builders),
5//! registers it in `db/migration/src/lib.rs`, and appends a `storage:` section to
6//! `config/development.yml` and `config/test.yml` when those files exist.
7
8use 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
63/// The `storage:` config block appended to a `config/<env>.yml`. `active` is the
64/// service selected for that environment.
65fn 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
71/// Append the storage section to `path` if the file exists and has none yet.
72fn 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        // Best-effort: wire the `storage` config section into existing env files.
111        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        // A migration file plus the updated lib.rs (config files depend on cwd).
130        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}