pub mod object;
pub mod relationship;
use crate::input::mutation::{is_deletable, is_insertable, is_updatable};
use crate::schema::object::{to_camel_case, to_pascal_case, TableObjectType};
use crate::schema::relationship::RelationshipField;
use postrust_core::schema_cache::{SchemaCache, Table};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct SchemaConfig {
pub exposed_schemas: Vec<String>,
pub enable_mutations: bool,
pub enable_subscriptions: bool,
pub query_prefix: Option<String>,
pub query_suffix: Option<String>,
pub use_camel_case: bool,
}
impl Default for SchemaConfig {
fn default() -> Self {
Self {
exposed_schemas: vec!["public".to_string()],
enable_mutations: true,
enable_subscriptions: false,
query_prefix: None,
query_suffix: None,
use_camel_case: true,
}
}
}
impl SchemaConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_schemas(mut self, schemas: Vec<String>) -> Self {
self.exposed_schemas = schemas;
self
}
pub fn with_mutations(mut self, enable: bool) -> Self {
self.enable_mutations = enable;
self
}
pub fn with_subscriptions(mut self, enable: bool) -> Self {
self.enable_subscriptions = enable;
self
}
pub fn is_schema_exposed(&self, schema: &str) -> bool {
self.exposed_schemas.iter().any(|s| s == schema)
}
}
#[derive(Debug, Clone)]
pub struct GeneratedSchema {
pub object_types: HashMap<String, TableObjectType>,
pub query_fields: Vec<QueryField>,
pub mutation_fields: Vec<MutationField>,
pub relationship_fields: HashMap<String, Vec<RelationshipField>>,
}
impl GeneratedSchema {
pub fn get_object_type(&self, name: &str) -> Option<&TableObjectType> {
self.object_types.get(name)
}
pub fn get_query_field(&self, table_name: &str) -> Option<&QueryField> {
self.query_fields.iter().find(|f| f.table_name == table_name)
}
pub fn get_mutation_fields(&self, table_name: &str) -> Vec<&MutationField> {
self.mutation_fields
.iter()
.filter(|f| f.table_name == table_name)
.collect()
}
pub fn get_relationship_fields(&self, type_name: &str) -> Option<&Vec<RelationshipField>> {
self.relationship_fields.get(type_name)
}
pub fn table_names(&self) -> Vec<&str> {
self.object_types.values().map(|t| t.table.name.as_str()).collect()
}
pub fn type_names(&self) -> Vec<&str> {
self.object_types.keys().map(|s| s.as_str()).collect()
}
}
#[derive(Debug, Clone)]
pub struct QueryField {
pub name: String,
pub table_name: String,
pub return_type: String,
pub is_list: bool,
pub is_by_pk: bool,
pub description: Option<String>,
}
impl QueryField {
pub fn list(table: &Table, config: &SchemaConfig) -> Self {
let type_name = to_pascal_case(&table.name);
let field_name = if config.use_camel_case {
to_camel_case(&table.name)
} else {
table.name.clone()
};
let name = match (&config.query_prefix, &config.query_suffix) {
(Some(prefix), None) => format!("{}{}", prefix, to_pascal_case(&field_name)),
(None, Some(suffix)) => format!("{}{}", field_name, suffix),
(Some(prefix), Some(suffix)) => {
format!("{}{}{}", prefix, to_pascal_case(&field_name), suffix)
}
(None, None) => field_name,
};
Self {
name,
table_name: table.name.clone(),
return_type: format!("[{}!]!", type_name),
is_list: true,
is_by_pk: false,
description: Some(format!("Query {} records", table.name)),
}
}
pub fn by_pk(table: &Table, config: &SchemaConfig) -> Option<Self> {
if table.pk_cols.is_empty() {
return None;
}
let type_name = to_pascal_case(&table.name);
let singular = singularize(&table.name);
let field_name = if config.use_camel_case {
format!("{}ByPk", to_camel_case(&singular))
} else {
format!("{}_by_pk", singular)
};
Some(Self {
name: field_name,
table_name: table.name.clone(),
return_type: type_name,
is_list: false,
is_by_pk: true,
description: Some(format!("Get a single {} by primary key", singular)),
})
}
}
#[derive(Debug, Clone)]
pub struct MutationField {
pub name: String,
pub table_name: String,
pub mutation_type: MutationType,
pub return_type: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MutationType {
Insert,
InsertOne,
Update,
UpdateByPk,
Delete,
DeleteByPk,
}
impl MutationField {
pub fn insert_fields(table: &Table, config: &SchemaConfig) -> Vec<Self> {
if !is_insertable(table) {
return vec![];
}
let type_name = to_pascal_case(&table.name);
let singular = singularize(&table.name);
let mut fields = vec![];
let name = if config.use_camel_case {
format!("insert{}", to_pascal_case(&table.name))
} else {
format!("insert_{}", table.name)
};
fields.push(Self {
name,
table_name: table.name.clone(),
mutation_type: MutationType::Insert,
return_type: format!("[{}!]!", type_name),
description: Some(format!("Insert multiple {} records", table.name)),
});
let name = if config.use_camel_case {
format!("insert{}One", to_pascal_case(&singular))
} else {
format!("insert_{}_one", singular)
};
fields.push(Self {
name,
table_name: table.name.clone(),
mutation_type: MutationType::InsertOne,
return_type: type_name.clone(),
description: Some(format!("Insert a single {} record", singular)),
});
fields
}
pub fn update_fields(table: &Table, config: &SchemaConfig) -> Vec<Self> {
if !is_updatable(table) {
return vec![];
}
let type_name = to_pascal_case(&table.name);
let singular = singularize(&table.name);
let mut fields = vec![];
let name = if config.use_camel_case {
format!("update{}", to_pascal_case(&table.name))
} else {
format!("update_{}", table.name)
};
fields.push(Self {
name,
table_name: table.name.clone(),
mutation_type: MutationType::Update,
return_type: format!("[{}!]!", type_name),
description: Some(format!("Update {} records", table.name)),
});
if !table.pk_cols.is_empty() {
let name = if config.use_camel_case {
format!("update{}ByPk", to_pascal_case(&singular))
} else {
format!("update_{}_by_pk", singular)
};
fields.push(Self {
name,
table_name: table.name.clone(),
mutation_type: MutationType::UpdateByPk,
return_type: type_name,
description: Some(format!("Update a single {} by primary key", singular)),
});
}
fields
}
pub fn delete_fields(table: &Table, config: &SchemaConfig) -> Vec<Self> {
if !is_deletable(table) {
return vec![];
}
let type_name = to_pascal_case(&table.name);
let singular = singularize(&table.name);
let mut fields = vec![];
let name = if config.use_camel_case {
format!("delete{}", to_pascal_case(&table.name))
} else {
format!("delete_{}", table.name)
};
fields.push(Self {
name,
table_name: table.name.clone(),
mutation_type: MutationType::Delete,
return_type: format!("[{}!]!", type_name),
description: Some(format!("Delete {} records", table.name)),
});
if !table.pk_cols.is_empty() {
let name = if config.use_camel_case {
format!("delete{}ByPk", to_pascal_case(&singular))
} else {
format!("delete_{}_by_pk", singular)
};
fields.push(Self {
name,
table_name: table.name.clone(),
mutation_type: MutationType::DeleteByPk,
return_type: type_name,
description: Some(format!("Delete a single {} by primary key", singular)),
});
}
fields
}
}
pub fn build_schema(schema_cache: &SchemaCache, config: &SchemaConfig) -> GeneratedSchema {
let mut object_types = HashMap::new();
let mut query_fields = Vec::new();
let mut mutation_fields = Vec::new();
let mut relationship_fields = HashMap::new();
for table in schema_cache.tables.values() {
if !config.is_schema_exposed(&table.schema) {
continue;
}
let obj_type = TableObjectType::from_table(table);
let type_name = obj_type.name.clone();
query_fields.push(QueryField::list(table, config));
if let Some(by_pk) = QueryField::by_pk(table, config) {
query_fields.push(by_pk);
}
if config.enable_mutations {
mutation_fields.extend(MutationField::insert_fields(table, config));
mutation_fields.extend(MutationField::update_fields(table, config));
mutation_fields.extend(MutationField::delete_fields(table, config));
}
let rels: Vec<RelationshipField> = schema_cache
.get_relationships(&table.qualified_identifier(), &table.schema)
.map(|relationships| {
relationships
.iter()
.map(|r| RelationshipField::from_relationship(r))
.collect()
})
.unwrap_or_default();
if !rels.is_empty() {
relationship_fields.insert(type_name.clone(), rels);
}
object_types.insert(type_name, obj_type);
}
GeneratedSchema {
object_types,
query_fields,
mutation_fields,
relationship_fields,
}
}
fn singularize(s: &str) -> String {
if s.ends_with("ies") {
format!("{}y", &s[..s.len() - 3])
} else if s.ends_with("es")
&& (s.ends_with("ses") || s.ends_with("xes") || s.ends_with("ches") || s.ends_with("shes"))
{
s[..s.len() - 2].to_string()
} else if s.ends_with('s') && !s.ends_with("ss") {
s[..s.len() - 1].to_string()
} else {
s.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;
use postrust_core::schema_cache::Column;
use pretty_assertions::assert_eq;
fn create_test_table(name: &str, insertable: bool, updatable: bool, deletable: bool) -> Table {
let mut columns = IndexMap::new();
columns.insert(
"id".into(),
Column {
name: "id".into(),
description: None,
nullable: false,
data_type: "integer".into(),
nominal_type: "int4".into(),
max_len: None,
default: Some("nextval('id_seq')".into()),
enum_values: vec![],
is_pk: true,
position: 1,
},
);
columns.insert(
"name".into(),
Column {
name: "name".into(),
description: None,
nullable: false,
data_type: "text".into(),
nominal_type: "text".into(),
max_len: None,
default: None,
enum_values: vec![],
is_pk: false,
position: 2,
},
);
Table {
schema: "public".into(),
name: name.into(),
description: None,
is_view: false,
insertable,
updatable,
deletable,
pk_cols: vec!["id".into()],
columns,
}
}
fn create_test_schema_cache() -> SchemaCache {
use std::collections::{HashMap, HashSet};
let mut tables = HashMap::new();
let users = create_test_table("users", true, true, true);
let posts = create_test_table("posts", true, true, true);
let comments = create_test_table("comments", true, false, false);
tables.insert(users.qualified_identifier(), users);
tables.insert(posts.qualified_identifier(), posts);
tables.insert(comments.qualified_identifier(), comments);
SchemaCache {
tables,
relationships: HashMap::new(),
routines: HashMap::new(),
timezones: HashSet::new(),
pg_version: 150000,
}
}
#[test]
fn test_schema_config_default() {
let config = SchemaConfig::default();
assert!(config.is_schema_exposed("public"));
assert!(!config.is_schema_exposed("private"));
assert!(config.enable_mutations);
assert!(!config.enable_subscriptions);
}
#[test]
fn test_schema_config_with_schemas() {
let config = SchemaConfig::new()
.with_schemas(vec!["api".to_string(), "public".to_string()]);
assert!(config.is_schema_exposed("api"));
assert!(config.is_schema_exposed("public"));
assert!(!config.is_schema_exposed("private"));
}
#[test]
fn test_schema_config_mutations_disabled() {
let config = SchemaConfig::new().with_mutations(false);
assert!(!config.enable_mutations);
}
#[test]
fn test_query_field_list() {
let table = create_test_table("users", true, true, true);
let config = SchemaConfig::default();
let field = QueryField::list(&table, &config);
assert_eq!(field.name, "users");
assert_eq!(field.return_type, "[Users!]!");
assert!(field.is_list);
assert!(!field.is_by_pk);
}
#[test]
fn test_query_field_list_with_prefix() {
let table = create_test_table("users", true, true, true);
let config = SchemaConfig {
query_prefix: Some("all".to_string()),
..Default::default()
};
let field = QueryField::list(&table, &config);
assert_eq!(field.name, "allUsers");
}
#[test]
fn test_query_field_list_with_suffix() {
let table = create_test_table("users", true, true, true);
let config = SchemaConfig {
query_suffix: Some("Collection".to_string()),
..Default::default()
};
let field = QueryField::list(&table, &config);
assert_eq!(field.name, "usersCollection");
}
#[test]
fn test_query_field_by_pk() {
let table = create_test_table("users", true, true, true);
let config = SchemaConfig::default();
let field = QueryField::by_pk(&table, &config).unwrap();
assert_eq!(field.name, "userByPk");
assert_eq!(field.return_type, "Users");
assert!(!field.is_list);
assert!(field.is_by_pk);
}
#[test]
fn test_query_field_by_pk_no_pk() {
let mut table = create_test_table("users", true, true, true);
table.pk_cols = vec![];
let config = SchemaConfig::default();
let field = QueryField::by_pk(&table, &config);
assert!(field.is_none());
}
#[test]
fn test_mutation_field_insert() {
let table = create_test_table("users", true, true, true);
let config = SchemaConfig::default();
let fields = MutationField::insert_fields(&table, &config);
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name, "insertUsers");
assert_eq!(fields[0].mutation_type, MutationType::Insert);
assert_eq!(fields[1].name, "insertUserOne");
assert_eq!(fields[1].mutation_type, MutationType::InsertOne);
}
#[test]
fn test_mutation_field_insert_not_insertable() {
let table = create_test_table("users", false, true, true);
let config = SchemaConfig::default();
let fields = MutationField::insert_fields(&table, &config);
assert!(fields.is_empty());
}
#[test]
fn test_mutation_field_update() {
let table = create_test_table("users", true, true, true);
let config = SchemaConfig::default();
let fields = MutationField::update_fields(&table, &config);
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name, "updateUsers");
assert_eq!(fields[0].mutation_type, MutationType::Update);
assert_eq!(fields[1].name, "updateUserByPk");
assert_eq!(fields[1].mutation_type, MutationType::UpdateByPk);
}
#[test]
fn test_mutation_field_update_not_updatable() {
let table = create_test_table("users", true, false, true);
let config = SchemaConfig::default();
let fields = MutationField::update_fields(&table, &config);
assert!(fields.is_empty());
}
#[test]
fn test_mutation_field_delete() {
let table = create_test_table("users", true, true, true);
let config = SchemaConfig::default();
let fields = MutationField::delete_fields(&table, &config);
assert_eq!(fields.len(), 2);
assert_eq!(fields[0].name, "deleteUsers");
assert_eq!(fields[0].mutation_type, MutationType::Delete);
assert_eq!(fields[1].name, "deleteUserByPk");
assert_eq!(fields[1].mutation_type, MutationType::DeleteByPk);
}
#[test]
fn test_mutation_field_delete_not_deletable() {
let table = create_test_table("users", true, true, false);
let config = SchemaConfig::default();
let fields = MutationField::delete_fields(&table, &config);
assert!(fields.is_empty());
}
#[test]
fn test_singularize() {
assert_eq!(singularize("users"), "user");
assert_eq!(singularize("posts"), "post");
assert_eq!(singularize("categories"), "category");
assert_eq!(singularize("boxes"), "box");
assert_eq!(singularize("matches"), "match");
assert_eq!(singularize("class"), "class");
}
#[test]
fn test_build_schema_object_types() {
let cache = create_test_schema_cache();
let config = SchemaConfig::default();
let schema = build_schema(&cache, &config);
assert_eq!(schema.object_types.len(), 3);
assert!(schema.get_object_type("Users").is_some());
assert!(schema.get_object_type("Posts").is_some());
assert!(schema.get_object_type("Comments").is_some());
}
#[test]
fn test_build_schema_query_fields() {
let cache = create_test_schema_cache();
let config = SchemaConfig::default();
let schema = build_schema(&cache, &config);
assert_eq!(schema.query_fields.len(), 6);
let users_field = schema.get_query_field("users").unwrap();
assert_eq!(users_field.name, "users");
assert!(users_field.is_list);
}
#[test]
fn test_build_schema_mutation_fields() {
let cache = create_test_schema_cache();
let config = SchemaConfig::default();
let schema = build_schema(&cache, &config);
assert_eq!(schema.mutation_fields.len(), 14);
let users_mutations = schema.get_mutation_fields("users");
assert_eq!(users_mutations.len(), 6);
}
#[test]
fn test_build_schema_mutations_disabled() {
let cache = create_test_schema_cache();
let config = SchemaConfig::new().with_mutations(false);
let schema = build_schema(&cache, &config);
assert!(schema.mutation_fields.is_empty());
}
#[test]
fn test_build_schema_table_names() {
let cache = create_test_schema_cache();
let config = SchemaConfig::default();
let schema = build_schema(&cache, &config);
let names = schema.table_names();
assert_eq!(names.len(), 3);
assert!(names.contains(&"users"));
assert!(names.contains(&"posts"));
assert!(names.contains(&"comments"));
}
#[test]
fn test_build_schema_type_names() {
let cache = create_test_schema_cache();
let config = SchemaConfig::default();
let schema = build_schema(&cache, &config);
let names = schema.type_names();
assert_eq!(names.len(), 3);
assert!(names.contains(&"Users"));
assert!(names.contains(&"Posts"));
assert!(names.contains(&"Comments"));
}
#[test]
fn test_build_schema_exposed_schemas() {
let mut cache = create_test_schema_cache();
let private_table = Table {
schema: "private".into(),
name: "secrets".into(),
description: None,
is_view: false,
insertable: true,
updatable: true,
deletable: true,
pk_cols: vec!["id".into()],
columns: indexmap::IndexMap::new(),
};
cache.tables.insert(private_table.qualified_identifier(), private_table);
let config = SchemaConfig::default(); let schema = build_schema(&cache, &config);
assert_eq!(schema.object_types.len(), 3);
assert!(schema.get_object_type("Secrets").is_none());
}
#[test]
fn test_generated_schema_get_object_type() {
let cache = create_test_schema_cache();
let config = SchemaConfig::default();
let schema = build_schema(&cache, &config);
let users = schema.get_object_type("Users").unwrap();
assert_eq!(users.table.name, "users");
}
#[test]
fn test_generated_schema_get_query_field() {
let cache = create_test_schema_cache();
let config = SchemaConfig::default();
let schema = build_schema(&cache, &config);
let field = schema.get_query_field("posts").unwrap();
assert_eq!(field.table_name, "posts");
}
#[test]
fn test_generated_schema_get_mutation_fields() {
let cache = create_test_schema_cache();
let config = SchemaConfig::default();
let schema = build_schema(&cache, &config);
let fields = schema.get_mutation_fields("comments");
assert_eq!(fields.len(), 2); }
}