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
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 @@ -7,7 +7,10 @@ use futures::future::try_join_all;
use tokio::fs;
use vespertide_config::VespertideConfig;
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::utils::load_config;

Expand All @@ -17,6 +20,7 @@ pub enum OrmArg {
Sqlalchemy,
Sqlmodel,
Jpa,
Prisma,
}

impl From<OrmArg> for Orm {
Expand All @@ -26,6 +30,7 @@ impl From<OrmArg> for Orm {
OrmArg::Sqlalchemy => Orm::SqlAlchemy,
OrmArg::Sqlmodel => Orm::SqlModel,
OrmArg::Jpa => Orm::Jpa,
OrmArg::Prisma => Orm::Prisma,
}
}
}
Expand All @@ -49,6 +54,11 @@ pub async fn cmd_export(orm: OrmArg, export_dir: Option<PathBuf>) -> Result<()>

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 @@ -203,6 +213,7 @@ async fn clean_export_dir(root: &Path, orm: Orm) -> Result<()> {
Orm::SeaOrm => "rs",
Orm::SqlAlchemy | Orm::SqlModel => "py",
Orm::Jpa => "java",
Orm::Prisma => "prisma",
};

clean_dir_recursive(root, ext).await?;
Expand Down Expand Up @@ -295,6 +306,7 @@ fn build_output_path(root: &Path, rel_path: &Path, orm: Orm) -> PathBuf {
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 @@ -395,6 +407,36 @@ async fn ensure_mod_chain(root: &Path, rel_path: &Path) -> Result<()> {
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() {
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
53 changes: 53 additions & 0 deletions crates/vespertide-config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,50 @@ impl SeaOrmConfig {
}
}

/// Prisma-specific export configuration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct PrismaConfig {
/// Database provider: postgresql, mysql, sqlite, sqlserver, mongodb, cockroachdb.
#[serde(default = "default_prisma_provider")]
pub provider: String,
/// Optional output path for the generated Prisma client.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_output: Option<String>,
/// Optional relationMode override ("foreignKeys" or "prisma").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relation_mode: Option<String>,
}

fn default_prisma_provider() -> String {
"postgresql".to_string()
}

impl Default for PrismaConfig {
fn default() -> Self {
Self {
provider: default_prisma_provider(),
client_output: None,
relation_mode: None,
}
}
}

impl PrismaConfig {
pub fn provider(&self) -> &str {
&self.provider
}

pub fn client_output(&self) -> Option<&str> {
self.client_output.as_deref()
}

pub fn relation_mode(&self) -> Option<&str> {
self.relation_mode.as_deref()
}
}

/// Top-level vespertide configuration.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
Expand All @@ -98,6 +142,9 @@ pub struct VespertideConfig {
/// 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 All @@ -120,6 +167,7 @@ impl Default for VespertideConfig {
migration_filename_pattern: default_migration_filename_pattern(),
model_export_dir: default_model_export_dir(),
seaorm: SeaOrmConfig::default(),
prisma: PrismaConfig::default(),
prefix: String::new(),
}
}
Expand Down Expand Up @@ -171,6 +219,11 @@ impl VespertideConfig {
&self.seaorm
}

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

/// Prefix to add to all table names.
pub fn prefix(&self) -> &str {
&self.prefix
Expand Down
2 changes: 1 addition & 1 deletion crates/vespertide-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ pub mod config;
pub mod file_format;
pub mod name_case;

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;

Expand Down
4 changes: 3 additions & 1 deletion crates/vespertide-exporter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
//! Helpers to convert `TableDef` models into ORM-specific representations
//! such as SeaORM, SQLAlchemy, SQLModel, and JPA.
//! such as SeaORM, SQLAlchemy, SQLModel, JPA, and Prisma.

pub mod jpa;
pub mod orm;
pub mod prisma;
pub mod seaorm;
pub mod sqlalchemy;
pub mod sqlmodel;

pub use jpa::JpaExporter;
pub use orm::{Orm, OrmExporter, render_entity, render_entity_with_schema};
pub use prisma::PrismaExporter;
pub use seaorm::{SeaOrmExporter, render_entity as render_seaorm_entity};
pub use sqlalchemy::SqlAlchemyExporter;
pub use sqlmodel::SqlModelExporter;
9 changes: 7 additions & 2 deletions crates/vespertide-exporter/src/orm.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use vespertide_core::TableDef;

use crate::{
jpa::JpaExporter, seaorm::SeaOrmExporter, sqlalchemy::SqlAlchemyExporter,
sqlmodel::SqlModelExporter,
jpa::JpaExporter, prisma::PrismaExporter, seaorm::SeaOrmExporter,
sqlalchemy::SqlAlchemyExporter, sqlmodel::SqlModelExporter,
};

/// Supported ORM targets.
Expand All @@ -12,6 +12,7 @@ pub enum Orm {
SqlAlchemy,
SqlModel,
Jpa,
Prisma,
}

/// Standardized exporter interface for all supported ORMs.
Expand All @@ -36,6 +37,7 @@ pub fn render_entity(orm: Orm, table: &TableDef) -> Result<String, String> {
Orm::SqlAlchemy => SqlAlchemyExporter.render_entity(table),
Orm::SqlModel => SqlModelExporter.render_entity(table),
Orm::Jpa => JpaExporter.render_entity(table),
Orm::Prisma => PrismaExporter.render_entity(table),
}
}

Expand All @@ -50,6 +52,7 @@ pub fn render_entity_with_schema(
Orm::SqlAlchemy => SqlAlchemyExporter.render_entity_with_schema(table, schema),
Orm::SqlModel => SqlModelExporter.render_entity_with_schema(table, schema),
Orm::Jpa => JpaExporter.render_entity_with_schema(table, schema),
Orm::Prisma => PrismaExporter.render_entity_with_schema(table, schema),
}
}

Expand Down Expand Up @@ -87,6 +90,7 @@ mod tests {
#[case("sqlalchemy", Orm::SqlAlchemy)]
#[case("sqlmodel", Orm::SqlModel)]
#[case("jpa", Orm::Jpa)]
#[case("prisma", Orm::Prisma)]
fn test_render_entity_snapshots(#[case] name: &str, #[case] orm: Orm) {
let table = simple_table();
let result = render_entity(orm, &table);
Expand All @@ -101,6 +105,7 @@ mod tests {
#[case("sqlalchemy", Orm::SqlAlchemy)]
#[case("sqlmodel", Orm::SqlModel)]
#[case("jpa", Orm::Jpa)]
#[case("prisma", Orm::Prisma)]
fn test_render_entity_with_schema_snapshots(#[case] name: &str, #[case] orm: Orm) {
let table = simple_table();
let schema = vec![table.clone()];
Expand Down
Loading