fraiseql_cli/commands/
generate_proto.rs1use std::{fs, path::Path};
4
5use anyhow::Context;
6use fraiseql_core::{
7 db::dialect::{MySqlDialect, PostgresDialect, SqlDialect, SqlServerDialect, SqliteDialect},
8 schema::CompiledSchema,
9};
10
11use crate::{
12 codegen::{proto_gen, row_views},
13 output::OutputFormatter,
14};
15
16pub(crate) fn resolve_dialect(name: &str) -> anyhow::Result<Box<dyn SqlDialect>> {
22 match name {
23 "postgres" | "postgresql" => Ok(Box::new(PostgresDialect)),
24 "mysql" => Ok(Box::new(MySqlDialect)),
25 "sqlite" => Ok(Box::new(SqliteDialect)),
26 "sqlserver" => Ok(Box::new(SqlServerDialect)),
27 other => Err(anyhow::anyhow!(
28 "Unknown dialect '{other}'. Expected: postgres, mysql, sqlite, sqlserver"
29 )),
30 }
31}
32
33pub(crate) fn build_file_descriptor_set(
43 proto_source: &str,
44 package: &str,
45) -> anyhow::Result<Vec<u8>> {
46 use prost::Message;
47 use prost_types::{FileDescriptorProto, FileDescriptorSet};
48
49 let mut file = FileDescriptorProto {
50 name: Some("service.proto".to_string()),
51 package: Some(package.to_string()),
52 syntax: Some("proto3".to_string()),
53 ..FileDescriptorProto::default()
54 };
55
56 if proto_source.contains("google/protobuf/timestamp.proto") {
58 file.dependency.push("google/protobuf/timestamp.proto".to_string());
59 }
60 if proto_source.contains("google/protobuf/struct.proto") {
61 file.dependency.push("google/protobuf/struct.proto".to_string());
62 }
63
64 let fds = FileDescriptorSet { file: vec![file] };
65
66 let mut buf = Vec::with_capacity(fds.encoded_len());
67 fds.encode(&mut buf).context("Failed to encode FileDescriptorSet")?;
68 Ok(buf)
69}
70
71pub fn run(
83 schema_path: &str,
84 output_dir: &str,
85 package: &str,
86 dialect_name: &str,
87 formatter: &OutputFormatter,
88) -> anyhow::Result<()> {
89 formatter.progress("Loading compiled schema...");
90
91 let content = fs::read_to_string(schema_path).context("Failed to read compiled schema file")?;
92 let schema: CompiledSchema =
93 serde_json::from_str(&content).context("Failed to parse compiled schema JSON")?;
94
95 let dialect = resolve_dialect(dialect_name)?;
96
97 let (include_types, exclude_types) = schema
99 .grpc_config
100 .as_ref()
101 .map(|g| (g.include_types.clone(), g.exclude_types.clone()))
102 .unwrap_or_default();
103
104 formatter.progress("Generating service.proto...");
106 let proto_source =
107 proto_gen::generate_proto_file(&schema, package, &include_types, &exclude_types);
108
109 formatter.progress("Generating vr_migrations.sql...");
111 let row_view_ddl = row_views::generate_all_row_views(
112 dialect.as_ref(),
113 &schema.types,
114 &include_types,
115 &exclude_types,
116 );
117
118 formatter.progress("Building descriptor.binpb...");
120 let descriptor_bytes = build_file_descriptor_set(&proto_source, package)?;
121
122 let out_path = Path::new(output_dir);
124 fs::create_dir_all(out_path).context("Failed to create output directory")?;
125
126 let proto_path = out_path.join("service.proto");
127 fs::write(&proto_path, &proto_source)
128 .with_context(|| format!("Failed to write {}", proto_path.display()))?;
129
130 let sql_path = out_path.join("vr_migrations.sql");
131 fs::write(&sql_path, &row_view_ddl)
132 .with_context(|| format!("Failed to write {}", sql_path.display()))?;
133
134 let desc_path = out_path.join("descriptor.binpb");
135 fs::write(&desc_path, &descriptor_bytes)
136 .with_context(|| format!("Failed to write {}", desc_path.display()))?;
137
138 formatter.section("Generated files");
139 formatter.progress(&format!(" {}", proto_path.display()));
140 formatter.progress(&format!(" {}", sql_path.display()));
141 formatter.progress(&format!(" {}", desc_path.display()));
142
143 Ok(())
144}