Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 43 additions & 1 deletion crates/vespertide-cli/src/commands/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
use futures::future::try_join_all;
use rayon::prelude::*;
use tokio::fs;
use vespertide_config::VespertideConfig;

Check warning on line 9 in crates/vespertide-cli/src/commands/export.rs

View workflow job for this annotation

GitHub Actions / fmt

Diff in /home/runner/work/vespertide/vespertide/crates/vespertide-cli/src/commands/export.rs
use vespertide_core::TableDef;
use vespertide_exporter::{Orm, render_entity_with_schema, seaorm::SeaOrmExporterWithConfig};
use vespertide_exporter::{
Orm, render_entity_with_schema, prisma::PrismaExporterWithConfig,
seaorm::SeaOrmExporterWithConfig,
};

use crate::parallel_config::{EXPORT_RENDER_PAR_MIN_LEN, EXPORT_RENDER_PAR_THRESHOLD};
use crate::utils::load_config;
Expand All @@ -19,6 +22,7 @@
Sqlalchemy,
Sqlmodel,
Jpa,
Prisma,
}

impl From<OrmArg> for Orm {
Expand All @@ -28,6 +32,7 @@
OrmArg::Sqlalchemy => Orm::SqlAlchemy,
OrmArg::Sqlmodel => Orm::SqlModel,
OrmArg::Jpa => Orm::Jpa,
OrmArg::Prisma => Orm::Prisma,
}
}
}
Expand All @@ -51,6 +56,11 @@

let target_root = resolve_export_dir(export_dir, &config);

// Prisma uses a single-file output strategy
if matches!(orm, OrmArg::Prisma) {
return cmd_export_prisma(config, normalized_models, target_root).await;
}

// Clean the export directory before regenerating
let orm_kind: Orm = orm.into();
clean_export_dir(&target_root, orm_kind).await?;
Expand Down Expand Up @@ -238,6 +248,7 @@
Orm::SeaOrm => "rs",
Orm::SqlAlchemy | Orm::SqlModel => "py",
Orm::Jpa => "java",
Orm::Prisma => "prisma",
};

clean_dir_recursive(root, ext).await?;
Expand Down Expand Up @@ -330,6 +341,7 @@
Orm::SeaOrm => "rs",
Orm::SqlAlchemy | Orm::SqlModel => "py",
Orm::Jpa => "java",
Orm::Prisma => "prisma",
};
// Java requires filename to match PascalCase class name
let file_stem = if matches!(orm, Orm::Jpa) {
Expand Down Expand Up @@ -426,6 +438,36 @@
Ok(())
}

async fn cmd_export_prisma(
config: VespertideConfig,
normalized_models: Vec<(TableDef, PathBuf)>,
target_root: PathBuf,
) -> Result<()> {
let all_tables: Vec<TableDef> = normalized_models.iter().map(|(t, _)| t.clone()).collect();
let content = PrismaExporterWithConfig::new(config.prisma()).render_schema(&all_tables);

clean_export_dir(&target_root, Orm::Prisma).await?;

if !target_root.exists() {

Check warning on line 451 in crates/vespertide-cli/src/commands/export.rs

View workflow job for this annotation

GitHub Actions / cargo-mutants (shard 4/16)

Missed mutant

delete ! in cmd_export_prisma
fs::create_dir_all(&target_root)
.await
.with_context(|| format!("create export dir {}", target_root.display()))?;
}

let out_path = target_root.join("schema.prisma");
fs::write(&out_path, &content)
.await
.with_context(|| format!("write {}", out_path.display()))?;

println!(
"Exported {} model(s) -> {}",
normalized_models.len(),
out_path.display()
);

Ok(())
}

#[async_recursion::async_recursion]
async fn walk_models(
root: &Path,
Expand Down
60 changes: 60 additions & 0 deletions crates/vespertide-config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use crate::file_format::FileFormat;
use crate::name_case::NameCase;
use crate::relation_mode::RelationMode;

/// Default migration filename pattern: zero-padded version + sanitized comment.
pub fn default_migration_filename_pattern() -> String {
Expand Down Expand Up @@ -78,6 +79,32 @@
}
}

/// Prisma-specific export configuration.
///
/// The `datasource` provider is always PostgreSQL (the exporter only emits
/// PostgreSQL-native `@db.*` types), and the generated Prisma Client's output
/// path is left to Prisma's own default — neither is configurable here, since
/// vespertide only manages the schema/model definition, not Prisma Client
/// codegen. See `RelationMode` for the one setting that is configurable.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PrismaConfig {
/// Override for the `datasource` `relationMode` setting. `None` (default)
/// omits the line and lets Prisma use its own default (`foreignKeys`).
/// See [`RelationMode`] for what each variant means.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relation_mode: Option<RelationMode>,
}

impl PrismaConfig {
/// The configured `relationMode` override, if any.
pub fn relation_mode(&self) -> Option<RelationMode> {
self.relation_mode
}
}

