Skip to content

Commit e1bebc6

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 113ad2d commit e1bebc6

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
@@ -9,8 +9,10 @@ use std::sync::Arc;
99
use conduit::{Request, Response};
1010
use conduit_router::RequestParams;
1111
use curl::easy::Easy;
12+
use diesel;
13+
use diesel::associations::Identifiable;
1214
use diesel::prelude::*;
13-
use diesel::pg::PgConnection;
15+
use diesel::pg::{Pg, PgConnection};
1416
use diesel::pg::upsert::*;
1517
use diesel_full_text_search::*;
1618
use license_exprs;
@@ -41,7 +43,8 @@ use util::{RequestUtils, CargoResult, internal, ChainError, human};
4143
use version::EncodableVersion;
4244
use schema::*;
4345

44-
#[derive(Clone, Queryable, Identifiable, AsChangeset)]
46+
#[derive(Clone, Queryable, Identifiable, AsChangeset, Associations)]
47+
#[has_many(versions)]
4548
pub struct Crate {
4649
pub id: i32,
4750
pub name: String,
@@ -70,6 +73,8 @@ pub const ALL_COLUMNS: AllColumns = (crates::id, crates::name,
7073
crates::readme, crates::license, crates::repository,
7174
crates::max_upload_size);
7275

76+
type CrateQuery<'a> = crates::BoxedQuery<'a, Pg, <AllColumns as Expression>::SqlType>;
77+
7378
#[derive(RustcEncodable, RustcDecodable)]
7479
pub struct EncodableCrate {
7580
pub id: String,
@@ -230,6 +235,15 @@ impl<'a> NewCrate<'a> {
230235
}
231236

232237
impl Crate {
238+
pub fn by_name(name: &str) -> CrateQuery {
239+
crates::table
240+
.select(ALL_COLUMNS)
241+
.filter(
242+
canon_crate_name(crates::name).eq(
243+
canon_crate_name(name))
244+
).into_boxed()
245+
}
246+
233247
pub fn find_by_name(conn: &GenericConnection,
234248
name: &str) -> CargoResult<Crate> {
235249
let stmt = conn.prepare("SELECT * FROM crates \
@@ -1149,44 +1163,61 @@ fn user_and_crate(req: &mut Request) -> CargoResult<(User, Crate)> {
11491163
Ok((user.clone(), krate))
11501164
}
11511165

1166+
#[derive(Insertable, Queryable, Identifiable, Associations)]
1167+
#[belongs_to(User)]
1168+
#[primary_key(user_id, crate_id)]
1169+
#[table_name="follows"]
1170+
pub struct Follow {
1171+
user_id: i32,
1172+
crate_id: i32,
1173+
}
1174+
1175+
fn follow_target(req: &mut Request) -> CargoResult<Follow> {
1176+
let user = req.user()?;
1177+
let conn = req.db_conn()?;
1178+
let crate_name = &req.params()["crate_id"];
1179+
let crate_id = Crate::by_name(crate_name)
1180+
.select(crates::id)
1181+
.first(conn)?;
1182+
Ok(Follow {
1183+
user_id: user.id,
1184+
crate_id: crate_id,
1185+
})
1186+
}
1187+
11521188
/// Handles the `PUT /crates/:crate_id/follow` route.
11531189
pub fn follow(req: &mut Request) -> CargoResult<Response> {
1154-
let (user, krate) = user_and_crate(req)?;
1155-
let tx = req.tx()?;
1156-
let stmt = tx.prepare("SELECT 1 FROM follows
1157-
WHERE user_id = $1 AND crate_id = $2")?;
1158-
let rows = stmt.query(&[&user.id, &krate.id])?;
1159-
if !rows.iter().next().is_some() {
1160-
tx.execute("INSERT INTO follows (user_id, crate_id)
1161-
VALUES ($1, $2)", &[&user.id, &krate.id])?;
1162-
}
1190+
let follow = follow_target(req)?;
1191+
let conn = req.db_conn()?;
1192+
diesel::insert(&follow.on_conflict_do_nothing())
1193+
.into(follows::table)
1194+
.execute(conn)?;
11631195
#[derive(RustcEncodable)]
11641196
struct R { ok: bool }
11651197
Ok(req.json(&R { ok: true }))
11661198
}
11671199

11681200
/// Handles the `DELETE /crates/:crate_id/follow` route.
11691201
pub fn unfollow(req: &mut Request) -> CargoResult<Response> {
1170-
let (user, krate) = user_and_crate(req)?;
1171-
let tx = req.tx()?;
1172-
tx.execute("DELETE FROM follows
1173-
WHERE user_id = $1 AND crate_id = $2",
1174-
&[&user.id, &krate.id])?;
1202+
let follow = follow_target(req)?;
1203+
let conn = req.db_conn()?;
1204+
diesel::delete(&follow).execute(conn)?;
11751205
#[derive(RustcEncodable)]
11761206
struct R { ok: bool }
11771207
Ok(req.json(&R { ok: true }))
11781208
}
11791209

11801210
/// Handles the `GET /crates/:crate_id/following` route.
11811211
pub fn following(req: &mut Request) -> CargoResult<Response> {
1182-
let (user, krate) = user_and_crate(req)?;
1183-
let tx = req.tx()?;
1184-
let stmt = tx.prepare("SELECT 1 FROM follows
1185-
WHERE user_id = $1 AND crate_id = $2")?;
1186-
let rows = stmt.query(&[&user.id, &krate.id])?;
1212+
use diesel::expression::dsl::exists;
1213+
1214+
let follow = follow_target(req)?;
1215+
let conn = req.db_conn()?;
1216+
let following = diesel::select(exists(follows::table.find(follow.id())))
1217+
.get_result(conn)?;
11871218
#[derive(RustcEncodable)]
11881219
struct R { following: bool }
1189-
Ok(req.json(&R { following: rows.iter().next().is_some() }))
1220+
Ok(req.json(&R { following: following }))
11901221
}
11911222

11921223
/// 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
@@ -753,63 +753,53 @@ fn dependencies() {
753753

754754
#[test]
755755
fn following() {
756-
// #[derive(RustcDecodable)] struct F { following: bool }
757-
// #[derive(RustcDecodable)] struct O { ok: bool }
756+
#[derive(RustcDecodable)] struct F { following: bool }
757+
#[derive(RustcDecodable)] struct O { ok: bool }
758758

759759
let (_b, app, middle) = ::app();
760760
let mut req = ::req(app.clone(), Method::Get, "/api/v1/crates/foo_following/following");
761761

762762
let user;
763-
let krate;
764763
{
765764
let conn = app.diesel_database.get().unwrap();
766765
user = ::new_user("foo").create_or_update(&conn).unwrap();
767766
::sign_in_as(&mut req, &user);
768-
krate = ::new_crate("foo_following").create_or_update(&conn, None, user.id).unwrap();
769-
770-
// FIXME: Go back to hitting the actual endpoint once it's using Diesel
771-
conn
772-
.execute(&format!("INSERT INTO follows (user_id, crate_id) VALUES ({}, {})",
773-
user.id, krate.id))
774-
.unwrap();
767+
::new_crate("foo_following").create_or_update(&conn, None, user.id).unwrap();
775768
}
776769

777-
// let mut response = ok_resp!(middle.call(&mut req));
778-
// assert!(!::json::<F>(&mut response).following);
770+
let mut response = ok_resp!(middle.call(&mut req));
771+
assert!(!::json::<F>(&mut response).following);
779772

780-
// req.with_path("/api/v1/crates/foo_following/follow")
781-
// .with_method(Method::Put);
782-
// let mut response = ok_resp!(middle.call(&mut req));
783-
// assert!(::json::<O>(&mut response).ok);
784-
// let mut response = ok_resp!(middle.call(&mut req));
785-
// assert!(::json::<O>(&mut response).ok);
773+
req.with_path("/api/v1/crates/foo_following/follow")
774+
.with_method(Method::Put);
775+
let mut response = ok_resp!(middle.call(&mut req));
776+
assert!(::json::<O>(&mut response).ok);
777+
let mut response = ok_resp!(middle.call(&mut req));
778+
assert!(::json::<O>(&mut response).ok);
786779

787-
// req.with_path("/api/v1/crates/foo_following/following")
788-
// .with_method(Method::Get);
789-
// let mut response = ok_resp!(middle.call(&mut req));
790-
// assert!(::json::<F>(&mut response).following);
780+
req.with_path("/api/v1/crates/foo_following/following")
781+
.with_method(Method::Get);
782+
let mut response = ok_resp!(middle.call(&mut req));
783+
assert!(::json::<F>(&mut response).following);
791784

792785
req.with_path("/api/v1/crates")
793-
.with_query("following=1");
786+
.with_method(Method::Get)
787+
.with_query("following=1");
794788
let mut response = ok_resp!(middle.call(&mut req));
795789
let l = ::json::<CrateList>(&mut response);
796790
assert_eq!(l.crates.len(), 1);
797791

798-
// FIXME: Go back to hitting the actual endpoint once it's using Diesel
799-
req.db_conn().unwrap()
800-
.execute("TRUNCATE TABLE follows")
801-
.unwrap();
802-
// req.with_path("/api/v1/crates/foo_following/follow")
803-
// .with_method(Method::Delete);
804-
// let mut response = ok_resp!(middle.call(&mut req));
805-
// assert!(::json::<O>(&mut response).ok);
806-
// let mut response = ok_resp!(middle.call(&mut req));
807-
// assert!(::json::<O>(&mut response).ok);
808-
809-
// req.with_path("/api/v1/crates/foo_following/following")
810-
// .with_method(Method::Get);
811-
// let mut response = ok_resp!(middle.call(&mut req));
812-
// assert!(!::json::<F>(&mut response).following);
792+
req.with_path("/api/v1/crates/foo_following/follow")
793+
.with_method(Method::Delete);
794+
let mut response = ok_resp!(middle.call(&mut req));
795+
assert!(::json::<O>(&mut response).ok);
796+
let mut response = ok_resp!(middle.call(&mut req));
797+
assert!(::json::<O>(&mut response).ok);
798+
799+
req.with_path("/api/v1/crates/foo_following/following")
800+
.with_method(Method::Get);
801+
let mut response = ok_resp!(middle.call(&mut req));
802+
assert!(!::json::<F>(&mut response).following);
813803

814804
req.with_path("/api/v1/crates")
815805
.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)