Skip to content

Commit

Permalink
add: infer migration
Browse files Browse the repository at this point in the history
  • Loading branch information
jondot committed Dec 15, 2024
1 parent 56ada7b commit 9a4cc0b
Show file tree
Hide file tree
Showing 14 changed files with 657 additions and 626 deletions.
283 changes: 122 additions & 161 deletions examples/demo/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions loco-gen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ path = "src/lib.rs"

[dependencies]

cruet = "0.14.0"
rrgen = "0.5.3"
serde = { workspace = true }
serde_json = { workspace = true }
Expand Down
101 changes: 101 additions & 0 deletions loco-gen/src/infer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use cruet::{case::snake::to_snake_case, Inflector}; // For pluralization and singularization

#[derive(Debug, PartialEq, Eq)]
pub enum MigrationType {
CreateTable { table: String },
AddColumns { table: String },
RemoveColumns { table: String },
AddReference { table: String },
CreateJoinTable { table_a: String, table_b: String },
Empty,
}

pub fn guess_migration_type(migration_name: &str) -> MigrationType {
let normalized_name = to_snake_case(migration_name);
let parts: Vec<&str> = normalized_name.split('_').collect();

match parts.as_slice() {
["create", table_name] => MigrationType::CreateTable {
table: table_name.to_plural(),
},
["add", _reference_name, "ref", "to", table_name] => MigrationType::AddReference {
table: table_name.to_plural(),
},
["add", _column_names @ .., "to", table_name] => MigrationType::AddColumns {
table: table_name.to_plural(),
},
["remove", _column_names @ .., "from", table_name] => MigrationType::RemoveColumns {
table: table_name.to_plural(),
},
["create", "join", "table", table_a, "and", table_b] => {
let table_a = table_a.to_singular();
let table_b = table_b.to_singular();
MigrationType::CreateJoinTable { table_a, table_b }
}
_ => MigrationType::Empty,
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_infer_create_table() {
assert_eq!(
guess_migration_type("CreateUsers"),
MigrationType::CreateTable {
table: "users".to_string(),
}
);
}

#[test]
fn test_infer_add_columns() {
assert_eq!(
guess_migration_type("AddNameAndAgeToUsers"),
MigrationType::AddColumns {
table: "users".to_string(),
}
);
}

#[test]
fn test_infer_remove_columns() {
assert_eq!(
guess_migration_type("RemoveNameAndAgeFromUsers"),
MigrationType::RemoveColumns {
table: "users".to_string(),
}
);
}

#[test]
fn test_infer_add_reference() {
assert_eq!(
guess_migration_type("AddUserRefToPosts"),
MigrationType::AddReference {
table: "posts".to_string(),
}
);
}

#[test]
fn test_infer_create_join_table() {
assert_eq!(
guess_migration_type("CreateJoinTableUsersAndGroups"),
MigrationType::CreateJoinTable {
table_a: "user".to_string(),
table_b: "group".to_string()
}
);
}

#[test]
fn test_empty_migration() {
assert_eq!(
guess_migration_type("UnknownMigrationType"),
MigrationType::Empty
);
}
}
25 changes: 9 additions & 16 deletions loco-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize};
use serde_json::json;

mod controller;
mod infer;
mod migration;
#[cfg(feature = "with-db")]
mod model;
#[cfg(feature = "with-db")]
Expand All @@ -20,8 +22,6 @@ const MAILER_SUB_T: &str = include_str!("templates/mailer/subject.t");
const MAILER_TEXT_T: &str = include_str!("templates/mailer/text.t");
const MAILER_HTML_T: &str = include_str!("templates/mailer/html.t");

const MIGRATION_T: &str = include_str!("templates/migration/migration.t");

const TASK_T: &str = include_str!("templates/task/task.t");
const TASK_TEST_T: &str = include_str!("templates/task/test.t");

