#![allow(unused_variables, unused_imports, dead_code, unused_mut)] mod helpers; use assert_json_diff::{assert_json_eq, assert_json_include}; use helpers::{SortedExtension, StatsHash}; use juniper::{Executor, FieldError, FieldResult}; use juniper_eager_loading::{ prelude::*, EagerLoading, HasMany, HasManyThrough, HasOne, OptionHasOne, }; use juniper_from_schema::graphql_schema; use models::{CityId, CompanyId, CountryId, EmploymentId, IssueId, UserId}; use serde_json::{json, Value}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::{borrow::Borrow, collections::HashMap, hash::Hash}; graphql_schema! { schema { query: Query mutation: Mutation } type Query { user(id: Int!): User! @juniper(ownership: "owned") users: [User!]! @juniper(ownership: "owned") } type Mutation { noop: Boolean! } type User { id: Int! country: Country! city: City employments: [Employment!]! @juniper(ownership: "owned") companies: [Company!]! @juniper(ownership: "owned") issues: [Issue!]! @juniper(ownership: "owned") primaryEmployment: Employment @juniper(ownership: "owned") primaryCompany: Company @juniper(ownership: "owned") } type Country { id: Int! cities: [City!]! } type City { id: Int! country: Country! } type Company { id: Int! name: String! } type Employment { id: Int! user: User! company: Company! } type Issue { id: Int! title: String! reviewer: User } } mod models { macro_rules! make_model_ids { ( $($name:ident),* ) => { $( #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash)] pub struct $name(i32); impl From for $name { fn from(id: i32) -> $name { $name(id) } } impl std::ops::Deref for $name { type Target = i32; fn deref(&self) -> &i32 { &self.0 } } )* } } make_model_ids!(UserId, CountryId, CityId, CompanyId, EmploymentId, IssueId); #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub struct User { pub id: UserId, pub country_id: CountryId, pub city_id: Option, } #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub struct Country { pub id: CountryId, } #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub struct City { pub id: CityId, pub country_id: CountryId, } #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub struct Company { pub id: CompanyId, pub name: String, } #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub struct Employment { pub id: EmploymentId, pub user_id: UserId, pub company_id: CompanyId, pub primary: bool, } impl Employment { pub fn primary(&self, _: &super::Context) -> bool { self.primary } } #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)] pub struct Issue { pub id: IssueId, pub title: String, pub reviewer_id: Option, } impl juniper_eager_loading::LoadFrom for Country { type Error = Box; type Context = super::Context; fn load(ids: &[CountryId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { let countries = ctx .db .countries .all_values() .into_iter() .filter(|value| ids.contains(&value.id)) .cloned() .collect::>(); Ok(countries) } } impl juniper_eager_loading::LoadFrom for City { type Error = Box; type Context = super::Context; fn load(ids: &[CityId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { let countries = ctx .db .cities .all_values() .into_iter() .filter(|value| ids.contains(&value.id)) .cloned() .collect::>(); Ok(countries) } } impl juniper_eager_loading::LoadFrom for User { type Error = Box; type Context = super::Context; fn load(ids: &[UserId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { let models = ctx .db .users .all_values() .into_iter() .filter(|value| ids.contains(&value.id)) .cloned() .collect::>(); Ok(models) } } impl juniper_eager_loading::LoadFrom for Company { type Error = Box; type Context = super::Context; fn load(ids: &[CompanyId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { let models = ctx .db .companies .all_values() .into_iter() .filter(|value| ids.contains(&value.id)) .cloned() .collect::>(); Ok(models) } } impl juniper_eager_loading::LoadFrom for Employment { type Error = Box; type Context = super::Context; fn load( ids: &[EmploymentId], _: &(), ctx: &Self::Context, ) -> Result, Self::Error> { let models = ctx .db .employments .all_values() .into_iter() .filter(|value| ids.contains(&value.id)) .cloned() .collect::>(); Ok(models) } } impl juniper_eager_loading::LoadFrom for Issue { type Error = Box; type Context = super::Context; fn load(ids: &[IssueId], _: &(), ctx: &Self::Context) -> Result, Self::Error> { let models = ctx .db .issues .all_values() .into_iter() .filter(|value| ids.contains(&value.id)) .cloned() .collect::>(); Ok(models) } } impl juniper_eager_loading::LoadFrom for City { type Error = Box; type Context = super::Context; fn load( countries: &[Country], _: &(), ctx: &Self::Context, ) -> Result, Self::Error> { let country_ids = countries .iter() .map(|country| country.id) .collect::>(); let mut cities = ctx .db .cities .all_values() .into_iter() .filter(|city| country_ids.contains(&city.country_id)) .cloned() .collect::>(); Ok(cities) } } impl juniper_eager_loading::LoadFrom for Employment { type Error = Box; type Context = super::Context; fn load(users: &[User], _: &(), ctx: &Self::Context) -> Result, Self::Error> { let user_ids = users.iter().map(|user| user.id).collect::>(); let employments = ctx .db .employments .all_values() .into_iter() .filter(|employment| user_ids.contains(&employment.user_id)) .cloned() .collect::>(); Ok(employments) } } impl juniper_eager_loading::LoadFrom for Company { type Error = Box; type Context = super::Context; fn load( employments: &[Employment], _: &(), ctx: &Self::Context, ) -> Result, Self::Error> { let company_ids = employments .iter() .map(|employment| employment.company_id) .collect::>(); let employments = ctx .db .companies .all_values() .into_iter() .filter(|company| company_ids.contains(&company.id)) .cloned() .collect::>(); Ok(employments) } } impl juniper_eager_loading::LoadFrom for Issue { type Error = Box; type Context = super::Context; fn load(users: &[User], _: &(), ctx: &Self::Context) -> Result, Self::Error> { let user_ids = users.iter().map(|user| Some(user.id)).collect::>(); let issues = ctx .db .issues .all_values() .into_iter() .filter(|issue| user_ids.contains(&issue.reviewer_id)) .cloned() .collect::>(); Ok(issues) } } } pub struct Db { users: StatsHash, countries: StatsHash, cities: StatsHash, companies: StatsHash, employments: StatsHash, issues: StatsHash, } pub struct Context { db: Db, } impl juniper::Context for Context {} pub struct Query; impl QueryFields for Query { fn field_user<'a>( &self, executor: &Executor<'a, Context>, trail: &QueryTrail<'a, User, Walked>, id: i32, ) -> FieldResult { let ctx = executor.context(); let user_model = ctx .db .users .get(&UserId::from(id)) .ok_or("User not found")? .clone(); let user = User::new_from_model(&user_model); let user = User::eager_load_all_children(user, &[user_model], ctx, trail)?; Ok(user) } fn field_users<'a>( &self, executor: &Executor<'a, Context>, trail: &QueryTrail<'a, User, Walked>, ) -> FieldResult> { let ctx = executor.context(); let mut user_models = ctx .db .users .all_values() .into_iter() .cloned() .collect::>(); user_models.sort_by_key(|user| user.id); let mut users = User::from_db_models(&user_models); User::eager_load_all_children_for_each(&mut users, &user_models, ctx, trail)?; Ok(users) } } pub struct Mutation; impl MutationFields for Mutation { fn field_noop(&self, _executor: &Executor<'_, Context>) -> FieldResult<&bool> { Ok(&true) } } // The default values are commented out #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, EagerLoading)] #[eager_loading( error = Box, context = Context, // model = "models::User", // id = "i32", // root_model_field = "user" )] pub struct User { user: models::User, // #[has_one( // foreign_key_field = country_id, // root_model_field = country // )] #[has_one(default)] country: HasOne, // #[has_one( // foreign_key_field = city_id, // root_model_field = city // )] #[option_has_one(default)] city: OptionHasOne, #[has_many(root_model_field = employment)] employments: HasMany, #[has_many_through( // model_field = company, join_model = models::Employment, )] companies: HasManyThrough, #[has_many( root_model_field = issue, foreign_key_field = reviewer_id, foreign_key_optional )] issues: HasMany, #[has_many( root_model_field = employment, graphql_field = primaryEmployment, predicate_method = primary )] primary_employments: HasMany, #[has_many_through( join_model = models::Employment, graphql_field = primaryCompany, predicate_method = primary )] primary_companies: HasManyThrough, } impl UserFields for User { fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { Ok(&self.user.id) } fn field_country( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, Country, Walked>, ) -> FieldResult<&Country> { Ok(self.country.try_unwrap()?) } fn field_city( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, City, Walked>, ) -> FieldResult<&Option> { Ok(self.city.try_unwrap()?) } fn field_employments( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, Employment, Walked>, ) -> FieldResult> { Ok(self.employments.try_unwrap()?.clone().sorted()) } fn field_companies( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, Company, Walked>, ) -> FieldResult> { Ok(self.companies.try_unwrap()?.clone().sorted()) } fn field_issues( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, Issue, Walked>, ) -> FieldResult> { Ok(self.issues.try_unwrap()?.clone().sorted()) } fn field_primary_employment( &self, executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, Employment, Walked>, ) -> FieldResult> { let employments = self.primary_employments.try_unwrap()?; match employments.len() { 0 => Ok(None), 1 => { let employment = employments[0].clone(); Ok(Some(employment)) } n => panic!("more than one primary employment: {}", n), } } fn field_primary_company( &self, executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, Company, Walked>, ) -> FieldResult> { let companies = self.primary_companies.try_unwrap()?; match companies.len() { 0 => Ok(None), 1 => { let company = companies[0].clone(); Ok(Some(company)) } n => panic!("more than one primary company: {}", n), } } } // #[derive(Clone, Eq, PartialEq, Debug)] #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] #[eager_loading( model = models::Country, context = Context, id = i32, error = Box, root_model_field = country )] pub struct Country { country: models::Country, #[has_many( root_model_field = city, )] cities: HasMany, } impl CountryFields for Country { fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { Ok(&self.country.id) } fn field_cities( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, City, Walked>, ) -> FieldResult<&Vec> { Ok(self.cities.try_unwrap()?) } } #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] #[eager_loading( model = models::City, id = i32, context = Context, error = Box, root_model_field = city )] pub struct City { city: models::City, #[has_one(foreign_key_field = country_id, root_model_field = country)] country: HasOne, } impl CityFields for City { fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { Ok(&self.city.id) } fn field_country( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, Country, Walked>, ) -> FieldResult<&Country> { Ok(self.country.try_unwrap()?) } } #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] #[eager_loading(context = Context, error = Box)] pub struct Company { company: models::Company, } impl CompanyFields for Company { fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { Ok(&self.company.id) } fn field_name(&self, _executor: &Executor<'_, Context>) -> FieldResult<&String> { Ok(&self.company.name) } } #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] #[eager_loading(context = Context, error = Box)] pub struct Employment { employment: models::Employment, #[has_one(default)] user: HasOne, #[has_one(default)] company: HasOne, } impl EmploymentFields for Employment { fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { Ok(&self.employment.id) } fn field_user( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, User, Walked>, ) -> FieldResult<&User> { Ok(self.user.try_unwrap()?) } fn field_company( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, Company, Walked>, ) -> FieldResult<&Company> { Ok(self.company.try_unwrap()?) } } #[derive(Clone, Eq, PartialEq, Debug, Ord, PartialOrd, EagerLoading)] #[eager_loading(context = Context, error = Box)] pub struct Issue { issue: models::Issue, #[option_has_one(root_model_field = user)] reviewer: OptionHasOne, } impl IssueFields for Issue { fn field_id(&self, _executor: &Executor<'_, Context>) -> FieldResult<&i32> { Ok(&self.issue.id) } fn field_title(&self, _executor: &Executor<'_, Context>) -> FieldResult<&String> { Ok(&self.issue.title) } fn field_reviewer( &self, _executor: &Executor<'_, Context>, _trail: &QueryTrail<'_, User, Walked>, ) -> FieldResult<&Option> { Ok(self.reviewer.try_unwrap()?) } } #[test] fn loading_user() { let mut countries = StatsHash::new("countries"); let cities = StatsHash::new("cities"); let mut users = StatsHash::new("users"); let mut country = models::Country { id: CountryId::from(10), }; let country_id = country.id; let other_city = models::City { id: CityId::from(30), country_id, }; countries.insert(country_id, country); users.insert( UserId::from(1), models::User { id: UserId::from(1), country_id, city_id: None, }, ); users.insert( UserId::from(2), models::User { id: UserId::from(2), country_id, city_id: None, }, ); let db = Db { users, countries, cities, employments: StatsHash::new("employments"), companies: StatsHash::new("companies"), issues: StatsHash::new("issues"), }; let (json, counts) = run_query("query Test { user(id: 1) { id } }", db); assert_eq!(1, counts.user_reads); assert_eq!(0, counts.country_reads); assert_eq!(0, counts.city_reads); assert_json_include!( expected: json!({ "user": { "id": 1 }, }), actual: json, ); } #[test] fn loading_users() { let mut countries = StatsHash::new("countries"); let cities = StatsHash::new("cities"); let mut users = StatsHash::new("users"); let mut country = models::Country { id: CountryId::from(10), }; let country_id = country.id; let other_city = models::City { id: CityId::from(30), country_id, }; countries.insert(country_id, country); users.insert( UserId::from(1), models::User { id: UserId::from(1), country_id, city_id: None, }, ); users.insert( UserId::from(2), models::User { id: UserId::from(2), country_id, city_id: None, }, ); let db = Db { users, countries, cities, employments: StatsHash::new("employments"), companies: StatsHash::new("companies"), issues: StatsHash::new("issues"), }; let (json, counts) = run_query("query Test { users { id } }", db); assert_eq!(1, counts.user_reads); assert_eq!(0, counts.country_reads); assert_eq!(0, counts.city_reads); assert_json_include!( expected: json!({ "users": [ { "id": 1 }, { "id": 2 }, ] }), actual: json, ); } #[test] fn loading_users_and_associations() { let mut countries = StatsHash::new("countries"); let mut cities = StatsHash::new("cities"); let mut users = StatsHash::new("users"); let country = models::Country { id: CountryId::from(10), }; countries.insert(country.id, country.clone()); let city = models::City { id: CityId::from(20), country_id: country.id, }; cities.insert(city.id, city.clone()); let other_city = models::City { id: CityId::from(30), country_id: country.id, }; cities.insert(other_city.id, other_city.clone()); users.insert( UserId::from(1), models::User { id: UserId::from(1), country_id: country.id, city_id: Some(other_city.id), }, ); users.insert( UserId::from(2), models::User { id: UserId::from(2), country_id: country.id, city_id: Some(city.id), }, ); users.insert( UserId::from(3), models::User { id: UserId::from(3), country_id: country.id, city_id: Some(city.id), }, ); users.insert( UserId::from(4), models::User { id: UserId::from(4), country_id: country.id, city_id: None, }, ); users.insert( UserId::from(5), models::User { id: UserId::from(5), country_id: country.id, city_id: Some(CityId::from(999)), }, ); let db = Db { users, countries, cities, employments: StatsHash::new("employments"), companies: StatsHash::new("companies"), issues: StatsHash::new("issue"), }; let (json, counts) = run_query( r#" query Test { users { id city { id } country { id cities { id } } } } "#, db, ); assert_json_include!( expected: json!({ "users": [ { "id": 1, "city": { "id": *other_city.id }, "country": { "id": *country.id, "cities": [ // the order of the citites doesn't matter {}, {}, ], }, }, { "id": 2, "city": { "id": *city.id } }, { "id": 3, "city": { "id": *city.id } }, { "id": 4, "city": null }, { "id": 5, "city": null }, ] }), actual: json.clone(), ); let json_cities = json["users"][0]["country"]["cities"].as_array().unwrap(); for json_city in json_cities { let id = json_city["id"].as_i64().unwrap() as i32; assert!([city.id, other_city.id].contains(&CityId::from(id))); } assert_eq!(1, counts.user_reads); assert_eq!(1, counts.country_reads); assert_eq!(2, counts.city_reads); } #[test] fn test_caching() { let mut users = StatsHash::new("users"); let mut countries = StatsHash::new("countries"); let mut cities = StatsHash::new("cities"); let mut country = models::Country { id: CountryId::from(1), }; let city = models::City { id: CityId::from(2), country_id: country.id, }; let user = models::User { id: UserId::from(3), country_id: country.id, city_id: Some(city.id), }; users.insert(user.id, user); countries.insert(country.id, country); cities.insert(city.id, city); let db = Db { users, countries, cities, employments: StatsHash::new("employments"), companies: StatsHash::new("companies"), issues: StatsHash::new("issues"), }; let (json, counts) = run_query( r#" query Test { users { id country { id cities { id country { id } } } city { id country { id } } } } "#, db, ); assert_json_eq!( json!({ "users": [ { "id": 3, "city": { "id": 2, "country": { "id": 1 } }, "country": { "id": 1, "cities": [ { "id": 2, "country": { "id": 1 } }, ], }, }, ] }), json, ); assert_eq!(1, counts.user_reads); assert_eq!(3, counts.country_reads); assert_eq!(2, counts.city_reads); } #[test] fn test_loading_has_many_through() { let mut cities = StatsHash::new("cities"); let mut companies = StatsHash::new("companies"); let mut countries = StatsHash::new("countries"); let mut employments = StatsHash::new("employments"); let mut users = StatsHash::new("users"); let mut country = models::Country { id: CountryId::from(1), }; countries.insert(country.id, country.clone()); let mut tonsser = models::Company { id: CompanyId::from(2), name: "Tonsser".to_string(), }; companies.insert(tonsser.id, tonsser.clone()); let mut peakon = models::Company { id: CompanyId::from(3), name: "Peakon".to_string(), }; companies.insert(peakon.id, peakon.clone()); let user = models::User { id: UserId::from(4), country_id: country.id, city_id: None, }; users.insert(user.id, user.clone()); let mut tonsser_employment = models::Employment { id: EmploymentId::from(5), user_id: user.id, company_id: tonsser.id, primary: true, }; employments.insert(tonsser_employment.id, tonsser_employment.clone()); let mut peakon_employment = models::Employment { id: EmploymentId::from(6), user_id: user.id, company_id: peakon.id, primary: false, }; employments.insert(peakon_employment.id, peakon_employment.clone()); let db = Db { cities, companies, countries, employments, users, issues: StatsHash::new("issues"), }; let (json, counts) = run_query( r#" query Test { users { id employments { user { id } company { id name } } companies { id name } primaryEmployment { id } primaryCompany { name } } } "#, db, ); assert_json_include!( expected: json!({ "users": [ { "id": *user.id, "employments": [ { "user": { "id": *user.id }, "company": { "id": *tonsser.id, "name": tonsser.name }, }, { "user": { "id": *user.id }, "company": { "id": *peakon.id, "name": peakon.name }, }, ], "companies": [ { "id": *tonsser.id, "name": tonsser.name }, { "id": *peakon.id, "name": peakon.name }, ], "primaryEmployment": { "id": *tonsser_employment.id, }, "primaryCompany": { "name": tonsser.name, }, }, ], }), actual: json, ); } #[test] fn test_loading_has_many_fk_optional() { let mut countries = StatsHash::new("countries"); let mut users = StatsHash::new("users"); let mut issues = StatsHash::new("issues"); let country = models::Country { id: CountryId::from(1), }; countries.insert(country.id, country.clone()); let user = models::User { id: UserId::from(2), country_id: country.id, city_id: None, }; users.insert(user.id, user.clone()); let assigned_issue = models::Issue { id: IssueId::from(3), title: "This issue is assigned to somebody".to_string(), reviewer_id: Some(user.id), }; issues.insert(assigned_issue.id, assigned_issue.clone()); let unassigned_issue = models::Issue { id: IssueId::from(4), title: "This issue hasn't been assigned to somebody".to_string(), reviewer_id: None, }; issues.insert(unassigned_issue.id, unassigned_issue.clone()); let db = Db { cities: StatsHash::new("cities"), companies: StatsHash::new("companies"), countries, employments: StatsHash::new("employments"), users, issues, }; let (json, _counts) = run_query( r#" query Test { users { id issues { id title } } } "#, db, ); assert_json_include!( expected: json!({ "users": [ { "id": *user.id, "issues": [ { "id": *assigned_issue.id, "title": assigned_issue.title, }, ], }, ], }), actual: json, ); } struct DbStats { user_reads: usize, country_reads: usize, city_reads: usize, company_reads: usize, employment_reads: usize, } fn run_query(query: &str, db: Db) -> (Value, DbStats) { let ctx = Context { db }; let (result, errors) = juniper::execute( query, None, &Schema::new(Query, Mutation), &juniper::Variables::new(), &ctx, ) .unwrap(); if !errors.is_empty() { panic!( "GraphQL errors\n{}", serde_json::to_string_pretty(&errors).unwrap() ); } let json: Value = serde_json::from_str(&serde_json::to_string(&result).unwrap()).unwrap(); println!("{}", serde_json::to_string_pretty(&json).unwrap()); ( json, DbStats { user_reads: ctx.db.users.reads_count(), country_reads: ctx.db.countries.reads_count(), city_reads: ctx.db.cities.reads_count(), company_reads: ctx.db.companies.reads_count(), employment_reads: ctx.db.employments.reads_count(), }, ) }