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
identification within third-party archives.
Copyright 2020 LaunchBadge, LLC
Copyright 2025 Matthieu Bessat
Licensed under the Apache License, Version 2.0 (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
person obtaining a copy of this software and associated

View file

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

View file

@ -4,7 +4,7 @@ use quote::{format_ident, quote};
use syn::File;
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};
@ -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> {
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 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_many_by_id_method_code = gen_delete_many_by_id_method(model);
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 }
).collect();
let related_entity_methods_codes: Vec<TokenStream> = fields_with_foreign_refs.iter().map(|field|
gen_get_many_by_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)
gen_get_many_of_related_entity_method(model, &field)
).collect();
// 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_many_by_id_method_code
#(#query_by_field_methods)*
#(#query_many_by_field_methods)*
#(#related_entity_methods_codes)*
#(#related_entities_methods_codes)*
}
};
// convert TokenStream into rust code as string

View file

@ -3,7 +3,9 @@ use quote::{format_ident, quote};
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 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 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! {
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(
ForeignRefParams {
reverse_relation_name: rrn,
target_resource_name: target_type_name.to_lowercase()
target_resource_name: target_type_name.to_case(Case::Snake)
}
);
}