├── .gitignore ├── Cargo.toml ├── .travis.yml ├── juniper-eager-loading-code-gen ├── README.md ├── Cargo.toml └── src │ ├── lib.rs │ ├── impl_load_from_for_diesel.rs │ ├── derive_eager_loading │ └── field_args.rs │ └── derive_eager_loading.rs ├── juniper-eager-loading ├── README.md ├── tests │ ├── compile_pass │ │ ├── impl_load_from_pg.rs │ │ ├── impl_load_from_mysql.rs │ │ └── impl_load_from_sqlite.rs │ ├── helpers.rs │ ├── recursive_types.rs │ ├── rename_id_field.rs │ ├── interfaces.rs │ ├── mixed_id_types.rs │ ├── fields_with_arguments_macro.rs │ ├── fields_with_arguments.rs │ └── integration_tests.rs ├── Cargo.toml └── src │ ├── association.rs │ └── macros.rs ├── README.md ├── examples ├── has_one.rs ├── option_has_one.rs ├── has_many.rs ├── field_with_arguments.rs ├── has_many_through.rs ├── has_many_with_arguments.rs ├── has_one_no_macros.rs ├── has_many_no_macros.rs ├── option_has_one_no_macros.rs └── has_many_through_no_macros.rs └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "juniper-eager-loading", 5 | "juniper-eager-loading-code-gen", 6 | ] 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | rust: 4 | - stable 5 | - beta 6 | 7 | cache: cargo 8 | 9 | before_script: 10 | - rustup component add rustfmt 11 | 12 | script: 13 | - cargo fmt -- --check 14 | - cargo test --all 15 | -------------------------------------------------------------------------------- /juniper-eager-loading-code-gen/README.md: -------------------------------------------------------------------------------- 1 | # juniper-eager-loading-code-gen 2 | 3 | Internal crate for [juniper-eager-loading](https://crates.io/crates/juniper-eager-loading). 4 | 5 | You shouldn't have to depend on this crate directly. The procedural macros are re-exported by [juniper-eager-loading](https://crates.io/crates/juniper-eager-loading). 6 | -------------------------------------------------------------------------------- /juniper-eager-loading/README.md: -------------------------------------------------------------------------------- 1 | # [juniper-eager-loading](https://crates.io/crates/juniper-eager-loading) 2 | 3 | 4 | This is a library for avoiding N+1 query bugs designed to work with 5 | [Juniper][] and [juniper-from-schema][]. 6 | 7 | It is designed to make the most common association setups easy to handle and while being 8 | flexible and allowing you to customize things as needed. It is also 100% data store agnostic. 9 | So regardless if your API is backed by an SQL database or another API you can still use this 10 | library. 11 | 12 | See the [crate documentation](https://docs.rs/juniper-eager-loading/) for a usage examples and more info. 13 | 14 | [Juniper]: https://github.com/graphql-rust/juniper 15 | [juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema 16 | -------------------------------------------------------------------------------- /juniper-eager-loading-code-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["David Pedersen "] 3 | categories = ["web-programming"] 4 | description = "Eliminate N+1 query bugs when using Juniper" 5 | documentation = "https://docs.rs/juniper-eager-loading-code-gen" 6 | edition = "2018" 7 | homepage = "https://github.com/davidpdrsn/juniper-eager-loading-code-gen" 8 | keywords = ["web", "graphql", "juniper"] 9 | license = "MIT" 10 | name = "juniper-eager-loading-code-gen" 11 | readme = "README.md" 12 | repository = "https://github.com/davidpdrsn/juniper-eager-loading.git" 13 | version = "0.5.1" 14 | 15 | [dependencies] 16 | proc-macro2 = "1" 17 | quote = "1" 18 | syn = { version = "1", features = ["extra-traits"] } 19 | heck = "0.3" 20 | proc-macro-error = "0.4" 21 | bae = "0.1" 22 | 23 | [lib] 24 | proc-macro = true 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [juniper-eager-loading](https://crates.io/crates/juniper-eager-loading) 2 | 3 | This is a library for avoiding N+1 query bugs, designed to work with 4 | [Juniper][] and [juniper-from-schema][]. 5 | 6 | It is designed to make the most common association setups easy to handle and while being 7 | flexible and allowing you to customize things as needed. It is also 100% data store agnostic. 8 | So whether your API is backed by an SQL database or by another API you can still use this 9 | library. 10 | 11 | See the [crate documentation](https://docs.rs/juniper-eager-loading/) for usage examples and more info. 12 | 13 | Please note that the API is not considered stable, so breaking changes might be made. 14 | 15 | [Juniper]: https://github.com/graphql-rust/juniper 16 | [juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema 17 | 18 | --- 19 | 20 | License: MIT 21 | -------------------------------------------------------------------------------- /juniper-eager-loading-code-gen/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! See the docs for "juniper-eager-loading" for more info about this. 2 | 3 | #![recursion_limit = "256"] 4 | #![deny( 5 | unused_variables, 6 | mutable_borrow_reservation_conflict, 7 | dead_code, 8 | unused_must_use, 9 | unused_imports 10 | )] 11 | 12 | extern crate proc_macro; 13 | extern crate proc_macro2; 14 | 15 | mod derive_eager_loading; 16 | mod impl_load_from_for_diesel; 17 | 18 | use impl_load_from_for_diesel::Backend; 19 | use proc_macro_error::*; 20 | 21 | #[proc_macro_derive( 22 | EagerLoading, 23 | attributes(eager_loading, has_one, option_has_one, has_many, has_many_through) 24 | )] 25 | #[proc_macro_error] 26 | pub fn derive_eager_loading(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 27 | derive_eager_loading::gen_tokens(input) 28 | } 29 | 30 | #[proc_macro] 31 | pub fn impl_load_from_for_diesel_pg(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 32 | impl_load_from_for_diesel::go(input, Backend::Pg) 33 | } 34 | 35 | #[proc_macro] 36 | pub fn impl_load_from_for_diesel_mysql(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 37 | impl_load_from_for_diesel::go(input, Backend::Mysql) 38 | } 39 | 40 | #[proc_macro] 41 | pub fn impl_load_from_for_diesel_sqlite(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 42 | impl_load_from_for_diesel::go(input, Backend::Sqlite) 43 | } 44 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/compile_pass/impl_load_from_pg.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | use static_assertions::assert_impl_all; 5 | use diesel::prelude::*; 6 | use juniper_eager_loading::{LoadFrom, impl_load_from_for_diesel_pg}; 7 | 8 | table! { 9 | users (id) { 10 | id -> Integer, 11 | } 12 | } 13 | 14 | table! { 15 | companies (id) { 16 | id -> Integer, 17 | } 18 | } 19 | 20 | table! { 21 | employments (id) { 22 | id -> Integer, 23 | user_id -> Integer, 24 | company_id -> Integer, 25 | } 26 | } 27 | 28 | #[derive(Queryable)] 29 | struct User { 30 | id: i32, 31 | } 32 | 33 | #[derive(Queryable)] 34 | struct Company { 35 | id: i32, 36 | } 37 | 38 | #[derive(Queryable)] 39 | struct Employment { 40 | id: i32, 41 | user_id: i32, 42 | company_id: i32, 43 | } 44 | 45 | struct Context { 46 | db: PgConnection, 47 | } 48 | 49 | impl Context { 50 | fn db(&self) -> &PgConnection { 51 | &self.db 52 | } 53 | } 54 | 55 | impl_load_from_for_diesel_pg! { 56 | ( 57 | error = diesel::result::Error, 58 | context = Context, 59 | ) => { 60 | i32 -> (users, User), 61 | i32 -> (companies, Company), 62 | i32 -> (employments, Employment), 63 | User.id -> (employments.user_id, Employment), 64 | Company.id -> (employments.company_id, Employment), 65 | Employment.company_id -> (companies.id, Company), 66 | Employment.user_id -> (users.id, User), 67 | } 68 | } 69 | 70 | assert_impl_all!(User: LoadFrom, LoadFrom, LoadFrom); 71 | assert_impl_all!(Company: LoadFrom); 72 | assert_impl_all!(Employment: LoadFrom, LoadFrom, LoadFrom); 73 | 74 | fn main() {} 75 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/compile_pass/impl_load_from_mysql.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | use static_assertions::assert_impl_all; 5 | use diesel::prelude::*; 6 | use juniper_eager_loading::{LoadFrom, impl_load_from_for_diesel_mysql}; 7 | 8 | table! { 9 | users (id) { 10 | id -> Integer, 11 | } 12 | } 13 | 14 | table! { 15 | companies (id) { 16 | id -> Integer, 17 | } 18 | } 19 | 20 | table! { 21 | employments (id) { 22 | id -> Integer, 23 | user_id -> Integer, 24 | company_id -> Integer, 25 | } 26 | } 27 | 28 | #[derive(Queryable)] 29 | struct User { 30 | id: i32, 31 | } 32 | 33 | #[derive(Queryable)] 34 | struct Company { 35 | id: i32, 36 | } 37 | 38 | #[derive(Queryable)] 39 | struct Employment { 40 | id: i32, 41 | user_id: i32, 42 | company_id: i32, 43 | } 44 | 45 | struct Context { 46 | db: MysqlConnection, 47 | } 48 | 49 | impl Context { 50 | fn db(&self) -> &MysqlConnection { 51 | &self.db 52 | } 53 | } 54 | 55 | impl_load_from_for_diesel_mysql! { 56 | ( 57 | error = diesel::result::Error, 58 | context = Context, 59 | ) => { 60 | i32 -> (users, User), 61 | i32 -> (companies, Company), 62 | i32 -> (employments, Employment), 63 | User.id -> (employments.user_id, Employment), 64 | Company.id -> (employments.company_id, Employment), 65 | Employment.company_id -> (companies.id, Company), 66 | Employment.user_id -> (users.id, User), 67 | } 68 | } 69 | 70 | assert_impl_all!(User: LoadFrom, LoadFrom, LoadFrom); 71 | assert_impl_all!(Company: LoadFrom); 72 | assert_impl_all!(Employment: LoadFrom, LoadFrom, LoadFrom); 73 | 74 | fn main() {} 75 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/compile_pass/impl_load_from_sqlite.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | use static_assertions::assert_impl_all; 5 | use diesel::prelude::*; 6 | use juniper_eager_loading::{LoadFrom, impl_load_from_for_diesel_mysql}; 7 | 8 | table! { 9 | users (id) { 10 | id -> Integer, 11 | } 12 | } 13 | 14 | table! { 15 | companies (id) { 16 | id -> Integer, 17 | } 18 | } 19 | 20 | table! { 21 | employments (id) { 22 | id -> Integer, 23 | user_id -> Integer, 24 | company_id -> Integer, 25 | } 26 | } 27 | 28 | #[derive(Queryable)] 29 | struct User { 30 | id: i32, 31 | } 32 | 33 | #[derive(Queryable)] 34 | struct Company { 35 | id: i32, 36 | } 37 | 38 | #[derive(Queryable)] 39 | struct Employment { 40 | id: i32, 41 | user_id: i32, 42 | company_id: i32, 43 | } 44 | 45 | struct Context { 46 | db: SqliteConnection, 47 | } 48 | 49 | impl Context { 50 | fn db(&self) -> &SqliteConnection { 51 | &self.db 52 | } 53 | } 54 | 55 | impl_load_from_for_diesel_mysql! { 56 | ( 57 | error = diesel::result::Error, 58 | context = Context, 59 | ) => { 60 | i32 -> (users, User), 61 | i32 -> (companies, Company), 62 | i32 -> (employments, Employment), 63 | User.id -> (employments.user_id, Employment), 64 | Company.id -> (employments.company_id, Employment), 65 | Employment.company_id -> (companies.id, Company), 66 | Employment.user_id -> (users.id, User), 67 | } 68 | } 69 | 70 | assert_impl_all!(User: LoadFrom, LoadFrom, LoadFrom); 71 | assert_impl_all!(Company: LoadFrom); 72 | assert_impl_all!(Employment: LoadFrom, LoadFrom, LoadFrom); 73 | 74 | fn main() {} 75 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/helpers.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::sync::atomic::{AtomicUsize, Ordering}; 4 | use std::{borrow::Borrow, collections::HashMap, hash::Hash}; 5 | 6 | pub struct StatsHash { 7 | map: HashMap, 8 | count: AtomicUsize, 9 | name: &'static str, 10 | } 11 | 12 | impl StatsHash { 13 | pub fn new(name: &'static str) -> Self { 14 | StatsHash { 15 | map: HashMap::default(), 16 | count: AtomicUsize::default(), 17 | name, 18 | } 19 | } 20 | 21 | #[allow(dead_code)] 22 | pub fn get(&self, k: &Q) -> Option<&V> 23 | where 24 | K: Borrow, 25 | Q: ?Sized + Hash + Eq, 26 | { 27 | self.increment_reads_count(); 28 | self.map.get(k) 29 | } 30 | 31 | #[allow(dead_code)] 32 | pub fn get_mut(&mut self, k: &Q) -> Option<&mut V> 33 | where 34 | K: Borrow, 35 | Q: Hash + Eq, 36 | { 37 | self.increment_reads_count(); 38 | self.map.get_mut(k) 39 | } 40 | 41 | pub fn all_values(&self) -> Vec<&V> { 42 | self.increment_reads_count(); 43 | self.map.iter().map(|(_, v)| v).collect() 44 | } 45 | 46 | pub fn reads_count(&self) -> usize { 47 | self.count.load(Ordering::SeqCst) 48 | } 49 | 50 | pub fn insert(&mut self, k: K, v: V) -> Option { 51 | self.map.insert(k, v) 52 | } 53 | 54 | pub fn increment_reads_count(&self) { 55 | self.count.fetch_add(1, Ordering::SeqCst); 56 | } 57 | } 58 | 59 | pub trait SortedExtension { 60 | fn sorted(self) -> Self; 61 | } 62 | 63 | impl SortedExtension for Vec { 64 | fn sorted(mut self) -> Self { 65 | self.sort(); 66 | self 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /juniper-eager-loading/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["David Pedersen "] 3 | categories = ["web-programming"] 4 | description = "Eliminate N+1 query bugs when using Juniper" 5 | documentation = "https://docs.rs/juniper-eager-loading" 6 | edition = "2018" 7 | homepage = "https://github.com/davidpdrsn/juniper-eager-loading" 8 | keywords = ["web", "graphql", "juniper"] 9 | license = "MIT" 10 | name = "juniper-eager-loading" 11 | readme = "README.md" 12 | repository = "https://github.com/davidpdrsn/juniper-eager-loading.git" 13 | version = "0.5.1" 14 | 15 | [dependencies] 16 | juniper-from-schema = "0.5" 17 | juniper-eager-loading-code-gen = { version = "0.5.1", path = "../juniper-eager-loading-code-gen" } 18 | thiserror = "1" 19 | 20 | [dev-dependencies] 21 | juniper = { version = "0.14", features = ["chrono"] } 22 | assert-json-diff = "1" 23 | serde_json = "1" 24 | backtrace = "0.3" 25 | diesel = { version = "1", features = ["postgres", "mysql", "sqlite", "chrono"] } 26 | trybuild = "1" 27 | static_assertions = "1" 28 | either = "1" 29 | chrono = "0.4" 30 | 31 | [[example]] 32 | name = "has_one" 33 | path = "../examples/has_one.rs" 34 | 35 | [[example]] 36 | name = "has_one_no_macros" 37 | path = "../examples/has_one_no_macros.rs" 38 | 39 | [[example]] 40 | name = "option_has_one" 41 | path = "../examples/option_has_one.rs" 42 | 43 | [[example]] 44 | name = "option_has_one_no_macros" 45 | path = "../examples/option_has_one_no_macros.rs" 46 | 47 | [[example]] 48 | name = "has_many" 49 | path = "../examples/has_many.rs" 50 | 51 | [[example]] 52 | name = "has_many_no_macros" 53 | path = "../examples/has_many_no_macros.rs" 54 | 55 | [[example]] 56 | name = "has_many_with_arguments" 57 | path = "../examples/has_many_with_arguments.rs" 58 | 59 | [[example]] 60 | name = "has_many_through" 61 | path = "../examples/has_many_through.rs" 62 | 63 | [[example]] 64 | name = "has_many_through_no_macros" 65 | path = "../examples/has_many_through_no_macros.rs" 66 | 67 | [[example]] 68 | name = "field_with_arguments" 69 | path = "../examples/field_with_arguments.rs" 70 | -------------------------------------------------------------------------------- /juniper-eager-loading/src/association.rs: -------------------------------------------------------------------------------- 1 | use crate::{HasMany, HasManyThrough, HasOne, HasOneInner, OptionHasOne}; 2 | 3 | /// Methods available for all association types. 4 | pub trait Association { 5 | /// Store the loaded child on the association. 6 | fn loaded_child(&mut self, child: T); 7 | 8 | /// The association should have been loaded by now, if not store an error inside the 9 | /// association (if applicable for the particular association). 10 | fn assert_loaded_otherwise_failed(&mut self); 11 | } 12 | 13 | // -- 14 | // -- impl for HasOne 15 | // -- 16 | impl Association for HasOne { 17 | fn loaded_child(&mut self, child: T) { 18 | has_one_loaded_child(self, child) 19 | } 20 | 21 | fn assert_loaded_otherwise_failed(&mut self) { 22 | has_one_assert_loaded_otherwise_failed(self) 23 | } 24 | } 25 | 26 | impl Association for HasOne> { 27 | fn loaded_child(&mut self, child: T) { 28 | has_one_loaded_child(self, Box::new(child)) 29 | } 30 | 31 | fn assert_loaded_otherwise_failed(&mut self) { 32 | has_one_assert_loaded_otherwise_failed(self) 33 | } 34 | } 35 | 36 | fn has_one_loaded_child(association: &mut HasOne, child: T) { 37 | association.0 = HasOneInner::Loaded(child); 38 | } 39 | 40 | fn has_one_assert_loaded_otherwise_failed(association: &mut HasOne) { 41 | association.0.assert_loaded_otherwise_failed() 42 | } 43 | 44 | // -- 45 | // -- impl for OptionHasOne 46 | // -- 47 | impl Association for OptionHasOne { 48 | fn loaded_child(&mut self, child: T) { 49 | option_has_one_loaded_child(self, Some(child)); 50 | } 51 | 52 | fn assert_loaded_otherwise_failed(&mut self) { 53 | option_has_one_assert_loaded_otherwise_failed(self) 54 | } 55 | } 56 | 57 | impl Association for OptionHasOne> { 58 | fn loaded_child(&mut self, child: T) { 59 | option_has_one_loaded_child(self, Some(Box::new(child))); 60 | } 61 | 62 | fn assert_loaded_otherwise_failed(&mut self) { 63 | option_has_one_assert_loaded_otherwise_failed(self) 64 | } 65 | } 66 | 67 | fn option_has_one_loaded_child(association: &mut OptionHasOne, child: Option) { 68 | association.0 = child; 69 | } 70 | 71 | fn option_has_one_assert_loaded_otherwise_failed(association: &mut OptionHasOne) { 72 | match association.0 { 73 | Some(_) => {} 74 | None => { 75 | association.0 = None; 76 | } 77 | } 78 | } 79 | 80 | // -- 81 | // -- impl for HasMany 82 | // -- 83 | impl Association for HasMany { 84 | fn loaded_child(&mut self, child: T) { 85 | self.0.push(child); 86 | } 87 | 88 | fn assert_loaded_otherwise_failed(&mut self) { 89 | // cannot fail, defaults to an empty vec 90 | } 91 | } 92 | 93 | // -- 94 | // -- impl for HasManyThrough 95 | // -- 96 | impl Association for HasManyThrough { 97 | fn loaded_child(&mut self, child: T) { 98 | self.0.push(child); 99 | } 100 | 101 | fn assert_loaded_otherwise_failed(&mut self) { 102 | // cannot fail, defaults to an empty vec 103 | } 104 | } 105 | 106 | // NOTE: We don't have to implement Association for HasMany> or HasManyThrough> 107 | // because they already have indirection through the inner Vec. So recursive types are supported. 108 | -------------------------------------------------------------------------------- /examples/has_one.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | 3 | #[macro_use] 4 | extern crate diesel; 5 | 6 | use juniper::{Executor, FieldResult}; 7 | use juniper_eager_loading::{prelude::*, EagerLoading, HasOne}; 8 | use juniper_from_schema::graphql_schema; 9 | use std::error::Error; 10 | 11 | // the examples all use Diesel, but this library is data store agnostic 12 | use diesel::prelude::*; 13 | 14 | graphql_schema! { 15 | schema { 16 | query: Query 17 | } 18 | 19 | type Query { 20 | users: [User!]! @juniper(ownership: "owned") 21 | } 22 | 23 | type User { 24 | id: Int! 25 | country: Country! 26 | } 27 | 28 | type Country { 29 | id: Int! 30 | } 31 | } 32 | 33 | mod db_schema { 34 | table! { 35 | users { 36 | id -> Integer, 37 | country_id -> Integer, 38 | } 39 | } 40 | 41 | table! { 42 | countries { 43 | id -> Integer, 44 | } 45 | } 46 | } 47 | 48 | mod models { 49 | use diesel::prelude::*; 50 | 51 | #[derive(Clone, Debug, Queryable)] 52 | pub struct User { 53 | pub id: i32, 54 | pub country_id: i32, 55 | } 56 | 57 | #[derive(Clone, Debug, Queryable)] 58 | pub struct Country { 59 | pub id: i32, 60 | } 61 | 62 | impl juniper_eager_loading::LoadFrom for Country { 63 | type Error = diesel::result::Error; 64 | type Context = super::Context; 65 | 66 | fn load( 67 | ids: &[i32], 68 | _field_args: &(), 69 | ctx: &Self::Context, 70 | ) -> Result, Self::Error> { 71 | use crate::db_schema::countries::dsl::*; 72 | use diesel::pg::expression::dsl::any; 73 | 74 | countries.filter(id.eq(any(ids))).load::(&ctx.db) 75 | } 76 | } 77 | } 78 | 79 | pub struct Query; 80 | 81 | impl QueryFields for Query { 82 | fn field_users( 83 | &self, 84 | executor: &Executor<'_, Context>, 85 | trail: &QueryTrail<'_, User, Walked>, 86 | ) -> FieldResult> { 87 | let ctx = executor.context(); 88 | let user_models = db_schema::users::table.load::(&ctx.db)?; 89 | let users = User::eager_load_each(&user_models, ctx, trail)?; 90 | 91 | Ok(users) 92 | } 93 | } 94 | 95 | pub struct Context { 96 | db: PgConnection, 97 | } 98 | 99 | impl juniper::Context for Context {} 100 | 101 | #[derive(Clone, EagerLoading)] 102 | #[eager_loading(context = Context, error = diesel::result::Error)] 103 | pub struct User { 104 | user: models::User, 105 | 106 | // these are the defaults. `#[has_one(default)]` would also work here. 107 | #[has_one( 108 | foreign_key_field = country_id, 109 | root_model_field = country, 110 | graphql_field = country 111 | )] 112 | country: HasOne, 113 | } 114 | 115 | impl UserFields for User { 116 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 117 | Ok(&self.user.id) 118 | } 119 | 120 | fn field_country( 121 | &self, 122 | executor: &Executor<'_, Context>, 123 | trail: &QueryTrail<'_, Country, Walked>, 124 | ) -> FieldResult<&Country> { 125 | self.country.try_unwrap().map_err(From::from) 126 | } 127 | } 128 | 129 | #[derive(Clone, EagerLoading)] 130 | #[eager_loading(context = Context, error = diesel::result::Error)] 131 | pub struct Country { 132 | country: models::Country, 133 | } 134 | 135 | impl CountryFields for Country { 136 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 137 | Ok(&self.country.id) 138 | } 139 | } 140 | 141 | fn main() {} 142 | -------------------------------------------------------------------------------- /examples/option_has_one.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | 3 | #[macro_use] 4 | extern crate diesel; 5 | 6 | use juniper::{Executor, FieldResult}; 7 | use juniper_eager_loading::{prelude::*, EagerLoading, OptionHasOne}; 8 | use juniper_from_schema::graphql_schema; 9 | use std::error::Error; 10 | 11 | // the examples all use Diesel, but this library is data store agnostic 12 | use diesel::prelude::*; 13 | 14 | graphql_schema! { 15 | schema { 16 | query: Query 17 | } 18 | 19 | type Query { 20 | users: [User!]! @juniper(ownership: "owned") 21 | } 22 | 23 | type User { 24 | id: Int! 25 | country: Country 26 | } 27 | 28 | type Country { 29 | id: Int! 30 | } 31 | } 32 | 33 | mod db_schema { 34 | table! { 35 | users { 36 | id -> Integer, 37 | country_id -> Nullable, 38 | } 39 | } 40 | 41 | table! { 42 | countries { 43 | id -> Integer, 44 | } 45 | } 46 | } 47 | 48 | mod models { 49 | use diesel::prelude::*; 50 | 51 | #[derive(Clone, Debug, Queryable)] 52 | pub struct User { 53 | pub id: i32, 54 | pub country_id: Option, 55 | } 56 | 57 | #[derive(Clone, Debug, Queryable)] 58 | pub struct Country { 59 | pub id: i32, 60 | } 61 | 62 | impl juniper_eager_loading::LoadFrom for Country { 63 | type Error = diesel::result::Error; 64 | type Context = super::Context; 65 | 66 | fn load( 67 | ids: &[i32], 68 | _field_args: &(), 69 | ctx: &Self::Context, 70 | ) -> Result, Self::Error> { 71 | use crate::db_schema::countries::dsl::*; 72 | use diesel::pg::expression::dsl::any; 73 | 74 | countries.filter(id.eq(any(ids))).load::(&ctx.db) 75 | } 76 | } 77 | } 78 | 79 | pub struct Query; 80 | 81 | impl QueryFields for Query { 82 | fn field_users( 83 | &self, 84 | executor: &Executor<'_, Context>, 85 | trail: &QueryTrail<'_, User, Walked>, 86 | ) -> FieldResult> { 87 | let ctx = executor.context(); 88 | let user_models = db_schema::users::table.load::(&ctx.db)?; 89 | let users = User::eager_load_each(&user_models, ctx, trail)?; 90 | 91 | Ok(users) 92 | } 93 | } 94 | 95 | pub struct Context { 96 | db: PgConnection, 97 | } 98 | 99 | impl juniper::Context for Context {} 100 | 101 | #[derive(Clone, EagerLoading)] 102 | #[eager_loading(context = Context, error = diesel::result::Error)] 103 | pub struct User { 104 | user: models::User, 105 | 106 | // these are the defaults. `#[has_one(default)]` would also work here. 107 | #[option_has_one( 108 | foreign_key_field = country_id, 109 | root_model_field = country, 110 | graphql_field = country 111 | )] 112 | country: OptionHasOne, 113 | } 114 | 115 | impl UserFields for User { 116 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 117 | Ok(&self.user.id) 118 | } 119 | 120 | fn field_country( 121 | &self, 122 | executor: &Executor<'_, Context>, 123 | trail: &QueryTrail<'_, Country, Walked>, 124 | ) -> FieldResult<&Option> { 125 | self.country.try_unwrap().map_err(From::from) 126 | } 127 | } 128 | 129 | #[derive(Clone, EagerLoading)] 130 | #[eager_loading(context = Context, error = diesel::result::Error)] 131 | pub struct Country { 132 | country: models::Country, 133 | } 134 | 135 | impl CountryFields for Country { 136 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 137 | Ok(&self.country.id) 138 | } 139 | } 140 | 141 | fn main() {} 142 | -------------------------------------------------------------------------------- /examples/has_many.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | 3 | #[macro_use] 4 | extern crate diesel; 5 | 6 | use juniper::{Executor, FieldResult}; 7 | use juniper_eager_loading::{prelude::*, EagerLoading, HasMany}; 8 | use juniper_from_schema::graphql_schema; 9 | use std::error::Error; 10 | 11 | // the examples all use Diesel, but this library is data store agnostic 12 | use diesel::prelude::*; 13 | 14 | graphql_schema! { 15 | schema { 16 | query: Query 17 | } 18 | 19 | type Query { 20 | countries: [Country!]! @juniper(ownership: "owned") 21 | } 22 | 23 | type User { 24 | id: Int! 25 | } 26 | 27 | type Country { 28 | id: Int! 29 | users: [User!]! 30 | } 31 | } 32 | 33 | mod db_schema { 34 | table! { 35 | users { 36 | id -> Integer, 37 | country_id -> Integer, 38 | } 39 | } 40 | 41 | table! { 42 | countries { 43 | id -> Integer, 44 | } 45 | } 46 | } 47 | 48 | mod models { 49 | use diesel::prelude::*; 50 | 51 | #[derive(Clone, Debug, Queryable)] 52 | pub struct User { 53 | pub id: i32, 54 | pub country_id: i32, 55 | } 56 | 57 | #[derive(Clone, Debug, Queryable)] 58 | pub struct Country { 59 | pub id: i32, 60 | } 61 | 62 | impl juniper_eager_loading::LoadFrom for User { 63 | type Error = diesel::result::Error; 64 | type Context = super::Context; 65 | 66 | fn load( 67 | countries: &[Country], 68 | _field_args: &(), 69 | ctx: &Self::Context, 70 | ) -> Result, Self::Error> { 71 | use crate::db_schema::users::dsl::*; 72 | use diesel::pg::expression::dsl::any; 73 | 74 | let country_ids = countries 75 | .iter() 76 | .map(|country| country.id) 77 | .collect::>(); 78 | 79 | users 80 | .filter(country_id.eq(any(country_ids))) 81 | .load::(&ctx.db) 82 | } 83 | } 84 | } 85 | 86 | pub struct Query; 87 | 88 | impl QueryFields for Query { 89 | fn field_countries( 90 | &self, 91 | executor: &Executor<'_, Context>, 92 | trail: &QueryTrail<'_, Country, Walked>, 93 | ) -> FieldResult> { 94 | let ctx = executor.context(); 95 | let country_models = db_schema::countries::table.load::(&ctx.db)?; 96 | let countries = Country::eager_load_each(&country_models, ctx, trail)?; 97 | 98 | Ok(countries) 99 | } 100 | } 101 | 102 | pub struct Context { 103 | db: PgConnection, 104 | } 105 | 106 | impl juniper::Context for Context {} 107 | 108 | #[derive(Clone, EagerLoading)] 109 | #[eager_loading(context = Context, error = diesel::result::Error)] 110 | pub struct User { 111 | user: models::User, 112 | } 113 | 114 | impl UserFields for User { 115 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 116 | Ok(&self.user.id) 117 | } 118 | } 119 | 120 | #[derive(Clone, EagerLoading)] 121 | #[eager_loading(context = Context, error = diesel::result::Error)] 122 | pub struct Country { 123 | country: models::Country, 124 | 125 | #[has_many(root_model_field = user)] 126 | users: HasMany, 127 | } 128 | 129 | impl CountryFields for Country { 130 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 131 | Ok(&self.country.id) 132 | } 133 | 134 | fn field_users( 135 | &self, 136 | executor: &Executor<'_, Context>, 137 | trail: &QueryTrail<'_, User, Walked>, 138 | ) -> FieldResult<&Vec> { 139 | self.users.try_unwrap().map_err(From::from) 140 | } 141 | } 142 | 143 | fn main() {} 144 | -------------------------------------------------------------------------------- /examples/field_with_arguments.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | 3 | #[macro_use] 4 | extern crate diesel; 5 | 6 | use chrono::prelude::*; 7 | use juniper::{Executor, FieldResult}; 8 | use juniper_eager_loading::{prelude::*, EagerLoading, HasMany}; 9 | use juniper_from_schema::graphql_schema; 10 | use std::error::Error; 11 | 12 | // the examples all use Diesel, but this library is data store agnostic 13 | use diesel::prelude::*; 14 | 15 | graphql_schema! { 16 | schema { 17 | query: Query 18 | } 19 | 20 | type Query { 21 | countries: [Country!]! @juniper(ownership: "owned") 22 | } 23 | 24 | type User { 25 | id: Int! 26 | } 27 | 28 | type Country { 29 | id: Int! 30 | users(activeSince: DateTimeUtc!): [User!]! 31 | } 32 | 33 | scalar DateTimeUtc 34 | } 35 | 36 | mod db_schema { 37 | table! { 38 | users { 39 | id -> Integer, 40 | country_id -> Integer, 41 | active_since -> Timestamptz, 42 | } 43 | } 44 | 45 | table! { 46 | countries { 47 | id -> Integer, 48 | } 49 | } 50 | } 51 | 52 | mod models { 53 | use super::CountryUsersArgs; 54 | use chrono::prelude::*; 55 | use diesel::prelude::*; 56 | 57 | #[derive(Clone, Debug, Queryable)] 58 | pub struct User { 59 | pub id: i32, 60 | pub country_id: i32, 61 | pub active_since: DateTime, 62 | } 63 | 64 | #[derive(Clone, Debug, Queryable)] 65 | pub struct Country { 66 | pub id: i32, 67 | } 68 | 69 | impl<'a> juniper_eager_loading::LoadFrom> for User { 70 | type Error = diesel::result::Error; 71 | type Context = super::Context; 72 | 73 | fn load( 74 | countries: &[Country], 75 | field_args: &CountryUsersArgs<'a>, 76 | ctx: &Self::Context, 77 | ) -> Result, Self::Error> { 78 | use crate::db_schema::users::dsl::*; 79 | use diesel::pg::expression::dsl::any; 80 | 81 | let country_ids = countries 82 | .iter() 83 | .map(|country| country.id) 84 | .collect::>(); 85 | 86 | users 87 | .filter(country_id.eq(any(country_ids))) 88 | .filter(active_since.gt(&field_args.active_since())) 89 | .load::(&ctx.db) 90 | } 91 | } 92 | } 93 | 94 | pub struct Query; 95 | 96 | impl QueryFields for Query { 97 | fn field_countries( 98 | &self, 99 | executor: &Executor<'_, Context>, 100 | trail: &QueryTrail<'_, Country, Walked>, 101 | ) -> FieldResult> { 102 | let ctx = executor.context(); 103 | let country_models = db_schema::countries::table.load::(&ctx.db)?; 104 | let country = Country::eager_load_each(&country_models, ctx, trail)?; 105 | 106 | Ok(country) 107 | } 108 | } 109 | 110 | pub struct Context { 111 | db: PgConnection, 112 | } 113 | 114 | impl juniper::Context for Context {} 115 | 116 | #[derive(Clone, EagerLoading)] 117 | #[eager_loading(context = Context, error = diesel::result::Error)] 118 | pub struct User { 119 | user: models::User, 120 | } 121 | 122 | impl UserFields for User { 123 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 124 | Ok(&self.user.id) 125 | } 126 | } 127 | 128 | #[derive(Clone, EagerLoading)] 129 | #[eager_loading(context = Context, error = diesel::result::Error)] 130 | pub struct Country { 131 | country: models::Country, 132 | 133 | #[has_many( 134 | root_model_field = user, 135 | field_arguments = CountryUsersArgs, 136 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The important line 137 | )] 138 | users: HasMany, 139 | } 140 | 141 | impl CountryFields for Country { 142 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 143 | Ok(&self.country.id) 144 | } 145 | 146 | fn field_users( 147 | &self, 148 | executor: &Executor<'_, Context>, 149 | trail: &QueryTrail<'_, User, Walked>, 150 | _active_since: DateTime, 151 | ) -> FieldResult<&Vec> { 152 | self.users.try_unwrap().map_err(From::from) 153 | } 154 | } 155 | 156 | fn main() {} 157 | -------------------------------------------------------------------------------- /examples/has_many_through.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | 3 | #[macro_use] 4 | extern crate diesel; 5 | 6 | use juniper::{Executor, FieldResult}; 7 | use juniper_eager_loading::{prelude::*, EagerLoading, HasManyThrough}; 8 | use juniper_from_schema::graphql_schema; 9 | use std::error::Error; 10 | 11 | // the examples all use Diesel, but this library is data store agnostic 12 | use diesel::prelude::*; 13 | 14 | graphql_schema! { 15 | schema { 16 | query: Query 17 | } 18 | 19 | type Query { 20 | users: [User!]! @juniper(ownership: "owned") 21 | } 22 | 23 | type User { 24 | id: Int! 25 | companies: [Company!]! 26 | } 27 | 28 | type Company { 29 | id: Int! 30 | } 31 | } 32 | 33 | mod db_schema { 34 | table! { 35 | users { 36 | id -> Integer, 37 | } 38 | } 39 | 40 | table! { 41 | companies { 42 | id -> Integer, 43 | } 44 | } 45 | 46 | table! { 47 | employments { 48 | id -> Integer, 49 | user_id -> Integer, 50 | company_id -> Integer, 51 | } 52 | } 53 | } 54 | 55 | mod models { 56 | use diesel::prelude::*; 57 | 58 | #[derive(Clone, Debug, Queryable)] 59 | pub struct User { 60 | pub id: i32, 61 | } 62 | 63 | #[derive(Clone, Debug, Queryable)] 64 | pub struct Company { 65 | pub id: i32, 66 | } 67 | 68 | #[derive(Clone, Debug, Queryable)] 69 | pub struct Employment { 70 | pub id: i32, 71 | pub user_id: i32, 72 | pub company_id: i32, 73 | } 74 | 75 | impl juniper_eager_loading::LoadFrom for Company { 76 | type Error = diesel::result::Error; 77 | type Context = super::Context; 78 | 79 | fn load( 80 | employments: &[Employment], 81 | _field_args: &(), 82 | ctx: &Self::Context, 83 | ) -> Result, Self::Error> { 84 | use crate::db_schema::companies::dsl::*; 85 | use diesel::pg::expression::dsl::any; 86 | 87 | let company_ids = employments 88 | .iter() 89 | .map(|employent| employent.company_id) 90 | .collect::>(); 91 | 92 | companies 93 | .filter(id.eq(any(company_ids))) 94 | .load::(&ctx.db) 95 | } 96 | } 97 | 98 | impl juniper_eager_loading::LoadFrom for Employment { 99 | type Error = diesel::result::Error; 100 | type Context = super::Context; 101 | 102 | fn load( 103 | users: &[User], 104 | _field_args: &(), 105 | ctx: &Self::Context, 106 | ) -> Result, Self::Error> { 107 | use crate::db_schema::employments::dsl::*; 108 | use diesel::pg::expression::dsl::any; 109 | 110 | let user_ids = users.iter().map(|user| user.id).collect::>(); 111 | 112 | employments 113 | .filter(user_id.eq(any(user_ids))) 114 | .load::(&ctx.db) 115 | } 116 | } 117 | } 118 | 119 | pub struct Query; 120 | 121 | impl QueryFields for Query { 122 | fn field_users( 123 | &self, 124 | executor: &Executor<'_, Context>, 125 | trail: &QueryTrail<'_, User, Walked>, 126 | ) -> FieldResult> { 127 | let ctx = executor.context(); 128 | let user_models = db_schema::users::table.load::(&ctx.db)?; 129 | let users = User::eager_load_each(&user_models, ctx, trail)?; 130 | 131 | Ok(users) 132 | } 133 | } 134 | 135 | pub struct Context { 136 | db: PgConnection, 137 | } 138 | 139 | impl juniper::Context for Context {} 140 | 141 | #[derive(Clone, EagerLoading)] 142 | #[eager_loading(context = Context, error = diesel::result::Error)] 143 | pub struct User { 144 | user: models::User, 145 | 146 | #[has_many_through(join_model = models::Employment)] 147 | companies: HasManyThrough, 148 | } 149 | 150 | impl UserFields for User { 151 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 152 | Ok(&self.user.id) 153 | } 154 | 155 | fn field_companies( 156 | &self, 157 | executor: &Executor<'_, Context>, 158 | trail: &QueryTrail<'_, Company, Walked>, 159 | ) -> FieldResult<&Vec> { 160 | self.companies.try_unwrap().map_err(From::from) 161 | } 162 | } 163 | 164 | #[derive(Clone, EagerLoading)] 165 | #[eager_loading(context = Context, error = diesel::result::Error)] 166 | pub struct Company { 167 | company: models::Company, 168 | } 169 | 170 | impl CompanyFields for Company { 171 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 172 | Ok(&self.company.id) 173 | } 174 | } 175 | 176 | fn main() {} 177 | -------------------------------------------------------------------------------- /examples/has_many_with_arguments.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | 3 | #[macro_use] 4 | extern crate diesel; 5 | 6 | use chrono::prelude::*; 7 | use juniper::{Executor, FieldResult}; 8 | use juniper_eager_loading::{prelude::*, EagerLoading, HasMany, LoadChildrenOutput, LoadFrom}; 9 | use juniper_from_schema::graphql_schema; 10 | use std::error::Error; 11 | 12 | // the examples all use Diesel, but this library is data store agnostic 13 | use diesel::prelude::*; 14 | 15 | graphql_schema! { 16 | schema { 17 | query: Query 18 | } 19 | 20 | type Query { 21 | countries: [Country!]! @juniper(ownership: "owned") 22 | } 23 | 24 | type User { 25 | id: Int! 26 | } 27 | 28 | type Country { 29 | id: Int! 30 | users(activeSince: DateTimeUtc!): [User!]! 31 | } 32 | 33 | scalar DateTimeUtc 34 | } 35 | 36 | mod db_schema { 37 | table! { 38 | users { 39 | id -> Integer, 40 | country_id -> Integer, 41 | last_active_last -> Timestamptz, 42 | } 43 | } 44 | 45 | table! { 46 | countries { 47 | id -> Integer, 48 | } 49 | } 50 | } 51 | 52 | mod models { 53 | use chrono::prelude::*; 54 | use diesel::prelude::*; 55 | 56 | #[derive(Clone, Debug, Queryable)] 57 | pub struct User { 58 | pub id: i32, 59 | pub country_id: i32, 60 | pub last_active_last: DateTime, 61 | } 62 | 63 | #[derive(Clone, Debug, Queryable)] 64 | pub struct Country { 65 | pub id: i32, 66 | } 67 | 68 | impl juniper_eager_loading::LoadFrom> for User { 69 | type Error = diesel::result::Error; 70 | type Context = super::Context; 71 | 72 | fn load( 73 | countries: &[Country], 74 | field_args: &super::CountryUsersArgs<'_>, 75 | ctx: &Self::Context, 76 | ) -> Result, Self::Error> { 77 | use crate::db_schema::users::dsl::*; 78 | use diesel::pg::expression::dsl::any; 79 | 80 | let country_ids = countries 81 | .iter() 82 | .map(|country| country.id) 83 | .collect::>(); 84 | 85 | users 86 | .filter(country_id.eq(any(country_ids))) 87 | .filter(last_active_last.gt(field_args.active_since())) 88 | .load::(&ctx.db) 89 | } 90 | } 91 | } 92 | 93 | pub struct Query; 94 | 95 | impl QueryFields for Query { 96 | fn field_countries( 97 | &self, 98 | executor: &Executor<'_, Context>, 99 | trail: &QueryTrail<'_, Country, Walked>, 100 | ) -> FieldResult> { 101 | let ctx = executor.context(); 102 | let country_models = db_schema::countries::table.load::(&ctx.db)?; 103 | let countries = Country::eager_load_each(&country_models, ctx, trail)?; 104 | 105 | Ok(countries) 106 | } 107 | } 108 | 109 | pub struct Context { 110 | db: PgConnection, 111 | } 112 | 113 | impl juniper::Context for Context {} 114 | 115 | #[derive(Clone, EagerLoading)] 116 | #[eager_loading(context = Context, error = diesel::result::Error)] 117 | pub struct User { 118 | user: models::User, 119 | } 120 | 121 | impl UserFields for User { 122 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 123 | Ok(&self.user.id) 124 | } 125 | } 126 | 127 | #[derive(Clone, EagerLoading)] 128 | #[eager_loading(context = Context, error = diesel::result::Error)] 129 | pub struct Country { 130 | country: models::Country, 131 | 132 | #[has_many(skip)] 133 | users: HasMany, 134 | } 135 | 136 | impl CountryFields for Country { 137 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 138 | Ok(&self.country.id) 139 | } 140 | 141 | fn field_users( 142 | &self, 143 | executor: &Executor<'_, Context>, 144 | trail: &QueryTrail<'_, User, Walked>, 145 | active_since: DateTime, 146 | ) -> FieldResult<&Vec> { 147 | self.users.try_unwrap().map_err(From::from) 148 | } 149 | } 150 | 151 | struct EagerLoadingContextCountryForUsers; 152 | 153 | // Fields that take arguments requires implementing this trait manually 154 | impl<'a> EagerLoadChildrenOfType<'a, User, EagerLoadingContextCountryForUsers, ()> for Country { 155 | type FieldArguments = CountryUsersArgs<'a>; 156 | 157 | fn load_children( 158 | models: &[Self::Model], 159 | field_args: &Self::FieldArguments, 160 | ctx: &Self::Context, 161 | ) -> Result, Self::Error> { 162 | let child_models: Vec = LoadFrom::load(&models, field_args, ctx)?; 163 | Ok(LoadChildrenOutput::ChildModels(child_models)) 164 | } 165 | 166 | fn is_child_of( 167 | node: &Self, 168 | child: &User, 169 | _join_model: &(), 170 | _field_args: &Self::FieldArguments, 171 | _ctx: &Self::Context, 172 | ) -> bool { 173 | node.country.id == child.user.country_id 174 | } 175 | 176 | fn association(node: &mut Self) -> &mut dyn Association { 177 | &mut node.users 178 | } 179 | } 180 | 181 | fn main() {} 182 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/recursive_types.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code, unused_mut)] 2 | 3 | mod helpers; 4 | 5 | use assert_json_diff::assert_json_include; 6 | use helpers::StatsHash; 7 | use juniper::{EmptyMutation, Executor, FieldResult, ID}; 8 | use juniper_eager_loading::{ 9 | prelude::*, EagerLoading, HasManyThrough, HasOne, LoadChildrenOutput, LoadFrom, OptionHasOne, 10 | }; 11 | use juniper_from_schema::graphql_schema; 12 | use serde_json::{json, Value}; 13 | 14 | graphql_schema! { 15 | schema { 16 | query: Query 17 | } 18 | 19 | type Query { 20 | users: [User!]! @juniper(ownership: "owned") 21 | } 22 | 23 | type User { 24 | id: Int! 25 | parent: User! 26 | grandParent: User @juniper(ownership: "as_ref") 27 | } 28 | } 29 | 30 | mod models { 31 | use juniper_eager_loading::LoadFrom; 32 | 33 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 34 | pub struct User { 35 | pub id: i32, 36 | pub parent_id: i32, 37 | pub grand_parent_id: Option, 38 | } 39 | 40 | impl LoadFrom for User { 41 | type Error = Box; 42 | type Context = super::Context; 43 | 44 | fn load(ids: &[i32], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 45 | let models = ctx 46 | .db 47 | .users 48 | .all_values() 49 | .into_iter() 50 | .filter(|value| ids.contains(&value.id)) 51 | .cloned() 52 | .collect::>(); 53 | Ok(models) 54 | } 55 | } 56 | } 57 | 58 | pub struct Db { 59 | users: StatsHash, 60 | } 61 | 62 | pub struct Context { 63 | db: Db, 64 | } 65 | 66 | impl juniper::Context for Context {} 67 | 68 | pub struct Query; 69 | 70 | impl QueryFields for Query { 71 | fn field_users<'a>( 72 | &self, 73 | executor: &Executor<'a, Context>, 74 | trail: &QueryTrail<'a, User, Walked>, 75 | ) -> FieldResult> { 76 | let ctx = executor.context(); 77 | 78 | let mut user_models = ctx 79 | .db 80 | .users 81 | .all_values() 82 | .into_iter() 83 | .cloned() 84 | .collect::>(); 85 | user_models.sort_by_key(|user| user.id); 86 | 87 | let users = User::eager_load_each(&user_models, ctx, trail)?; 88 | 89 | Ok(users) 90 | } 91 | } 92 | 93 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 94 | #[eager_loading(context = Context, error = Box)] 95 | pub struct User { 96 | user: models::User, 97 | 98 | #[has_one(root_model_field = user)] 99 | parent: HasOne>, 100 | 101 | #[option_has_one(root_model_field = user)] 102 | grand_parent: OptionHasOne>, 103 | } 104 | 105 | impl UserFields for User { 106 | fn field_id<'a>(&self, _: &Executor<'a, Context>) -> FieldResult<&i32> { 107 | Ok(&self.user.id) 108 | } 109 | 110 | fn field_parent<'a>( 111 | &self, 112 | executor: &Executor<'a, Context>, 113 | trail: &QueryTrail<'a, User, Walked>, 114 | ) -> FieldResult<&User> { 115 | Ok(self.parent.try_unwrap()?) 116 | } 117 | 118 | fn field_grand_parent<'a>( 119 | &self, 120 | executor: &Executor<'a, Context>, 121 | trail: &QueryTrail<'a, User, Walked>, 122 | ) -> FieldResult> { 123 | let grand_parent = self 124 | .grand_parent 125 | .try_unwrap()? 126 | .as_ref() 127 | .map(|boxed| &**boxed); 128 | 129 | Ok(grand_parent) 130 | } 131 | } 132 | 133 | #[test] 134 | fn loading_recursive_type() { 135 | let mut users = StatsHash::new("users"); 136 | 137 | users.insert( 138 | 1, 139 | models::User { 140 | id: 1, 141 | parent_id: 1, 142 | grand_parent_id: Some(1), 143 | }, 144 | ); 145 | 146 | let db = Db { users }; 147 | 148 | let (json, counts) = run_query( 149 | r#" 150 | query Test { 151 | users { 152 | id 153 | parent { 154 | id 155 | parent { 156 | id 157 | grandParent { 158 | id 159 | } 160 | } 161 | } 162 | } 163 | } 164 | "#, 165 | db, 166 | ); 167 | 168 | assert_json_include!( 169 | expected: json!({ 170 | "users": [ 171 | { 172 | "id": 1, 173 | "parent": { 174 | "id": 1, 175 | "parent": { 176 | "id": 1, 177 | "grandParent": { 178 | "id": 1, 179 | }, 180 | }, 181 | }, 182 | }, 183 | ] 184 | }), 185 | actual: json, 186 | ); 187 | } 188 | 189 | struct DbStats { 190 | user_reads: usize, 191 | } 192 | 193 | fn run_query(query: &str, db: Db) -> (Value, DbStats) { 194 | let ctx = Context { db }; 195 | 196 | let (result, errors) = juniper::execute( 197 | query, 198 | None, 199 | &Schema::new(Query, EmptyMutation::new()), 200 | &juniper::Variables::new(), 201 | &ctx, 202 | ) 203 | .unwrap(); 204 | 205 | if !errors.is_empty() { 206 | panic!( 207 | "GraphQL errors\n{}", 208 | serde_json::to_string_pretty(&errors).unwrap() 209 | ); 210 | } 211 | 212 | let json: Value = serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap(); 213 | 214 | println!("{}", serde_json::to_string_pretty(&json).unwrap()); 215 | 216 | ( 217 | json, 218 | DbStats { 219 | user_reads: ctx.db.users.reads_count(), 220 | }, 221 | ) 222 | } 223 | -------------------------------------------------------------------------------- /examples/has_one_no_macros.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | #![allow(clippy::let_unit_value)] 3 | 4 | #[macro_use] 5 | extern crate diesel; 6 | 7 | use juniper::{Executor, FieldResult}; 8 | use juniper_eager_loading::{prelude::*, EagerLoading, HasOne, LoadChildrenOutput, LoadFrom}; 9 | use juniper_from_schema::graphql_schema; 10 | use std::error::Error; 11 | 12 | // the examples all use Diesel, but this library is data store agnostic 13 | use diesel::prelude::*; 14 | 15 | graphql_schema! { 16 | schema { 17 | query: Query 18 | } 19 | 20 | type Query { 21 | users: [User!]! @juniper(ownership: "owned") 22 | } 23 | 24 | type User { 25 | id: Int! 26 | country: Country! 27 | } 28 | 29 | type Country { 30 | id: Int! 31 | } 32 | } 33 | 34 | mod db_schema { 35 | table! { 36 | users { 37 | id -> Integer, 38 | country_id -> Integer, 39 | } 40 | } 41 | 42 | table! { 43 | countries { 44 | id -> Integer, 45 | } 46 | } 47 | } 48 | 49 | mod models { 50 | use diesel::prelude::*; 51 | 52 | #[derive(Clone, Debug, Queryable)] 53 | pub struct User { 54 | pub id: i32, 55 | pub country_id: i32, 56 | } 57 | 58 | #[derive(Clone, Debug, Queryable)] 59 | pub struct Country { 60 | pub id: i32, 61 | } 62 | 63 | impl juniper_eager_loading::LoadFrom for Country { 64 | type Error = diesel::result::Error; 65 | type Context = super::Context; 66 | 67 | fn load( 68 | ids: &[i32], 69 | _field_args: &(), 70 | ctx: &Self::Context, 71 | ) -> Result, Self::Error> { 72 | use crate::db_schema::countries::dsl::*; 73 | use diesel::pg::expression::dsl::any; 74 | 75 | countries.filter(id.eq(any(ids))).load::(&ctx.db) 76 | } 77 | } 78 | } 79 | 80 | pub struct Query; 81 | 82 | impl QueryFields for Query { 83 | fn field_users( 84 | &self, 85 | executor: &Executor<'_, Context>, 86 | trail: &QueryTrail<'_, User, Walked>, 87 | ) -> FieldResult> { 88 | let ctx = executor.context(); 89 | let user_models = db_schema::users::table.load::(&ctx.db)?; 90 | let users = User::eager_load_each(&user_models, ctx, trail)?; 91 | 92 | Ok(users) 93 | } 94 | } 95 | 96 | pub struct Context { 97 | db: PgConnection, 98 | } 99 | 100 | impl juniper::Context for Context {} 101 | 102 | #[derive(Clone)] 103 | pub struct User { 104 | user: models::User, 105 | country: HasOne, 106 | } 107 | 108 | impl UserFields for User { 109 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 110 | Ok(&self.user.id) 111 | } 112 | 113 | fn field_country( 114 | &self, 115 | executor: &Executor<'_, Context>, 116 | trail: &QueryTrail<'_, Country, Walked>, 117 | ) -> FieldResult<&Country> { 118 | self.country.try_unwrap().map_err(From::from) 119 | } 120 | } 121 | 122 | #[derive(Clone)] 123 | pub struct Country { 124 | country: models::Country, 125 | } 126 | 127 | impl CountryFields for Country { 128 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 129 | Ok(&self.country.id) 130 | } 131 | } 132 | 133 | impl EagerLoading for User { 134 | type Model = models::User; 135 | type Id = i32; 136 | type Context = Context; 137 | type Error = diesel::result::Error; 138 | 139 | fn new_from_model(model: &Self::Model) -> Self { 140 | Self { 141 | user: model.clone(), 142 | country: Default::default(), 143 | } 144 | } 145 | 146 | fn eager_load_each( 147 | models: &[models::User], 148 | ctx: &Self::Context, 149 | trail: &QueryTrail<'_, Self, Walked>, 150 | ) -> Result, Self::Error> { 151 | let mut nodes = Self::from_db_models(models); 152 | if let Some(child_trail) = trail.country().walk() { 153 | let field_args = trail.country_args(); 154 | 155 | EagerLoadChildrenOfType::< 156 | Country, 157 | EagerLoadingContextUserForCountry, 158 | _>::eager_load_children(&mut nodes, models, ctx, &child_trail, &field_args)?; 159 | } 160 | Ok(nodes) 161 | } 162 | } 163 | 164 | struct EagerLoadingContextUserForCountry; 165 | 166 | impl<'a> EagerLoadChildrenOfType<'a, Country, EagerLoadingContextUserForCountry, ()> for User { 167 | type FieldArguments = (); 168 | 169 | fn load_children( 170 | models: &[models::User], 171 | field_args: &Self::FieldArguments, 172 | ctx: &Self::Context, 173 | ) -> Result, diesel::result::Error> { 174 | let ids = models 175 | .iter() 176 | .map(|model| model.country_id) 177 | .collect::>(); 178 | let ids = juniper_eager_loading::unique(ids); 179 | 180 | let child_models: Vec = LoadFrom::load(&ids, field_args, ctx)?; 181 | 182 | Ok(LoadChildrenOutput::ChildModels(child_models)) 183 | } 184 | 185 | fn is_child_of( 186 | node: &User, 187 | child: &Country, 188 | _join_model: &(), 189 | _field_args: &Self::FieldArguments, 190 | _ctx: &Self::Context, 191 | ) -> bool { 192 | node.user.country_id == child.country.id 193 | } 194 | 195 | fn association(node: &mut Self) -> &mut dyn Association { 196 | &mut node.country 197 | } 198 | } 199 | 200 | impl EagerLoading for Country { 201 | type Model = models::Country; 202 | type Id = i32; 203 | type Context = Context; 204 | type Error = diesel::result::Error; 205 | 206 | fn new_from_model(model: &Self::Model) -> Self { 207 | Self { 208 | country: model.clone(), 209 | } 210 | } 211 | 212 | fn eager_load_each( 213 | models: &[models::Country], 214 | ctx: &Self::Context, 215 | trail: &QueryTrail<'_, Country, Walked>, 216 | ) -> Result, diesel::result::Error> { 217 | Ok(Vec::new()) 218 | } 219 | } 220 | 221 | fn main() {} 222 | -------------------------------------------------------------------------------- /examples/has_many_no_macros.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | #![allow(clippy::let_unit_value)] 3 | 4 | #[macro_use] 5 | extern crate diesel; 6 | 7 | use juniper::{Executor, FieldResult}; 8 | use juniper_eager_loading::{prelude::*, EagerLoading, HasMany, LoadChildrenOutput, LoadFrom}; 9 | use juniper_from_schema::graphql_schema; 10 | use std::error::Error; 11 | 12 | // the examples all use Diesel, but this library is data store agnostic 13 | use diesel::prelude::*; 14 | 15 | graphql_schema! { 16 | schema { 17 | query: Query 18 | } 19 | 20 | type Query { 21 | countries: [Country!]! @juniper(ownership: "owned") 22 | } 23 | 24 | type User { 25 | id: Int! 26 | } 27 | 28 | type Country { 29 | id: Int! 30 | users: [User!]! 31 | } 32 | } 33 | 34 | mod db_schema { 35 | table! { 36 | users { 37 | id -> Integer, 38 | country_id -> Integer, 39 | } 40 | } 41 | 42 | table! { 43 | countries { 44 | id -> Integer, 45 | } 46 | } 47 | } 48 | 49 | mod models { 50 | use diesel::prelude::*; 51 | 52 | #[derive(Clone, Debug, Queryable)] 53 | pub struct User { 54 | pub id: i32, 55 | pub country_id: i32, 56 | } 57 | 58 | #[derive(Clone, Debug, Queryable)] 59 | pub struct Country { 60 | pub id: i32, 61 | } 62 | 63 | impl juniper_eager_loading::LoadFrom for User { 64 | type Error = diesel::result::Error; 65 | type Context = super::Context; 66 | 67 | fn load( 68 | countries: &[Country], 69 | _field_args: &(), 70 | ctx: &Self::Context, 71 | ) -> Result, Self::Error> { 72 | use crate::db_schema::users::dsl::*; 73 | use diesel::pg::expression::dsl::any; 74 | 75 | let country_ids = countries 76 | .iter() 77 | .map(|country| country.id) 78 | .collect::>(); 79 | 80 | users 81 | .filter(country_id.eq(any(country_ids))) 82 | .load::(&ctx.db) 83 | } 84 | } 85 | } 86 | 87 | pub struct Query; 88 | 89 | impl QueryFields for Query { 90 | fn field_countries( 91 | &self, 92 | executor: &Executor<'_, Context>, 93 | trail: &QueryTrail<'_, Country, Walked>, 94 | ) -> FieldResult> { 95 | let ctx = executor.context(); 96 | let country_models = db_schema::countries::table.load::(&ctx.db)?; 97 | let countries = Country::eager_load_each(&country_models, ctx, trail)?; 98 | 99 | Ok(countries) 100 | } 101 | } 102 | 103 | pub struct Context { 104 | db: PgConnection, 105 | } 106 | 107 | impl juniper::Context for Context {} 108 | 109 | #[derive(Clone)] 110 | pub struct User { 111 | user: models::User, 112 | } 113 | 114 | impl UserFields for User { 115 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 116 | Ok(&self.user.id) 117 | } 118 | } 119 | 120 | #[derive(Clone)] 121 | pub struct Country { 122 | country: models::Country, 123 | users: HasMany, 124 | } 125 | 126 | impl CountryFields for Country { 127 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 128 | Ok(&self.country.id) 129 | } 130 | 131 | fn field_users( 132 | &self, 133 | executor: &Executor<'_, Context>, 134 | trail: &QueryTrail<'_, User, Walked>, 135 | ) -> FieldResult<&Vec> { 136 | self.users.try_unwrap().map_err(From::from) 137 | } 138 | } 139 | 140 | impl EagerLoading for User { 141 | type Model = models::User; 142 | type Id = i32; 143 | type Context = Context; 144 | type Error = diesel::result::Error; 145 | 146 | fn new_from_model(model: &Self::Model) -> Self { 147 | Self { 148 | user: model.clone(), 149 | } 150 | } 151 | 152 | fn eager_load_each( 153 | models: &[Self::Model], 154 | ctx: &Self::Context, 155 | trail: &QueryTrail<'_, Self, Walked>, 156 | ) -> Result, Self::Error> { 157 | Ok(Vec::new()) 158 | } 159 | } 160 | 161 | impl EagerLoading for Country { 162 | type Model = models::Country; 163 | type Id = i32; 164 | type Context = Context; 165 | type Error = diesel::result::Error; 166 | 167 | fn new_from_model(model: &Self::Model) -> Self { 168 | Self { 169 | country: model.clone(), 170 | users: Default::default(), 171 | } 172 | } 173 | 174 | fn eager_load_each( 175 | models: &[Self::Model], 176 | ctx: &Self::Context, 177 | trail: &QueryTrail<'_, Self, Walked>, 178 | ) -> Result, Self::Error> { 179 | let mut nodes = Self::from_db_models(models); 180 | if let Some(child_trail) = trail.users().walk() { 181 | let field_args = trail.users_args(); 182 | 183 | EagerLoadChildrenOfType::< 184 | User, 185 | EagerLoadingContextCountryForUsers, 186 | _, 187 | >::eager_load_children(&mut nodes, models, ctx, &child_trail, &field_args)?; 188 | } 189 | 190 | Ok(nodes) 191 | } 192 | } 193 | 194 | struct EagerLoadingContextCountryForUsers; 195 | 196 | impl<'a> EagerLoadChildrenOfType<'a, User, EagerLoadingContextCountryForUsers, ()> for Country { 197 | type FieldArguments = (); 198 | 199 | fn load_children( 200 | models: &[Self::Model], 201 | field_args: &Self::FieldArguments, 202 | ctx: &Self::Context, 203 | ) -> Result, Self::Error> { 204 | let child_models: Vec = LoadFrom::load(&models, field_args, ctx)?; 205 | Ok(LoadChildrenOutput::ChildModels(child_models)) 206 | } 207 | 208 | fn is_child_of( 209 | node: &Self, 210 | child: &User, 211 | _join_model: &(), 212 | _field_args: &Self::FieldArguments, 213 | _ctx: &Self::Context, 214 | ) -> bool { 215 | node.country.id == child.user.country_id 216 | } 217 | 218 | fn association(node: &mut Self) -> &mut dyn Association { 219 | &mut node.users 220 | } 221 | } 222 | 223 | fn main() {} 224 | -------------------------------------------------------------------------------- /examples/option_has_one_no_macros.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | #![allow(clippy::let_unit_value)] 3 | 4 | #[macro_use] 5 | extern crate diesel; 6 | 7 | use juniper::{Executor, FieldResult}; 8 | use juniper_eager_loading::{prelude::*, EagerLoading, LoadChildrenOutput, LoadFrom, OptionHasOne}; 9 | use juniper_from_schema::graphql_schema; 10 | use std::error::Error; 11 | 12 | // the examples all use Diesel, but this library is data store agnostic 13 | use diesel::prelude::*; 14 | 15 | graphql_schema! { 16 | schema { 17 | query: Query 18 | } 19 | 20 | type Query { 21 | users: [User!]! @juniper(ownership: "owned") 22 | } 23 | 24 | type User { 25 | id: Int! 26 | country: Country 27 | } 28 | 29 | type Country { 30 | id: Int! 31 | } 32 | } 33 | 34 | mod db_schema { 35 | table! { 36 | users { 37 | id -> Integer, 38 | country_id -> Nullable, 39 | } 40 | } 41 | 42 | table! { 43 | countries { 44 | id -> Integer, 45 | } 46 | } 47 | } 48 | 49 | mod models { 50 | use diesel::prelude::*; 51 | 52 | #[derive(Clone, Debug, Queryable)] 53 | pub struct User { 54 | pub id: i32, 55 | pub country_id: Option, 56 | } 57 | 58 | #[derive(Clone, Debug, Queryable)] 59 | pub struct Country { 60 | pub id: i32, 61 | } 62 | 63 | impl juniper_eager_loading::LoadFrom for Country { 64 | type Error = diesel::result::Error; 65 | type Context = super::Context; 66 | 67 | fn load( 68 | ids: &[i32], 69 | _field_args: &(), 70 | ctx: &Self::Context, 71 | ) -> Result, Self::Error> { 72 | use crate::db_schema::countries::dsl::*; 73 | use diesel::pg::expression::dsl::any; 74 | 75 | countries.filter(id.eq(any(ids))).load::(&ctx.db) 76 | } 77 | } 78 | } 79 | 80 | pub struct Query; 81 | 82 | impl QueryFields for Query { 83 | fn field_users( 84 | &self, 85 | executor: &Executor<'_, Context>, 86 | trail: &QueryTrail<'_, User, Walked>, 87 | ) -> FieldResult> { 88 | let ctx = executor.context(); 89 | let user_models = db_schema::users::table.load::(&ctx.db)?; 90 | let users = User::eager_load_each(&user_models, ctx, trail)?; 91 | 92 | Ok(users) 93 | } 94 | } 95 | 96 | pub struct Context { 97 | db: PgConnection, 98 | } 99 | 100 | impl juniper::Context for Context {} 101 | 102 | #[derive(Clone)] 103 | pub struct User { 104 | user: models::User, 105 | country: OptionHasOne, 106 | } 107 | 108 | impl UserFields for User { 109 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 110 | Ok(&self.user.id) 111 | } 112 | 113 | fn field_country( 114 | &self, 115 | executor: &Executor<'_, Context>, 116 | trail: &QueryTrail<'_, Country, Walked>, 117 | ) -> FieldResult<&Option> { 118 | self.country.try_unwrap().map_err(From::from) 119 | } 120 | } 121 | 122 | #[derive(Clone)] 123 | pub struct Country { 124 | country: models::Country, 125 | } 126 | 127 | impl CountryFields for Country { 128 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 129 | Ok(&self.country.id) 130 | } 131 | } 132 | 133 | impl EagerLoading for User { 134 | type Model = models::User; 135 | type Id = i32; 136 | type Context = Context; 137 | type Error = diesel::result::Error; 138 | 139 | fn new_from_model(model: &Self::Model) -> Self { 140 | Self { 141 | user: model.clone(), 142 | country: Default::default(), 143 | } 144 | } 145 | 146 | fn eager_load_each( 147 | models: &[Self::Model], 148 | ctx: &Self::Context, 149 | trail: &QueryTrail<'_, Self, Walked>, 150 | ) -> Result, Self::Error> { 151 | let mut nodes = Self::from_db_models(models); 152 | if let Some(child_trail) = trail.country().walk() { 153 | let field_args = trail.country_args(); 154 | 155 | EagerLoadChildrenOfType::< 156 | Country, 157 | EagerLoadingContextUserForCountry, 158 | _ 159 | >::eager_load_children(&mut nodes, models, ctx, &child_trail, &field_args)?; 160 | } 161 | Ok(nodes) 162 | } 163 | } 164 | 165 | struct EagerLoadingContextUserForCountry; 166 | 167 | impl<'a> EagerLoadChildrenOfType<'a, Country, EagerLoadingContextUserForCountry, ()> for User { 168 | type FieldArguments = (); 169 | 170 | fn load_children( 171 | models: &[Self::Model], 172 | field_args: &Self::FieldArguments, 173 | ctx: &Self::Context, 174 | ) -> Result::Model, ()>, Self::Error> { 175 | let ids = models 176 | .iter() 177 | .filter_map(|model| model.country_id) 178 | .collect::>(); 179 | let ids = juniper_eager_loading::unique(ids); 180 | 181 | let child_models: Vec = LoadFrom::load(&ids, field_args, ctx)?; 182 | 183 | Ok(LoadChildrenOutput::ChildModels(child_models)) 184 | } 185 | 186 | fn is_child_of( 187 | node: &Self, 188 | child: &Country, 189 | join_model: &(), 190 | _field_args: &Self::FieldArguments, 191 | _ctx: &Self::Context, 192 | ) -> bool { 193 | node.user.country_id == Some(child.country.id) 194 | } 195 | 196 | fn association(node: &mut Self) -> &mut dyn Association { 197 | &mut node.country 198 | } 199 | } 200 | 201 | impl EagerLoading for Country { 202 | type Model = models::Country; 203 | type Id = i32; 204 | type Context = Context; 205 | type Error = diesel::result::Error; 206 | 207 | fn new_from_model(model: &Self::Model) -> Self { 208 | Self { 209 | country: model.clone(), 210 | } 211 | } 212 | 213 | fn eager_load_each( 214 | models: &[Self::Model], 215 | ctx: &Self::Context, 216 | trail: &QueryTrail<'_, Self, Walked>, 217 | ) -> Result, Self::Error> { 218 | Ok(Vec::new()) 219 | } 220 | } 221 | 222 | fn main() {} 223 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/rename_id_field.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code, unused_mut)] 2 | 3 | mod helpers; 4 | 5 | use assert_json_diff::{assert_json_eq, assert_json_include}; 6 | use helpers::{SortedExtension, StatsHash}; 7 | use juniper::{Executor, FieldError, FieldResult}; 8 | use juniper_eager_loading::{ 9 | prelude::*, EagerLoading, HasMany, HasManyThrough, HasOne, OptionHasOne, 10 | }; 11 | use juniper_from_schema::graphql_schema; 12 | use serde_json::{json, Value}; 13 | 14 | graphql_schema! { 15 | schema { 16 | query: Query 17 | } 18 | 19 | type Query { 20 | foo: Boolean! 21 | } 22 | 23 | type User { 24 | id: Int! 25 | country: Country! 26 | countryMaybe: Country 27 | countries: [Country!]! 28 | countriesMaybe: [Country!]! 29 | companies: [Company!]! 30 | } 31 | 32 | type Country { 33 | id: Int! 34 | } 35 | 36 | type Company { 37 | id: Int! 38 | } 39 | } 40 | 41 | mod models { 42 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 43 | pub struct User { 44 | pub own_user_id: i32, 45 | pub referenced_country_id: i32, 46 | pub referenced_country_maybe_id: Option, 47 | } 48 | 49 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 50 | pub struct Country { 51 | pub own_country_id: i32, 52 | pub referenced_user_id: i32, 53 | pub referenced_user_id_maybe: Option, 54 | } 55 | 56 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 57 | pub struct Employment { 58 | pub referenced_user_id: i32, 59 | pub referenced_company_id: i32, 60 | } 61 | 62 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 63 | pub struct Company { 64 | pub own_company_id: i32, 65 | } 66 | 67 | impl juniper_eager_loading::LoadFrom for Country { 68 | type Error = Box; 69 | type Context = super::Context; 70 | 71 | fn load(ids: &[i32], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 72 | todo!() 73 | } 74 | } 75 | 76 | impl juniper_eager_loading::LoadFrom for Country { 77 | type Error = Box; 78 | type Context = super::Context; 79 | 80 | fn load(ids: &[User], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 81 | todo!() 82 | } 83 | } 84 | 85 | impl juniper_eager_loading::LoadFrom for Employment { 86 | type Error = Box; 87 | type Context = super::Context; 88 | 89 | fn load(ids: &[User], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 90 | todo!() 91 | } 92 | } 93 | 94 | impl juniper_eager_loading::LoadFrom for Company { 95 | type Error = Box; 96 | type Context = super::Context; 97 | 98 | fn load(ids: &[Employment], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 99 | todo!() 100 | } 101 | } 102 | } 103 | 104 | pub struct Context; 105 | 106 | impl juniper::Context for Context {} 107 | 108 | pub struct Query; 109 | 110 | impl QueryFields for Query { 111 | // Query has to have at least one field 112 | fn field_foo<'a>(&self, executor: &Executor<'a, Context>) -> FieldResult<&bool> { 113 | todo!() 114 | } 115 | } 116 | 117 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 118 | #[eager_loading( 119 | error = Box, 120 | context = Context, 121 | primary_key_field = own_user_id, 122 | )] 123 | pub struct User { 124 | user: models::User, 125 | 126 | #[has_one( 127 | child_primary_key_field = own_country_id, 128 | foreign_key_field = referenced_country_id, 129 | )] 130 | country: HasOne, 131 | 132 | #[option_has_one( 133 | child_primary_key_field = own_country_id, 134 | foreign_key_field = referenced_country_maybe_id, 135 | root_model_field = country, 136 | )] 137 | country_maybe: OptionHasOne, 138 | 139 | #[has_many( 140 | root_model_field = country, 141 | foreign_key_field = referenced_user_id, 142 | )] 143 | countries: HasMany, 144 | 145 | #[has_many( 146 | root_model_field = country, 147 | foreign_key_optional, 148 | foreign_key_field = referenced_user_id_maybe, 149 | )] 150 | countries_maybe: HasMany, 151 | 152 | #[has_many_through( 153 | join_model = models::Employment, 154 | child_primary_key_field = own_company_id, 155 | foreign_key_field = referenced_user_id, 156 | child_primary_key_field_on_join_model = referenced_company_id, 157 | )] 158 | companies: HasManyThrough, 159 | } 160 | 161 | impl UserFields for User { 162 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 163 | todo!() 164 | } 165 | 166 | fn field_country( 167 | &self, 168 | _executor: &Executor<'_, Context>, 169 | trail: &QueryTrail<'_, Country, Walked>, 170 | ) -> FieldResult<&Country> { 171 | todo!() 172 | } 173 | 174 | fn field_country_maybe( 175 | &self, 176 | _executor: &Executor<'_, Context>, 177 | trail: &QueryTrail<'_, Country, Walked>, 178 | ) -> FieldResult<&Option> { 179 | todo!() 180 | } 181 | 182 | fn field_countries( 183 | &self, 184 | _executor: &Executor<'_, Context>, 185 | trail: &QueryTrail<'_, Country, Walked>, 186 | ) -> FieldResult<&Vec> { 187 | todo!() 188 | } 189 | 190 | fn field_countries_maybe( 191 | &self, 192 | _executor: &Executor<'_, Context>, 193 | trail: &QueryTrail<'_, Country, Walked>, 194 | ) -> FieldResult<&Vec> { 195 | todo!() 196 | } 197 | 198 | fn field_companies( 199 | &self, 200 | _executor: &Executor<'_, Context>, 201 | trail: &QueryTrail<'_, Company, Walked>, 202 | ) -> FieldResult<&Vec> { 203 | todo!() 204 | } 205 | } 206 | 207 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 208 | #[eager_loading( 209 | error = Box, 210 | context = Context, 211 | )] 212 | pub struct Country { 213 | country: models::Country, 214 | } 215 | 216 | impl CountryFields for Country { 217 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 218 | todo!() 219 | } 220 | } 221 | 222 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 223 | #[eager_loading( 224 | error = Box, 225 | context = Context, 226 | )] 227 | pub struct Company { 228 | company: models::Company, 229 | } 230 | 231 | impl CompanyFields for Company { 232 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 233 | todo!() 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /juniper-eager-loading-code-gen/src/impl_load_from_for_diesel.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | braced, parenthesized, 5 | parse::{Parse, ParseStream}, 6 | punctuated::Punctuated, 7 | Ident, Token, Type, 8 | }; 9 | 10 | pub fn go(input: proc_macro::TokenStream, backend: Backend) -> proc_macro::TokenStream { 11 | let input = match syn::parse::(input) { 12 | Ok(x) => x, 13 | Err(err) => return err.to_compile_error().into(), 14 | }; 15 | 16 | let mut tokens = TokenStream::new(); 17 | 18 | for impl_ in &input.impls { 19 | impl_.gen_tokens(&input, &backend, &mut tokens); 20 | } 21 | 22 | tokens.into() 23 | } 24 | 25 | #[derive(Debug)] 26 | pub enum Backend { 27 | Pg, 28 | Mysql, 29 | Sqlite, 30 | } 31 | 32 | mod kw { 33 | syn::custom_keyword!(error); 34 | syn::custom_keyword!(context); 35 | } 36 | 37 | #[derive(Debug)] 38 | struct Input { 39 | error_ty: Type, 40 | context_ty: Type, 41 | impls: Punctuated, 42 | } 43 | 44 | impl Parse for Input { 45 | fn parse(input: ParseStream) -> syn::parse::Result { 46 | let prelude; 47 | parenthesized!(prelude in input); 48 | 49 | prelude.parse::()?; 50 | prelude.parse::()?; 51 | let error_ty = prelude.parse::()?; 52 | 53 | prelude.parse::()?; 54 | 55 | prelude.parse::()?; 56 | prelude.parse::()?; 57 | let context_ty = prelude.parse::()?; 58 | 59 | if prelude.peek(Token![,]) { 60 | prelude.parse::()?; 61 | } 62 | 63 | input.parse::]>()?; 64 | 65 | let content; 66 | braced!(content in input); 67 | let impls = Punctuated::parse_terminated(&content)?; 68 | 69 | Ok(Self { 70 | error_ty, 71 | context_ty, 72 | impls, 73 | }) 74 | } 75 | } 76 | 77 | #[derive(Debug)] 78 | enum InputImpl { 79 | HasOne(HasOne), 80 | HasMany(HasMany), 81 | } 82 | 83 | #[derive(Debug)] 84 | struct HasOne { 85 | id_ty: Type, 86 | table: Ident, 87 | self_ty: Type, 88 | } 89 | 90 | #[derive(Debug)] 91 | struct HasMany { 92 | join_ty: Type, 93 | join_from: Ident, 94 | table: Ident, 95 | join_to: Ident, 96 | self_ty: Type, 97 | } 98 | 99 | impl Parse for InputImpl { 100 | fn parse(input: ParseStream) -> syn::parse::Result { 101 | let id_ty = input.parse::()?; 102 | 103 | if input.peek(Token![.]) { 104 | let join_ty = id_ty; 105 | input.parse::()?; 106 | let join_from = input.parse::()?; 107 | 108 | input.parse::]>()?; 109 | 110 | let inside; 111 | parenthesized!(inside in input); 112 | let table = inside.parse::()?; 113 | inside.parse::()?; 114 | let join_to = inside.parse::()?; 115 | inside.parse::()?; 116 | let self_ty = inside.parse::()?; 117 | 118 | Ok(InputImpl::HasMany(HasMany { 119 | join_ty, 120 | join_from, 121 | table, 122 | join_to, 123 | self_ty, 124 | })) 125 | } else { 126 | input.parse::]>()?; 127 | 128 | let inside; 129 | parenthesized!(inside in input); 130 | 131 | let table = inside.parse::()?; 132 | inside.parse::()?; 133 | let self_ty = inside.parse::()?; 134 | 135 | Ok(InputImpl::HasOne(HasOne { 136 | id_ty, 137 | table, 138 | self_ty, 139 | })) 140 | } 141 | } 142 | } 143 | 144 | impl InputImpl { 145 | fn gen_tokens(&self, input: &Input, backend: &Backend, out: &mut TokenStream) { 146 | match self { 147 | InputImpl::HasOne(has_one) => has_one.gen_tokens(input, backend, out), 148 | InputImpl::HasMany(has_many) => has_many.gen_tokens(input, backend, out), 149 | } 150 | } 151 | } 152 | 153 | impl HasOne { 154 | fn gen_tokens(&self, input: &Input, backend: &Backend, out: &mut TokenStream) { 155 | let error_ty = &input.error_ty; 156 | let context_ty = &input.context_ty; 157 | 158 | let id_ty = &self.id_ty; 159 | let self_ty = &self.self_ty; 160 | let table = &self.table; 161 | 162 | let filter = match backend { 163 | Backend::Pg => { 164 | quote! { 165 | #table::table.primary_key().eq(diesel::pg::expression::dsl::any(ids)) 166 | } 167 | } 168 | Backend::Mysql | Backend::Sqlite => { 169 | quote! { 170 | #table::table.primary_key().eq_any(ids) 171 | } 172 | } 173 | }; 174 | 175 | out.extend(quote! { 176 | impl juniper_eager_loading::LoadFrom<#id_ty> for #self_ty { 177 | type Error = #error_ty; 178 | type Context = #context_ty; 179 | 180 | fn load( 181 | ids: &[#id_ty], 182 | _field_args: &(), 183 | ctx: &Self::Context, 184 | ) -> Result, Self::Error> { 185 | #table::table 186 | .filter(#filter) 187 | .load::<#self_ty>(ctx.db()) 188 | .map_err(From::from) 189 | } 190 | } 191 | }); 192 | } 193 | } 194 | 195 | impl HasMany { 196 | fn gen_tokens(&self, input: &Input, backend: &Backend, out: &mut TokenStream) { 197 | let error_ty = &input.error_ty; 198 | let context_ty = &input.context_ty; 199 | 200 | let join_ty = &self.join_ty; 201 | let join_from = &self.join_from; 202 | let table = &self.table; 203 | let join_to = &self.join_to; 204 | let self_ty = &self.self_ty; 205 | 206 | let filter = match backend { 207 | Backend::Pg => { 208 | quote! { 209 | #table::#join_to.eq(diesel::pg::expression::dsl::any(from_ids)) 210 | } 211 | } 212 | Backend::Mysql | Backend::Sqlite => { 213 | quote! { 214 | #table::#join_to.eq_any(from_ids) 215 | } 216 | } 217 | }; 218 | 219 | out.extend(quote! { 220 | impl juniper_eager_loading::LoadFrom<#join_ty> for #self_ty { 221 | type Error = #error_ty; 222 | type Context = #context_ty; 223 | 224 | fn load( 225 | froms: &[#join_ty], 226 | _field_args: &(), 227 | ctx: &Self::Context, 228 | ) -> Result, Self::Error> { 229 | let from_ids = froms 230 | .iter() 231 | .map(|other| other.#join_from) 232 | .collect::>(); 233 | 234 | #table::table 235 | .filter(#filter) 236 | .load(ctx.db()) 237 | .map_err(From::from) 238 | } 239 | } 240 | }) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/interfaces.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code, unused_mut)] 2 | 3 | mod helpers; 4 | 5 | use assert_json_diff::{assert_json_eq, assert_json_include}; 6 | use helpers::{SortedExtension, StatsHash}; 7 | use juniper::{Executor, FieldError, FieldResult}; 8 | use juniper_eager_loading::{ 9 | prelude::*, EagerLoading, HasMany, HasManyThrough, HasOne, OptionHasOne, 10 | }; 11 | use juniper_from_schema::graphql_schema; 12 | use serde_json::{json, Value}; 13 | use std::sync::atomic::{AtomicUsize, Ordering}; 14 | use std::{borrow::Borrow, collections::HashMap, hash::Hash}; 15 | 16 | graphql_schema! { 17 | schema { 18 | query: Query 19 | } 20 | 21 | type Query { 22 | search: [HasCountry!]! @juniper(ownership: "owned") 23 | } 24 | 25 | type User implements HasCountry { 26 | id: Int! 27 | country: Country! 28 | } 29 | 30 | type City implements HasCountry { 31 | id: Int! 32 | country: Country! 33 | } 34 | 35 | interface HasCountry { 36 | country: Country! 37 | } 38 | 39 | type Country { 40 | id: Int! 41 | } 42 | 43 | } 44 | 45 | mod models { 46 | use juniper_eager_loading::LoadFrom; 47 | 48 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 49 | pub struct User { 50 | pub id: i32, 51 | pub country_id: i32, 52 | } 53 | 54 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 55 | pub struct City { 56 | pub id: i32, 57 | pub country_id: i32, 58 | } 59 | 60 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 61 | pub struct Country { 62 | pub id: i32, 63 | } 64 | 65 | impl LoadFrom for Country { 66 | type Error = Box; 67 | type Context = super::Context; 68 | 69 | fn load(ids: &[i32], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 70 | let models = ctx 71 | .db 72 | .countries 73 | .all_values() 74 | .into_iter() 75 | .filter(|value| ids.contains(&value.id)) 76 | .cloned() 77 | .collect::>(); 78 | Ok(models) 79 | } 80 | } 81 | } 82 | 83 | pub struct Db { 84 | users: StatsHash, 85 | cities: StatsHash, 86 | countries: StatsHash, 87 | } 88 | 89 | pub struct Context { 90 | db: Db, 91 | } 92 | 93 | impl juniper::Context for Context {} 94 | 95 | pub struct Query; 96 | 97 | impl QueryFields for Query { 98 | fn field_search<'a>( 99 | &self, 100 | executor: &Executor<'a, Context>, 101 | trail: &QueryTrail<'a, HasCountry, Walked>, 102 | ) -> FieldResult> { 103 | let ctx = executor.context(); 104 | 105 | let mut user_models = ctx 106 | .db 107 | .users 108 | .all_values() 109 | .into_iter() 110 | .cloned() 111 | .collect::>(); 112 | let users = User::eager_load_each(&user_models, &ctx, &trail.downcast())?; 113 | 114 | let mut city_models = ctx 115 | .db 116 | .cities 117 | .all_values() 118 | .into_iter() 119 | .cloned() 120 | .collect::>(); 121 | let cities = City::eager_load_each(&city_models, &ctx, &trail.downcast())?; 122 | 123 | let mut has_countries = vec![]; 124 | has_countries.extend(users.into_iter().map(HasCountry::from).collect::>()); 125 | has_countries.extend(cities.into_iter().map(HasCountry::from).collect::>()); 126 | 127 | Ok(has_countries) 128 | } 129 | } 130 | 131 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 132 | #[eager_loading(context = Context, error = Box)] 133 | pub struct User { 134 | user: models::User, 135 | #[has_one(default)] 136 | country: HasOne, 137 | } 138 | 139 | impl UserFields for User { 140 | fn field_id<'a>(&self, _: &Executor<'a, Context>) -> FieldResult<&i32> { 141 | Ok(&self.user.id) 142 | } 143 | 144 | fn field_country<'a>( 145 | &self, 146 | executor: &Executor<'a, Context>, 147 | trail: &QueryTrail<'a, Country, Walked>, 148 | ) -> FieldResult<&Country> { 149 | Ok(self.country.try_unwrap()?) 150 | } 151 | } 152 | 153 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 154 | #[eager_loading(context = Context, error = Box)] 155 | pub struct City { 156 | city: models::City, 157 | #[has_one(default)] 158 | country: HasOne, 159 | } 160 | 161 | impl CityFields for City { 162 | fn field_id<'a>(&self, _: &Executor<'a, Context>) -> FieldResult<&i32> { 163 | Ok(&self.city.id) 164 | } 165 | 166 | fn field_country<'a>( 167 | &self, 168 | executor: &Executor<'a, Context>, 169 | trail: &QueryTrail<'a, Country, Walked>, 170 | ) -> FieldResult<&Country> { 171 | Ok(self.country.try_unwrap()?) 172 | } 173 | } 174 | 175 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 176 | #[eager_loading(context = Context, error = Box)] 177 | pub struct Country { 178 | country: models::Country, 179 | } 180 | 181 | impl CountryFields for Country { 182 | fn field_id<'a>(&self, _: &Executor<'a, Context>) -> FieldResult<&i32> { 183 | Ok(&self.country.id) 184 | } 185 | } 186 | 187 | #[test] 188 | fn loading_users_and_associations() { 189 | let mut countries = StatsHash::new("countries"); 190 | let country = models::Country { id: 10 }; 191 | countries.insert(country.id, country.clone()); 192 | 193 | let mut users = StatsHash::new("users"); 194 | let user = models::User { 195 | id: 10, 196 | country_id: country.id, 197 | }; 198 | users.insert(user.id, user); 199 | 200 | let mut cities = StatsHash::new("cities"); 201 | let city = models::City { 202 | id: 10, 203 | country_id: country.id, 204 | }; 205 | cities.insert(city.id, city); 206 | 207 | let db = Db { 208 | users, 209 | countries, 210 | cities, 211 | }; 212 | 213 | let (json, counts) = run_query( 214 | r#" 215 | query Test { 216 | search { 217 | country { 218 | id 219 | } 220 | } 221 | } 222 | "#, 223 | db, 224 | ); 225 | 226 | assert_json_include!( 227 | expected: json!({ 228 | "search": [ 229 | { "country": { "id": country.id } }, 230 | { "country": { "id": country.id } }, 231 | ] 232 | }), 233 | actual: json, 234 | ); 235 | 236 | assert_eq!(1, counts.user_reads); 237 | assert_eq!(1, counts.city_reads); 238 | assert_eq!(2, counts.country_reads); 239 | } 240 | 241 | struct DbStats { 242 | user_reads: usize, 243 | city_reads: usize, 244 | country_reads: usize, 245 | } 246 | 247 | fn run_query(query: &str, db: Db) -> (Value, DbStats) { 248 | let ctx = Context { db }; 249 | 250 | let (result, errors) = juniper::execute( 251 | query, 252 | None, 253 | &Schema::new(Query, juniper::EmptyMutation::new()), 254 | &juniper::Variables::new(), 255 | &ctx, 256 | ) 257 | .unwrap(); 258 | 259 | if !errors.is_empty() { 260 | panic!( 261 | "GraphQL errors\n{}", 262 | serde_json::to_string_pretty(&errors).unwrap() 263 | ); 264 | } 265 | 266 | let json: Value = serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap(); 267 | 268 | println!("{}", serde_json::to_string_pretty(&json).unwrap()); 269 | 270 | ( 271 | json, 272 | DbStats { 273 | user_reads: ctx.db.users.reads_count(), 274 | country_reads: ctx.db.countries.reads_count(), 275 | city_reads: ctx.db.cities.reads_count(), 276 | }, 277 | ) 278 | } 279 | -------------------------------------------------------------------------------- /examples/has_many_through_no_macros.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code)] 2 | #![allow(clippy::let_unit_value)] 3 | 4 | #[macro_use] 5 | extern crate diesel; 6 | 7 | use juniper::{Executor, FieldResult}; 8 | use juniper_eager_loading::{ 9 | prelude::*, EagerLoading, HasManyThrough, LoadChildrenOutput, LoadFrom, 10 | }; 11 | use juniper_from_schema::graphql_schema; 12 | use std::error::Error; 13 | 14 | // the examples all use Diesel, but this library is data store agnostic 15 | use diesel::prelude::*; 16 | 17 | graphql_schema! { 18 | schema { 19 | query: Query 20 | } 21 | 22 | type Query { 23 | users: [User!]! @juniper(ownership: "owned") 24 | } 25 | 26 | type User { 27 | id: Int! 28 | companies: [Company!]! 29 | } 30 | 31 | type Company { 32 | id: Int! 33 | } 34 | } 35 | 36 | mod db_schema { 37 | table! { 38 | users { 39 | id -> Integer, 40 | } 41 | } 42 | 43 | table! { 44 | companies { 45 | id -> Integer, 46 | } 47 | } 48 | 49 | table! { 50 | employments { 51 | id -> Integer, 52 | user_id -> Integer, 53 | company_id -> Integer, 54 | } 55 | } 56 | } 57 | 58 | mod models { 59 | use diesel::prelude::*; 60 | 61 | #[derive(Clone, Debug, Queryable)] 62 | pub struct User { 63 | pub id: i32, 64 | } 65 | 66 | #[derive(Clone, Debug, Queryable)] 67 | pub struct Company { 68 | pub id: i32, 69 | } 70 | 71 | #[derive(Clone, Debug, Queryable)] 72 | pub struct Employment { 73 | pub id: i32, 74 | pub user_id: i32, 75 | pub company_id: i32, 76 | } 77 | 78 | impl juniper_eager_loading::LoadFrom for Company { 79 | type Error = diesel::result::Error; 80 | type Context = super::Context; 81 | 82 | fn load( 83 | employments: &[Employment], 84 | _field_args: &(), 85 | ctx: &Self::Context, 86 | ) -> Result, Self::Error> { 87 | use crate::db_schema::companies::dsl::*; 88 | use diesel::pg::expression::dsl::any; 89 | 90 | let company_ids = employments 91 | .iter() 92 | .map(|employent| employent.company_id) 93 | .collect::>(); 94 | 95 | companies 96 | .filter(id.eq(any(company_ids))) 97 | .load::(&ctx.db) 98 | } 99 | } 100 | 101 | impl juniper_eager_loading::LoadFrom for Employment { 102 | type Error = diesel::result::Error; 103 | type Context = super::Context; 104 | 105 | fn load( 106 | users: &[User], 107 | _field_args: &(), 108 | ctx: &Self::Context, 109 | ) -> Result, Self::Error> { 110 | use crate::db_schema::employments::dsl::*; 111 | use diesel::pg::expression::dsl::any; 112 | 113 | let user_ids = users.iter().map(|user| user.id).collect::>(); 114 | 115 | employments 116 | .filter(user_id.eq(any(user_ids))) 117 | .load::(&ctx.db) 118 | } 119 | } 120 | } 121 | 122 | pub struct Query; 123 | 124 | impl QueryFields for Query { 125 | fn field_users( 126 | &self, 127 | executor: &Executor<'_, Context>, 128 | trail: &QueryTrail<'_, User, Walked>, 129 | ) -> FieldResult> { 130 | let ctx = executor.context(); 131 | let user_models = db_schema::users::table.load::(&ctx.db)?; 132 | let users = User::eager_load_each(&user_models, ctx, trail)?; 133 | 134 | Ok(users) 135 | } 136 | } 137 | 138 | pub struct Context { 139 | db: PgConnection, 140 | } 141 | 142 | impl juniper::Context for Context {} 143 | 144 | #[derive(Clone)] 145 | pub struct User { 146 | user: models::User, 147 | companies: HasManyThrough, 148 | } 149 | 150 | impl UserFields for User { 151 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 152 | Ok(&self.user.id) 153 | } 154 | 155 | fn field_companies( 156 | &self, 157 | executor: &Executor<'_, Context>, 158 | trail: &QueryTrail<'_, Company, Walked>, 159 | ) -> FieldResult<&Vec> { 160 | self.companies.try_unwrap().map_err(From::from) 161 | } 162 | } 163 | 164 | #[derive(Clone)] 165 | pub struct Company { 166 | company: models::Company, 167 | } 168 | 169 | impl CompanyFields for Company { 170 | fn field_id(&self, executor: &Executor<'_, Context>) -> FieldResult<&i32> { 171 | Ok(&self.company.id) 172 | } 173 | } 174 | 175 | impl EagerLoading for User { 176 | type Model = models::User; 177 | type Id = i32; 178 | type Context = Context; 179 | type Error = diesel::result::Error; 180 | 181 | fn new_from_model(model: &Self::Model) -> Self { 182 | Self { 183 | user: model.clone(), 184 | companies: Default::default(), 185 | } 186 | } 187 | 188 | fn eager_load_each( 189 | models: &[Self::Model], 190 | ctx: &Self::Context, 191 | trail: &juniper_from_schema::QueryTrail<'_, Self, juniper_from_schema::Walked>, 192 | ) -> Result, Self::Error> { 193 | let mut nodes = Self::from_db_models(models); 194 | 195 | if let Some(child_trail) = trail.companies().walk() { 196 | let field_args = trail.companies_args(); 197 | 198 | EagerLoadChildrenOfType::< 199 | Company, 200 | EagerLoadingContextUserForCompanies, 201 | _ 202 | >::eager_load_children(&mut nodes, models, ctx, &child_trail, &field_args)?; 203 | } 204 | 205 | Ok(nodes) 206 | } 207 | } 208 | 209 | #[allow(missing_docs, dead_code)] 210 | struct EagerLoadingContextUserForCompanies; 211 | 212 | impl<'a> 213 | EagerLoadChildrenOfType<'a, Company, EagerLoadingContextUserForCompanies, models::Employment> 214 | for User 215 | { 216 | type FieldArguments = (); 217 | 218 | #[allow(unused_variables)] 219 | fn load_children( 220 | models: &[Self::Model], 221 | field_args: &Self::FieldArguments, 222 | ctx: &Self::Context, 223 | ) -> Result, Self::Error> { 224 | let join_models: Vec = LoadFrom::load(&models, field_args, ctx)?; 225 | let child_models: Vec = LoadFrom::load(&join_models, field_args, ctx)?; 226 | 227 | let mut child_and_join_model_pairs = Vec::new(); 228 | 229 | for join_model in join_models { 230 | for child_model in &child_models { 231 | if join_model.company_id == child_model.id { 232 | let pair = (child_model.clone(), join_model.clone()); 233 | child_and_join_model_pairs.push(pair); 234 | } 235 | } 236 | } 237 | 238 | Ok(LoadChildrenOutput::ChildAndJoinModels( 239 | child_and_join_model_pairs, 240 | )) 241 | } 242 | 243 | fn is_child_of( 244 | node: &Self, 245 | child: &Company, 246 | join_model: &models::Employment, 247 | _field_args: &Self::FieldArguments, 248 | _ctx: &Self::Context, 249 | ) -> bool { 250 | node.user.id == join_model.user_id && join_model.company_id == child.company.id 251 | } 252 | 253 | fn association(node: &mut Self) -> &mut dyn Association { 254 | &mut node.companies 255 | } 256 | } 257 | 258 | impl EagerLoading for Company { 259 | type Model = models::Company; 260 | type Id = i32; 261 | type Context = Context; 262 | type Error = diesel::result::Error; 263 | 264 | fn new_from_model(model: &Self::Model) -> Self { 265 | Self { 266 | company: model.clone(), 267 | } 268 | } 269 | 270 | fn eager_load_each( 271 | _models: &[Self::Model], 272 | _ctx: &Self::Context, 273 | _trail: &juniper_from_schema::QueryTrail<'_, Self, juniper_from_schema::Walked>, 274 | ) -> Result, Self::Error> { 275 | Ok(Vec::new()) 276 | } 277 | } 278 | 279 | fn main() {} 280 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/mixed_id_types.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code, unused_mut)] 2 | 3 | mod helpers; 4 | 5 | use assert_json_diff::assert_json_include; 6 | use helpers::StatsHash; 7 | use juniper::{EmptyMutation, Executor, FieldResult, ID}; 8 | use juniper_eager_loading::{prelude::*, EagerLoading, HasManyThrough, HasOne}; 9 | use juniper_from_schema::graphql_schema; 10 | use serde_json::{json, Value}; 11 | 12 | graphql_schema! { 13 | schema { 14 | query: Query 15 | } 16 | 17 | type Query { 18 | users: [User!]! @juniper(ownership: "owned") 19 | } 20 | 21 | type User { 22 | id: Int! 23 | country: Country! 24 | visitedCountries: [Country!]! 25 | } 26 | 27 | type Country { 28 | id: ID! @juniper(ownership: "owned") 29 | } 30 | } 31 | 32 | mod models { 33 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 34 | pub struct User { 35 | pub id: i32, 36 | pub country_id: i64, 37 | } 38 | 39 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 40 | pub struct Country { 41 | pub id: i64, 42 | } 43 | 44 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 45 | pub struct Visit { 46 | pub person_id: i32, 47 | pub country_id: i64, 48 | } 49 | 50 | impl juniper_eager_loading::LoadFrom for User { 51 | type Error = Box; 52 | type Context = super::Context; 53 | 54 | fn load(ids: &[i32], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 55 | let models = ctx 56 | .db 57 | .users 58 | .all_values() 59 | .into_iter() 60 | .filter(|value| ids.contains(&value.id)) 61 | .cloned() 62 | .collect::>(); 63 | Ok(models) 64 | } 65 | } 66 | 67 | impl juniper_eager_loading::LoadFrom for Country { 68 | type Error = Box; 69 | type Context = super::Context; 70 | 71 | fn load(ids: &[i64], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 72 | let countries = ctx 73 | .db 74 | .countries 75 | .all_values() 76 | .into_iter() 77 | .filter(|value| ids.contains(&value.id)) 78 | .cloned() 79 | .collect::>(); 80 | Ok(countries) 81 | } 82 | } 83 | 84 | impl juniper_eager_loading::LoadFrom for Visit { 85 | type Error = Box; 86 | type Context = super::Context; 87 | 88 | fn load(users: &[User], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 89 | let user_ids = users.iter().map(|user| user.id).collect::>(); 90 | let visits = ctx 91 | .db 92 | .visits 93 | .iter() 94 | .filter(|visit| user_ids.contains(&visit.person_id)) 95 | .cloned() 96 | .collect::>(); 97 | Ok(visits) 98 | } 99 | } 100 | 101 | impl juniper_eager_loading::LoadFrom for Country { 102 | type Error = Box; 103 | type Context = super::Context; 104 | 105 | fn load(visits: &[Visit], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 106 | let country_ids = visits 107 | .iter() 108 | .map(|visit| visit.country_id) 109 | .collect::>(); 110 | let countries = ctx 111 | .db 112 | .countries 113 | .all_values() 114 | .into_iter() 115 | .filter(|country| country_ids.contains(&country.id)) 116 | .cloned() 117 | .collect::>(); 118 | Ok(countries) 119 | } 120 | } 121 | } 122 | 123 | pub struct Db { 124 | users: StatsHash, 125 | countries: StatsHash, 126 | visits: Vec, 127 | } 128 | 129 | pub struct Context { 130 | db: Db, 131 | } 132 | 133 | impl juniper::Context for Context {} 134 | 135 | pub struct Query; 136 | 137 | impl QueryFields for Query { 138 | fn field_users<'a>( 139 | &self, 140 | executor: &Executor<'a, Context>, 141 | trail: &QueryTrail<'a, User, Walked>, 142 | ) -> FieldResult> { 143 | let ctx = executor.context(); 144 | 145 | let mut user_models = ctx 146 | .db 147 | .users 148 | .all_values() 149 | .into_iter() 150 | .cloned() 151 | .collect::>(); 152 | user_models.sort_by_key(|user| user.id); 153 | 154 | let users = User::eager_load_each(&user_models, ctx, trail)?; 155 | 156 | Ok(users) 157 | } 158 | } 159 | 160 | // The default values are commented out 161 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 162 | #[eager_loading(context = Context, error = Box)] 163 | pub struct User { 164 | user: models::User, 165 | 166 | #[has_one(default)] 167 | country: HasOne, 168 | 169 | #[has_many_through(join_model = models::Visit, foreign_key_field = person_id)] 170 | visited_countries: HasManyThrough, 171 | } 172 | 173 | impl UserFields for User { 174 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 175 | Ok(&self.user.id) 176 | } 177 | 178 | fn field_country( 179 | &self, 180 | _executor: &Executor<'_, Context>, 181 | _trail: &QueryTrail<'_, Country, Walked>, 182 | ) -> FieldResult<&Country> { 183 | Ok(self.country.try_unwrap()?) 184 | } 185 | 186 | fn field_visited_countries( 187 | &self, 188 | _executor: &Executor<'_, Context>, 189 | _trail: &QueryTrail<'_, Country, Walked>, 190 | ) -> FieldResult<&Vec> { 191 | Ok(self.visited_countries.try_unwrap()?) 192 | } 193 | } 194 | 195 | // #[derive(Clone, Eq, PartialEq, Debug)] 196 | #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] 197 | #[eager_loading( 198 | model = models::Country, 199 | context = Context, 200 | id = i64, 201 | error = Box, 202 | root_model_field = country 203 | )] 204 | pub struct Country { 205 | country: models::Country, 206 | } 207 | 208 | impl CountryFields for Country { 209 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult { 210 | Ok(self.country.id.to_string().into()) 211 | } 212 | } 213 | 214 | #[test] 215 | fn loading_users_and_associations() { 216 | let mut countries = StatsHash::new("countries"); 217 | let mut users = StatsHash::new("users"); 218 | 219 | let country = models::Country { id: 10 }; 220 | 221 | countries.insert(country.id, country.clone()); 222 | 223 | users.insert( 224 | 1, 225 | models::User { 226 | id: 1, 227 | country_id: country.id, 228 | }, 229 | ); 230 | 231 | let db = Db { 232 | users, 233 | countries, 234 | visits: vec![], 235 | }; 236 | 237 | let (json, counts) = run_query( 238 | r#" 239 | query Test { 240 | users { 241 | id 242 | country { 243 | id 244 | } 245 | } 246 | } 247 | "#, 248 | db, 249 | ); 250 | 251 | assert_json_include!( 252 | expected: json!({ 253 | "users": [ 254 | { 255 | "id": 1, 256 | "country": { 257 | "id": country.id.to_string(), 258 | }, 259 | }, 260 | ] 261 | }), 262 | actual: json, 263 | ); 264 | 265 | assert_eq!(1, counts.user_reads); 266 | assert_eq!(1, counts.country_reads); 267 | } 268 | 269 | #[test] 270 | fn has_many_through_fkey() { 271 | let mut countries = StatsHash::new("countries"); 272 | let mut users = StatsHash::new("users"); 273 | let mut visits = vec![]; 274 | 275 | let country = models::Country { id: 10 }; 276 | countries.insert(country.id, country.clone()); 277 | 278 | let user = models::User { 279 | id: 1, 280 | country_id: country.id, 281 | }; 282 | users.insert(1, user.clone()); 283 | 284 | visits.push(models::Visit { 285 | country_id: country.id, 286 | person_id: user.id, 287 | }); 288 | 289 | let db = Db { 290 | users, 291 | countries, 292 | visits, 293 | }; 294 | 295 | let (json, counts) = run_query( 296 | r#" 297 | query Test { 298 | users { 299 | id 300 | visitedCountries { 301 | id 302 | } 303 | } 304 | } 305 | "#, 306 | db, 307 | ); 308 | 309 | assert_json_include!( 310 | expected: json!({ 311 | "users": [ 312 | { 313 | "id": 1, 314 | "visitedCountries": [ 315 | { 316 | "id": country.id.to_string(), 317 | } 318 | ] 319 | }, 320 | ] 321 | }), 322 | actual: json, 323 | ); 324 | 325 | assert_eq!(1, counts.user_reads); 326 | assert_eq!(1, counts.country_reads); 327 | } 328 | 329 | struct DbStats { 330 | user_reads: usize, 331 | country_reads: usize, 332 | } 333 | 334 | fn run_query(query: &str, db: Db) -> (Value, DbStats) { 335 | let ctx = Context { db }; 336 | 337 | let (result, errors) = juniper::execute( 338 | query, 339 | None, 340 | &Schema::new(Query, EmptyMutation::new()), 341 | &juniper::Variables::new(), 342 | &ctx, 343 | ) 344 | .unwrap(); 345 | 346 | if !errors.is_empty() { 347 | panic!( 348 | "GraphQL errors\n{}", 349 | serde_json::to_string_pretty(&errors).unwrap() 350 | ); 351 | } 352 | 353 | let json: Value = serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap(); 354 | 355 | println!("{}", serde_json::to_string_pretty(&json).unwrap()); 356 | 357 | ( 358 | json, 359 | DbStats { 360 | user_reads: ctx.db.users.reads_count(), 361 | country_reads: ctx.db.countries.reads_count(), 362 | }, 363 | ) 364 | } 365 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All user visible changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/), as described 5 | for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/text/1105-api-evolution.md) 6 | 7 | ## Unreleased 8 | 9 | None. 10 | 11 | ### Breaking changes 12 | 13 | Some nice simplifications of the APIs: 14 | 15 | - `GraphqlNodeForModel` and `EagerLoadAllChildren` has been combined into one trait called `EagerLoading`. 16 | - `EagerLoadAllChildren::eager_load_all_children_for_each` has been renamed to `EagerLoading::eager_load_each` 17 | - `EagerLoadAllChildren::eager_load_all_children` has been renamed to `EagerLoading::eager_load` 18 | - The two eager loading methods (formerly `eager_load_all_children_for_each` and `eager_load_all_children`) no longer take `nodes: &mut [Self]`. The modes will be created generically from the models by calling `EagerLoading::from_db_models`. 19 | 20 | With these changes eager loading from a `Query` resolver now looks like this: 21 | 22 | ```rust 23 | let ctx = executor.context(); 24 | let user_models = ctx.db.load_all_users(); 25 | let users = User::eager_load_each(&user_models, ctx, trail)?; 26 | Ok(users) 27 | ``` 28 | 29 | `Error` is now `#[non_exhaustive]`. 30 | 31 | ## 0.5.1 - 2020-03-04 32 | 33 | - Support generating code for fields that take arguments with 34 | - `#[has_one(field_arguments = YourArgType)]` 35 | - `#[option_has_one(field_arguments = YourArgType)]` 36 | - `#[has_many(field_arguments = YourArgType)]` 37 | - `#[has_many_through(field_arguments = YourArgType)]` 38 | 39 | The `impl_load_from_for_diesel_(pg|mysql|sqlite)` macros now use the [`primary_key` method](http://docs.diesel.rs/diesel/query_source/trait.Table.html#tymethod.primary_key) generated by Diesel. [#47](https://github.com/davidpdrsn/juniper-eager-loading/pull/47) 40 | 41 | New arguments on attribute macros that make it possible to customize all id fields. The new attributes are: 42 | - For `#[eager_loading]`, `#[has_one]`, and `#[option_has_one]`: 43 | - `primary_key_field` 44 | - For `#[has_many_through]` 45 | - `child_primary_key_field` 46 | - `child_primary_key_field_on_join_model` 47 | - For `#[has_many]` 48 | - Nothing new. Instead it can use `child_primary_key_field` if set in `#[eager_loading]` 49 | See the docs for details about the specific attributes and what they do. 50 | 51 | `#[eager_loading]` now supports the argument `print` which will cause it to print the generated code during compilation. This attribute was also supported previously but it wasn't documented. 52 | 53 | ## 0.5.0 - 2019-11-27 54 | 55 | ### Breaking changes 56 | 57 | **Rename `GraphqlNodeForModel::Connection` to `Context`** 58 | 59 | You might need more than just a database connection to eager load data, for example the currently logged in user or other data about the HTTP request. Since `Connection` was previously generic it was technically possible but it was awkward in practice. `Connection` is now renamed to `Context` and is supposed to be your Juniper context which can contain whatever data you need. 60 | 61 | **`impl_load_from_for_diesel_(pg|mysql|sqlite)` syntax changed** 62 | 63 | The `impl_load_from_for_diesel_(pg|mysql|sqlite)` macro now requires `context = YourContextType` rather than `connection = YourConnectionType`. 64 | 65 | **Require `Context` to have `db` method for Diesel macro** 66 | 67 | `impl_load_from_for_diesel_(pg|mysql|sqlite)` now requires that your context type has a method called `db` that returns a reference to a Diesel connection. 68 | 69 | For example: 70 | 71 | ```rust 72 | struct Context { 73 | db: PgConnection, 74 | } 75 | 76 | impl Context { 77 | fn db(&self) -> &PgConnection { 78 | &self.db 79 | } 80 | } 81 | 82 | // Whatever the method returns has to work with Diesel's `load` method 83 | users::table 84 | .filter(users::id.eq(any(user_ids))) 85 | .load::(ctx.db()) 86 | ``` 87 | 88 | This is _only_ necessary if you're using the `impl_load_from_for_diesel_(pg|mysql|sqlite)`. 89 | 90 | **Attribute values should no longer be surrounded by quotes** 91 | 92 | Before: 93 | 94 | ```rust 95 | #[derive(Clone, EagerLoading)] 96 | #[eager_loading( 97 | context = "Context", 98 | error = "Box", 99 | model = "models::User", 100 | id = "i32", 101 | root_model_field = "user," 102 | )] 103 | pub struct User { 104 | user: models::User, 105 | #[has_one( 106 | foreign_key_field = "country_id," 107 | root_model_field = "country," 108 | graphql_field = "country," 109 | )] 110 | country: HasOne, 111 | } 112 | ``` 113 | 114 | After: 115 | 116 | ```rust 117 | #[derive(Clone, EagerLoading)] 118 | #[eager_loading( 119 | context = Context, 120 | error = Box, 121 | model = models::User, 122 | id = i32, 123 | root_model_field = user, 124 | )] 125 | pub struct User { 126 | user: models::User, 127 | #[has_one( 128 | foreign_key_field = country_id, 129 | root_model_field = country, 130 | graphql_field = country, 131 | )] 132 | country: HasOne, 133 | } 134 | ``` 135 | 136 | This change is made for all attributes: 137 | - `#[has_one]` 138 | - `#[option_has_one]` 139 | - `#[has_many]` 140 | - `#[has_many_through]` 141 | 142 | **Remove `join_model_field` option from `HasManyThrough`** 143 | 144 | Turns out it wasn't being used and therefore didn't do anything. 145 | 146 | ## 0.4.2 - 2019-11-14 147 | 148 | - Support recursive types for `HasOne` and `OptionHasOne` associations. You can now use `HasOne>` or `OptionHasOne>` in your GraphQL types. `HasMany` and `HasManyThrough` already support recursive types because they're backed by `Vec`s. 149 | 150 | ## 0.4.1 - 2019-10-29 151 | 152 | - The [examples](https://github.com/davidpdrsn/juniper-eager-loading/tree/master/examples) has been much improved. 153 | - Remove warning about this library being experimental. It is safe to use in production (: 154 | 155 | ## 0.4.0 - 2019-10-23 156 | 157 | - Move `impl_load_from_for_diesel_{pg|mysql|sqlite}!` to proc-macros. Are fully backwards compatible but will give better errors. 158 | - Tweak docs for `impl_load_from_for_diesel_{pg|mysql|sqlite}!`. 159 | - `Association` trait has been added to abstraction over `HasOne`, `OptionHasOne`, `HasMany`, and `HasManyThrough` associations. 160 | 161 | ### Breaking changes 162 | 163 | - `EagerLoadChildrenOfType::child_ids` has been removed. Use `EagerLoadChildrenOfType::load_children` instead. See [#27](https://github.com/davidpdrsn/juniper-eager-loading/issues/27) for more context. 164 | - `EagerLoadChildrenOfType::ChildId` has been removed. It was only used by `child_ids` and was therefore no longer necessary. 165 | - `LoadResult` has been renamed to `LoadChildrenOutput`. Including `Result` in the name made it seem like it might related to errors, which it wasn't. 166 | - `LoadResult::Ids` has been renamed to `LoadChildrenOutput::ChildModel` to match the changes to `EagerLoadChildrenOfType::load_children`. 167 | - `LoadResult::Models` has been renamed to `LoadChildrenOutput::ChildAndJoinModels` for the same reason. 168 | - The second type parameter (used for the join model) now defaults to `()`. 169 | - The signature of `EagerLoadChildrenOfType::is_child_of` has been changed to `parent: &Self, child: &Child, join_model: &JoinModel`. Manually pulling things out of the tuple was tedious. 170 | - `EagerLoadChildrenOfType::association` has been added. This methods allows for some boilerplate to be removed from `EagerLoadChildrenOfType`. 171 | - `EagerLoadChildrenOfType::loaded_child` and `EagerLoadChildrenOfType::assert_loaded_otherwise_failed` has been removed and implemented generically using the new `Association` trait. 172 | - The deprecated macro `impl_load_from_for_diesel` has been removed completely. Use `impl_load_from_for_diesel_{pg|mysql|sqlite}` instead. 173 | - Support eager loading GraphQL fields that take arguments. See the docs for more information and examples. 174 | - Add `EagerLoadChildrenOfType::FieldArguments` 175 | - The following methods take the arguments: 176 | - `EagerLoadChildrenOfType::load_children` 177 | - `EagerLoadChildrenOfType::is_child_of` 178 | - `EagerLoadChildrenOfType::eager_load_children` 179 | - Add second generic argument to `LoadFrom` which will be the arguments and accept argument of that type in `LoadFrom::load`. 180 | 181 | If you're using the derive macros for everything in your app you shouldn't have to care about any of these changes. The generated code will automatically handle them. 182 | 183 | ## 0.3.1 - 2019-10-09 184 | 185 | ### Added 186 | 187 | - Add specific versions of `impl_load_from_for_diesel_*` for each backend supported by Diesel: 188 | - `impl_load_from_for_diesel_pg` (formerly `impl_load_from_for_diesel`) 189 | - `impl_load_from_for_diesel_sqlite` 190 | - `impl_load_from_for_diesel_mysql` 191 | 192 | ### Changed 193 | 194 | - Deprecate `impl_load_from_for_diesel`. `impl_load_from_for_diesel_pg` should be used instead. `impl_load_from_for_diesel` will be removed in 0.4.0. 195 | 196 | ## 0.3.0 - 2019-10-05 197 | 198 | ### Added 199 | 200 | - Documentation section about eager loading interface or union types. [#19](https://github.com/davidpdrsn/juniper-eager-loading/pull/19) 201 | 202 | ### Removed 203 | 204 | - `GenericQueryTrail` has been removed since it is no longer necessary thanks to . This also lead to the removal of the `QueryTrail` type parameter on `EagerLoadChildrenOfType` and `EagerLoadAllChildren`. [#20](https://github.com/davidpdrsn/juniper-eager-loading/pull/20) 205 | 206 | ### Fixed 207 | 208 | - Fixed "mutable_borrow_reservation_conflict" warnings. 209 | 210 | ## 0.2.0 - 2019-06-30 211 | 212 | ### Added 213 | 214 | - Support juniper-from-schema ^0.3. 215 | - Allow specifying foreign key for `has_many_through`. 216 | 217 | ### Changed 218 | 219 | - Renamed `impl_LoadFrom_for_diesel` to `impl_load_from_for_diesel`. 220 | 221 | ### Removed 222 | 223 | - The associated type `ChildModel` on `EagerLoadChildrenOfType` has been removed because it wasn't necessary. 224 | 225 | ## 0.1.2 - 2019-06-18 226 | 227 | ### Fixed 228 | 229 | * Fixed spelling mistake in `eager_load_all_children` (from `eager_load_all_chilren`). [#11](https://github.com/davidpdrsn/juniper-eager-loading/pull/11) 230 | * Previously, using mixed ID types between parent and child types would not compile. This now actually works. [#10](https://github.com/davidpdrsn/juniper-eager-loading/pull/10) 231 | 232 | ## 0.1.1 233 | 234 | ### Added 235 | 236 | * Support for optional foreign keys when using `HasMany` by using the `foreign_key_optional` attribute. 237 | 238 | ## 0.1.0 239 | 240 | Initial release. 241 | -------------------------------------------------------------------------------- /juniper-eager-loading-code-gen/src/derive_eager_loading/field_args.rs: -------------------------------------------------------------------------------- 1 | use bae::FromAttributes; 2 | use heck::SnakeCase; 3 | use proc_macro2::{Span, TokenStream}; 4 | use proc_macro_error::*; 5 | use quote::{format_ident, quote}; 6 | use std::ops::{Deref, DerefMut}; 7 | use syn::{self, Ident}; 8 | 9 | macro_rules! token_stream_getter { 10 | ( $name:ident ) => { 11 | pub fn $name(&self) -> TokenStream { 12 | let value = &self.$name; 13 | quote! { #value } 14 | } 15 | }; 16 | } 17 | 18 | #[derive(Debug, FromAttributes)] 19 | pub struct EagerLoading { 20 | model: Option, 21 | id: Option, 22 | context: syn::Type, 23 | error: syn::Type, 24 | root_model_field: Option, 25 | print: Option<()>, 26 | primary_key_field: Option, 27 | } 28 | 29 | impl EagerLoading { 30 | token_stream_getter!(context); 31 | token_stream_getter!(error); 32 | 33 | pub fn model(&self, struct_name: &syn::Ident) -> TokenStream { 34 | if let Some(inner) = &self.model { 35 | quote! { #inner } 36 | } else { 37 | quote! { models::#struct_name } 38 | } 39 | } 40 | 41 | pub fn id(&self) -> TokenStream { 42 | if let Some(inner) = &self.id { 43 | quote! { #inner } 44 | } else { 45 | quote! { i32 } 46 | } 47 | } 48 | 49 | pub fn root_model_field(&self, struct_name: &syn::Ident) -> TokenStream { 50 | if let Some(inner) = &self.root_model_field { 51 | quote! { #inner } 52 | } else { 53 | let struct_name = struct_name.to_string().to_snake_case(); 54 | let struct_name = Ident::new(&struct_name, Span::call_site()); 55 | quote! { #struct_name } 56 | } 57 | } 58 | 59 | pub fn print(&self) -> bool { 60 | self.print.is_some() 61 | } 62 | 63 | pub fn primary_key_field(&self) -> syn::Ident { 64 | if let Some(id) = &self.primary_key_field { 65 | id.clone() 66 | } else { 67 | format_ident!("id") 68 | } 69 | } 70 | } 71 | 72 | #[derive(Debug, Clone, FromAttributes)] 73 | pub struct HasOne { 74 | print: Option<()>, 75 | skip: Option<()>, 76 | field_arguments: Option, 77 | foreign_key_field: Option, 78 | root_model_field: Option, 79 | graphql_field: Option, 80 | default: Option<()>, 81 | child_primary_key_field: Option, 82 | } 83 | 84 | impl HasOne { 85 | pub fn child_primary_key_field(&self) -> syn::Ident { 86 | let child_primary_key_field = &self.child_primary_key_field; 87 | 88 | if let Some(id) = child_primary_key_field { 89 | id.clone() 90 | } else { 91 | format_ident!("id") 92 | } 93 | } 94 | } 95 | 96 | #[derive(Debug, Clone, FromAttributes)] 97 | pub struct OptionHasOne { 98 | print: Option<()>, 99 | skip: Option<()>, 100 | foreign_key_field: Option, 101 | root_model_field: Option, 102 | graphql_field: Option, 103 | default: Option<()>, 104 | field_arguments: Option, 105 | child_primary_key_field: Option, 106 | } 107 | 108 | impl OptionHasOne { 109 | pub fn child_primary_key_field(&self) -> syn::Ident { 110 | let child_primary_key_field = &self.child_primary_key_field; 111 | 112 | if let Some(id) = child_primary_key_field { 113 | id.clone() 114 | } else { 115 | format_ident!("id") 116 | } 117 | } 118 | } 119 | 120 | #[derive(Debug, Clone, FromAttributes)] 121 | pub struct HasMany { 122 | print: Option<()>, 123 | skip: Option<()>, 124 | field_arguments: Option, 125 | foreign_key_field: Option, 126 | pub foreign_key_optional: Option<()>, 127 | root_model_field: Option, 128 | predicate_method: Option, 129 | graphql_field: Option, 130 | } 131 | 132 | impl HasMany { 133 | pub fn predicate_method(&self) -> &Option { 134 | &self.predicate_method 135 | } 136 | } 137 | 138 | #[derive(Debug, Clone, FromAttributes)] 139 | pub struct HasManyThrough { 140 | print: Option<()>, 141 | skip: Option<()>, 142 | field_arguments: Option, 143 | model_field: Option, 144 | join_model: Option, 145 | foreign_key_field: Option, 146 | predicate_method: Option, 147 | graphql_field: Option, 148 | child_primary_key_field_on_join_model: Option, 149 | child_primary_key_field: Option, 150 | } 151 | 152 | impl HasManyThrough { 153 | pub fn join_model(&self, span: Span) -> syn::Type { 154 | self.join_model 155 | .as_ref() 156 | .cloned() 157 | .map(syn::Type::Path) 158 | .unwrap_or_else(|| abort!(span, "`#[has_many_through]` missing `join_model`")) 159 | } 160 | 161 | pub fn model_field(&self, inner_type: &syn::Type) -> TokenStream { 162 | if let Some(inner) = &self.model_field { 163 | quote! { #inner } 164 | } else { 165 | let inner_type = type_to_string(inner_type).to_snake_case(); 166 | let inner_type = Ident::new(&inner_type, Span::call_site()); 167 | quote! { #inner_type } 168 | } 169 | } 170 | 171 | pub fn child_primary_key_field_on_join_model(&self, inner_type: &syn::Type) -> Ident { 172 | if let Some(id) = &self.child_primary_key_field_on_join_model { 173 | id.clone() 174 | } else { 175 | Ident::new( 176 | &format!("{}_id", self.model_field(inner_type)), 177 | Span::call_site(), 178 | ) 179 | } 180 | } 181 | 182 | pub fn predicate_method(&self) -> &Option { 183 | &self.predicate_method 184 | } 185 | 186 | pub fn child_primary_key_field(&self) -> syn::Ident { 187 | if let Some(id) = &self.child_primary_key_field { 188 | id.clone() 189 | } else { 190 | format_ident!("id") 191 | } 192 | } 193 | } 194 | 195 | #[derive(Debug, Clone)] 196 | pub enum FieldArgs { 197 | HasOne(Spanned), 198 | OptionHasOne(Spanned), 199 | HasMany(Spanned), 200 | HasManyThrough(Spanned>), 201 | } 202 | 203 | impl FieldArgs { 204 | pub fn skip(&self) -> bool { 205 | match self { 206 | FieldArgs::HasOne(inner) => inner.skip.is_some(), 207 | FieldArgs::OptionHasOne(inner) => inner.skip.is_some(), 208 | FieldArgs::HasMany(inner) => inner.skip.is_some(), 209 | FieldArgs::HasManyThrough(inner) => inner.skip.is_some(), 210 | } 211 | } 212 | 213 | pub fn print(&self) -> bool { 214 | match self { 215 | FieldArgs::HasOne(inner) => inner.print.is_some(), 216 | FieldArgs::OptionHasOne(inner) => inner.print.is_some(), 217 | FieldArgs::HasMany(inner) => inner.print.is_some(), 218 | FieldArgs::HasManyThrough(inner) => inner.print.is_some(), 219 | } 220 | } 221 | 222 | pub fn graphql_field(&self) -> &Option { 223 | match self { 224 | FieldArgs::HasOne(inner) => &inner.graphql_field, 225 | FieldArgs::OptionHasOne(inner) => &inner.graphql_field, 226 | FieldArgs::HasMany(inner) => &inner.graphql_field, 227 | FieldArgs::HasManyThrough(inner) => &inner.graphql_field, 228 | } 229 | } 230 | 231 | pub fn field_arguments(&self) -> syn::Type { 232 | let field_arguments = match self { 233 | FieldArgs::HasOne(inner) => &inner.field_arguments, 234 | FieldArgs::OptionHasOne(inner) => &inner.field_arguments, 235 | FieldArgs::HasMany(inner) => &inner.field_arguments, 236 | FieldArgs::HasManyThrough(inner) => &inner.field_arguments, 237 | }; 238 | 239 | if let Some(field_arguments) = field_arguments { 240 | syn::parse2(quote! { #field_arguments<'a> }).unwrap() 241 | } else { 242 | syn::parse_str("()").unwrap() 243 | } 244 | } 245 | 246 | pub fn foreign_key_field(&self, field_name: &Ident) -> TokenStream { 247 | let foreign_key_field = match self { 248 | FieldArgs::HasOne(inner) => &inner.foreign_key_field, 249 | FieldArgs::OptionHasOne(inner) => &inner.foreign_key_field, 250 | FieldArgs::HasMany(inner) => &inner.foreign_key_field, 251 | FieldArgs::HasManyThrough(inner) => &inner.foreign_key_field, 252 | }; 253 | 254 | if let Some(inner) = foreign_key_field { 255 | quote! { #inner } 256 | } else { 257 | let field_name = field_name.to_string().to_snake_case(); 258 | let field_name = format_ident!("{}_id", field_name); 259 | quote! { #field_name } 260 | } 261 | } 262 | } 263 | 264 | pub trait RootModelField { 265 | fn get_root_model_field(&self) -> &Option; 266 | 267 | fn root_model_field(&self, field_name: &Ident) -> TokenStream { 268 | if let Some(inner) = self.get_root_model_field() { 269 | quote! { #inner } 270 | } else { 271 | let field_name = field_name.to_string().to_snake_case(); 272 | let field_name = Ident::new(&field_name, Span::call_site()); 273 | quote! { #field_name } 274 | } 275 | } 276 | } 277 | 278 | impl RootModelField for HasOne { 279 | fn get_root_model_field(&self) -> &Option { 280 | &self.root_model_field 281 | } 282 | } 283 | 284 | impl RootModelField for OptionHasOne { 285 | fn get_root_model_field(&self) -> &Option { 286 | &self.root_model_field 287 | } 288 | } 289 | 290 | impl RootModelField for HasMany { 291 | fn get_root_model_field(&self) -> &Option { 292 | &self.root_model_field 293 | } 294 | } 295 | 296 | fn type_to_string(ty: &syn::Type) -> String { 297 | use quote::ToTokens; 298 | let mut tokenized = quote! {}; 299 | ty.to_tokens(&mut tokenized); 300 | tokenized.to_string() 301 | } 302 | 303 | #[derive(Debug, Clone)] 304 | pub struct Spanned(Span, T); 305 | 306 | impl Spanned { 307 | pub fn new(span: Span, t: T) -> Self { 308 | Self(span, t) 309 | } 310 | 311 | pub fn span(&self) -> Span { 312 | self.0 313 | } 314 | } 315 | 316 | impl Deref for Spanned { 317 | type Target = T; 318 | 319 | fn deref(&self) -> &T { 320 | &self.1 321 | } 322 | } 323 | 324 | impl DerefMut for Spanned { 325 | fn deref_mut(&mut self) -> &mut T { 326 | &mut self.1 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/fields_with_arguments_macro.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code, unused_mut)] 2 | #![allow(clippy::type_complexity)] 3 | 4 | mod helpers; 5 | 6 | use assert_json_diff::{assert_json_eq, assert_json_include}; 7 | use helpers::{SortedExtension, StatsHash}; 8 | use juniper::{Executor, FieldError, FieldResult}; 9 | use juniper_eager_loading::{ 10 | prelude::*, EagerLoading, HasMany, HasManyThrough, HasOne, LoadChildrenOutput, LoadFrom, 11 | OptionHasOne, 12 | }; 13 | use juniper_from_schema::graphql_schema; 14 | use serde_json::{json, Value}; 15 | use std::sync::atomic::{AtomicUsize, Ordering}; 16 | use std::{borrow::Borrow, collections::HashMap, hash::Hash}; 17 | 18 | graphql_schema! { 19 | schema { 20 | query: Query 21 | mutation: Mutation 22 | } 23 | 24 | type Query { 25 | countries: [Country!]! @juniper(ownership: "owned") 26 | } 27 | 28 | type Mutation { 29 | noop: Boolean! 30 | } 31 | 32 | type User { 33 | id: Int! 34 | isAdmin: Boolean! 35 | country: Country! 36 | } 37 | 38 | type Country { 39 | id: Int! 40 | users(onlyAdmins: Boolean!): [User!]! 41 | } 42 | } 43 | 44 | mod models { 45 | use super::*; 46 | use either::Either; 47 | 48 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 49 | pub struct User { 50 | pub id: i32, 51 | pub country_id: i32, 52 | pub admin: bool, 53 | } 54 | 55 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 56 | pub struct Country { 57 | pub id: i32, 58 | } 59 | 60 | impl juniper_eager_loading::LoadFrom for Country { 61 | type Error = Box; 62 | type Context = super::Context; 63 | 64 | fn load(ids: &[i32], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 65 | let mut models = ctx 66 | .db 67 | .countries 68 | .all_values() 69 | .into_iter() 70 | .filter(|value| ids.contains(&value.id)) 71 | .cloned() 72 | .collect::>(); 73 | models.sort_by_key(|model| model.id); 74 | Ok(models) 75 | } 76 | } 77 | 78 | impl juniper_eager_loading::LoadFrom> for User { 79 | type Error = Box; 80 | type Context = super::Context; 81 | 82 | fn load( 83 | ids: &[i32], 84 | field_args: &CountryUsersArgs, 85 | ctx: &Self::Context, 86 | ) -> Result, Self::Error> { 87 | let models = ctx 88 | .db 89 | .users 90 | .all_values() 91 | .into_iter() 92 | .filter(|value| ids.contains(&value.id)); 93 | 94 | let mut models = if field_args.only_admins() { 95 | Either::Left(models.filter(|user| user.admin)) 96 | } else { 97 | Either::Right(models) 98 | } 99 | .cloned() 100 | .collect::>(); 101 | 102 | models.sort_by_key(|model| model.id); 103 | 104 | Ok(models) 105 | } 106 | } 107 | 108 | impl juniper_eager_loading::LoadFrom for User { 109 | type Error = Box; 110 | type Context = super::Context; 111 | 112 | fn load(ids: &[i32], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 113 | let mut models = ctx 114 | .db 115 | .users 116 | .all_values() 117 | .into_iter() 118 | .filter(|value| ids.contains(&value.id)) 119 | .cloned() 120 | .collect::>(); 121 | models.sort_by_key(|model| model.id); 122 | Ok(models) 123 | } 124 | } 125 | 126 | impl juniper_eager_loading::LoadFrom for User { 127 | type Error = Box; 128 | type Context = super::Context; 129 | 130 | fn load( 131 | countries: &[Country], 132 | _: &(), 133 | ctx: &Self::Context, 134 | ) -> Result, Self::Error> { 135 | let country_ids = countries.iter().map(|c| c.id).collect::>(); 136 | let mut models = ctx 137 | .db 138 | .users 139 | .all_values() 140 | .into_iter() 141 | .filter(|user| country_ids.contains(&user.country_id)) 142 | .cloned() 143 | .collect::>(); 144 | models.sort_by_key(|model| model.id); 145 | Ok(models) 146 | } 147 | } 148 | 149 | impl LoadFrom> for User { 150 | type Error = Box; 151 | type Context = super::Context; 152 | 153 | fn load( 154 | countries: &[Country], 155 | args: &CountryUsersArgs, 156 | ctx: &Self::Context, 157 | ) -> Result, Self::Error> { 158 | let only_admins = args.only_admins(); 159 | 160 | let country_ids = countries.iter().map(|c| c.id).collect::>(); 161 | 162 | let models = ctx 163 | .db 164 | .users 165 | .all_values() 166 | .into_iter() 167 | .filter(|user| country_ids.contains(&user.country_id)); 168 | 169 | let models = if only_admins { 170 | Either::Left(models.filter(|user| user.admin)) 171 | } else { 172 | Either::Right(models) 173 | }; 174 | 175 | let mut models = models.cloned().collect::>(); 176 | models.sort_by_key(|model| model.id); 177 | Ok(models) 178 | } 179 | } 180 | } 181 | 182 | pub struct Db { 183 | users: StatsHash, 184 | countries: StatsHash, 185 | } 186 | 187 | pub struct Context { 188 | db: Db, 189 | } 190 | 191 | impl juniper::Context for Context {} 192 | 193 | pub struct Query; 194 | 195 | impl QueryFields for Query { 196 | fn field_countries( 197 | &self, 198 | executor: &Executor<'_, Context>, 199 | trail: &QueryTrail<'_, Country, Walked>, 200 | ) -> FieldResult> { 201 | let ctx = executor.context(); 202 | 203 | let mut country_models = ctx 204 | .db 205 | .countries 206 | .all_values() 207 | .into_iter() 208 | .cloned() 209 | .collect::>(); 210 | country_models.sort_by_key(|country| country.id); 211 | 212 | let countries = Country::eager_load_each(&country_models, ctx, trail)?; 213 | 214 | Ok(countries) 215 | } 216 | } 217 | 218 | pub struct Mutation; 219 | 220 | impl MutationFields for Mutation { 221 | fn field_noop(&self, _executor: &Executor<'_, Context>) -> FieldResult<&bool> { 222 | Ok(&true) 223 | } 224 | } 225 | 226 | // The default values are commented out 227 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 228 | #[eager_loading( 229 | model = models::User, 230 | id = i32, 231 | context = Context, 232 | error = Box, 233 | )] 234 | pub struct User { 235 | user: models::User, 236 | 237 | #[has_one(default)] 238 | country: HasOne, 239 | } 240 | 241 | impl UserFields for User { 242 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 243 | Ok(&self.user.id) 244 | } 245 | 246 | fn field_is_admin(&self, _executor: &Executor<'_, Context>) -> FieldResult<&bool> { 247 | Ok(&self.user.admin) 248 | } 249 | 250 | fn field_country( 251 | &self, 252 | _executor: &Executor<'_, Context>, 253 | _trail: &QueryTrail<'_, Country, Walked>, 254 | ) -> FieldResult<&Country> { 255 | Ok(self.country.try_unwrap()?) 256 | } 257 | } 258 | 259 | #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] 260 | #[eager_loading( 261 | model = models::Country, 262 | id = i32, 263 | context = Context, 264 | error = Box, 265 | )] 266 | pub struct Country { 267 | country: models::Country, 268 | 269 | #[has_many(root_model_field = user, field_arguments = CountryUsersArgs)] 270 | users: HasMany, 271 | } 272 | 273 | impl CountryFields for Country { 274 | fn field_users( 275 | &self, 276 | executor: &Executor<'_, Context>, 277 | trail: &QueryTrail<'_, User, Walked>, 278 | _only_admins: bool, 279 | ) -> FieldResult<&Vec> { 280 | Ok(self.users.try_unwrap()?) 281 | } 282 | 283 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 284 | Ok(&self.country.id) 285 | } 286 | } 287 | 288 | #[test] 289 | fn loading_user() { 290 | let mut countries = StatsHash::new("countries"); 291 | let mut users = StatsHash::new("users"); 292 | 293 | let mut country = models::Country { id: 10 }; 294 | let country_id = country.id; 295 | 296 | countries.insert(country_id, country.clone()); 297 | 298 | let bob = models::User { 299 | id: 1, 300 | country_id, 301 | admin: true, 302 | }; 303 | let alice = models::User { 304 | id: 2, 305 | country_id, 306 | admin: false, 307 | }; 308 | users.insert(bob.id, bob.clone()); 309 | users.insert(alice.id, alice); 310 | 311 | let db = Db { users, countries }; 312 | let (json, counts) = run_query( 313 | r#" 314 | query Test { 315 | countries { 316 | id 317 | users(onlyAdmins: true) { 318 | id 319 | isAdmin 320 | country { 321 | id 322 | } 323 | } 324 | } 325 | } 326 | "#, 327 | db, 328 | ); 329 | 330 | assert_eq!(1, counts.user_reads); 331 | assert_eq!(2, counts.country_reads); 332 | 333 | assert_json_eq!( 334 | json!({ 335 | "countries": [ 336 | { 337 | "id": country.id, 338 | "users": [ 339 | { 340 | "id": bob.id, 341 | "isAdmin": true, 342 | "country": { "id": country.id } 343 | }, 344 | ] 345 | } 346 | ], 347 | }), 348 | json, 349 | ); 350 | } 351 | 352 | struct DbStats { 353 | user_reads: usize, 354 | country_reads: usize, 355 | } 356 | 357 | fn run_query(query: &str, db: Db) -> (Value, DbStats) { 358 | let ctx = Context { db }; 359 | 360 | let (result, errors) = juniper::execute( 361 | query, 362 | None, 363 | &Schema::new(Query, Mutation), 364 | &juniper::Variables::new(), 365 | &ctx, 366 | ) 367 | .unwrap(); 368 | 369 | if !errors.is_empty() { 370 | panic!( 371 | "GraphQL errors\n{}", 372 | serde_json::to_string_pretty(&errors).unwrap() 373 | ); 374 | } 375 | 376 | let json: Value = serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap(); 377 | 378 | println!("{}", serde_json::to_string_pretty(&json).unwrap()); 379 | 380 | ( 381 | json, 382 | DbStats { 383 | user_reads: ctx.db.users.reads_count(), 384 | country_reads: ctx.db.countries.reads_count(), 385 | }, 386 | ) 387 | } 388 | -------------------------------------------------------------------------------- /juniper-eager-loading/src/macros.rs: -------------------------------------------------------------------------------- 1 | /// This macro will implement [`LoadFrom`][] for Diesel models using the Postgres backend. 2 | /// 3 | /// It'll use an [`= ANY`] which is only supported by Postgres. 4 | /// 5 | /// [`LoadFrom`]: trait.LoadFrom.html 6 | /// [`= ANY`]: http://docs.diesel.rs/diesel/expression_methods/trait.ExpressionMethods.html#method.eq_any 7 | /// 8 | /// # Example usage 9 | /// 10 | /// ``` 11 | /// #[macro_use] 12 | /// extern crate diesel; 13 | /// 14 | /// use diesel::pg::PgConnection; 15 | /// use diesel::prelude::*; 16 | /// use juniper_eager_loading::impl_load_from_for_diesel_pg; 17 | /// # 18 | /// # fn main() {} 19 | /// 20 | /// table! { 21 | /// users (id) { 22 | /// id -> Integer, 23 | /// } 24 | /// } 25 | /// 26 | /// table! { 27 | /// companies (id) { 28 | /// id -> Integer, 29 | /// } 30 | /// } 31 | /// 32 | /// table! { 33 | /// employments (id) { 34 | /// id -> Integer, 35 | /// user_id -> Integer, 36 | /// company_id -> Integer, 37 | /// } 38 | /// } 39 | /// 40 | /// #[derive(Queryable)] 41 | /// struct User { 42 | /// id: i32, 43 | /// } 44 | /// 45 | /// #[derive(Queryable)] 46 | /// struct Company { 47 | /// id: i32, 48 | /// } 49 | /// 50 | /// #[derive(Queryable)] 51 | /// struct Employment { 52 | /// id: i32, 53 | /// user_id: i32, 54 | /// company_id: i32, 55 | /// } 56 | /// 57 | /// struct Context { 58 | /// db: PgConnection, 59 | /// } 60 | /// 61 | /// impl Context { 62 | /// // The macro assumes this method exists 63 | /// fn db(&self) -> &PgConnection { 64 | /// &self.db 65 | /// } 66 | /// } 67 | /// 68 | /// impl_load_from_for_diesel_pg! { 69 | /// ( 70 | /// error = diesel::result::Error, 71 | /// context = Context, 72 | /// ) => { 73 | /// i32 -> (users, User), 74 | /// i32 -> (companies, Company), 75 | /// i32 -> (employments, Employment), 76 | /// 77 | /// User.id -> (employments.user_id, Employment), 78 | /// Company.id -> (employments.company_id, Employment), 79 | /// 80 | /// Employment.company_id -> (companies.id, Company), 81 | /// Employment.user_id -> (users.id, User), 82 | /// } 83 | /// } 84 | /// ``` 85 | /// 86 | /// # Syntax 87 | /// 88 | /// First you specify your error and connection type with 89 | /// 90 | /// ```text 91 | /// ( 92 | /// error = diesel::result::Error, 93 | /// context = Context, 94 | /// ) => { 95 | /// // ... 96 | /// } 97 | /// ``` 98 | /// 99 | /// Then you define each model type you want to implement [`LoadFrom`] for and which columns and 100 | /// tables to use. There are two possible syntaxes for different purposes. 101 | /// 102 | /// ```text 103 | /// i32 -> (users, User), 104 | /// ``` 105 | /// 106 | /// The first syntax implements `LoadFrom for User`, meaning from a `Vec` we can load a 107 | /// `Vec`. It just takes the id type, the table, and the model struct. 108 | /// 109 | /// ```text 110 | /// User.id -> (employments.user_id, Employment), 111 | /// ``` 112 | /// 113 | /// This syntax is required when using [`HasMany`][] and [`HasManyThrough`][]. In this case it 114 | /// implements `LoadFrom for Employment`, meaning from a `Vec` we can get 115 | /// `Vec`. It does this by loading the users, mapping the list to the user ids, 116 | /// then finding the employments with those ids. 117 | /// 118 | /// [`HasMany`]: trait.HasMany.html 119 | /// [`HasManyThrough`]: trait.HasManyThrough.html 120 | /// 121 | /// # `Context::db` 122 | /// 123 | /// It is required that your context type has a method called `db` which returns a reference to a 124 | /// Diesel connection that can be passed to `.load(_)`. 125 | /// 126 | /// Example: 127 | /// 128 | /// ```rust,ignore 129 | /// struct Context { 130 | /// db: PgConnection, 131 | /// } 132 | /// 133 | /// impl Context { 134 | /// fn db(&self) -> &PgConnection { 135 | /// &self.db 136 | /// } 137 | /// } 138 | /// 139 | /// // Whatever the method returns has to work with Diesel's `load` method 140 | /// users::table 141 | /// .filter(users::id.eq(any(user_ids))) 142 | /// .load::(ctx.db()) 143 | /// ``` 144 | /// 145 | /// # What gets generated 146 | /// 147 | /// The two syntaxes generates code like this: 148 | /// 149 | /// ``` 150 | /// # #[macro_use] 151 | /// # extern crate diesel; 152 | /// # use diesel::pg::PgConnection; 153 | /// # use diesel::prelude::*; 154 | /// # use juniper_eager_loading::impl_load_from_for_diesel_pg; 155 | /// # fn main() {} 156 | /// # table! { 157 | /// # users (id) { 158 | /// # id -> Integer, 159 | /// # } 160 | /// # } 161 | /// # table! { 162 | /// # companies (id) { 163 | /// # id -> Integer, 164 | /// # } 165 | /// # } 166 | /// # table! { 167 | /// # employments (id) { 168 | /// # id -> Integer, 169 | /// # user_id -> Integer, 170 | /// # company_id -> Integer, 171 | /// # } 172 | /// # } 173 | /// # #[derive(Queryable)] 174 | /// # struct User { 175 | /// # id: i32, 176 | /// # } 177 | /// # #[derive(Queryable)] 178 | /// # struct Company { 179 | /// # id: i32, 180 | /// # } 181 | /// # #[derive(Queryable)] 182 | /// # struct Employment { 183 | /// # id: i32, 184 | /// # user_id: i32, 185 | /// # company_id: i32, 186 | /// # } 187 | /// # struct Context { db: PgConnection } 188 | /// # impl Context { 189 | /// # fn db(&self) -> &PgConnection { 190 | /// # &self.db 191 | /// # } 192 | /// # } 193 | /// 194 | /// // i32 -> (users, User), 195 | /// impl juniper_eager_loading::LoadFrom for User { 196 | /// type Error = diesel::result::Error; 197 | /// type Context = Context; 198 | /// 199 | /// fn load(ids: &[i32], field_args: &(), ctx: &Self::Context) -> Result, Self::Error> { 200 | /// use diesel::pg::expression::dsl::any; 201 | /// 202 | /// users::table 203 | /// .filter(users::id.eq(any(ids))) 204 | /// .load::(ctx.db()) 205 | /// .map_err(From::from) 206 | /// } 207 | /// } 208 | /// 209 | /// // User.id -> (employments.user_id, Employment), 210 | /// impl juniper_eager_loading::LoadFrom for Employment { 211 | /// type Error = diesel::result::Error; 212 | /// type Context = Context; 213 | /// 214 | /// fn load(froms: &[User], field_args: &(), ctx: &Self::Context) -> Result, Self::Error> { 215 | /// use diesel::pg::expression::dsl::any; 216 | /// 217 | /// let from_ids = froms.iter().map(|other| other.id).collect::>(); 218 | /// employments::table 219 | /// .filter(employments::user_id.eq(any(from_ids))) 220 | /// .load(ctx.db()) 221 | /// .map_err(From::from) 222 | /// } 223 | /// } 224 | /// ``` 225 | #[macro_export] 226 | macro_rules! impl_load_from_for_diesel_pg { 227 | ( $($token:tt)* ) => { 228 | $crate::proc_macros::impl_load_from_for_diesel_pg!($($token)*); 229 | } 230 | } 231 | 232 | /// This macro will implement [`LoadFrom`][] for Diesel models using the MySQL backend. 233 | /// 234 | /// For more details see [`impl_load_from_for_diesel_pg`][]. 235 | /// 236 | /// [`impl_load_from_for_diesel_pg`]: macro.impl_load_from_for_diesel_pg.html 237 | /// [`LoadFrom`]: trait.LoadFrom.html 238 | /// 239 | /// # Example usage 240 | /// 241 | /// ``` 242 | /// #[macro_use] 243 | /// extern crate diesel; 244 | /// 245 | /// use diesel::mysql::MysqlConnection; 246 | /// use diesel::prelude::*; 247 | /// use juniper_eager_loading::impl_load_from_for_diesel_mysql; 248 | /// # 249 | /// # fn main() {} 250 | /// 251 | /// table! { 252 | /// users (id) { 253 | /// id -> Integer, 254 | /// } 255 | /// } 256 | /// 257 | /// table! { 258 | /// companies (id) { 259 | /// id -> Integer, 260 | /// } 261 | /// } 262 | /// 263 | /// table! { 264 | /// employments (id) { 265 | /// id -> Integer, 266 | /// user_id -> Integer, 267 | /// company_id -> Integer, 268 | /// } 269 | /// } 270 | /// 271 | /// #[derive(Queryable)] 272 | /// struct User { 273 | /// id: i32, 274 | /// } 275 | /// 276 | /// #[derive(Queryable)] 277 | /// struct Company { 278 | /// id: i32, 279 | /// } 280 | /// 281 | /// #[derive(Queryable)] 282 | /// struct Employment { 283 | /// id: i32, 284 | /// user_id: i32, 285 | /// company_id: i32, 286 | /// } 287 | /// 288 | /// struct Context { 289 | /// db: MysqlConnection, 290 | /// } 291 | /// 292 | /// impl Context { 293 | /// // The macro assumes this method exists 294 | /// fn db(&self) -> &MysqlConnection { 295 | /// &self.db 296 | /// } 297 | /// } 298 | /// 299 | /// impl_load_from_for_diesel_mysql! { 300 | /// ( 301 | /// error = diesel::result::Error, 302 | /// context = Context, 303 | /// ) => { 304 | /// i32 -> (users, User), 305 | /// i32 -> (companies, Company), 306 | /// i32 -> (employments, Employment), 307 | /// 308 | /// User.id -> (employments.user_id, Employment), 309 | /// Company.id -> (employments.company_id, Employment), 310 | /// 311 | /// Employment.company_id -> (companies.id, Company), 312 | /// Employment.user_id -> (users.id, User), 313 | /// } 314 | /// } 315 | /// ``` 316 | #[macro_export] 317 | macro_rules! impl_load_from_for_diesel_mysql { 318 | ( $($token:tt)* ) => { 319 | $crate::proc_macros::impl_load_from_for_diesel_mysql!($($token)*); 320 | } 321 | } 322 | 323 | /// This macro will implement [`LoadFrom`][] for Diesel models using the SQLite backend. 324 | /// 325 | /// For more details see [`impl_load_from_for_diesel_pg`][]. 326 | /// 327 | /// [`impl_load_from_for_diesel_pg`]: macro.impl_load_from_for_diesel_pg.html 328 | /// [`LoadFrom`]: trait.LoadFrom.html 329 | /// 330 | /// # Example usage 331 | /// 332 | /// ``` 333 | /// #[macro_use] 334 | /// extern crate diesel; 335 | /// 336 | /// use diesel::sqlite::SqliteConnection; 337 | /// use diesel::prelude::*; 338 | /// use juniper_eager_loading::impl_load_from_for_diesel_sqlite; 339 | /// # 340 | /// # fn main() {} 341 | /// 342 | /// table! { 343 | /// users (id) { 344 | /// id -> Integer, 345 | /// } 346 | /// } 347 | /// 348 | /// table! { 349 | /// companies (id) { 350 | /// id -> Integer, 351 | /// } 352 | /// } 353 | /// 354 | /// table! { 355 | /// employments (id) { 356 | /// id -> Integer, 357 | /// user_id -> Integer, 358 | /// company_id -> Integer, 359 | /// } 360 | /// } 361 | /// 362 | /// #[derive(Queryable)] 363 | /// struct User { 364 | /// id: i32, 365 | /// } 366 | /// 367 | /// #[derive(Queryable)] 368 | /// struct Company { 369 | /// id: i32, 370 | /// } 371 | /// 372 | /// #[derive(Queryable)] 373 | /// struct Employment { 374 | /// id: i32, 375 | /// user_id: i32, 376 | /// company_id: i32, 377 | /// } 378 | /// 379 | /// struct Context { 380 | /// db: SqliteConnection, 381 | /// } 382 | /// 383 | /// impl Context { 384 | /// // The macro assumes this method exists 385 | /// fn db(&self) -> &SqliteConnection { 386 | /// &self.db 387 | /// } 388 | /// } 389 | /// 390 | /// impl_load_from_for_diesel_sqlite! { 391 | /// ( 392 | /// error = diesel::result::Error, 393 | /// context = Context, 394 | /// ) => { 395 | /// i32 -> (users, User), 396 | /// i32 -> (companies, Company), 397 | /// i32 -> (employments, Employment), 398 | /// 399 | /// User.id -> (employments.user_id, Employment), 400 | /// Company.id -> (employments.company_id, Employment), 401 | /// 402 | /// Employment.company_id -> (companies.id, Company), 403 | /// Employment.user_id -> (users.id, User), 404 | /// } 405 | /// } 406 | /// ``` 407 | #[macro_export] 408 | macro_rules! impl_load_from_for_diesel_sqlite { 409 | ( $($token:tt)* ) => { 410 | $crate::proc_macros::impl_load_from_for_diesel_sqlite!($($token)*); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/fields_with_arguments.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code, unused_mut)] 2 | #![allow(clippy::type_complexity)] 3 | 4 | mod helpers; 5 | 6 | use assert_json_diff::{assert_json_eq, assert_json_include}; 7 | use helpers::{SortedExtension, StatsHash}; 8 | use juniper::{Executor, FieldError, FieldResult}; 9 | use juniper_eager_loading::{ 10 | prelude::*, EagerLoading, HasMany, HasManyThrough, HasOne, LoadChildrenOutput, LoadFrom, 11 | OptionHasOne, 12 | }; 13 | use juniper_from_schema::graphql_schema; 14 | use serde_json::{json, Value}; 15 | use std::sync::atomic::{AtomicUsize, Ordering}; 16 | use std::{borrow::Borrow, collections::HashMap, hash::Hash}; 17 | 18 | graphql_schema! { 19 | schema { 20 | query: Query 21 | mutation: Mutation 22 | } 23 | 24 | type Query { 25 | countries: [Country!]! @juniper(ownership: "owned") 26 | } 27 | 28 | type Mutation { 29 | noop: Boolean! 30 | } 31 | 32 | type User { 33 | id: Int! 34 | isAdmin: Boolean! 35 | country: Country! 36 | } 37 | 38 | type Country { 39 | id: Int! 40 | users(onlyAdmins: Boolean!): [User!]! 41 | } 42 | } 43 | 44 | mod models { 45 | use super::*; 46 | use either::Either; 47 | 48 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 49 | pub struct User { 50 | pub id: i32, 51 | pub country_id: i32, 52 | pub admin: bool, 53 | } 54 | 55 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 56 | pub struct Country { 57 | pub id: i32, 58 | } 59 | 60 | impl juniper_eager_loading::LoadFrom for Country { 61 | type Error = Box; 62 | type Context = super::Context; 63 | 64 | fn load(ids: &[i32], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 65 | let mut models = ctx 66 | .db 67 | .countries 68 | .all_values() 69 | .into_iter() 70 | .filter(|value| ids.contains(&value.id)) 71 | .cloned() 72 | .collect::>(); 73 | models.sort_by_key(|model| model.id); 74 | Ok(models) 75 | } 76 | } 77 | 78 | impl juniper_eager_loading::LoadFrom> for User { 79 | type Error = Box; 80 | type Context = super::Context; 81 | 82 | fn load( 83 | ids: &[i32], 84 | field_args: &CountryUsersArgs, 85 | ctx: &Self::Context, 86 | ) -> Result, Self::Error> { 87 | let models = ctx 88 | .db 89 | .users 90 | .all_values() 91 | .into_iter() 92 | .filter(|value| ids.contains(&value.id)); 93 | 94 | let mut models = if field_args.only_admins() { 95 | Either::Left(models.filter(|user| user.admin)) 96 | } else { 97 | Either::Right(models) 98 | } 99 | .cloned() 100 | .collect::>(); 101 | 102 | models.sort_by_key(|model| model.id); 103 | 104 | Ok(models) 105 | } 106 | } 107 | 108 | impl juniper_eager_loading::LoadFrom for User { 109 | type Error = Box; 110 | type Context = super::Context; 111 | 112 | fn load(ids: &[i32], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 113 | let mut models = ctx 114 | .db 115 | .users 116 | .all_values() 117 | .into_iter() 118 | .filter(|value| ids.contains(&value.id)) 119 | .cloned() 120 | .collect::>(); 121 | models.sort_by_key(|model| model.id); 122 | Ok(models) 123 | } 124 | } 125 | 126 | impl juniper_eager_loading::LoadFrom for User { 127 | type Error = Box; 128 | type Context = super::Context; 129 | 130 | fn load( 131 | countries: &[Country], 132 | _: &(), 133 | ctx: &Self::Context, 134 | ) -> Result, Self::Error> { 135 | let country_ids = countries.iter().map(|c| c.id).collect::>(); 136 | let mut models = ctx 137 | .db 138 | .users 139 | .all_values() 140 | .into_iter() 141 | .filter(|user| country_ids.contains(&user.country_id)) 142 | .cloned() 143 | .collect::>(); 144 | models.sort_by_key(|model| model.id); 145 | Ok(models) 146 | } 147 | } 148 | 149 | impl LoadFrom> for User { 150 | type Error = Box; 151 | type Context = super::Context; 152 | 153 | fn load( 154 | countries: &[Country], 155 | args: &CountryUsersArgs, 156 | ctx: &Self::Context, 157 | ) -> Result, Self::Error> { 158 | let only_admins = args.only_admins(); 159 | 160 | let country_ids = countries.iter().map(|c| c.id).collect::>(); 161 | 162 | let models = ctx 163 | .db 164 | .users 165 | .all_values() 166 | .into_iter() 167 | .filter(|user| country_ids.contains(&user.country_id)); 168 | 169 | let models = if only_admins { 170 | Either::Left(models.filter(|user| user.admin)) 171 | } else { 172 | Either::Right(models) 173 | }; 174 | 175 | let mut models = models.cloned().collect::>(); 176 | models.sort_by_key(|model| model.id); 177 | Ok(models) 178 | } 179 | } 180 | } 181 | 182 | pub struct Db { 183 | users: StatsHash, 184 | countries: StatsHash, 185 | } 186 | 187 | pub struct Context { 188 | db: Db, 189 | } 190 | 191 | impl juniper::Context for Context {} 192 | 193 | pub struct Query; 194 | 195 | impl QueryFields for Query { 196 | fn field_countries( 197 | &self, 198 | executor: &Executor<'_, Context>, 199 | trail: &QueryTrail<'_, Country, Walked>, 200 | ) -> FieldResult> { 201 | let ctx = executor.context(); 202 | 203 | let mut country_models = ctx 204 | .db 205 | .countries 206 | .all_values() 207 | .into_iter() 208 | .cloned() 209 | .collect::>(); 210 | country_models.sort_by_key(|country| country.id); 211 | 212 | let countries = Country::eager_load_each(&country_models, ctx, trail)?; 213 | 214 | Ok(countries) 215 | } 216 | } 217 | 218 | pub struct Mutation; 219 | 220 | impl MutationFields for Mutation { 221 | fn field_noop(&self, _executor: &Executor<'_, Context>) -> FieldResult<&bool> { 222 | Ok(&true) 223 | } 224 | } 225 | 226 | // The default values are commented out 227 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 228 | #[eager_loading( 229 | model = models::User, 230 | id = i32, 231 | context = Context, 232 | error = Box, 233 | )] 234 | pub struct User { 235 | user: models::User, 236 | 237 | #[has_one(default)] 238 | country: HasOne, 239 | } 240 | 241 | impl UserFields for User { 242 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 243 | Ok(&self.user.id) 244 | } 245 | 246 | fn field_is_admin(&self, _executor: &Executor<'_, Context>) -> FieldResult<&bool> { 247 | Ok(&self.user.admin) 248 | } 249 | 250 | fn field_country( 251 | &self, 252 | _executor: &Executor<'_, Context>, 253 | _trail: &QueryTrail<'_, Country, Walked>, 254 | ) -> FieldResult<&Country> { 255 | Ok(self.country.try_unwrap()?) 256 | } 257 | } 258 | 259 | #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] 260 | #[eager_loading( 261 | model = models::Country, 262 | id = i32, 263 | context = Context, 264 | error = Box, 265 | )] 266 | pub struct Country { 267 | country: models::Country, 268 | 269 | #[has_many(skip)] 270 | users: HasMany, 271 | } 272 | 273 | #[allow(missing_docs, dead_code)] 274 | struct EagerLoadingContextCountryForUsers; 275 | 276 | impl<'a> EagerLoadChildrenOfType<'a, User, EagerLoadingContextCountryForUsers, ()> for Country { 277 | type FieldArguments = CountryUsersArgs<'a>; 278 | 279 | fn load_children( 280 | models: &[Self::Model], 281 | field_args: &Self::FieldArguments, 282 | ctx: &Self::Context, 283 | ) -> Result< 284 | LoadChildrenOutput<::Model, ()>, 285 | Self::Error, 286 | > { 287 | let children = LoadFrom::load(&models, field_args, ctx)?; 288 | Ok(LoadChildrenOutput::ChildModels(children)) 289 | } 290 | 291 | fn is_child_of( 292 | node: &Self, 293 | child: &User, 294 | _join_model: &(), 295 | _field_args: &Self::FieldArguments, 296 | _ctx: &Self::Context, 297 | ) -> bool { 298 | node.country.id == child.user.country_id 299 | } 300 | 301 | fn association(node: &mut Country) -> &mut dyn Association { 302 | &mut node.users 303 | } 304 | } 305 | 306 | impl CountryFields for Country { 307 | fn field_users( 308 | &self, 309 | executor: &Executor<'_, Context>, 310 | trail: &QueryTrail<'_, User, Walked>, 311 | _only_admins: bool, 312 | ) -> FieldResult<&Vec> { 313 | Ok(self.users.try_unwrap()?) 314 | } 315 | 316 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 317 | Ok(&self.country.id) 318 | } 319 | } 320 | 321 | #[test] 322 | fn loading_user() { 323 | let mut countries = StatsHash::new("countries"); 324 | let mut users = StatsHash::new("users"); 325 | 326 | let mut country = models::Country { id: 10 }; 327 | let country_id = country.id; 328 | 329 | countries.insert(country_id, country.clone()); 330 | 331 | let bob = models::User { 332 | id: 1, 333 | country_id, 334 | admin: true, 335 | }; 336 | let alice = models::User { 337 | id: 2, 338 | country_id, 339 | admin: false, 340 | }; 341 | users.insert(bob.id, bob.clone()); 342 | users.insert(alice.id, alice); 343 | 344 | let db = Db { users, countries }; 345 | let (json, counts) = run_query( 346 | r#" 347 | query Test { 348 | countries { 349 | id 350 | users(onlyAdmins: true) { 351 | id 352 | isAdmin 353 | country { 354 | id 355 | } 356 | } 357 | } 358 | } 359 | "#, 360 | db, 361 | ); 362 | 363 | assert_eq!(1, counts.user_reads); 364 | assert_eq!(2, counts.country_reads); 365 | 366 | assert_json_eq!( 367 | json!({ 368 | "countries": [ 369 | { 370 | "id": country.id, 371 | "users": [ 372 | { 373 | "id": bob.id, 374 | "isAdmin": true, 375 | "country": { "id": country.id } 376 | }, 377 | ] 378 | } 379 | ], 380 | }), 381 | json, 382 | ); 383 | } 384 | 385 | struct DbStats { 386 | user_reads: usize, 387 | country_reads: usize, 388 | } 389 | 390 | fn run_query(query: &str, db: Db) -> (Value, DbStats) { 391 | let ctx = Context { db }; 392 | 393 | let (result, errors) = juniper::execute( 394 | query, 395 | None, 396 | &Schema::new(Query, Mutation), 397 | &juniper::Variables::new(), 398 | &ctx, 399 | ) 400 | .unwrap(); 401 | 402 | if !errors.is_empty() { 403 | panic!( 404 | "GraphQL errors\n{}", 405 | serde_json::to_string_pretty(&errors).unwrap() 406 | ); 407 | } 408 | 409 | let json: Value = serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap(); 410 | 411 | println!("{}", serde_json::to_string_pretty(&json).unwrap()); 412 | 413 | ( 414 | json, 415 | DbStats { 416 | user_reads: ctx.db.users.reads_count(), 417 | country_reads: ctx.db.countries.reads_count(), 418 | }, 419 | ) 420 | } 421 | -------------------------------------------------------------------------------- /juniper-eager-loading-code-gen/src/derive_eager_loading.rs: -------------------------------------------------------------------------------- 1 | mod field_args; 2 | 3 | use field_args::{ 4 | EagerLoading, FieldArgs, HasMany, HasManyThrough, HasOne, OptionHasOne, RootModelField, Spanned, 5 | }; 6 | use heck::{CamelCase, SnakeCase}; 7 | use proc_macro2::{Span, TokenStream}; 8 | use proc_macro_error::*; 9 | use quote::{format_ident, quote}; 10 | use syn::spanned::Spanned as _; 11 | use syn::{parse_macro_input, Fields, GenericArgument, Ident, ItemStruct, PathArguments, Type}; 12 | 13 | pub fn gen_tokens(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream { 14 | let item_struct = parse_macro_input!(tokens as ItemStruct); 15 | 16 | let ItemStruct { 17 | ident: struct_name, 18 | attrs, 19 | fields, 20 | .. 21 | } = item_struct; 22 | 23 | let args = match EagerLoading::from_attributes(&attrs) { 24 | Ok(args) => args, 25 | Err(err) => return err.to_compile_error().into(), 26 | }; 27 | 28 | let out = DeriveData { 29 | struct_name, 30 | args, 31 | fields, 32 | out: TokenStream::new(), 33 | }; 34 | 35 | out.build_derive_output().into() 36 | } 37 | 38 | struct DeriveData { 39 | struct_name: Ident, 40 | fields: Fields, 41 | args: EagerLoading, 42 | out: TokenStream, 43 | } 44 | 45 | impl DeriveData { 46 | fn build_derive_output(mut self) -> TokenStream { 47 | self.gen_eager_loading(); 48 | self.gen_eager_load_children_of_type(); 49 | 50 | if self.args.print() { 51 | eprintln!("{}", self.out); 52 | } 53 | 54 | self.out 55 | } 56 | 57 | fn gen_eager_load_children_of_type(&mut self) { 58 | let impls = self 59 | .struct_fields() 60 | .filter_map(|field| self.gen_eager_load_children_of_type_for_field(field)); 61 | 62 | let code = quote! { #(#impls)* }; 63 | self.out.extend(code); 64 | } 65 | 66 | fn gen_eager_load_children_of_type_for_field(&self, field: &syn::Field) -> Option { 67 | let data = self.parse_field_args(field)?; 68 | 69 | let inner_type = &data.inner_type; 70 | let struct_name = self.struct_name(); 71 | let join_model_impl = self.join_model_impl(&data); 72 | let load_children_impl = self.load_children_impl(&data); 73 | let association_impl = self.association_impl(&data); 74 | let is_child_of_impl = self.is_child_of_impl(&data); 75 | let context = self.field_impl_context_name(&field); 76 | let field_arguments = data.args.field_arguments(); 77 | 78 | let full_output = quote! { 79 | #[allow(missing_docs, dead_code)] 80 | struct #context; 81 | 82 | impl<'a> juniper_eager_loading::EagerLoadChildrenOfType< 83 | 'a, 84 | #inner_type, 85 | #context, 86 | #join_model_impl, 87 | > for #struct_name { 88 | type FieldArguments = #field_arguments; 89 | 90 | #load_children_impl 91 | #is_child_of_impl 92 | #association_impl 93 | } 94 | }; 95 | 96 | if data.args.print() { 97 | eprintln!("{}", full_output); 98 | } 99 | 100 | if data.args.skip() { 101 | return Some(quote! {}); 102 | } 103 | 104 | Some(full_output) 105 | } 106 | 107 | fn parse_field_args(&self, field: &syn::Field) -> Option { 108 | let inner_type = get_type_from_association(&field.ty)?.clone(); 109 | let association_type = association_type(&field.ty)?; 110 | let span = field.span(); 111 | 112 | let args = match association_type { 113 | AssociationType::HasOne => { 114 | let args = HasOne::from_attributes(&field.attrs) 115 | .unwrap_or_else(|e| abort!(e.span(), "{}", e)); 116 | FieldArgs::HasOne(Spanned::new(span, args)) 117 | } 118 | AssociationType::OptionHasOne => { 119 | let args = OptionHasOne::from_attributes(&field.attrs) 120 | .unwrap_or_else(|e| abort!(e.span(), "{}", e)); 121 | FieldArgs::OptionHasOne(Spanned::new(span, args)) 122 | } 123 | AssociationType::HasMany => { 124 | let args = HasMany::from_attributes(&field.attrs) 125 | .unwrap_or_else(|e| abort!(e.span(), "{}", e)); 126 | FieldArgs::HasMany(Spanned::new(span, args)) 127 | } 128 | AssociationType::HasManyThrough => { 129 | let args = HasManyThrough::from_attributes(&field.attrs) 130 | .unwrap_or_else(|e| abort!(e.span(), "{}", e)); 131 | FieldArgs::HasManyThrough(Spanned::new(span, Box::new(args))) 132 | } 133 | }; 134 | 135 | let field_name = field 136 | .ident 137 | .as_ref() 138 | .cloned() 139 | .unwrap_or_else(|| abort!(span, "Found association field without a name")); 140 | 141 | let foreign_key_field_default = match args { 142 | FieldArgs::HasOne(_) | FieldArgs::OptionHasOne(_) => &field_name, 143 | FieldArgs::HasMany(_) | FieldArgs::HasManyThrough(_) => self.struct_name(), 144 | } 145 | .clone(); 146 | 147 | let data = FieldDeriveData { 148 | field_name, 149 | inner_type, 150 | foreign_key_field_default, 151 | args, 152 | }; 153 | 154 | Some(data) 155 | } 156 | 157 | fn join_model_impl(&self, data: &FieldDeriveData) -> TokenStream { 158 | match &data.args { 159 | FieldArgs::HasMany(_) | FieldArgs::HasOne(_) | FieldArgs::OptionHasOne(_) => { 160 | quote! { () } 161 | } 162 | FieldArgs::HasManyThrough(has_many_through) => { 163 | let join_model = has_many_through.join_model(has_many_through.span()); 164 | quote! { #join_model } 165 | } 166 | } 167 | } 168 | 169 | fn load_children_impl(&self, data: &FieldDeriveData) -> TokenStream { 170 | let join_model: syn::Type; 171 | let foreign_key_field = &data.args.foreign_key_field(&data.foreign_key_field_default); 172 | let inner_type = &data.inner_type; 173 | 174 | let load_children_impl = match &data.args { 175 | FieldArgs::HasOne(_) => { 176 | join_model = syn::parse_str::("()").unwrap(); 177 | 178 | quote! { 179 | let ids = models 180 | .iter() 181 | .map(|model| model.#foreign_key_field.clone()) 182 | .collect::>(); 183 | let ids = juniper_eager_loading::unique(ids); 184 | 185 | let child_models: Vec<<#inner_type as juniper_eager_loading::EagerLoading>::Model> = 186 | juniper_eager_loading::LoadFrom::load(&ids, field_args, ctx)?; 187 | 188 | Ok(juniper_eager_loading::LoadChildrenOutput::ChildModels(child_models)) 189 | } 190 | } 191 | FieldArgs::OptionHasOne(_) => { 192 | join_model = syn::parse_str::("()").unwrap(); 193 | 194 | quote! { 195 | let ids = models 196 | .iter() 197 | .filter_map(|model| model.#foreign_key_field) 198 | .map(|id| id.clone()) 199 | .collect::>(); 200 | let ids = juniper_eager_loading::unique(ids); 201 | 202 | let child_models: Vec<<#inner_type as juniper_eager_loading::EagerLoading>::Model> = 203 | juniper_eager_loading::LoadFrom::load(&ids, field_args, ctx)?; 204 | 205 | Ok(juniper_eager_loading::LoadChildrenOutput::ChildModels(child_models)) 206 | } 207 | } 208 | FieldArgs::HasMany(has_many) => { 209 | join_model = syn::parse_str::("()").unwrap(); 210 | 211 | let filter = if let Some(predicate_method) = has_many.predicate_method() { 212 | quote! { 213 | let child_models = child_models 214 | .into_iter() 215 | .filter(|child_model| child_model.#predicate_method(ctx)) 216 | .collect::>(); 217 | } 218 | } else { 219 | quote! {} 220 | }; 221 | 222 | quote! { 223 | let child_models: Vec<<#inner_type as juniper_eager_loading::EagerLoading>::Model> = 224 | juniper_eager_loading::LoadFrom::load(&models, field_args, ctx)?; 225 | 226 | #filter 227 | 228 | Ok(juniper_eager_loading::LoadChildrenOutput::ChildModels(child_models)) 229 | } 230 | } 231 | FieldArgs::HasManyThrough(has_many_through) => { 232 | join_model = has_many_through.join_model(has_many_through.span()); 233 | let child_primary_key_field = has_many_through.child_primary_key_field(); 234 | 235 | let child_primary_key_field_on_join_model = 236 | has_many_through.child_primary_key_field_on_join_model(&data.inner_type); 237 | 238 | let filter = if let Some(predicate_method) = has_many_through.predicate_method() { 239 | quote! { 240 | let join_models = join_models 241 | .into_iter() 242 | .filter(|child_model| child_model.#predicate_method(ctx)) 243 | .collect::>(); 244 | } 245 | } else { 246 | quote! {} 247 | }; 248 | 249 | quote! { 250 | let join_models: Vec<#join_model> = 251 | juniper_eager_loading::LoadFrom::load(&models, field_args, ctx)?; 252 | 253 | #filter 254 | 255 | let child_models: Vec<<#inner_type as juniper_eager_loading::EagerLoading>::Model> = 256 | juniper_eager_loading::LoadFrom::load(&join_models, field_args, ctx)?; 257 | 258 | let mut child_and_join_model_pairs = Vec::new(); 259 | for join_model in join_models { 260 | for child_model in &child_models { 261 | if join_model.#child_primary_key_field_on_join_model == child_model.#child_primary_key_field { 262 | let pair = ( 263 | std::clone::Clone::clone(child_model), 264 | std::clone::Clone::clone(&join_model), 265 | ); 266 | child_and_join_model_pairs.push(pair); 267 | } 268 | } 269 | } 270 | 271 | Ok(juniper_eager_loading::LoadChildrenOutput::ChildAndJoinModels( 272 | child_and_join_model_pairs 273 | )) 274 | } 275 | } 276 | }; 277 | 278 | quote! { 279 | #[allow(unused_variables)] 280 | fn load_children( 281 | models: &[Self::Model], 282 | field_args: &Self::FieldArguments, 283 | ctx: &Self::Context, 284 | ) -> Result< 285 | juniper_eager_loading::LoadChildrenOutput< 286 | <#inner_type as juniper_eager_loading::EagerLoading>::Model, 287 | #join_model 288 | >, 289 | Self::Error, 290 | > { 291 | #load_children_impl 292 | } 293 | } 294 | } 295 | 296 | fn is_child_of_impl(&self, data: &FieldDeriveData) -> TokenStream { 297 | let root_model_field = self.root_model_field(); 298 | let foreign_key_field = &data.args.foreign_key_field(&data.foreign_key_field_default); 299 | let inner_type = &data.inner_type; 300 | let mut join_model = syn::parse_str::("()").unwrap(); 301 | let field_name = &data.field_name; 302 | 303 | let is_child_of_impl = match &data.args { 304 | FieldArgs::HasOne(has_one) => { 305 | let child_primary_key_field = has_one.child_primary_key_field(); 306 | let field_root_model_field = has_one.root_model_field(field_name); 307 | 308 | quote! { 309 | node.#root_model_field.#foreign_key_field == child.#field_root_model_field.#child_primary_key_field 310 | } 311 | } 312 | FieldArgs::OptionHasOne(option_has_one) => { 313 | let field_root_model_field = option_has_one.root_model_field(field_name); 314 | let child_primary_key_field = option_has_one.child_primary_key_field(); 315 | 316 | quote! { 317 | node.#root_model_field.#foreign_key_field == Some(child.#field_root_model_field.#child_primary_key_field) 318 | } 319 | } 320 | FieldArgs::HasMany(has_many) => { 321 | let field_root_model_field = has_many.root_model_field(field_name); 322 | let node_primary_key_field = self.primary_key_field(); 323 | 324 | if has_many.foreign_key_optional.is_some() { 325 | quote! { 326 | Some(node.#root_model_field.#node_primary_key_field) == 327 | child.#field_root_model_field.#foreign_key_field 328 | } 329 | } else { 330 | quote! { 331 | node.#root_model_field.#node_primary_key_field == 332 | child.#field_root_model_field.#foreign_key_field 333 | } 334 | } 335 | } 336 | FieldArgs::HasManyThrough(has_many_through) => { 337 | join_model = has_many_through.join_model(has_many_through.span()); 338 | let model_field = has_many_through.model_field(&data.inner_type); 339 | let child_primary_key_field_on_join_model = 340 | has_many_through.child_primary_key_field_on_join_model(&data.inner_type); 341 | let child_primary_key_field = has_many_through.child_primary_key_field(); 342 | let node_primary_key_field = self.primary_key_field(); 343 | 344 | quote! { 345 | node.#root_model_field.#node_primary_key_field == join_model.#foreign_key_field && 346 | join_model.#child_primary_key_field_on_join_model == child.#model_field.#child_primary_key_field 347 | } 348 | } 349 | }; 350 | 351 | quote! { 352 | fn is_child_of( 353 | node: &Self, 354 | child: &#inner_type, 355 | join_model: &#join_model, 356 | _field_args: &Self::FieldArguments, 357 | context: &Self::Context, 358 | ) -> bool { 359 | #is_child_of_impl 360 | } 361 | } 362 | } 363 | 364 | fn association_impl(&self, data: &FieldDeriveData) -> TokenStream { 365 | let field_name = &data.field_name; 366 | let inner_type = &data.inner_type; 367 | 368 | quote! { 369 | fn association(node: &mut Self) -> 370 | &mut dyn juniper_eager_loading::Association<#inner_type> 371 | { 372 | &mut node.#field_name 373 | } 374 | } 375 | } 376 | 377 | fn gen_eager_loading(&mut self) { 378 | let struct_name = self.struct_name(); 379 | let model = self.model(); 380 | let id = self.id(); 381 | let context = self.context(); 382 | let error = self.error(); 383 | 384 | let field_setters = self.struct_fields().map(|field| { 385 | let ident = &field.ident; 386 | 387 | if is_association_field(&field.ty) { 388 | quote! { #ident: std::default::Default::default() } 389 | } else { 390 | quote! { #ident: std::clone::Clone::clone(model) } 391 | } 392 | }); 393 | 394 | let eager_load_children_calls = self 395 | .struct_fields() 396 | .filter_map(|field| self.gen_eager_load_for_field(field)); 397 | 398 | let code = quote! { 399 | impl juniper_eager_loading::EagerLoading for #struct_name { 400 | type Model = #model; 401 | type Id = #id; 402 | type Context = #context; 403 | type Error = #error; 404 | 405 | fn new_from_model(model: &Self::Model) -> Self { 406 | Self { 407 | #(#field_setters),* 408 | } 409 | } 410 | 411 | fn eager_load_each( 412 | models: &[Self::Model], 413 | ctx: &Self::Context, 414 | trail: &juniper_from_schema::QueryTrail<'_, Self, juniper_from_schema::Walked>, 415 | ) -> Result, Self::Error> { 416 | let mut nodes = Self::from_db_models(models); 417 | 418 | #(#eager_load_children_calls)* 419 | 420 | Ok(nodes) 421 | } 422 | } 423 | }; 424 | self.out.extend(code); 425 | } 426 | 427 | fn gen_eager_load_for_field(&self, field: &syn::Field) -> Option { 428 | let inner_type = get_type_from_association(&field.ty)?; 429 | 430 | let data = self.parse_field_args(field)?; 431 | let args = data.args; 432 | 433 | let field_name = args 434 | .graphql_field() 435 | .clone() 436 | .map(|ident| { 437 | let ident = ident.to_string().to_snake_case(); 438 | Ident::new(&ident, Span::call_site()) 439 | }) 440 | .unwrap_or_else(|| { 441 | field.ident.clone().unwrap_or_else(|| { 442 | abort!(field.span(), "Found association field without a name") 443 | }) 444 | }); 445 | let field_args_name = format_ident!("{}_args", field_name); 446 | 447 | let impl_context = self.field_impl_context_name(&field); 448 | 449 | Some(quote! { 450 | if let Some(child_trail) = trail.#field_name().walk() { 451 | let field_args = trail.#field_args_name(); 452 | 453 | EagerLoadChildrenOfType::<#inner_type, #impl_context, _>::eager_load_children( 454 | &mut nodes, 455 | models, 456 | &ctx, 457 | &child_trail, 458 | &field_args, 459 | )?; 460 | } 461 | }) 462 | } 463 | 464 | fn struct_name(&self) -> &syn::Ident { 465 | &self.struct_name 466 | } 467 | 468 | fn model(&self) -> TokenStream { 469 | self.args.model(&self.struct_name()) 470 | } 471 | 472 | fn id(&self) -> TokenStream { 473 | self.args.id() 474 | } 475 | 476 | fn context(&self) -> TokenStream { 477 | self.args.context() 478 | } 479 | 480 | fn error(&self) -> TokenStream { 481 | self.args.error() 482 | } 483 | 484 | fn root_model_field(&self) -> TokenStream { 485 | self.args.root_model_field(&self.struct_name()) 486 | } 487 | 488 | fn primary_key_field(&self) -> Ident { 489 | self.args.primary_key_field() 490 | } 491 | 492 | fn struct_fields(&self) -> syn::punctuated::Iter { 493 | self.fields.iter() 494 | } 495 | 496 | fn field_impl_context_name(&self, field: &syn::Field) -> Ident { 497 | let camel_name = field 498 | .ident 499 | .as_ref() 500 | .expect("field without name") 501 | .to_string() 502 | .to_camel_case(); 503 | let full_name = format!("EagerLoadingContext{}For{}", self.struct_name(), camel_name); 504 | Ident::new(&full_name, Span::call_site()) 505 | } 506 | } 507 | 508 | macro_rules! if_let_or_none { 509 | ( $path:path , $($tokens:tt)* ) => { 510 | if let $path(inner) = $($tokens)* { 511 | inner 512 | } else { 513 | return None 514 | } 515 | }; 516 | } 517 | 518 | fn get_type_from_association(ty: &syn::Type) -> Option<&syn::Type> { 519 | if !is_association_field(ty) { 520 | return None; 521 | } 522 | 523 | let type_path = if_let_or_none!(Type::Path, ty); 524 | let path = &type_path.path; 525 | let segments = &path.segments; 526 | let segment = segments.last()?; 527 | let args = if_let_or_none!(PathArguments::AngleBracketed, &segment.arguments); 528 | let generic_argument: &syn::GenericArgument = args.args.last()?; 529 | let ty = if_let_or_none!(GenericArgument::Type, generic_argument); 530 | Some(remove_possible_box_wrapper(ty)) 531 | } 532 | 533 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 534 | enum AssociationType { 535 | HasOne, 536 | OptionHasOne, 537 | HasMany, 538 | HasManyThrough, 539 | } 540 | 541 | fn association_type(ty: &syn::Type) -> Option { 542 | if *last_ident_in_type_segment(ty)? == "OptionHasOne" { 543 | return Some(AssociationType::OptionHasOne); 544 | } 545 | 546 | if *last_ident_in_type_segment(ty)? == "HasManyThrough" { 547 | return Some(AssociationType::HasManyThrough); 548 | } 549 | 550 | if *last_ident_in_type_segment(ty)? == "HasMany" { 551 | return Some(AssociationType::HasMany); 552 | } 553 | 554 | if *last_ident_in_type_segment(ty)? == "HasOne" { 555 | return Some(AssociationType::HasOne); 556 | } 557 | 558 | None 559 | } 560 | 561 | fn is_association_field(ty: &syn::Type) -> bool { 562 | association_type(ty).is_some() 563 | } 564 | 565 | fn last_ident_in_type_segment(ty: &syn::Type) -> Option<&syn::Ident> { 566 | let type_path = if_let_or_none!(Type::Path, ty); 567 | let path = &type_path.path; 568 | let segments = &path.segments; 569 | let segment = segments.last()?; 570 | Some(&segment.ident) 571 | } 572 | 573 | #[derive(Debug)] 574 | struct FieldDeriveData { 575 | field_name: Ident, 576 | inner_type: syn::Type, 577 | args: FieldArgs, 578 | foreign_key_field_default: Ident, 579 | } 580 | 581 | fn remove_possible_box_wrapper(ty: &Type) -> &syn::Type { 582 | if let Type::Path(type_path) = ty { 583 | let last_segment = if let Some(x) = type_path.path.segments.last() { 584 | x 585 | } else { 586 | return ty; 587 | }; 588 | 589 | if last_segment.ident == "Box" { 590 | let args = if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments { 591 | args 592 | } else { 593 | return ty; 594 | }; 595 | 596 | let generic_argument = if let Some(x) = args.args.last() { 597 | x 598 | } else { 599 | return ty; 600 | }; 601 | 602 | if let syn::GenericArgument::Type(inner_ty) = generic_argument { 603 | inner_ty 604 | } else { 605 | ty 606 | } 607 | } else { 608 | ty 609 | } 610 | } else { 611 | ty 612 | } 613 | } 614 | -------------------------------------------------------------------------------- /juniper-eager-loading/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables, unused_imports, dead_code, unused_mut)] 2 | 3 | mod helpers; 4 | 5 | use assert_json_diff::{assert_json_eq, assert_json_include}; 6 | use helpers::{SortedExtension, StatsHash}; 7 | use juniper::{Executor, FieldError, FieldResult}; 8 | use juniper_eager_loading::{ 9 | prelude::*, EagerLoading, HasMany, HasManyThrough, HasOne, OptionHasOne, 10 | }; 11 | use juniper_from_schema::graphql_schema; 12 | use models::{CityId, CompanyId, CountryId, EmploymentId, IssueId, UserId}; 13 | use serde_json::{json, Value}; 14 | use std::sync::atomic::{AtomicUsize, Ordering}; 15 | use std::{borrow::Borrow, collections::HashMap, hash::Hash}; 16 | 17 | graphql_schema! { 18 | schema { 19 | query: Query 20 | mutation: Mutation 21 | } 22 | 23 | type Query { 24 | user(id: Int!): User! @juniper(ownership: "owned") 25 | users: [User!]! @juniper(ownership: "owned") 26 | } 27 | 28 | type Mutation { 29 | noop: Boolean! 30 | } 31 | 32 | type User { 33 | id: Int! 34 | country: Country! 35 | city: City 36 | employments: [Employment!]! @juniper(ownership: "owned") 37 | companies: [Company!]! @juniper(ownership: "owned") 38 | issues: [Issue!]! @juniper(ownership: "owned") 39 | primaryEmployment: Employment @juniper(ownership: "owned") 40 | primaryCompany: Company @juniper(ownership: "owned") 41 | } 42 | 43 | type Country { 44 | id: Int! 45 | cities: [City!]! 46 | } 47 | 48 | type City { 49 | id: Int! 50 | country: Country! 51 | } 52 | 53 | type Company { 54 | id: Int! 55 | name: String! 56 | } 57 | 58 | type Employment { 59 | id: Int! 60 | user: User! 61 | company: Company! 62 | } 63 | 64 | type Issue { 65 | id: Int! 66 | title: String! 67 | reviewer: User 68 | } 69 | } 70 | 71 | mod models { 72 | macro_rules! make_model_ids { 73 | ( $($name:ident),* ) => { 74 | $( 75 | #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash)] 76 | pub struct $name(i32); 77 | 78 | impl From for $name { 79 | fn from(id: i32) -> $name { 80 | $name(id) 81 | } 82 | } 83 | 84 | impl std::ops::Deref for $name { 85 | type Target = i32; 86 | 87 | fn deref(&self) -> &i32 { 88 | &self.0 89 | } 90 | } 91 | )* 92 | } 93 | } 94 | 95 | make_model_ids!(UserId, CountryId, CityId, CompanyId, EmploymentId, IssueId); 96 | 97 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 98 | pub struct User { 99 | pub id: UserId, 100 | pub country_id: CountryId, 101 | pub city_id: Option, 102 | } 103 | 104 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 105 | pub struct Country { 106 | pub id: CountryId, 107 | } 108 | 109 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 110 | pub struct City { 111 | pub id: CityId, 112 | pub country_id: CountryId, 113 | } 114 | 115 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 116 | pub struct Company { 117 | pub id: CompanyId, 118 | pub name: String, 119 | } 120 | 121 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 122 | pub struct Employment { 123 | pub id: EmploymentId, 124 | pub user_id: UserId, 125 | pub company_id: CompanyId, 126 | pub primary: bool, 127 | } 128 | 129 | impl Employment { 130 | pub fn primary(&self, _: &super::Context) -> bool { 131 | self.primary 132 | } 133 | } 134 | 135 | #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] 136 | pub struct Issue { 137 | pub id: IssueId, 138 | pub title: String, 139 | pub reviewer_id: Option, 140 | } 141 | 142 | impl juniper_eager_loading::LoadFrom for Country { 143 | type Error = Box; 144 | type Context = super::Context; 145 | 146 | fn load(ids: &[CountryId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 147 | let countries = ctx 148 | .db 149 | .countries 150 | .all_values() 151 | .into_iter() 152 | .filter(|value| ids.contains(&value.id)) 153 | .cloned() 154 | .collect::>(); 155 | Ok(countries) 156 | } 157 | } 158 | 159 | impl juniper_eager_loading::LoadFrom for City { 160 | type Error = Box; 161 | type Context = super::Context; 162 | 163 | fn load(ids: &[CityId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 164 | let countries = ctx 165 | .db 166 | .cities 167 | .all_values() 168 | .into_iter() 169 | .filter(|value| ids.contains(&value.id)) 170 | .cloned() 171 | .collect::>(); 172 | Ok(countries) 173 | } 174 | } 175 | 176 | impl juniper_eager_loading::LoadFrom for User { 177 | type Error = Box; 178 | type Context = super::Context; 179 | 180 | fn load(ids: &[UserId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 181 | let models = ctx 182 | .db 183 | .users 184 | .all_values() 185 | .into_iter() 186 | .filter(|value| ids.contains(&value.id)) 187 | .cloned() 188 | .collect::>(); 189 | Ok(models) 190 | } 191 | } 192 | 193 | impl juniper_eager_loading::LoadFrom for Company { 194 | type Error = Box; 195 | type Context = super::Context; 196 | 197 | fn load(ids: &[CompanyId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 198 | let models = ctx 199 | .db 200 | .companies 201 | .all_values() 202 | .into_iter() 203 | .filter(|value| ids.contains(&value.id)) 204 | .cloned() 205 | .collect::>(); 206 | Ok(models) 207 | } 208 | } 209 | 210 | impl juniper_eager_loading::LoadFrom for Employment { 211 | type Error = Box; 212 | type Context = super::Context; 213 | 214 | fn load( 215 | ids: &[EmploymentId], 216 | _: &(), 217 | ctx: &Self::Context, 218 | ) -> Result, Self::Error> { 219 | let models = ctx 220 | .db 221 | .employments 222 | .all_values() 223 | .into_iter() 224 | .filter(|value| ids.contains(&value.id)) 225 | .cloned() 226 | .collect::>(); 227 | Ok(models) 228 | } 229 | } 230 | 231 | impl juniper_eager_loading::LoadFrom for Issue { 232 | type Error = Box; 233 | type Context = super::Context; 234 | 235 | fn load(ids: &[IssueId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 236 | let models = ctx 237 | .db 238 | .issues 239 | .all_values() 240 | .into_iter() 241 | .filter(|value| ids.contains(&value.id)) 242 | .cloned() 243 | .collect::>(); 244 | Ok(models) 245 | } 246 | } 247 | 248 | impl juniper_eager_loading::LoadFrom for City { 249 | type Error = Box; 250 | type Context = super::Context; 251 | 252 | fn load( 253 | countries: &[Country], 254 | _: &(), 255 | ctx: &Self::Context, 256 | ) -> Result, Self::Error> { 257 | let country_ids = countries 258 | .iter() 259 | .map(|country| country.id) 260 | .collect::>(); 261 | let mut cities = ctx 262 | .db 263 | .cities 264 | .all_values() 265 | .into_iter() 266 | .filter(|city| country_ids.contains(&city.country_id)) 267 | .cloned() 268 | .collect::>(); 269 | Ok(cities) 270 | } 271 | } 272 | 273 | impl juniper_eager_loading::LoadFrom for Employment { 274 | type Error = Box; 275 | type Context = super::Context; 276 | 277 | fn load(users: &[User], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 278 | let user_ids = users.iter().map(|user| user.id).collect::>(); 279 | let employments = ctx 280 | .db 281 | .employments 282 | .all_values() 283 | .into_iter() 284 | .filter(|employment| user_ids.contains(&employment.user_id)) 285 | .cloned() 286 | .collect::>(); 287 | Ok(employments) 288 | } 289 | } 290 | 291 | impl juniper_eager_loading::LoadFrom for Company { 292 | type Error = Box; 293 | type Context = super::Context; 294 | 295 | fn load( 296 | employments: &[Employment], 297 | _: &(), 298 | ctx: &Self::Context, 299 | ) -> Result, Self::Error> { 300 | let company_ids = employments 301 | .iter() 302 | .map(|employment| employment.company_id) 303 | .collect::>(); 304 | 305 | let employments = ctx 306 | .db 307 | .companies 308 | .all_values() 309 | .into_iter() 310 | .filter(|company| company_ids.contains(&company.id)) 311 | .cloned() 312 | .collect::>(); 313 | 314 | Ok(employments) 315 | } 316 | } 317 | 318 | impl juniper_eager_loading::LoadFrom for Issue { 319 | type Error = Box; 320 | type Context = super::Context; 321 | 322 | fn load(users: &[User], _: &(), ctx: &Self::Context) -> Result, Self::Error> { 323 | let user_ids = users.iter().map(|user| Some(user.id)).collect::>(); 324 | let issues = ctx 325 | .db 326 | .issues 327 | .all_values() 328 | .into_iter() 329 | .filter(|issue| user_ids.contains(&issue.reviewer_id)) 330 | .cloned() 331 | .collect::>(); 332 | Ok(issues) 333 | } 334 | } 335 | } 336 | 337 | pub struct Db { 338 | users: StatsHash, 339 | countries: StatsHash, 340 | cities: StatsHash, 341 | companies: StatsHash, 342 | employments: StatsHash, 343 | issues: StatsHash, 344 | } 345 | 346 | pub struct Context { 347 | db: Db, 348 | } 349 | 350 | impl juniper::Context for Context {} 351 | 352 | pub struct Query; 353 | 354 | impl QueryFields for Query { 355 | fn field_user<'a>( 356 | &self, 357 | executor: &Executor<'a, Context>, 358 | trail: &QueryTrail<'a, User, Walked>, 359 | id: i32, 360 | ) -> FieldResult { 361 | let ctx = executor.context(); 362 | 363 | let user_model = ctx 364 | .db 365 | .users 366 | .get(&UserId::from(id)) 367 | .ok_or("User not found")? 368 | .clone(); 369 | let user = User::eager_load(user_model, ctx, trail)?; 370 | Ok(user) 371 | } 372 | 373 | fn field_users<'a>( 374 | &self, 375 | executor: &Executor<'a, Context>, 376 | trail: &QueryTrail<'a, User, Walked>, 377 | ) -> FieldResult> { 378 | let ctx = executor.context(); 379 | 380 | let mut user_models = ctx 381 | .db 382 | .users 383 | .all_values() 384 | .into_iter() 385 | .cloned() 386 | .collect::>(); 387 | user_models.sort_by_key(|user| user.id); 388 | 389 | let users = User::eager_load_each(&user_models, ctx, trail)?; 390 | 391 | Ok(users) 392 | } 393 | } 394 | 395 | pub struct Mutation; 396 | 397 | impl MutationFields for Mutation { 398 | fn field_noop(&self, _executor: &Executor<'_, Context>) -> FieldResult<&bool> { 399 | Ok(&true) 400 | } 401 | } 402 | 403 | // The default values are commented out 404 | #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] 405 | #[eager_loading( 406 | error = Box, 407 | context = Context, 408 | // model = "models::User", 409 | // id = "i32", 410 | // root_model_field = "user" 411 | )] 412 | pub struct User { 413 | user: models::User, 414 | 415 | // #[has_one( 416 | // foreign_key_field = country_id, 417 | // root_model_field = country 418 | // )] 419 | #[has_one(default)] 420 | country: HasOne, 421 | 422 | // #[has_one( 423 | // foreign_key_field = city_id, 424 | // root_model_field = city 425 | // )] 426 | #[option_has_one(default)] 427 | city: OptionHasOne, 428 | 429 | #[has_many(root_model_field = employment)] 430 | employments: HasMany, 431 | 432 | #[has_many_through( 433 | // model_field = company, 434 | join_model = models::Employment, 435 | )] 436 | companies: HasManyThrough, 437 | 438 | #[has_many( 439 | root_model_field = issue, 440 | foreign_key_field = reviewer_id, 441 | foreign_key_optional 442 | )] 443 | issues: HasMany, 444 | 445 | #[has_many( 446 | root_model_field = employment, 447 | graphql_field = primaryEmployment, 448 | predicate_method = primary 449 | )] 450 | primary_employments: HasMany, 451 | 452 | #[has_many_through( 453 | join_model = models::Employment, 454 | graphql_field = primaryCompany, 455 | predicate_method = primary 456 | )] 457 | primary_companies: HasManyThrough, 458 | } 459 | 460 | impl UserFields for User { 461 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 462 | Ok(&self.user.id) 463 | } 464 | 465 | fn field_country( 466 | &self, 467 | _executor: &Executor<'_, Context>, 468 | _trail: &QueryTrail<'_, Country, Walked>, 469 | ) -> FieldResult<&Country> { 470 | Ok(self.country.try_unwrap()?) 471 | } 472 | 473 | fn field_city( 474 | &self, 475 | _executor: &Executor<'_, Context>, 476 | _trail: &QueryTrail<'_, City, Walked>, 477 | ) -> FieldResult<&Option> { 478 | Ok(self.city.try_unwrap()?) 479 | } 480 | 481 | fn field_employments( 482 | &self, 483 | _executor: &Executor<'_, Context>, 484 | _trail: &QueryTrail<'_, Employment, Walked>, 485 | ) -> FieldResult> { 486 | Ok(self.employments.try_unwrap()?.clone().sorted()) 487 | } 488 | 489 | fn field_companies( 490 | &self, 491 | _executor: &Executor<'_, Context>, 492 | _trail: &QueryTrail<'_, Company, Walked>, 493 | ) -> FieldResult> { 494 | Ok(self.companies.try_unwrap()?.clone().sorted()) 495 | } 496 | 497 | fn field_issues( 498 | &self, 499 | _executor: &Executor<'_, Context>, 500 | _trail: &QueryTrail<'_, Issue, Walked>, 501 | ) -> FieldResult> { 502 | Ok(self.issues.try_unwrap()?.clone().sorted()) 503 | } 504 | 505 | fn field_primary_employment( 506 | &self, 507 | executor: &Executor<'_, Context>, 508 | _trail: &QueryTrail<'_, Employment, Walked>, 509 | ) -> FieldResult> { 510 | let employments = self.primary_employments.try_unwrap()?; 511 | 512 | match employments.len() { 513 | 0 => Ok(None), 514 | 1 => { 515 | let employment = employments[0].clone(); 516 | Ok(Some(employment)) 517 | } 518 | n => panic!("more than one primary employment: {}", n), 519 | } 520 | } 521 | 522 | fn field_primary_company( 523 | &self, 524 | executor: &Executor<'_, Context>, 525 | _trail: &QueryTrail<'_, Company, Walked>, 526 | ) -> FieldResult> { 527 | let companies = self.primary_companies.try_unwrap()?; 528 | 529 | match companies.len() { 530 | 0 => Ok(None), 531 | 1 => { 532 | let company = companies[0].clone(); 533 | Ok(Some(company)) 534 | } 535 | n => panic!("more than one primary company: {}", n), 536 | } 537 | } 538 | } 539 | 540 | // #[derive(Clone, Eq, PartialEq, Debug)] 541 | #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] 542 | #[eager_loading( 543 | model = models::Country, 544 | context = Context, 545 | id = i32, 546 | error = Box, 547 | root_model_field = country 548 | )] 549 | pub struct Country { 550 | country: models::Country, 551 | 552 | #[has_many( 553 | root_model_field = city, 554 | )] 555 | cities: HasMany, 556 | } 557 | 558 | impl CountryFields for Country { 559 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 560 | Ok(&self.country.id) 561 | } 562 | 563 | fn field_cities( 564 | &self, 565 | _executor: &Executor<'_, Context>, 566 | _trail: &QueryTrail<'_, City, Walked>, 567 | ) -> FieldResult<&Vec> { 568 | Ok(self.cities.try_unwrap()?) 569 | } 570 | } 571 | 572 | #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] 573 | #[eager_loading( 574 | model = models::City, 575 | id = i32, 576 | context = Context, 577 | error = Box, 578 | root_model_field = city 579 | )] 580 | pub struct City { 581 | city: models::City, 582 | #[has_one(foreign_key_field = country_id, root_model_field = country)] 583 | country: HasOne, 584 | } 585 | 586 | impl CityFields for City { 587 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 588 | Ok(&self.city.id) 589 | } 590 | 591 | fn field_country( 592 | &self, 593 | _executor: &Executor<'_, Context>, 594 | _trail: &QueryTrail<'_, Country, Walked>, 595 | ) -> FieldResult<&Country> { 596 | Ok(self.country.try_unwrap()?) 597 | } 598 | } 599 | 600 | #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] 601 | #[eager_loading(context = Context, error = Box)] 602 | pub struct Company { 603 | company: models::Company, 604 | } 605 | 606 | impl CompanyFields for Company { 607 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 608 | Ok(&self.company.id) 609 | } 610 | 611 | fn field_name(&self, _executor: &Executor<'_, Context>) -> FieldResult<&String> { 612 | Ok(&self.company.name) 613 | } 614 | } 615 | 616 | #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] 617 | #[eager_loading(context = Context, error = Box)] 618 | pub struct Employment { 619 | employment: models::Employment, 620 | #[has_one(default)] 621 | user: HasOne, 622 | #[has_one(default)] 623 | company: HasOne, 624 | } 625 | 626 | impl EmploymentFields for Employment { 627 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 628 | Ok(&self.employment.id) 629 | } 630 | 631 | fn field_user( 632 | &self, 633 | _executor: &Executor<'_, Context>, 634 | _trail: &QueryTrail<'_, User, Walked>, 635 | ) -> FieldResult<&User> { 636 | Ok(self.user.try_unwrap()?) 637 | } 638 | 639 | fn field_company( 640 | &self, 641 | _executor: &Executor<'_, Context>, 642 | _trail: &QueryTrail<'_, Company, Walked>, 643 | ) -> FieldResult<&Company> { 644 | Ok(self.company.try_unwrap()?) 645 | } 646 | } 647 | 648 | #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] 649 | #[eager_loading(context = Context, error = Box)] 650 | pub struct Issue { 651 | issue: models::Issue, 652 | #[option_has_one(root_model_field = user)] 653 | reviewer: OptionHasOne, 654 | } 655 | 656 | impl IssueFields for Issue { 657 | fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { 658 | Ok(&self.issue.id) 659 | } 660 | 661 | fn field_title(&self, _executor: &Executor<'_, Context>) -> FieldResult<&String> { 662 | Ok(&self.issue.title) 663 | } 664 | 665 | fn field_reviewer( 666 | &self, 667 | _executor: &Executor<'_, Context>, 668 | _trail: &QueryTrail<'_, User, Walked>, 669 | ) -> FieldResult<&Option> { 670 | Ok(self.reviewer.try_unwrap()?) 671 | } 672 | } 673 | 674 | #[test] 675 | fn loading_user() { 676 | let mut countries = StatsHash::new("countries"); 677 | let cities = StatsHash::new("cities"); 678 | let mut users = StatsHash::new("users"); 679 | 680 | let mut country = models::Country { 681 | id: CountryId::from(10), 682 | }; 683 | let country_id = country.id; 684 | 685 | let other_city = models::City { 686 | id: CityId::from(30), 687 | country_id, 688 | }; 689 | 690 | countries.insert(country_id, country); 691 | 692 | users.insert( 693 | UserId::from(1), 694 | models::User { 695 | id: UserId::from(1), 696 | country_id, 697 | city_id: None, 698 | }, 699 | ); 700 | users.insert( 701 | UserId::from(2), 702 | models::User { 703 | id: UserId::from(2), 704 | country_id, 705 | city_id: None, 706 | }, 707 | ); 708 | 709 | let db = Db { 710 | users, 711 | countries, 712 | cities, 713 | employments: StatsHash::new("employments"), 714 | companies: StatsHash::new("companies"), 715 | issues: StatsHash::new("issues"), 716 | }; 717 | let (json, counts) = run_query("query Test { user(id: 1) { id } }", db); 718 | 719 | assert_eq!(1, counts.user_reads); 720 | assert_eq!(0, counts.country_reads); 721 | assert_eq!(0, counts.city_reads); 722 | 723 | assert_json_include!( 724 | expected: json!({ 725 | "user": { "id": 1 }, 726 | }), 727 | actual: json, 728 | ); 729 | } 730 | 731 | #[test] 732 | fn loading_users() { 733 | let mut countries = StatsHash::new("countries"); 734 | let cities = StatsHash::new("cities"); 735 | let mut users = StatsHash::new("users"); 736 | 737 | let mut country = models::Country { 738 | id: CountryId::from(10), 739 | }; 740 | let country_id = country.id; 741 | 742 | let other_city = models::City { 743 | id: CityId::from(30), 744 | country_id, 745 | }; 746 | 747 | countries.insert(country_id, country); 748 | 749 | users.insert( 750 | UserId::from(1), 751 | models::User { 752 | id: UserId::from(1), 753 | country_id, 754 | city_id: None, 755 | }, 756 | ); 757 | users.insert( 758 | UserId::from(2), 759 | models::User { 760 | id: UserId::from(2), 761 | country_id, 762 | city_id: None, 763 | }, 764 | ); 765 | 766 | let db = Db { 767 | users, 768 | countries, 769 | cities, 770 | employments: StatsHash::new("employments"), 771 | companies: StatsHash::new("companies"), 772 | issues: StatsHash::new("issues"), 773 | }; 774 | let (json, counts) = run_query("query Test { users { id } }", db); 775 | 776 | assert_eq!(1, counts.user_reads); 777 | assert_eq!(0, counts.country_reads); 778 | assert_eq!(0, counts.city_reads); 779 | 780 | assert_json_include!( 781 | expected: json!({ 782 | "users": [ 783 | { "id": 1 }, 784 | { "id": 2 }, 785 | ] 786 | }), 787 | actual: json, 788 | ); 789 | } 790 | 791 | #[test] 792 | fn loading_users_and_associations() { 793 | let mut countries = StatsHash::new("countries"); 794 | let mut cities = StatsHash::new("cities"); 795 | let mut users = StatsHash::new("users"); 796 | 797 | let country = models::Country { 798 | id: CountryId::from(10), 799 | }; 800 | 801 | countries.insert(country.id, country.clone()); 802 | 803 | let city = models::City { 804 | id: CityId::from(20), 805 | country_id: country.id, 806 | }; 807 | cities.insert(city.id, city.clone()); 808 | 809 | let other_city = models::City { 810 | id: CityId::from(30), 811 | country_id: country.id, 812 | }; 813 | cities.insert(other_city.id, other_city.clone()); 814 | 815 | users.insert( 816 | UserId::from(1), 817 | models::User { 818 | id: UserId::from(1), 819 | country_id: country.id, 820 | city_id: Some(other_city.id), 821 | }, 822 | ); 823 | users.insert( 824 | UserId::from(2), 825 | models::User { 826 | id: UserId::from(2), 827 | country_id: country.id, 828 | city_id: Some(city.id), 829 | }, 830 | ); 831 | users.insert( 832 | UserId::from(3), 833 | models::User { 834 | id: UserId::from(3), 835 | country_id: country.id, 836 | city_id: Some(city.id), 837 | }, 838 | ); 839 | users.insert( 840 | UserId::from(4), 841 | models::User { 842 | id: UserId::from(4), 843 | country_id: country.id, 844 | city_id: None, 845 | }, 846 | ); 847 | users.insert( 848 | UserId::from(5), 849 | models::User { 850 | id: UserId::from(5), 851 | country_id: country.id, 852 | city_id: Some(CityId::from(999)), 853 | }, 854 | ); 855 | 856 | let db = Db { 857 | users, 858 | countries, 859 | cities, 860 | employments: StatsHash::new("employments"), 861 | companies: StatsHash::new("companies"), 862 | issues: StatsHash::new("issue"), 863 | }; 864 | 865 | let (json, counts) = run_query( 866 | r#" 867 | query Test { 868 | users { 869 | id 870 | city { id } 871 | country { 872 | id 873 | cities { 874 | id 875 | } 876 | } 877 | } 878 | } 879 | "#, 880 | db, 881 | ); 882 | 883 | assert_json_include!( 884 | expected: json!({ 885 | "users": [ 886 | { 887 | "id": 1, 888 | "city": { "id": *other_city.id }, 889 | "country": { 890 | "id": *country.id, 891 | "cities": [ 892 | // the order of the citites doesn't matter 893 | {}, 894 | {}, 895 | ], 896 | }, 897 | }, 898 | { 899 | "id": 2, 900 | "city": { "id": *city.id } 901 | }, 902 | { 903 | "id": 3, 904 | "city": { "id": *city.id } 905 | }, 906 | { 907 | "id": 4, 908 | "city": null 909 | }, 910 | { 911 | "id": 5, 912 | "city": null 913 | }, 914 | ] 915 | }), 916 | actual: json.clone(), 917 | ); 918 | 919 | let json_cities = json["users"][0]["country"]["cities"].as_array().unwrap(); 920 | for json_city in json_cities { 921 | let id = json_city["id"].as_i64().unwrap() as i32; 922 | assert!([city.id, other_city.id].contains(&CityId::from(id))); 923 | } 924 | 925 | assert_eq!(1, counts.user_reads); 926 | assert_eq!(1, counts.country_reads); 927 | assert_eq!(2, counts.city_reads); 928 | } 929 | 930 | #[test] 931 | fn test_caching() { 932 | let mut users = StatsHash::new("users"); 933 | let mut countries = StatsHash::new("countries"); 934 | let mut cities = StatsHash::new("cities"); 935 | 936 | let mut country = models::Country { 937 | id: CountryId::from(1), 938 | }; 939 | 940 | let city = models::City { 941 | id: CityId::from(2), 942 | country_id: country.id, 943 | }; 944 | 945 | let user = models::User { 946 | id: UserId::from(3), 947 | country_id: country.id, 948 | city_id: Some(city.id), 949 | }; 950 | 951 | users.insert(user.id, user); 952 | countries.insert(country.id, country); 953 | cities.insert(city.id, city); 954 | 955 | let db = Db { 956 | users, 957 | countries, 958 | cities, 959 | employments: StatsHash::new("employments"), 960 | companies: StatsHash::new("companies"), 961 | issues: StatsHash::new("issues"), 962 | }; 963 | 964 | let (json, counts) = run_query( 965 | r#" 966 | query Test { 967 | users { 968 | id 969 | country { 970 | id 971 | cities { 972 | id 973 | country { id } 974 | } 975 | } 976 | city { 977 | id 978 | country { id } 979 | } 980 | } 981 | } 982 | "#, 983 | db, 984 | ); 985 | 986 | assert_json_eq!( 987 | json!({ 988 | "users": [ 989 | { 990 | "id": 3, 991 | "city": { 992 | "id": 2, 993 | "country": { "id": 1 } 994 | }, 995 | "country": { 996 | "id": 1, 997 | "cities": [ 998 | { 999 | "id": 2, 1000 | "country": { "id": 1 } 1001 | }, 1002 | ], 1003 | }, 1004 | }, 1005 | ] 1006 | }), 1007 | json, 1008 | ); 1009 | 1010 | assert_eq!(1, counts.user_reads); 1011 | assert_eq!(3, counts.country_reads); 1012 | assert_eq!(2, counts.city_reads); 1013 | } 1014 | 1015 | #[test] 1016 | fn test_loading_has_many_through() { 1017 | let mut cities = StatsHash::new("cities"); 1018 | let mut companies = StatsHash::new("companies"); 1019 | let mut countries = StatsHash::new("countries"); 1020 | let mut employments = StatsHash::new("employments"); 1021 | let mut users = StatsHash::new("users"); 1022 | 1023 | let mut country = models::Country { 1024 | id: CountryId::from(1), 1025 | }; 1026 | countries.insert(country.id, country.clone()); 1027 | 1028 | let mut tonsser = models::Company { 1029 | id: CompanyId::from(2), 1030 | name: "Tonsser".to_string(), 1031 | }; 1032 | companies.insert(tonsser.id, tonsser.clone()); 1033 | 1034 | let mut peakon = models::Company { 1035 | id: CompanyId::from(3), 1036 | name: "Peakon".to_string(), 1037 | }; 1038 | companies.insert(peakon.id, peakon.clone()); 1039 | 1040 | let user = models::User { 1041 | id: UserId::from(4), 1042 | country_id: country.id, 1043 | city_id: None, 1044 | }; 1045 | users.insert(user.id, user.clone()); 1046 | 1047 | let mut tonsser_employment = models::Employment { 1048 | id: EmploymentId::from(5), 1049 | user_id: user.id, 1050 | company_id: tonsser.id, 1051 | primary: true, 1052 | }; 1053 | employments.insert(tonsser_employment.id, tonsser_employment.clone()); 1054 | 1055 | let mut peakon_employment = models::Employment { 1056 | id: EmploymentId::from(6), 1057 | user_id: user.id, 1058 | company_id: peakon.id, 1059 | primary: false, 1060 | }; 1061 | employments.insert(peakon_employment.id, peakon_employment); 1062 | 1063 | let db = Db { 1064 | cities, 1065 | companies, 1066 | countries, 1067 | employments, 1068 | users, 1069 | issues: StatsHash::new("issues"), 1070 | }; 1071 | 1072 | let (json, counts) = run_query( 1073 | r#" 1074 | query Test { 1075 | users { 1076 | id 1077 | employments { 1078 | user { id } 1079 | company { id name } 1080 | } 1081 | companies { id name } 1082 | primaryEmployment { 1083 | id 1084 | } 1085 | primaryCompany { 1086 | name 1087 | } 1088 | } 1089 | } 1090 | "#, 1091 | db, 1092 | ); 1093 | 1094 | assert_json_include!( 1095 | expected: json!({ 1096 | "users": [ 1097 | { 1098 | "id": *user.id, 1099 | "employments": [ 1100 | { 1101 | "user": { "id": *user.id }, 1102 | "company": { "id": *tonsser.id, "name": tonsser.name }, 1103 | }, 1104 | { 1105 | "user": { "id": *user.id }, 1106 | "company": { "id": *peakon.id, "name": peakon.name }, 1107 | }, 1108 | ], 1109 | "companies": [ 1110 | { "id": *tonsser.id, "name": tonsser.name }, 1111 | { "id": *peakon.id, "name": peakon.name }, 1112 | ], 1113 | "primaryEmployment": { 1114 | "id": *tonsser_employment.id, 1115 | }, 1116 | "primaryCompany": { 1117 | "name": tonsser.name, 1118 | }, 1119 | }, 1120 | ], 1121 | }), 1122 | actual: json, 1123 | ); 1124 | } 1125 | 1126 | #[test] 1127 | fn test_loading_has_many_fk_optional() { 1128 | let mut countries = StatsHash::new("countries"); 1129 | let mut users = StatsHash::new("users"); 1130 | let mut issues = StatsHash::new("issues"); 1131 | 1132 | let country = models::Country { 1133 | id: CountryId::from(1), 1134 | }; 1135 | countries.insert(country.id, country.clone()); 1136 | 1137 | let user = models::User { 1138 | id: UserId::from(2), 1139 | country_id: country.id, 1140 | city_id: None, 1141 | }; 1142 | users.insert(user.id, user.clone()); 1143 | 1144 | let assigned_issue = models::Issue { 1145 | id: IssueId::from(3), 1146 | title: "This issue is assigned to somebody".to_string(), 1147 | reviewer_id: Some(user.id), 1148 | }; 1149 | issues.insert(assigned_issue.id, assigned_issue.clone()); 1150 | 1151 | let unassigned_issue = models::Issue { 1152 | id: IssueId::from(4), 1153 | title: "This issue hasn't been assigned to somebody".to_string(), 1154 | reviewer_id: None, 1155 | }; 1156 | issues.insert(unassigned_issue.id, unassigned_issue); 1157 | 1158 | let db = Db { 1159 | cities: StatsHash::new("cities"), 1160 | companies: StatsHash::new("companies"), 1161 | countries, 1162 | employments: StatsHash::new("employments"), 1163 | users, 1164 | issues, 1165 | }; 1166 | 1167 | let (json, _counts) = run_query( 1168 | r#" 1169 | query Test { 1170 | users { 1171 | id 1172 | issues { 1173 | id 1174 | title 1175 | } 1176 | } 1177 | } 1178 | "#, 1179 | db, 1180 | ); 1181 | 1182 | assert_json_include!( 1183 | expected: json!({ 1184 | "users": [ 1185 | { 1186 | "id": *user.id, 1187 | "issues": [ 1188 | { 1189 | "id": *assigned_issue.id, 1190 | "title": assigned_issue.title, 1191 | }, 1192 | ], 1193 | }, 1194 | ], 1195 | }), 1196 | actual: json, 1197 | ); 1198 | } 1199 | 1200 | struct DbStats { 1201 | user_reads: usize, 1202 | country_reads: usize, 1203 | city_reads: usize, 1204 | company_reads: usize, 1205 | employment_reads: usize, 1206 | } 1207 | 1208 | fn run_query(query: &str, db: Db) -> (Value, DbStats) { 1209 | let ctx = Context { db }; 1210 | 1211 | let (result, errors) = juniper::execute( 1212 | query, 1213 | None, 1214 | &Schema::new(Query, Mutation), 1215 | &juniper::Variables::new(), 1216 | &ctx, 1217 | ) 1218 | .unwrap(); 1219 | 1220 | if !errors.is_empty() { 1221 | panic!( 1222 | "GraphQL errors\n{}", 1223 | serde_json::to_string_pretty(&errors).unwrap() 1224 | ); 1225 | } 1226 | 1227 | let json: Value = serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap(); 1228 | 1229 | println!("{}", serde_json::to_string_pretty(&json).unwrap()); 1230 | 1231 | ( 1232 | json, 1233 | DbStats { 1234 | user_reads: ctx.db.users.reads_count(), 1235 | country_reads: ctx.db.countries.reads_count(), 1236 | city_reads: ctx.db.cities.reads_count(), 1237 | company_reads: ctx.db.companies.reads_count(), 1238 | employment_reads: ctx.db.employments.reads_count(), 1239 | }, 1240 | ) 1241 | } 1242 | --------------------------------------------------------------------------------