diff --git a/Cargo.lock b/Cargo.lock index 6b6ef90..4def962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,30 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "actix" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be241f88f3b1e7e9a3fbe3b5a8a0f6915b5a1d7ee0d9a248d3376d01068cc60" +dependencies = [ + "actix-rt", + "actix_derive", + "bitflags", + "bytes 0.5.6", + "crossbeam-channel", + "derive_more", + "futures-channel", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project 0.4.27", + "smallvec", + "tokio", + "tokio-util", + "trust-dns-proto", + "trust-dns-resolver", +] + [[package]] name = "actix-codec" version = "0.3.0" @@ -286,6 +311,22 @@ dependencies = [ "url", ] +[[package]] +name = "actix-web-actors" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6edf3c2693e2a8c422800c87ee89a6a4eac7dd01109bc172a1093ce1f4f001" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes 0.5.6", + "futures-channel", + "futures-core", + "pin-project 0.4.27", +] + [[package]] name = "actix-web-codegen" version = "0.4.0" @@ -311,6 +352,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "actix_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95aceadaf327f18f0df5962fedc1bde2f870566a0b9f65c89508a3b1f79334c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "addr2line" version = "0.14.1" @@ -398,6 +450,18 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "anyhow" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" + +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + [[package]] name = "askama" version = "0.9.0" @@ -560,6 +624,23 @@ dependencies = [ "libc", ] +[[package]] +name = "bson" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2abd828df036867b89a07c5a4824df1168b42d74f41af7253f20e55a32e5ca4b" +dependencies = [ + "base64 0.12.3", + "chrono", + "hex", + "lazy_static", + "linked-hash-map", + "rand 0.7.3", + "serde", + "serde_json", + "uuid 0.8.2", +] + [[package]] name = "buf-min" version = "0.4.0" @@ -686,6 +767,8 @@ dependencies = [ "failure_derive", "futures", "hyperx", + "juniper", + "juniper_actix", "lazy_static", "listenfd", "log", @@ -713,6 +796,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + [[package]] name = "const_fn" version = "0.4.5" @@ -779,6 +875,27 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crossbeam-channel" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +dependencies = [ + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", +] + [[package]] name = "crypto-mac" version = "0.10.0" @@ -809,6 +926,17 @@ dependencies = [ "syn", ] +[[package]] +name = "derive_utils" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "diesel" version = "1.4.5" @@ -996,9 +1124,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futures" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" +checksum = "7f55667319111d593ba876406af7c409c0ebb44dc4be6132a783ccf163ea14c1" dependencies = [ "futures-channel", "futures-core", @@ -1011,9 +1139,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +checksum = "8c2dd2df839b57db9ab69c2c9d8f3e8c81984781937fe2807dc6dcf3b2ad2939" dependencies = [ "futures-core", "futures-sink", @@ -1021,15 +1149,26 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" +checksum = "15496a72fabf0e62bdc3df11a59a3787429221dd0710ba8ef163d6f7a9112c94" + +[[package]] +name = "futures-enum" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3422d14de7903a52e9dbc10ae05a7e14445ec61890100e098754e120b2bd7b1e" +dependencies = [ + "derive_utils", + "quote", + "syn", +] [[package]] name = "futures-executor" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +checksum = "891a4b7b96d84d5940084b2a37632dd65deeae662c114ceaa2c879629c9c0ad1" dependencies = [ "futures-core", "futures-task", @@ -1038,15 +1177,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" +checksum = "d71c2c65c57704c32f5241c1223167c2c3294fd34ac020c807ddbe6db287ba59" [[package]] name = "futures-macro" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" +checksum = "ea405816a5139fb39af82c2beb921d52143f556038378d6db21183a5c37fbfb7" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -1056,24 +1195,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" +checksum = "85754d98985841b7d4f5e8e6fbfa4a4ac847916893ec511a2917ccd8525b8bb3" [[package]] name = "futures-task" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" -dependencies = [ - "once_cell", -] +checksum = "fa189ef211c15ee602667a6fcfe1c1fd9e07d42250d2156382820fba33c9df80" [[package]] name = "futures-util" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +checksum = "1812c7ab8aedf8d6f2701a43e1243acdbcc2b36ab26e2ad421eb99ac963d96d1" dependencies = [ "futures-channel", "futures-core", @@ -1146,6 +1282,16 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" +[[package]] +name = "graphql-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1abd4ce5247dfc04a03ccde70f87a048458c9356c7e41d21ad8c407b3dde6f2" +dependencies = [ + "combine", + "thiserror", +] + [[package]] name = "h2" version = "0.2.7" @@ -1190,6 +1336,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" + [[package]] name = "hkdf" version = "0.10.0" @@ -1342,6 +1494,7 @@ checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" dependencies = [ "autocfg", "hashbrown", + "serde", ] [[package]] @@ -1395,6 +1548,57 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "juniper" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce9122e520b17ce7593f628d2b440318a5fd77b8923b1918e59ec28cf2c2815" +dependencies = [ + "async-trait", + "bson", + "chrono", + "fnv", + "futures", + "futures-enum", + "graphql-parser", + "indexmap", + "juniper_codegen", + "serde", + "static_assertions", + "url", + "uuid 0.8.2", +] + +[[package]] +name = "juniper_actix" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f52c6c31e7f470e7597fda98cf75726ea8f5f119c6bd54a9e058d94150a7e15" +dependencies = [ + "actix", + "actix-web", + "actix-web-actors", + "anyhow", + "futures", + "juniper", + "serde", + "serde_json", + "thiserror", + "tokio", +] + +[[package]] +name = "juniper_codegen" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aad9bb6febeb76eeb97aa39b1ec4e640ee0eb37db98379f89972b2d0da85d18" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1436,7 +1640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "492158e732f2e2de81c592f0a2427e57e12cd3d59877378fe7af624b6bbe0ca1" dependencies = [ "libc", - "uuid", + "uuid 0.6.5", "winapi 0.3.9", ] @@ -1479,6 +1683,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "memchr" version = "2.3.4" @@ -2191,6 +2401,7 @@ version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -2293,6 +2504,12 @@ dependencies = [ "version_check 0.9.2", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stdweb" version = "0.4.20" @@ -2573,6 +2790,7 @@ checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" dependencies = [ "bytes 0.5.6", "futures-core", + "futures-io", "futures-sink", "log", "pin-project-lite 0.1.11", @@ -2732,6 +2950,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + [[package]] name = "url" version = "2.2.1" @@ -2753,6 +2980,12 @@ dependencies = [ "cfg-if 0.1.10", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" + [[package]] name = "v_escape" version = "0.15.0" @@ -2809,6 +3042,12 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 5e052fe..fb3468c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,38 +5,50 @@ authors = ["Joe Jackson "] edition = "2018" [dependencies] +# Serialization serde = "1.0" serde_derive = "1.0" serde_json = "1.0" serde_yaml = "0.8" -regex = "1" -lazy_static = "1.4" -base64 = "0.12" -rand = "0.7" +# Web Framework actix-session = "0.4" actix-files = "0.5" actix-web-flash = { version = "0.5.0", git = "https://github.com/joeyjoejoejr/actix-web-flash" } actix-web = "3" actix-rt = "1.0" actix-service = "1.0" +askama = "0.9" + +# GraphQl +juniper = "0.15" +juniper_actix = "0.2" + +# Database +diesel = { version = "1.4", features = ["postgres", "r2d2", "chrono"] } +r2d2 = "0.8" +chrono = "0.4" + +# Testing +mockito = "0.14.0" +url = "2.1" +hyperx = "1.0" + +# Util +regex = "1" +lazy_static = "1.4" +base64 = "0.12" +rand = "0.7" log = "0.4" env_logger = "0.7" futures = "0.3" failure = "0.1" failure_derive = "0.1" -askama = "0.9" - reqwest = { version = "0.10", features = ["json"] } -mockito = "0.14.0" -url = "2.1" -hyperx = "1.0" +# Development structopt = "0.3" listenfd = "0.3" dotenv = "0.15" -diesel = { version = "1.4", features = ["postgres", "r2d2", "chrono"] } -r2d2 = "0.8" -chrono = "0.4" diff --git a/migrations/2021-02-26-221355_add_foreign_keys/down.sql b/migrations/2021-02-26-221355_add_foreign_keys/down.sql new file mode 100644 index 0000000..7689287 --- /dev/null +++ b/migrations/2021-02-26-221355_add_foreign_keys/down.sql @@ -0,0 +1,10 @@ +-- This file should undo anything in `up.sql` +/* ALTER TABLE pull_requests */ +/* DROP CONSTRAINT fk_pullrequestsgithubuser; */ + +ALTER TABLE reviews + DROP CONSTRAINT fk_pullrequestreviews; + +ALTER TABLE reviews + DROP CONSTRAINT fk_githubusersreviews; + diff --git a/migrations/2021-02-26-221355_add_foreign_keys/up.sql b/migrations/2021-02-26-221355_add_foreign_keys/up.sql new file mode 100644 index 0000000..1375f38 --- /dev/null +++ b/migrations/2021-02-26-221355_add_foreign_keys/up.sql @@ -0,0 +1,16 @@ +-- Your SQL goes here +/* ALTER TABLE pull_requests */ +/* ADD CONSTRAINT fk_pullrequestsgithubusers */ +/* FOREIGN KEY (github_user_id) */ +/* REFERENCES github_users(id); */ + +ALTER TABLE reviews + ADD CONSTRAINT fk_pullrequestreviews + FOREIGN KEY (pull_request_id) + REFERENCES pull_requests(id); + +ALTER TABLE reviews + ADD CONSTRAINT fk_githubusersreviews + FOREIGN KEY (github_user_id) + REFERENCES github_users(id); + diff --git a/migrations/2021-03-01-154039_add_foreign_key_to_pull_request/down.sql b/migrations/2021-03-01-154039_add_foreign_key_to_pull_request/down.sql new file mode 100644 index 0000000..89827ff --- /dev/null +++ b/migrations/2021-03-01-154039_add_foreign_key_to_pull_request/down.sql @@ -0,0 +1,10 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE pull_requests + DROP CONSTRAINT fk_pullrequestsgithubusers; + +UPDATE pull_requests + SET github_user_id = ( + SELECT github_id + FROM github_users + WHERE github_users.id = pull_requests.github_user_id + ); diff --git a/migrations/2021-03-01-154039_add_foreign_key_to_pull_request/up.sql b/migrations/2021-03-01-154039_add_foreign_key_to_pull_request/up.sql new file mode 100644 index 0000000..d4dc775 --- /dev/null +++ b/migrations/2021-03-01-154039_add_foreign_key_to_pull_request/up.sql @@ -0,0 +1,12 @@ +-- Your SQL goes here +UPDATE pull_requests + SET github_user_id = ( + SELECT id + FROM github_users + WHERE github_users.github_id = pull_requests.github_user_id + ); + +ALTER TABLE pull_requests + ADD CONSTRAINT fk_pullrequestsgithubusers + FOREIGN KEY (github_user_id) + REFERENCES github_users(id); diff --git a/migrations/2021-03-01-155845_add_foreign_key_to_gh_users/down.sql b/migrations/2021-03-01-155845_add_foreign_key_to_gh_users/down.sql new file mode 100644 index 0000000..f7259b5 --- /dev/null +++ b/migrations/2021-03-01-155845_add_foreign_key_to_gh_users/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE github_users + DROP CONSTRAINT fk_githubuserusers; diff --git a/migrations/2021-03-01-155845_add_foreign_key_to_gh_users/up.sql b/migrations/2021-03-01-155845_add_foreign_key_to_gh_users/up.sql new file mode 100644 index 0000000..93374ff --- /dev/null +++ b/migrations/2021-03-01-155845_add_foreign_key_to_gh_users/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE github_users + ADD CONSTRAINT fk_githubuserusers + FOREIGN KEY (user_id) + REFERENCES users(id); diff --git a/src/graphql/mod.rs b/src/graphql/mod.rs new file mode 100644 index 0000000..1ce7e17 --- /dev/null +++ b/src/graphql/mod.rs @@ -0,0 +1 @@ +pub mod schema; diff --git a/src/graphql/schema.rs b/src/graphql/schema.rs new file mode 100644 index 0000000..bfac0f5 --- /dev/null +++ b/src/graphql/schema.rs @@ -0,0 +1,309 @@ +use chrono::{Duration, NaiveDateTime, Utc}; +use diesel::prelude::*; +use juniper::{ + graphql_object, Context, EmptyMutation, EmptySubscription, FieldResult, GraphQLInputObject, + GraphQLObject, GraphQLUnion, RootNode, +}; + +use crate::db; + +impl Context for db::DBExecutor {} + +pub type Schema = + RootNode<'static, Query, EmptyMutation, EmptySubscription>; + +pub fn schema() -> Schema { + Schema::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ) +} + +#[derive(GraphQLObject)] +struct PageInfo { + page: i32, + per_page: i32, + count: i32, +} + +#[derive(GraphQLUnion)] +#[graphql(context=db::DBExecutor)] +enum PaginatedItem { + PullRequests(PullRequestType), + Reviews(ReviewType), +} + +#[derive(GraphQLObject)] +#[graphql(context=db::DBExecutor)] +struct PaginatedItems { + items: Vec, + page_info: PageInfo, +} + +#[derive(GraphQLInputObject)] +struct PullRequestSearchInput { + start_at: Option, + end_at: Option, +} + +#[derive(Queryable)] +struct UserType { + id: i32, + username: Option, + created_at: NaiveDateTime, + updated_at: NaiveDateTime, +} + +#[graphql_object(context=db::DBExecutor)] +impl UserType { + fn id(&self) -> i32 { + self.id + } + + fn username(&self) -> Option<&str> { + self.username.as_deref() + } + + fn created_at(&self) -> NaiveDateTime { + self.created_at + } + + fn updated_at(&self) -> NaiveDateTime { + self.updated_at + } + + #[graphql(arguments(page(default = 1), per_page(default = 25)))] + fn pull_requests( + &self, + db: &db::DBExecutor, + page: i32, + per_page: i32, + search: Option, + ) -> FieldResult { + paginated_pull_requests(db, page, per_page, search, Some(self.id)) + } + + #[graphql(arguments(page(default = 1), per_page(default = 25)))] + fn reviews( + &self, + db: &db::DBExecutor, + page: i32, + per_page: i32, + ) -> FieldResult { + use crate::schema::github_users::dsl::{github_users, user_id}; + use crate::schema::reviews::dsl::*; + let conn = db.0.get()?; + + let query = reviews.inner_join(github_users).filter(user_id.eq(self.id)); + let count: i64 = query.clone().count().first(&conn)?; + + let revs = query + .select((id, state, user_id, created_at)) + .order(created_at.desc()) + .limit(per_page as i64) + .offset((page as i64 - 1) * per_page as i64) + .load(&conn)?; + + Ok(PaginatedItems { + items: revs.into_iter().map(PaginatedItem::Reviews).collect(), + page_info: PageInfo { + page, + per_page, + count: count as i32, + }, + }) + } +} + +#[derive(Queryable)] +struct PullRequestType { + id: i32, + state: String, + channel: String, + user_id: Option, + display_text: String, +} + +#[graphql_object(context = db::DBExecutor)] +impl PullRequestType { + fn state(&self) -> &str { + self.state.as_str() + } + + fn channel(&self) -> &str { + self.channel.as_str() + } + + fn display_text(&self) -> &str { + self.display_text.as_str() + } + + fn user(&self, db: &db::DBExecutor) -> FieldResult> { + if let Some(current_user_id) = self.user_id { + use crate::schema::github_users::{self, dsl::*}; + use crate::schema::users::dsl::{created_at, id, updated_at, users}; + let conn = db.0.get()?; + + return Ok(Some( + users + .left_join(github_users::table.on(github_users::user_id.eq(id.nullable()))) + .select((id, login.nullable(), created_at, updated_at)) + .filter(id.eq(current_user_id)) + .first(&conn)?, + )); + } + Ok(None) + } + + fn reviews(&self, db: &db::DBExecutor) -> FieldResult> { + use crate::schema::github_users::dsl::{github_users, user_id}; + use crate::schema::reviews::dsl::*; + let conn = db.0.get()?; + + reviews + .filter(pull_request_id.eq(self.id)) + .inner_join(github_users) + .select((id, state, user_id, created_at)) + .load(&conn) + .map_err(|e| e.into()) + } +} + +#[derive(Queryable)] +struct ReviewType { + id: i32, + state: String, + user_id: Option, + created_at: NaiveDateTime, +} + +#[graphql_object(context = db::DBExecutor)] +impl ReviewType { + fn id(&self) -> i32 { + self.id + } + + fn state(&self) -> &str { + self.state.as_str() + } + + fn created_at(&self) -> NaiveDateTime { + self.created_at + } + + fn user(&self, db: &db::DBExecutor) -> FieldResult> { + if let Some(current_user_id) = self.user_id { + use crate::schema::github_users::{self, dsl::*}; + use crate::schema::users::dsl::{created_at, id, updated_at, users}; + let conn = db.0.get()?; + + return Ok(Some( + users + .left_join(github_users::table.on(github_users::user_id.eq(id.nullable()))) + .select((id, login.nullable(), created_at, updated_at)) + .filter(id.eq(current_user_id)) + .first(&conn)?, + )); + } + Ok(None) + } +} + +pub struct Query; +#[graphql_object(context = db::DBExecutor)] +impl Query { + fn api_version() -> String { + "0.1".to_string() + } + + #[graphql(arguments(uid(name = "id")))] + fn user(db: &db::DBExecutor, uid: i32) -> FieldResult { + use crate::schema::github_users::dsl::*; + use crate::schema::users::dsl::{created_at, id, updated_at, users}; + let conn = db.0.get()?; + + users + .left_join(github_users) + .select((id, login.nullable(), created_at, updated_at)) + .filter(id.eq(uid)) + .first(&conn) + .map_err(|e| e.into()) + } + + fn users(db: &db::DBExecutor) -> FieldResult> { + use crate::schema::github_users::dsl::*; + use crate::schema::users::dsl::{created_at, id, updated_at, users}; + let conn = db.0.get()?; + + users + .left_join(github_users) + .select((id, login.nullable(), created_at, updated_at)) + .load(&conn) + .map_err(|e| e.into()) + } + + #[graphql(arguments(page(default = 1), per_page(default = 25)))] + fn pull_requests( + db: &db::DBExecutor, + page: i32, + per_page: i32, + search: Option, + ) -> FieldResult { + paginated_pull_requests(db, page, per_page, search, None) + } +} + +fn paginated_pull_requests( + db: &db::DBExecutor, + page: i32, + per_page: i32, + search: Option, + user_id: Option, +) -> FieldResult { + use crate::schema::github_users::dsl::{github_users, user_id as gh_user_id}; + use crate::schema::pull_requests::dsl::*; + let conn = db.0.get()?; + + let (start_at, end_at) = if let Some(PullRequestSearchInput { start_at, end_at }) = search { + ( + start_at.unwrap_or_else(|| Utc::now().naive_utc() - Duration::days(7)), + end_at.unwrap_or_else(|| Utc::now().naive_utc()), + ) + } else { + ( + Utc::now().naive_utc() - Duration::days(7), + Utc::now().naive_utc(), + ) + }; + + let count: i64; + let query = pull_requests + .filter(created_at.between(start_at, end_at)) + .inner_join(github_users); + + let new_query = if let Some(user_id) = user_id { + let q = query.filter(gh_user_id.eq(user_id)); + count = q.clone().count().first(&conn)?; + q.into_boxed() + } else { + count = query.clone().count().first(&conn)?; + query.into_boxed() + }; + + let prs = new_query + .select((id, state, channel, gh_user_id, display_text)) + .order(created_at.desc()) + .limit(per_page as i64) + .offset((page as i64 - 1) * per_page as i64) + .load(&conn)?; + + Ok(PaginatedItems { + items: prs.into_iter().map(PaginatedItem::PullRequests).collect(), + page_info: PageInfo { + page, + per_page, + count: count as i32, + }, + }) +} diff --git a/src/lib.rs b/src/lib.rs index 437df9d..ded6849 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ extern crate failure_derive; mod error; mod github; +mod graphql; mod middlewares; mod models; mod routes; @@ -83,6 +84,11 @@ pub fn configure_app(cfg: &mut web::ServiceConfig) { web::resource("/setup") .route(web::get().to(routes::web::new_setup)) .route(web::post().to(routes::web::create_setup)), + ) + .service( + web::resource("graphql") + .route(web::get().to(routes::graphql::playground_route)) + .route(web::post().to(routes::graphql::graphql_route)), ); } @@ -122,6 +128,7 @@ pub async fn start_dev_server( .service(fs::Files::new("/public", "./public")) .data(app_config.clone()) .data(db.clone()) + .data(graphql::schema::schema()) .configure(configure_app) }); diff --git a/src/middlewares.rs b/src/middlewares.rs index da0472e..8814397 100644 --- a/src/middlewares.rs +++ b/src/middlewares.rs @@ -1,5 +1,5 @@ use actix_service::{Service, Transform}; -use actix_web::{dev::ServiceRequest, dev::ServiceResponse, http, Error, HttpResponse}; +use actix_web::{dev::ServiceRequest, dev::ServiceResponse, http, web::Data, Error, HttpResponse}; use futures::future::{ok, Either, Future, Ready}; use std::pin::Pin; use std::task::{Context, Poll}; @@ -49,7 +49,7 @@ where fn call(&mut self, req: ServiceRequest) -> Self::Future { let config = req - .app_data::() + .app_data::>() .expect("AppConfig must be setup"); let app_data = { diff --git a/src/models.rs b/src/models.rs index e385e7d..85024f0 100644 --- a/src/models.rs +++ b/src/models.rs @@ -111,7 +111,7 @@ impl PullRequest { let conn = db.0.get()?; let gh_user = github_users - .filter(github_id.eq(self.github_user_id)) + .filter(id.eq(self.github_user_id)) .first::(&conn)?; gh_user.user(db) @@ -258,24 +258,29 @@ struct RemoveGithubToken<'a> { } impl User { - pub fn create_or_udpate(new_user: &NewUser, db: &DBExecutor) -> Result { + pub fn create_or_update(new_user: &NewUser, db: &DBExecutor) -> Result<(bool, User)> { use crate::schema::users::dsl::*; let conn = db.0.get()?; + let mut created = false; let user_res: Result = users .filter(slack_user_id.eq(&new_user.slack_user_id)) .first(&conn) .map_err(|e| e.into()); - match user_res { - Ok(user) => diesel::update(users.find(user.id)) - .set(slack_access_token.eq(&new_user.slack_access_token)) - .get_result(&conn), + let user = match user_res { + Ok(user) => { + created = true; + diesel::update(users.find(user.id)) + .set(slack_access_token.eq(&new_user.slack_access_token)) + .get_result(&conn) + } Err(_) => diesel::insert_into(users) .values(new_user) .get_result(&conn), - } - .map_err(|e| e.into()) + }?; + + Ok((created, user)) } pub fn find(find_id: i32, db: &DBExecutor) -> Result> { diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 057b923..11c98f4 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -24,7 +24,7 @@ pub async fn slack( ) -> Result { let response = state.slack.get_token(&query.code).await?; let user_data = response.authed_user; - let user = User::create_or_udpate( + let (created, user) = User::create_or_update( &NewUser { slack_user_id: user_data.id, slack_access_token: user_data.access_token, @@ -46,6 +46,7 @@ pub async fn github( let github_user = state.github.get_user(&response.access_token).await?; let user = helpers::get_current_user(&db, &session)?.ok_or(crate::error::Error::NotFoundError)?; + dbg!(&user); user.connect_to_github_user(&response.access_token, &github_user, &db)?; Ok(redirect_to("/")) diff --git a/src/routes/github.rs b/src/routes/github.rs index f330e9c..9f706f5 100644 --- a/src/routes/github.rs +++ b/src/routes/github.rs @@ -103,14 +103,14 @@ pub async fn delete_webhook( state: AppData, db: Data, session: Session, - path: Path, + Path((webhook_id,)): Path<(i32,)>, ) -> Result { let current_user = get_current_user(&db, &session)?.ok_or(Error::NotAuthedError)?; let access_token = current_user .github_access_token .ok_or(Error::NotAuthedError)?; - match Webhook::find(path.0, &db) { + match Webhook::find(webhook_id, &db) { Ok(webhook) => { state.github.delete_webhook(&webhook, &access_token).await?; webhook.delete(&db) diff --git a/src/routes/github_webhook.rs b/src/routes/github_webhook.rs index c78b578..311223d 100644 --- a/src/routes/github_webhook.rs +++ b/src/routes/github_webhook.rs @@ -57,7 +57,7 @@ async fn handle_pull_request_opened( slack_message_id: result.ts.unwrap_or_else(|| "".to_string()), channel: result.channel.unwrap_or_else(|| "".to_string()), display_text: format!("{}", json.pull_request), - github_user_id: requester.github_id, + github_user_id: requester.id, }, &db, )?; diff --git a/src/routes/graphql.rs b/src/routes/graphql.rs new file mode 100644 index 0000000..68eb488 --- /dev/null +++ b/src/routes/graphql.rs @@ -0,0 +1,18 @@ +use actix_web::{web::Data, Error, HttpResponse}; +use juniper_actix::{graphql_handler, playground_handler}; + +use crate::db; +use crate::graphql::schema::Schema; + +pub async fn playground_route() -> Result { + playground_handler("graphql", None).await +} + +pub async fn graphql_route( + req: actix_web::HttpRequest, + payload: actix_web::web::Payload, + schema: Data, + db: Data, +) -> Result { + graphql_handler(&schema, &db, req, payload).await +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 59126c7..a8a69ca 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,5 +1,6 @@ pub mod auth; pub mod github; pub mod github_webhook; +pub mod graphql; pub mod slack_webhook; pub mod web; diff --git a/src/routes/web.rs b/src/routes/web.rs index 1bc63d6..86c7161 100644 --- a/src/routes/web.rs +++ b/src/routes/web.rs @@ -87,6 +87,7 @@ pub async fn root( ) -> Result { let flash = flash_message.map(|flash| flash.into_inner()); let current_user = get_current_user(&db, &session)?; + dbg!(¤t_user); let is_gh_authed = current_user .clone() .and_then(|u| if u.is_gh_authed() { Some(u) } else { None }) diff --git a/src/schema.rs b/src/schema.rs index ab7fc54..2287efe 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -89,6 +89,10 @@ table! { joinable!(file_extensions -> icon_mappings (icon_mapping_id)); joinable!(file_names -> icon_mappings (icon_mapping_id)); +joinable!(github_users -> users (user_id)); +joinable!(pull_requests -> github_users (github_user_id)); +joinable!(reviews -> github_users (github_user_id)); +joinable!(reviews -> pull_requests (pull_request_id)); allow_tables_to_appear_in_same_query!( configs, diff --git a/src/utils/app_config.rs b/src/utils/app_config.rs index 0820603..f1a4aea 100644 --- a/src/utils/app_config.rs +++ b/src/utils/app_config.rs @@ -1,10 +1,10 @@ use actix_web::error::ErrorBadRequest; use actix_web::web::Data; use actix_web::{dev, Error, FromRequest, HttpRequest}; -use std::sync::{Arc, Mutex}; -use std::pin::Pin; -use futures::future::{ok, err, Future}; +use futures::future::{err, ok, Future}; use futures::TryFutureExt; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; use crate::github::{GithubClient, GithubOauthClient}; use crate::slack::SlackClient;