Skip to content

Commit c10cf87

Browse files
committed
Follow the Diesel
Get it? Because this ports the following endpoints. To Diesel. ...Anyone? This ports 4 endpoints over to Diesel. The 3 endpoints which manipulate the `following` endpoint, as well as the `/me/updates` endpoint since it is only hit by the tests for the following endpoints. I ended up changing the updates endpoint quite a bit. I wanted to eliminate the N+1 queries on the max version, and was wondering why we needed the max version at all here. I went to go look at it in the UI, and it turns out that the dashboard page which displayed it is actually broken as well. After fixing it, I noticed that it doesn't need the crates at all, just the name (which we tell Ember is the id). I couldn't actually find a good way in Ember to reference the ID of an association without loading the whole thing. If anybody knows a better way to do it than what I'm doing here, please let me know. Since we don't need the crates, I've just opted not to include that data in the response body (note that just not including the max version is a bad idea, since ember caches stuff and it could result in a page that does need the max version displaying wrong later). While I was touching these endpoints, I also went ahead and reduced them all to a single query. Fixes #438.
1 parent 3c14e8a commit c10cf87

File tree

8 files changed

+148
-120
lines changed

8 files changed

+148
-120
lines changed

app/controllers/dashboard.js

-3
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@ export default Ember.Controller.extend({
3838
var page = (this.get('myFeed').length / 10) + 1;
3939

4040
ajax(`/me/updates?page=${page}`).then((data) => {
41-
data.crates.forEach(crate =>
42-
this.store.push(this.store.normalize('crate', crate)));
43-
4441
var versions = data.versions.map(version =>
4542
this.store.push(this.store.normalize('version', version)));
4643

app/models/version.js

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import DS from 'ember-data';
2+
import Ember from 'ember';
23

34
export default DS.Model.extend({
45
num: DS.attr('string'),
@@ -15,6 +16,10 @@ export default DS.Model.extend({
1516
dependencies: DS.hasMany('dependency', { async: true }),
1617
version_downloads: DS.hasMany('version-download', { async: true }),
1718

19+
crateName: Ember.computed('crate', function() {
20+
return this.belongsTo('crate').id();
21+
}),
22+
1823
getDownloadUrl() {
1924
return this.store.adapterFor('version').getDownloadUrl(this.get('dl_path'));
2025
},

app/templates/dashboard.hbs

+4-2
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@
4949
{{#each myFeed as |version|}}
5050
<div class='row'>
5151
<div class='info'>
52-
{{link-to version.crate.name 'crate.version' version.num}}
52+
{{#link-to 'crate.version' version.crateName version.num}}
53+
{{ version.crateName }}
5354
<span class='small'>{{ version.num }}</span>
55+
{{/link-to}}
5456
<span class='date small'>
55-
{{from-now version.created_at}}
57+
{{moment-from-now version.created_at}}
5658
</span>
5759
</div>
5860
</div>

src/krate.rs

+53-22
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ use std::collections::HashMap;
44

55
use conduit::{Request, Response};
66
use conduit_router::RequestParams;
7-
use diesel::pg::PgConnection;
7+
use diesel::associations::Identifiable;
88
use diesel::pg::upsert::*;
9+
use diesel::pg::{Pg, PgConnection};
910
use diesel::prelude::*;
11+
use diesel;
1012
use diesel_full_text_search::*;
1113
use license_exprs;
1214
use pg::GenericConnection;
@@ -35,7 +37,8 @@ use util::{RequestUtils, CargoResult, internal, ChainError, human};
3537
use version::EncodableVersion;
3638
use {Model, User, Keyword, Version, Category, Badge, Replica};
3739

38-
#[derive(Clone, Queryable, Identifiable, AsChangeset)]
40+
#[derive(Clone, Queryable, Identifiable, AsChangeset, Associations)]
41+
#[has_many(versions)]
3942
pub struct Crate {
4043
pub id: i32,
4144
pub name: String,
@@ -64,6 +67,8 @@ pub const ALL_COLUMNS: AllColumns = (crates::id, crates::name,
6467
crates::readme, crates::license, crates::repository,
6568
crates::max_upload_size);
6669

70+
type CrateQuery<'a> = crates::BoxedQuery<'a, Pg, <AllColumns as Expression>::SqlType>;
71+
6772
#[derive(RustcEncodable, RustcDecodable)]
6873
pub struct EncodableCrate {
6974
pub id: String,
@@ -224,6 +229,15 @@ impl<'a> NewCrate<'a> {
224229
}
225230

226231
impl Crate {
232+
pub fn by_name(name: &str) -> CrateQuery {
233+
crates::table
234+
.select(ALL_COLUMNS)
235+
.filter(
236+
canon_crate_name(crates::name).eq(
237+
canon_crate_name(name))
238+
).into_boxed()
239+
}
240+
227241
pub fn find_by_name(conn: &GenericConnection,
228242
name: &str) -> CargoResult<Crate> {
229243
let stmt = conn.prepare("SELECT * FROM crates \
@@ -1093,44 +1107,61 @@ fn user_and_crate(req: &mut Request) -> CargoResult<(User, Crate)> {
10931107
Ok((user.clone(), krate))
10941108
}
10951109

1110+
#[derive(Insertable, Queryable, Identifiable, Associations)]
1111+
#[belongs_to(User)]
1112+
#[primary_key(user_id, crate_id)]
1113+
#[table_name="follows"]
1114+
pub struct Follow {
1115+
user_id: i32,
1116+
crate_id: i32,
1117+
}
1118+
1119+
fn follow_target(req: &mut Request) -> CargoResult<Follow> {
1120+
let user = req.user()?;
1121+
let conn = req.db_conn()?;
1122+
let crate_name = &req.params()["crate_id"];
1123+
let crate_id = Crate::by_name(crate_name)
1124+
.select(crates::id)
1125+
.first(conn)?;
1126+
Ok(Follow {
1127+
user_id: user.id,
1128+
crate_id: crate_id,
1129+
})
1130+
}
1131+
10961132
/// Handles the `PUT /crates/:crate_id/follow` route.
10971133
pub fn follow(req: &mut Request) -> CargoResult<Response> {
1098-
let (user, krate) = user_and_crate(req)?;
1099-
let tx = req.tx()?;
1100-
let stmt = tx.prepare("SELECT 1 FROM follows
1101-
WHERE user_id = $1 AND crate_id = $2")?;
1102-
let rows = stmt.query(&[&user.id, &krate.id])?;
1103-
if !rows.iter().next().is_some() {
1104-
tx.execute("INSERT INTO follows (user_id, crate_id)
1105-
VALUES ($1, $2)", &[&user.id, &krate.id])?;
1106-
}
1134+
let follow = follow_target(req)?;
1135+
let conn = req.db_conn()?;
1136+
diesel::insert(&follow.on_conflict_do_nothing())
1137+
.into(follows::table)
1138+
.execute(conn)?;
11071139
#[derive(RustcEncodable)]
11081140
struct R { ok: bool }
11091141
Ok(req.json(&R { ok: true }))
11101142
}
11111143

11121144
/// Handles the `DELETE /crates/:crate_id/follow` route.
11131145
pub fn unfollow(req: &mut Request) -> CargoResult<Response> {
1114-
let (user, krate) = user_and_crate(req)?;
1115-
let tx = req.tx()?;
1116-
tx.execute("DELETE FROM follows
1117-
WHERE user_id = $1 AND crate_id = $2",
1118-
&[&user.id, &krate.id])?;
1146+
let follow = follow_target(req)?;
1147+
let conn = req.db_conn()?;
1148+
diesel::delete(&follow).execute(conn)?;
11191149
#[derive(RustcEncodable)]
11201150
struct R { ok: bool }
11211151
Ok(req.json(&R { ok: true }))
11221152
}
11231153

11241154
/// Handles the `GET /crates/:crate_id/following` route.
11251155
pub fn following(req: &mut Request) -> CargoResult<Response> {
1126-
let (user, krate) = user_and_crate(req)?;
1127-
let tx = req.tx()?;
1128-
let stmt = tx.prepare("SELECT 1 FROM follows
1129-
WHERE user_id = $1 AND crate_id = $2")?;
1130-
let rows = stmt.query(&[&user.id, &krate.id])?;
1156+
use diesel::expression::dsl::exists;
1157+
1158+
let follow = follow_target(req)?;
1159+
let conn = req.db_conn()?;
1160+
let following = diesel::select(exists(follows::table.find(follow.id())))
1161+
.get_result(conn)?;
11311162
#[derive(RustcEncodable)]
11321163
struct R { following: bool }
1133-
Ok(req.json(&R { following: rows.iter().next().is_some() }))
1164+
Ok(req.json(&R { following: following }))
11341165
}
11351166

11361167
/// Handles the `GET /crates/:crate_id/versions` route.

src/tests/all.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
#![deny(warnings)]
22

3+
#[macro_use] extern crate diesel;
4+
#[macro_use] extern crate diesel_codegen;
35
extern crate bufstream;
46
extern crate cargo_registry;
57
extern crate conduit;
68
extern crate conduit_middleware;
79
extern crate conduit_test;
810
extern crate curl;
9-
extern crate diesel;
1011
extern crate dotenv;
1112
extern crate git2;
1213
extern crate postgres;

src/tests/krate.rs

+28-38
Original file line numberDiff line numberDiff line change
@@ -769,63 +769,53 @@ fn dependencies() {
769769

770770
#[test]
771771
fn following() {
772-
// #[derive(RustcDecodable)] struct F { following: bool }
773-
// #[derive(RustcDecodable)] struct O { ok: bool }
772+
#[derive(RustcDecodable)] struct F { following: bool }
773+
#[derive(RustcDecodable)] struct O { ok: bool }
774774

775775
let (_b, app, middle) = ::app();
776776
let mut req = ::req(app.clone(), Method::Get, "/api/v1/crates/foo_following/following");
777777

778778
let user;
779-
let krate;
780779
{
781780
let conn = app.diesel_database.get().unwrap();
782781
user = ::new_user("foo").create_or_update(&conn).unwrap();
783782
::sign_in_as(&mut req, &user);
784-
krate = ::new_crate("foo_following").create_or_update(&conn, None, user.id).unwrap();
785-
786-
// FIXME: Go back to hitting the actual endpoint once it's using Diesel
787-
conn
788-
.execute(&format!("INSERT INTO follows (user_id, crate_id) VALUES ({}, {})",
789-
user.id, krate.id))
790-
.unwrap();
783+
::new_crate("foo_following").create_or_update(&conn, None, user.id).unwrap();
791784
}
792785

793-
// let mut response = ok_resp!(middle.call(&mut req));
794-
// assert!(!::json::<F>(&mut response).following);
786+
let mut response = ok_resp!(middle.call(&mut req));
787+
assert!(!::json::<F>(&mut response).following);
795788

796-
// req.with_path("/api/v1/crates/foo_following/follow")
797-
// .with_method(Method::Put);
798-
// let mut response = ok_resp!(middle.call(&mut req));
799-
// assert!(::json::<O>(&mut response).ok);
800-
// let mut response = ok_resp!(middle.call(&mut req));
801-
// assert!(::json::<O>(&mut response).ok);
789+
req.with_path("/api/v1/crates/foo_following/follow")
790+
.with_method(Method::Put);
791+
let mut response = ok_resp!(middle.call(&mut req));
792+
assert!(::json::<O>(&mut response).ok);
793+
let mut response = ok_resp!(middle.call(&mut req));
794+
assert!(::json::<O>(&mut response).ok);
802795

803-
// req.with_path("/api/v1/crates/foo_following/following")
804-
// .with_method(Method::Get);
805-
// let mut response = ok_resp!(middle.call(&mut req));
806-
// assert!(::json::<F>(&mut response).following);
796+
req.with_path("/api/v1/crates/foo_following/following")
797+
.with_method(Method::Get);
798+
let mut response = ok_resp!(middle.call(&mut req));
799+
assert!(::json::<F>(&mut response).following);
807800

808801
req.with_path("/api/v1/crates")
809-
.with_query("following=1");
802+
.with_method(Method::Get)
803+
.with_query("following=1");
810804
let mut response = ok_resp!(middle.call(&mut req));
811805
let l = ::json::<CrateList>(&mut response);
812806
assert_eq!(l.crates.len(), 1);
813807

814-
// FIXME: Go back to hitting the actual endpoint once it's using Diesel
815-
req.db_conn().unwrap()
816-
.execute("TRUNCATE TABLE follows")
817-
.unwrap();
818-
// req.with_path("/api/v1/crates/foo_following/follow")
819-
// .with_method(Method::Delete);
820-
// let mut response = ok_resp!(middle.call(&mut req));
821-
// assert!(::json::<O>(&mut response).ok);
822-
// let mut response = ok_resp!(middle.call(&mut req));
823-
// assert!(::json::<O>(&mut response).ok);
824-
825-
// req.with_path("/api/v1/crates/foo_following/following")
826-
// .with_method(Method::Get);
827-
// let mut response = ok_resp!(middle.call(&mut req));
828-
// assert!(!::json::<F>(&mut response).following);
808+
req.with_path("/api/v1/crates/foo_following/follow")
809+
.with_method(Method::Delete);
810+
let mut response = ok_resp!(middle.call(&mut req));
811+
assert!(::json::<O>(&mut response).ok);
812+
let mut response = ok_resp!(middle.call(&mut req));
813+
assert!(::json::<O>(&mut response).ok);
814+
815+
req.with_path("/api/v1/crates/foo_following/following")
816+
.with_method(Method::Get);
817+
let mut response = ok_resp!(middle.call(&mut req));
818+
assert!(!::json::<F>(&mut response).following);
829819

830820
req.with_path("/api/v1/crates")
831821
.with_query("following=1")

src/tests/user.rs

+25-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use conduit::{Handler, Method};
2+
use diesel::prelude::*;
3+
use diesel::insert;
24

35
use cargo_registry::Model;
6+
use cargo_registry::db::RequestTransaction;
47
use cargo_registry::krate::EncodableCrate;
8+
use cargo_registry::schema::versions;
59
use cargo_registry::user::{User, NewUser, EncodableUser};
6-
use cargo_registry::db::RequestTransaction;
710
use cargo_registry::version::EncodableVersion;
811

912
#[derive(RustcDecodable)]
@@ -139,10 +142,27 @@ fn following() {
139142
#[derive(RustcDecodable)] struct Meta { more: bool }
140143

141144
let (_b, app, middle) = ::app();
142-
let mut req = ::req(app, Method::Get, "/");
143-
::mock_user(&mut req, ::user("foo"));
144-
::mock_crate(&mut req, ::krate("foo_fighters"));
145-
::mock_crate(&mut req, ::krate("bar_fighters"));
145+
let mut req = ::req(app.clone(), Method::Get, "/");
146+
{
147+
let conn = app.diesel_database.get().unwrap();
148+
let user = ::new_user("foo").create_or_update(&conn).unwrap();
149+
::sign_in_as(&mut req, &user);
150+
#[derive(Insertable)]
151+
#[table_name="versions"]
152+
struct NewVersion<'a> {
153+
crate_id: i32,
154+
num: &'a str,
155+
}
156+
let id1 = ::new_crate("foo_fighters").create_or_update(&conn, None, user.id)
157+
.unwrap().id;
158+
let id2 = ::new_crate("bar_fighters").create_or_update(&conn, None, user.id)
159+
.unwrap().id;
160+
let new_versions = vec![
161+
NewVersion { crate_id: id1, num: "1.0.0" },
162+
NewVersion { crate_id: id2, num: "1.0.0" },
163+
];
164+
insert(&new_versions).into(versions::table).execute(&*conn).unwrap();
165+
}
146166

147167
let mut response = ok_resp!(middle.call(req.with_path("/me/updates")
148168
.with_method(Method::Get)));

0 commit comments

Comments
 (0)