use super::schema::{DbSchema, ScalarType, Table};
use std::fmt::Write;
const SHARED_INPUTS: &str = "\
enum order_by { asc desc }
input Int_comparison_exp { _eq: Int _neq: Int _gt: Int _gte: Int _lt: Int _lte: Int _in: [Int!] _is_null: Boolean }
input Float_comparison_exp { _eq: Float _neq: Float _gt: Float _gte: Float _lt: Float _lte: Float _in: [Float!] _is_null: Boolean }
input String_comparison_exp { _eq: String _neq: String _gt: String _gte: String _lt: String _lte: String _in: [String!] _like: String _is_null: Boolean }
input Boolean_comparison_exp { _eq: Boolean _neq: Boolean _is_null: Boolean }
input ID_comparison_exp { _eq: ID _neq: ID _in: [ID!] _is_null: Boolean }
";
pub(crate) fn generate_sdl(schema: &DbSchema) -> String {
let mut out = String::new();
out.push_str(SHARED_INPUTS);
for table in &schema.tables {
out.push('\n');
push_object_type(&mut out, table);
push_bool_exp(&mut out, table);
push_order_by(&mut out, table);
}
out.push('\n');
push_query_root(&mut out, schema);
out
}
pub(crate) fn generate_federation_sdl(schema: &DbSchema) -> String {
let mut out = String::new();
for table in &schema.tables {
if table.primary_key.is_empty() {
push_object_type(&mut out, table);
} else {
let key = table.primary_key.join(" ");
let _ = writeln!(out, "type {} @key(fields: \"{key}\") {{", table.name);
for col in &table.columns {
let bang = if col.nullable { "" } else { "!" };
let _ = writeln!(out, " {}: {}{bang}", col.name, col.ty.graphql_name());
}
out.push_str("}\n");
}
}
out.push_str("type Query {\n");
for table in &schema.tables {
let _ = writeln!(out, " {t}: [{t}!]!", t = table.name);
if !table.primary_key.is_empty() {
let _ = writeln!(out, " {t}_by_pk({}): {t}", pk_args(table), t = table.name);
}
}
out.push_str("}\n");
out
}
fn push_object_type(out: &mut String, table: &Table) {
let _ = writeln!(out, "type {} {{", table.name);
for col in &table.columns {
let bang = if col.nullable { "" } else { "!" };
let _ = writeln!(out, " {}: {}{bang}", col.name, col.ty.graphql_name());
}
out.push_str("}\n");
}
fn push_bool_exp(out: &mut String, table: &Table) {
let _ = writeln!(out, "input {}_bool_exp {{", table.name);
let _ = writeln!(out, " _and: [{}_bool_exp!]", table.name);
let _ = writeln!(out, " _or: [{}_bool_exp!]", table.name);
let _ = writeln!(out, " _not: {}_bool_exp", table.name);
for col in &table.columns {
let _ = writeln!(
out,
" {}: {}_comparison_exp",
col.name,
col.ty.graphql_name()
);
}
out.push_str("}\n");
}
fn push_order_by(out: &mut String, table: &Table) {
let _ = writeln!(out, "input {}_order_by {{", table.name);
for col in &table.columns {
let _ = writeln!(out, " {}: order_by", col.name);
}
out.push_str("}\n");
}
fn push_query_root(out: &mut String, schema: &DbSchema) {
out.push_str("type Query {\n");
for table in &schema.tables {
let _ = writeln!(
out,
" {t}(where: {t}_bool_exp, order_by: [{t}_order_by!], limit: Int, offset: Int): [{t}!]!",
t = table.name
);
if !table.primary_key.is_empty() {
let args = pk_args(table);
let _ = writeln!(out, " {t}_by_pk({args}): {t}", t = table.name);
}
}
out.push_str("}\n");
}
fn pk_args(table: &Table) -> String {
table
.primary_key
.iter()
.map(|pk| {
let ty = table
.column(pk)
.map_or(ScalarType::Id, |c| c.ty)
.graphql_name();
format!("{pk}: {ty}!")
})
.collect::<Vec<_>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use super::super::schema::{Column, DbSchema, ForeignKey, ScalarType, Table};
use super::*;
fn users_and_posts() -> DbSchema {
DbSchema {
tables: vec![
Table {
name: "users".into(),
columns: vec![
Column {
name: "id".into(),
ty: ScalarType::Id,
nullable: false,
},
Column {
name: "name".into(),
ty: ScalarType::String,
nullable: true,
},
],
primary_key: vec!["id".into()],
foreign_keys: vec![],
},
Table {
name: "posts".into(),
columns: vec![
Column {
name: "id".into(),
ty: ScalarType::Id,
nullable: false,
},
Column {
name: "author_id".into(),
ty: ScalarType::Id,
nullable: false,
},
Column {
name: "views".into(),
ty: ScalarType::Int,
nullable: true,
},
],
primary_key: vec!["id".into()],
foreign_keys: vec![ForeignKey {
columns: vec!["author_id".into()],
ref_table: "users".into(),
ref_columns: vec!["id".into()],
}],
},
],
}
}
#[test]
fn emits_an_object_type_per_table_with_nullability() {
let sdl = generate_sdl(&users_and_posts());
assert!(sdl.contains("type users {"));
assert!(sdl.contains(" id: ID!"));
assert!(sdl.contains(" name: String\n")); assert!(sdl.contains("type posts {"));
assert!(sdl.contains(" views: Int\n"));
}
#[test]
fn emits_list_and_by_pk_root_fields() {
let sdl = generate_sdl(&users_and_posts());
assert!(sdl.contains(
"users(where: users_bool_exp, order_by: [users_order_by!], limit: Int, offset: Int): [users!]!"
));
assert!(sdl.contains("users_by_pk(id: ID!): users"));
}
#[test]
fn a_pkless_table_gets_no_by_pk_field() {
let schema = DbSchema {
tables: vec![Table {
name: "events".into(),
columns: vec![Column {
name: "kind".into(),
ty: ScalarType::String,
nullable: false,
}],
primary_key: vec![],
foreign_keys: vec![],
}],
};
let sdl = generate_sdl(&schema);
assert!(sdl.contains("events(where:"));
assert!(!sdl.contains("events_by_pk"));
}
#[test]
fn federation_sdl_gives_entities_a_key_directive() {
let sdl = generate_federation_sdl(&users_and_posts());
assert!(sdl.contains(r#"type users @key(fields: "id") {"#));
assert!(sdl.contains(r#"type posts @key(fields: "id") {"#));
assert!(sdl.contains("users: [users!]!"));
assert!(sdl.contains("users_by_pk(id: ID!): users"));
}
#[test]
fn emits_shared_and_per_table_filter_inputs() {
let sdl = generate_sdl(&users_and_posts());
assert!(sdl.contains("enum order_by { asc desc }"));
assert!(sdl.contains("input String_comparison_exp"));
assert!(sdl.contains("input users_bool_exp {"));
assert!(sdl.contains(" name: String_comparison_exp"));
assert!(sdl.contains("input users_order_by {"));
assert!(sdl.contains(" id: order_by"));
}
}