use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelMapping {
model_name: String,
db_table_name: String,
field_to_column: BTreeMap<String, String>,
column_to_field: BTreeMap<String, String>,
}
impl ModelMapping {
pub fn new(model_name: impl Into<String>) -> Self {
let model_name = model_name.into();
Self {
db_table_name: model_name.clone(),
model_name,
field_to_column: BTreeMap::new(),
column_to_field: BTreeMap::new(),
}
}
pub fn map_model(mut self, table_or_collection: impl Into<String>) -> Self {
self.db_table_name = table_or_collection.into();
self
}
pub fn map_field(
mut self,
field_name: impl Into<String>,
column_name: impl Into<String>,
) -> Self {
let field_name = field_name.into();
let column_name = column_name.into();
self.field_to_column
.insert(field_name.clone(), column_name.clone());
self.column_to_field.insert(column_name, field_name);
self
}
pub fn model_name(&self) -> &str {
&self.model_name
}
pub fn db_table_name(&self) -> &str {
&self.db_table_name
}
pub fn db_column_name<'a>(&'a self, field_name: &'a str) -> &'a str {
self.field_to_column
.get(field_name)
.map(String::as_str)
.unwrap_or(field_name)
}
pub fn schema_field_name<'a>(&'a self, column_name: &'a str) -> &'a str {
self.column_to_field
.get(column_name)
.map(String::as_str)
.unwrap_or(column_name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumMapping {
schema_enum_name: String,
db_enum_name: String,
schema_to_db_value: BTreeMap<String, String>,
db_to_schema_value: BTreeMap<String, String>,
}
impl EnumMapping {
pub fn new(schema_enum_name: impl Into<String>) -> Self {
let schema_enum_name = schema_enum_name.into();
Self {
db_enum_name: schema_enum_name.clone(),
schema_enum_name,
schema_to_db_value: BTreeMap::new(),
db_to_schema_value: BTreeMap::new(),
}
}
pub fn map_enum(mut self, db_enum_name: impl Into<String>) -> Self {
self.db_enum_name = db_enum_name.into();
self
}
pub fn map_value(
mut self,
schema_value: impl Into<String>,
db_value: impl Into<String>,
) -> Self {
let schema_value = schema_value.into();
let db_value = db_value.into();
self.schema_to_db_value
.insert(schema_value.clone(), db_value.clone());
self.db_to_schema_value.insert(db_value, schema_value);
self
}
pub fn schema_enum_name(&self) -> &str {
&self.schema_enum_name
}
pub fn db_enum_name(&self) -> &str {
&self.db_enum_name
}
pub fn db_value<'a>(&'a self, schema_value: &'a str) -> &'a str {
self.schema_to_db_value
.get(schema_value)
.map(String::as_str)
.unwrap_or(schema_value)
}
pub fn schema_value<'a>(&'a self, db_value: &'a str) -> &'a str {
self.db_to_schema_value
.get(db_value)
.map(String::as_str)
.unwrap_or(db_value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConstraintKind {
PrimaryKey,
UniqueConstraint,
NonUniqueIndex,
ForeignKey,
}
impl ConstraintKind {
fn suffix(self) -> &'static str {
match self {
ConstraintKind::PrimaryKey => "_pkey",
ConstraintKind::UniqueConstraint => "_key",
ConstraintKind::NonUniqueIndex => "_idx",
ConstraintKind::ForeignKey => "_fkey",
}
}
}
pub fn prisma_default_constraint_name(
table_name_in_db: &str,
column_names_in_db: &[&str],
kind: ConstraintKind,
max_identifier_len: usize,
) -> String {
let suffix = kind.suffix();
let mut prefix = match kind {
ConstraintKind::PrimaryKey => table_name_in_db.to_string(),
_ => {
let cols = column_names_in_db.join("_");
if cols.is_empty() {
table_name_in_db.to_string()
} else {
format!("{table_name_in_db}_{cols}")
}
}
};
let full_len = prefix.len() + suffix.len();
if full_len > max_identifier_len {
let keep = max_identifier_len.saturating_sub(suffix.len());
prefix.truncate(keep);
}
format!("{prefix}{suffix}")
}
pub fn should_render_constraint_map_argument(
actual_db_name: &str,
table_name_in_db: &str,
column_names_in_db: &[&str],
kind: ConstraintKind,
max_identifier_len: usize,
) -> bool {
actual_db_name
!= prisma_default_constraint_name(
table_name_in_db,
column_names_in_db,
kind,
max_identifier_len,
)
}
pub fn resolve_constraint_db_name(
map_argument: Option<&str>,
table_name_in_db: &str,
column_names_in_db: &[&str],
kind: ConstraintKind,
max_identifier_len: usize,
) -> String {
map_argument.map(str::to_owned).unwrap_or_else(|| {
prisma_default_constraint_name(
table_name_in_db,
column_names_in_db,
kind,
max_identifier_len,
)
})
}
pub fn default_compound_selector_name(schema_field_names: &[&str]) -> String {
schema_field_names.join("_")
}
pub fn resolve_compound_selector_name(
name_argument: Option<&str>,
schema_field_names: &[&str],
) -> String {
name_argument
.map(str::to_owned)
.unwrap_or_else(|| default_compound_selector_name(schema_field_names))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn model_mapping_works() {
let m = ModelMapping::new("Comment")
.map_model("comments")
.map_field("content", "comment_text")
.map_field("email", "commenter_email");
assert_eq!(m.model_name(), "Comment");
assert_eq!(m.db_table_name(), "comments");
assert_eq!(m.db_column_name("content"), "comment_text");
assert_eq!(m.db_column_name("type"), "type");
assert_eq!(m.schema_field_name("commenter_email"), "email");
assert_eq!(m.schema_field_name("unknown_col"), "unknown_col");
}
#[test]
fn enum_mapping_works() {
let e = EnumMapping::new("Type")
.map_enum("comment_source_enum")
.map_value("Twitter", "comment_twitter");
assert_eq!(e.schema_enum_name(), "Type");
assert_eq!(e.db_enum_name(), "comment_source_enum");
assert_eq!(e.db_value("Twitter"), "comment_twitter");
assert_eq!(e.db_value("Blog"), "Blog");
assert_eq!(e.schema_value("comment_twitter"), "Twitter");
assert_eq!(e.schema_value("other"), "other");
}
#[test]
fn default_constraint_names_follow_prisma_shape() {
assert_eq!(
prisma_default_constraint_name("User", &["id"], ConstraintKind::PrimaryKey, 63),
"User_pkey"
);
assert_eq!(
prisma_default_constraint_name(
"User",
&["firstName", "lastName"],
ConstraintKind::UniqueConstraint,
63
),
"User_firstName_lastName_key"
);
assert_eq!(
prisma_default_constraint_name("User", &["age"], ConstraintKind::NonUniqueIndex, 63),
"User_age_idx"
);
assert_eq!(
prisma_default_constraint_name("Post", &["authorName"], ConstraintKind::ForeignKey, 63),
"Post_authorName_fkey"
);
}
#[test]
fn default_constraint_name_is_trimmed_before_suffix() {
let table = "VeryLongTableName";
let cols = ["veryLongColumnName", "anotherColumnName"];
let out = prisma_default_constraint_name(table, &cols, ConstraintKind::NonUniqueIndex, 20);
assert!(out.ends_with("_idx"));
assert!(out.len() <= 20);
}
#[test]
fn map_argument_rendering_detection_matches_default() {
let should_hide = should_render_constraint_map_argument(
"Post_title_authorName_idx",
"Post",
&["title", "authorName"],
ConstraintKind::NonUniqueIndex,
63,
);
let should_show = should_render_constraint_map_argument(
"My_Custom_Index_Name",
"Post",
&["title", "authorName"],
ConstraintKind::NonUniqueIndex,
63,
);
assert!(!should_hide);
assert!(should_show);
}
#[test]
fn resolve_constraint_name_prefers_map() {
let from_default = resolve_constraint_db_name(
None,
"User",
&["name"],
ConstraintKind::UniqueConstraint,
63,
);
let from_map = resolve_constraint_db_name(
Some("unique_user_name"),
"User",
&["name"],
ConstraintKind::UniqueConstraint,
63,
);
assert_eq!(from_default, "User_name_key");
assert_eq!(from_map, "unique_user_name");
}
#[test]
fn compound_selector_name_supports_name_argument() {
assert_eq!(
default_compound_selector_name(&["firstName", "lastName"]),
"firstName_lastName"
);
assert_eq!(
resolve_compound_selector_name(None, &["firstName", "lastName"]),
"firstName_lastName"
);
assert_eq!(
resolve_compound_selector_name(Some("fullName"), &["firstName", "lastName"]),
"fullName"
);
}
}