Compare commits

..

4 commits

Author SHA1 Message Date
2da8721778 feat(repositories): add methods get_many_of_related_entity and delete_many_by_id 2026-01-11 16:30:57 +01:00
5e0ffe67c3 feat: one-to-many relation helper
Allow one to specify that a field of a model is a foreign key.
It will generate a bunch of helper methods to query related entities
from one entity.
2026-01-11 16:28:07 +01:00
cbe60d1bd2 fix(migrations): add indicatives comment at start of SQL files 2026-01-11 16:28:07 +01:00
dc77c71f68 build: workspace packages metadata and license 2026-01-11 16:28:06 +01:00
6 changed files with 49 additions and 55 deletions

View file

@ -186,7 +186,7 @@ file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2020 LaunchBadge, LLC Copyright 2025 Matthieu Bessat
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View file

@ -1,4 +1,4 @@
Copyright (c) 2020 LaunchBadge, LLC Copyright (c) 2025 Matthieu Bessat
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

View file

@ -14,7 +14,7 @@
- insert - insert
- update - update
- delete_by_id - delete_by_id
- custom queries - [ ] delete_many
- [ ] Config file for project - [ ] Config file for project
- configure models path - configure models path

View file

@ -4,7 +4,7 @@ use quote::{format_ident, quote};
use syn::File; use syn::File;
use heck::ToSnakeCase; use heck::ToSnakeCase;
use crate::{generators::repositories::relations::{gen_get_many_by_related_entities_method, gen_get_many_by_related_entity_method}, models::{Field, FieldForeignMode, Model}}; use crate::{generators::repositories::relations::gen_get_many_of_related_entity_method, models::{Field, FieldForeignMode, Model}};
use crate::generators::{SourceNode, SourceNodeContainer}; use crate::generators::{SourceNode, SourceNodeContainer};
@ -242,6 +242,42 @@ fn gen_delete_by_id_method(model: &Model) -> TokenStream {
} }
} }
fn gen_delete_many_by_id_method(model: &Model) -> TokenStream {
let primary_key = &model.fields.iter()
.find(|f| f.is_primary)
.expect("A model must have at least one primary key")
.name;
let func_name_ident = format_ident!("delete_many_by_{}", primary_key);
let delete_query_tmpl = format!(
"DELETE FROM {} WHERE {} IN ({{}})",
model.table_name,
primary_key
);
quote! {
pub async fn #func_name_ident(&self, ids: &[&str]) -> Result<(), sqlx::Error> {
if ids.is_empty() {
return Ok(())
}
let placeholder_params: String = (1..=(ids.len()))
.map(|i| format!("${}", i))
.collect::<Vec<String>>()
.join(",");
let query_sql = format!(#delete_query_tmpl, placeholder_params);
let mut query = sqlx::query(&query_sql);
for item_id in ids {
query = query.bind(item_id)
}
query
.execute(&self.db.0)
.await?;
Ok(())
}
}
}
pub fn generate_repository_file(all_models: &[Model], model: &Model) -> Result<SourceNodeContainer> { pub fn generate_repository_file(all_models: &[Model], model: &Model) -> Result<SourceNodeContainer> {
let resource_name = model.name.clone(); let resource_name = model.name.clone();
@ -268,6 +304,7 @@ pub fn generate_repository_file(all_models: &[Model], model: &Model) -> Result<S
let insert_many_method_code = gen_insert_many_method(model); let insert_many_method_code = gen_insert_many_method(model);
let update_by_id_method_code = gen_update_by_id_method(model); let update_by_id_method_code = gen_update_by_id_method(model);
let delete_by_id_method_code = gen_delete_by_id_method(model); let delete_by_id_method_code = gen_delete_by_id_method(model);
let delete_many_by_id_method_code = gen_delete_many_by_id_method(model);
let query_by_field_methods: Vec<TokenStream> = let query_by_field_methods: Vec<TokenStream> =
@ -295,10 +332,7 @@ pub fn generate_repository_file(all_models: &[Model], model: &Model) -> Result<S
match f.foreign_mode { FieldForeignMode::ForeignRef(_) => true, FieldForeignMode::NotRef => false } match f.foreign_mode { FieldForeignMode::ForeignRef(_) => true, FieldForeignMode::NotRef => false }
).collect(); ).collect();
let related_entity_methods_codes: Vec<TokenStream> = fields_with_foreign_refs.iter().map(|field| let related_entity_methods_codes: Vec<TokenStream> = fields_with_foreign_refs.iter().map(|field|
gen_get_many_by_related_entity_method(model, &field) gen_get_many_of_related_entity_method(model, &field)
).collect();
let related_entities_methods_codes: Vec<TokenStream> = fields_with_foreign_refs.iter().map(|field|
gen_get_many_by_related_entities_method(all_models, model, &field)
).collect(); ).collect();
// TODO: add import line // TODO: add import line
@ -331,13 +365,13 @@ pub fn generate_repository_file(all_models: &[Model], model: &Model) -> Result<S
#delete_by_id_method_code #delete_by_id_method_code
#delete_many_by_id_method_code
#(#query_by_field_methods)* #(#query_by_field_methods)*
#(#query_many_by_field_methods)* #(#query_many_by_field_methods)*
#(#related_entity_methods_codes)* #(#related_entity_methods_codes)*
#(#related_entities_methods_codes)*
} }
}; };
// convert TokenStream into rust code as string // convert TokenStream into rust code as string

