Compare commits

..

4 commits

Author SHA1 Message Date
e3ce642226 WIP 2026-01-06 00:27:31 +01:00
5f45671b74 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-04 22:14:00 +01:00
32ef1f7b33 fix(migrations): add indicatives comment at start of SQL files 2025-10-17 20:30:35 +02:00
d8624a762d build: workspace packages metadata and license 2025-10-12 15:12:08 +02:00
6 changed files with 55 additions and 49 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 2025 Matthieu Bessat Copyright 2020 LaunchBadge, LLC
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) 2025 Matthieu Bessat Copyright (c) 2020 LaunchBadge, LLC
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
- [ ] delete_many - custom queries
- [ ] 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_of_related_entity_method, models::{Field, FieldForeignMode, Model}}; 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::{SourceNode, SourceNodeContainer}; use crate::generators::{SourceNode, SourceNodeContainer};
@ -242,42 +242,6 @@ 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();
@ -304,7 +268,6 @@ 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> =
@ -332,7 +295,10 @@ 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_of_related_entity_method(model, &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)
).collect(); ).collect();
// TODO: add import line // TODO: add import line
@ -365,13 +331,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,9 +3,7 @@ use quote::{format_ident, quote};
use crate::models::{Field, FieldForeignMode, Model}; use crate::models::{Field, FieldForeignMode, Model};
/// method that can be used to retreive a list of entities of type X that are the children of a parent type Y pub fn gen_get_many_by_related_entity_method(model: &Model, foreign_key_field: &Field) -> TokenStream {
/// 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 {
@ -17,7 +15,7 @@ pub fn gen_get_many_of_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_of_{}", foreign_ref_params.target_resource_name); let func_name_ident = format_ident!("get_many_{}_by_{}", foreign_ref_params.reverse_relation_name, 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> {
@ -29,3 +27,45 @@ pub fn gen_get_many_of_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_case(Case::Snake) target_resource_name: target_type_name.to_lowercase()
} }
); );
} }