/// Top-level vespertide configuration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
Expand All @@ -100,6 +127,9 @@
/// SeaORM-specific export configuration.
#[serde(default)]
pub seaorm: SeaOrmConfig,
/// Prisma-specific export configuration.
#[serde(default)]
pub prisma: PrismaConfig,
/// Prefix to add to all table names (including migration version table).
/// Default: "" (no prefix)
#[serde(default)]
Expand Down Expand Up @@ -138,6 +168,7 @@
migration_filename_pattern: default_migration_filename_pattern(),
model_export_dir: default_model_export_dir(),
seaorm: SeaOrmConfig::default(),
prisma: PrismaConfig::default(),
prefix: String::new(),
lock_timeout_ms: None,
statement_timeout_ms: None,
Expand Down Expand Up @@ -191,6 +222,11 @@
&self.seaorm
}

/// Prisma-specific export configuration.
pub fn prisma(&self) -> &PrismaConfig {
&self.prisma

Check warning on line 227 in crates/vespertide-config/src/config.rs

View workflow job for this annotation

GitHub Actions / cargo-mutants (shard 7/16)

Missed mutant

replace VespertideConfig::prisma -> &PrismaConfig with Box::leak(Box::new(Default::default()))
}

/// Prefix to add to all table names.
pub fn prefix(&self) -> &str {
&self.prefix
Expand Down Expand Up @@ -291,4 +327,28 @@
assert_eq!(config.prefix(), "");
assert_eq!(config.apply_prefix("users"), "users");
}

#[test]
fn prisma_config_default_has_no_relation_mode() {
let config = PrismaConfig::default();
assert_eq!(config.relation_mode(), None);
}

#[test]
fn prisma_config_deserializes_from_empty_object() {
let config: PrismaConfig = serde_json::from_str("{}").unwrap();
assert_eq!(config.relation_mode(), None);
}

#[test]
fn prisma_config_relation_mode_roundtrips() {
let config = PrismaConfig {
relation_mode: Some(RelationMode::Prisma),
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
assert_eq!(json, "{\"relationMode\":\"prisma\"}");
let back: PrismaConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.relation_mode(), Some(RelationMode::Prisma));
}
}
4 changes: 3 additions & 1 deletion crates/vespertide-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

pub mod config;
pub mod file_format;
pub mod name_case;

Check warning on line 8 in crates/vespertide-config/src/lib.rs

View workflow job for this annotation

GitHub Actions / fmt

Diff in /home/runner/work/vespertide/vespertide/crates/vespertide-config/src/lib.rs
pub mod relation_mode;

pub use config::{SeaOrmConfig, VespertideConfig, default_migration_filename_pattern};
pub use config::{PrismaConfig, SeaOrmConfig, VespertideConfig, default_migration_filename_pattern};
pub use file_format::FileFormat;
pub use name_case::NameCase;
pub use relation_mode::RelationMode;

#[cfg(test)]
mod tests {
Expand Down
60 changes: 60 additions & 0 deletions crates/vespertide-config/src/relation_mode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use serde::{Deserialize, Serialize};

/// Prisma `relationMode` datasource setting.
///
/// Controls whether Prisma relies on real database-level foreign key
/// constraints or emulates referential integrity in Prisma Client.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum RelationMode {
/// Use real database-level foreign key constraints. This is Prisma's
/// default and is correct for any backend that supports FKs (e.g.
/// PostgreSQL, MySQL, SQLite).
ForeignKeys,
/// Emulate referential integrity in Prisma Client instead of relying on
/// database-level foreign keys. Needed for datastores that don't support
/// FK constraints (e.g. PlanetScale, MongoDB).
Prisma,
}

impl RelationMode {
/// The literal string Prisma expects for `relationMode = "..."`.
pub fn as_str(self) -> &'static str {
match self {
RelationMode::ForeignKeys => "foreignKeys",
RelationMode::Prisma => "prisma",
}
}
}

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

#[test]
fn as_str_matches_prisma_literals() {
assert_eq!(RelationMode::ForeignKeys.as_str(), "foreignKeys");
assert_eq!(RelationMode::Prisma.as_str(), "prisma");
}

#[test]
fn serde_round_trip() {
assert_eq!(
serde_json::to_string(&RelationMode::ForeignKeys).unwrap(),
"\"foreignKeys\""
);
assert_eq!(
serde_json::to_string(&RelationMode::Prisma).unwrap(),
"\"prisma\""
);
assert_eq!(
serde_json::from_str::<RelationMode>("\"foreignKeys\"").unwrap(),
RelationMode::ForeignKeys
);
assert_eq!(
serde_json::from_str::<RelationMode>("\"prisma\"").unwrap(),
RelationMode::Prisma
);
}
}
1 change: 1 addition & 0 deletions crates/vespertide-exporter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ criterion = { version = "0.8", features = ["html_reports"] }
rstest = "0.26"
insta = { version = "1.47", features = ["yaml"] }
proptest = "1"
serde_json = "1"

[[bench]]
name = "codegen_benchmarks"
Expand Down
Loading
Loading