View file

@ -3,7 +3,9 @@ use quote::{format_ident, quote};
use crate::models::{Field, FieldForeignMode, Model}; use crate::models::{Field, FieldForeignMode, Model};
pub fn gen_get_many_by_related_entity_method(model: &Model, foreign_key_field: &Field) -> TokenStream { /// method that can be used to retreive a list of entities of type X that are the children of a parent type Y
/// ex: get all comments of a post
pub fn gen_get_many_of_related_entity_method(model: &Model, foreign_key_field: &Field) -> TokenStream {
let resource_ident = format_ident!("{}", &model.name); let resource_ident = format_ident!("{}", &model.name);
let foreign_ref_params = match &foreign_key_field.foreign_mode { let foreign_ref_params = match &foreign_key_field.foreign_mode {
@ -15,7 +17,7 @@ pub fn gen_get_many_by_related_entity_method(model: &Model, foreign_key_field: &
let select_query = format!("SELECT * FROM {} WHERE {} = $1", model.table_name, foreign_key_field.name); let select_query = format!("SELECT * FROM {} WHERE {} = $1", model.table_name, foreign_key_field.name);
let func_name_ident = format_ident!("get_many_{}_by_{}", foreign_ref_params.reverse_relation_name, foreign_ref_params.target_resource_name); let func_name_ident = format_ident!("get_many_of_{}", foreign_ref_params.target_resource_name);
quote! { quote! {
pub async fn #func_name_ident(&self, item_id: &str) -> Result<Vec<#resource_ident>, sqlx::Error> { pub async fn #func_name_ident(&self, item_id: &str) -> Result<Vec<#resource_ident>, sqlx::Error> {
@ -27,45 +29,3 @@ pub fn gen_get_many_by_related_entity_method(model: &Model, foreign_key_field: &
} }
} }
pub fn gen_get_many_by_related_entities_method(all_models: &[Model], model: &Model, foreign_key_field: &Field) -> TokenStream {
let resource_ident = format_ident!("{}", &model.name);
let foreign_ref_params = match &foreign_key_field.foreign_mode {
FieldForeignMode::ForeignRef(params) => params,
FieldForeignMode::NotRef => {
panic!("Expected foreign key");
}
};
let select_query = format!("SELECT * FROM {} WHERE {} IN ({{}})", model.table_name, foreign_key_field.name);
let target_resource = all_models.iter()
.find(|m| m.name.to_lowercase() == foreign_ref_params.target_resource_name.to_lowercase())
.expect("Could not find foreign ref target type associated resource");
let func_name_ident = format_ident!("get_many_{}_by_{}", foreign_ref_params.reverse_relation_name, target_resource.table_name);
quote! {
pub async fn #func_name_ident(&self, items_ids: Vec<String>) -> Result<Vec<#resource_ident>, sqlx::Error> {
if items_ids.is_empty() {
return Ok(vec![])
}
let placeholder_params: String = (1..=(items_ids.len()))
.map(|i| format!("${i}"))
.collect::<Vec<String>>()
.join(",");
let query_tmpl = format!(
#select_query,
placeholder_params
);
let mut query = sqlx::query_as::<_, #resource_ident>(&query_tmpl);
for id in items_ids {
query = query.bind(id)
}
query
.fetch_all(&self.db.0)
.await
}
}
}

View file

@ -248,7 +248,7 @@ pub fn parse_models(source_code_path: &Path) -> Result<Vec<Model>> {
output_field.foreign_mode = FieldForeignMode::ForeignRef( output_field.foreign_mode = FieldForeignMode::ForeignRef(
ForeignRefParams { ForeignRefParams {
reverse_relation_name: rrn, reverse_relation_name: rrn,
target_resource_name: target_type_name.to_lowercase() target_resource_name: target_type_name.to_case(Case::Snake)
} }
); );
} }