Expand Down Expand Up @@ -151,14 +151,14 @@ pub enum Component {

/// Model fields, eg. title:string hits:int
fields: Vec<(String, String)>,

/// Generate migration code and stop, don't run the migration
migration_only: bool,
},
#[cfg(feature = "with-db")]
Migration {
/// Name of the migration file
name: String,

/// Params fields, eg. title:string hits:int
fields: Vec<(String, String)>,
},
#[cfg(feature = "with-db")]
Scaffold {
Expand Down Expand Up @@ -222,15 +222,10 @@ pub fn generate(component: Component, appinfo: &AppInfo) -> Result<()> {
*/
match component {
#[cfg(feature = "with-db")]
Component::Model {
name,
link,
fields,
migration_only,
} => {
Component::Model { name, link, fields } => {
println!(
"{}",
model::generate(&rrgen, &name, link, migration_only, &fields, appinfo)?
model::generate(&rrgen, &name, link, &fields, appinfo)?
);
}
#[cfg(feature = "with-db")]
Expand All @@ -241,10 +236,8 @@ pub fn generate(component: Component, appinfo: &AppInfo) -> Result<()> {
);
}
#[cfg(feature = "with-db")]
Component::Migration { name } => {
let vars =
json!({ "name": name, "ts": chrono::Utc::now(), "pkg_name": appinfo.app_name});
rrgen.generate(MIGRATION_T, &vars)?;
Component::Migration { name, fields } => {
migration::generate(&rrgen, &name, &fields, appinfo)?;
}
Component::Controller {
name,
Expand Down
75 changes: 75 additions & 0 deletions loco-gen/src/migration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use chrono::Utc;
use rrgen::RRgen;
use serde_json::json;

use super::Result;
use crate::{
infer,
model::{get_columns_and_references, MODEL_T},
};

const MIGRATION_T: &str = include_str!("templates/migration/empty.t");
const ADD_COLS_T: &str = include_str!("templates/migration/add_columns.t");
const ADD_REFS_T: &str = include_str!("templates/migration/add_references.t");
const REMOVE_COLS_T: &str = include_str!("templates/migration/remove_columns.t");
const JOIN_TABLE_T: &str = include_str!("templates/migration/join_table.t");

use super::{collect_messages, AppInfo};

/// skipping some fields from the generated models.
/// For example, the `created_at` and `updated_at` fields are automatically
/// generated by the Loco app and should be given
pub const IGNORE_FIELDS: &[&str] = &["created_at", "updated_at", "create_at", "update_at"];

pub fn generate(
rrgen: &RRgen,
name: &str,
fields: &[(String, String)],
appinfo: &AppInfo,
) -> Result<String> {
let pkg_name: &str = &appinfo.app_name;
let ts = Utc::now();

let res = infer::guess_migration_type(name);
let migration_gen = match res {
// NOTE: re-uses the 'new model' migration template!
infer::MigrationType::CreateTable { table } => {
let (columns, references) = get_columns_and_references(fields)?;
let vars = json!({"name": table, "ts": ts, "pkg_name": pkg_name, "is_link": false, "columns": columns, "references": references});
rrgen.generate(MODEL_T, &vars)?
}
infer::MigrationType::AddColumns { table } => {
let (columns, references) = get_columns_and_references(fields)?;
let vars = json!({"name": name, "table": table, "ts": ts, "pkg_name": pkg_name, "is_link": false, "columns": columns, "references": references});
rrgen.generate(ADD_COLS_T, &vars)?
}
infer::MigrationType::RemoveColumns { table } => {
let (columns, _references) = get_columns_and_references(fields)?;
let vars = json!({"name": name, "table": table, "ts": ts, "pkg_name": pkg_name, "columns": columns});
rrgen.generate(REMOVE_COLS_T, &vars)?
}
infer::MigrationType::AddReference { table } => {
let (columns, references) = get_columns_and_references(fields)?;
let vars = json!({"name": name, "table": table, "ts": ts, "pkg_name": pkg_name, "columns": columns, "references": references});
rrgen.generate(ADD_REFS_T, &vars)?
}
infer::MigrationType::CreateJoinTable { table_a, table_b } => {
let mut tables = [table_a.clone(), table_b.clone()];
tables.sort();
let table = tables.join("_");
let (columns, references) = get_columns_and_references(&[
(table_a, "references".to_string()),
(table_b, "references".to_string()),
])?;
let vars = json!({"name": name, "table": table, "ts": ts, "pkg_name": pkg_name, "columns": columns, "references": references});
rrgen.generate(JOIN_TABLE_T, &vars)?
}
infer::MigrationType::Empty => {
let vars = json!({"name": name, "ts": ts, "pkg_name": pkg_name});
rrgen.generate(MIGRATION_T, &vars)?
}
};

let messages = collect_messages(vec![migration_gen]);
Ok(messages)
}
88 changes: 48 additions & 40 deletions loco-gen/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde_json::json;
use super::{Error, Result};
use crate::get_mappings;

const MODEL_T: &str = include_str!("templates/model/model.t");
pub const MODEL_T: &str = include_str!("templates/model/model.t");
const MODEL_TEST_T: &str = include_str!("templates/model/test.t");

use super::{collect_messages, AppInfo};
Expand All @@ -18,17 +18,14 @@ use super::{collect_messages, AppInfo};
/// generated by the Loco app and should be given
pub const IGNORE_FIELDS: &[&str] = &["created_at", "updated_at", "create_at", "update_at"];

pub fn generate(
rrgen: &RRgen,
name: &str,
is_link: bool,
migration_only: bool,
/// columns are <name>, <dbtype>: ("content", "string")
/// references are <to table, id col in from table>: ("user", `user_id`)
/// parsed from e.g.: model article content:string user:references
/// puts a `user_id` in articles, then fk to users
#[allow(clippy::type_complexity)]
pub fn get_columns_and_references(
fields: &[(String, String)],
appinfo: &AppInfo,
) -> Result<String> {
let pkg_name: &str = &appinfo.app_name;
let ts = Utc::now();

) -> Result<(Vec<(String, String)>, Vec<(String, String)>)> {
let mut columns = Vec::new();
let mut references = Vec::new();
for (fname, ftype) in fields {
Expand All @@ -41,12 +38,12 @@ pub fn generate(
}
if ftype == "references" {
let fkey = format!("{fname}_id");
columns.push((fkey.clone(), "integer"));
columns.push((fkey.clone(), "integer".to_string()));
// user, user_id
references.push((fname.to_string(), fkey));
} else if let Some(refname) = ftype.strip_prefix("references:") {
let fkey = format!("{fname}_id");
columns.push((fkey.clone(), "integer"));
columns.push((fkey.clone(), "integer".to_string()));
references.push((refname.to_string(), fkey));
} else {
let mappings = get_mappings();
Expand All @@ -57,39 +54,51 @@ pub fn generate(
mappings.schema_fields()
))
})?;
columns.push((fname.to_string(), schema_type.as_str()));
columns.push((fname.to_string(), schema_type.to_string()));
}
}
Ok((columns, references))
}
pub fn generate(
rrgen: &RRgen,
name: &str,
is_link: bool,
fields: &[(String, String)],
appinfo: &AppInfo,
) -> Result<String> {
let pkg_name: &str = &appinfo.app_name;
let ts = Utc::now();

let (columns, references) = get_columns_and_references(fields)?;

let vars = json!({"name": name, "ts": ts, "pkg_name": pkg_name, "is_link": is_link, "columns": columns, "references": references});
let res1 = rrgen.generate(MODEL_T, &vars)?;
let res2 = rrgen.generate(MODEL_TEST_T, &vars)?;

if !migration_only {
let cwd = current_dir()?;
let env_map: HashMap<_, _> = std::env::vars().collect();

let _ = cmd!("cargo", "loco-tool", "db", "migrate",)
.stderr_to_stdout()
.dir(cwd.as_path())
.full_env(&env_map)
.run()
.map_err(|err| {
Error::Message(format!(
"failed to run loco db migration. error details: `{err}`",
))
})?;
let _ = cmd!("cargo", "loco-tool", "db", "entities",)
.stderr_to_stdout()
.dir(cwd.as_path())
.full_env(&env_map)
.run()
.map_err(|err| {
Error::Message(format!(
"failed to run loco db entities. error details: `{err}`",
))
})?;
}
// generate the model files by migrating and re-running seaorm
let cwd = current_dir()?;
let env_map: HashMap<_, _> = std::env::vars().collect();

let _ = cmd!("cargo", "loco-tool", "db", "migrate",)
.stderr_to_stdout()
.dir(cwd.as_path())
.full_env(&env_map)
.run()
.map_err(|err| {
Error::Message(format!(
"failed to run loco db migration. error details: `{err}`",
))
})?;
let _ = cmd!("cargo", "loco-tool", "db", "entities",)
.stderr_to_stdout()
.dir(cwd.as_path())
.full_env(&env_map)
.run()
.map_err(|err| {
Error::Message(format!(
"failed to run loco db entities. error details: `{err}`",
))
})?;

let messages = collect_messages(vec![res1, res2]);
Ok(messages)
Expand Down Expand Up @@ -141,7 +150,6 @@ mod tests {
&rrgen,
"movies",
false,
true,
&[("title".to_string(), "string".to_string())],
&AppInfo {
app_name: "saas".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion loco-gen/src/scaffold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub fn generate(
// - scaffold is never a link table
// - never run with migration_only, because the controllers will refer to the
// models. the models only arrive after migration and entities sync.
let model_messages = model::generate(rrgen, name, false, false, fields, appinfo)?;
let model_messages = model::generate(rrgen, name, false, fields, appinfo)?;
let mappings = get_mappings();

let mut columns = Vec::new();
Expand Down
Loading

0 comments on commit 9a4cc0b

Please sign in to comment.