WIP - ActixWeb multi-tenant blog and newsletter API server. Originally forked from LukeMathWalker/zero-to-production.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fixes property value mapping in associated user for public newsletter queries

+71 -13
+2 -2
.sqlx/query-cdb1a6f44a09c14eeb797205c71c15493f9b15791173985a33c6c01d48206996.json .sqlx/query-d193d6ec6eb816835fb673502b9a718339e5949934d5ab3a2cbdb57ff0477a4b.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "\n SELECT\n newsletter_issues.content,\n newsletter_issues.description,\n newsletter_issues.published_at,\n newsletter_issues.slug,\n newsletter_issues.title,\n (\n users.username,\n user_profiles.avatar_url,\n user_profiles.banner_url,\n user_profiles.display_name,\n user_profiles.description\n ) AS \"user!: AssociatedUser\"\n FROM newsletter_issues\n JOIN users ON newsletter_issues.user_id = users.user_id\n JOIN user_profiles ON newsletter_issues.user_id = user_profiles.user_id\n WHERE newsletter_issues.published_at IS NOT NULL\n AND users.username = $1\n AND newsletter_issues.slug = $2\n LIMIT 1\n ", 3 + "query": "\n SELECT\n newsletter_issues.content,\n newsletter_issues.description,\n newsletter_issues.published_at,\n newsletter_issues.slug,\n newsletter_issues.title,\n (\n user_profiles.avatar_url,\n user_profiles.banner_url,\n user_profiles.description,\n user_profiles.display_name,\n users.username\n ) AS \"user!: AssociatedUser\"\n FROM newsletter_issues\n JOIN users ON newsletter_issues.user_id = users.user_id\n JOIN user_profiles ON newsletter_issues.user_id = user_profiles.user_id\n WHERE newsletter_issues.published_at IS NOT NULL\n AND users.username = $1\n AND newsletter_issues.slug = $2\n LIMIT 1\n ", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 49 49 null 50 50 ] 51 51 }, 52 - "hash": "cdb1a6f44a09c14eeb797205c71c15493f9b15791173985a33c6c01d48206996" 52 + "hash": "d193d6ec6eb816835fb673502b9a718339e5949934d5ab3a2cbdb57ff0477a4b" 53 53 }
+2 -2
.sqlx/query-ee6b8b1e87856f97194cd46467bad42d2f4671337c75d822435ef30a2e4b5afa.json .sqlx/query-9d3037e3bfef9b0c2b00f3e306fec3babfb52920048f7b32eb30dd1f028aff32.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "\n SELECT\n newsletter_issues.description,\n newsletter_issues.published_at,\n newsletter_issues.slug,\n newsletter_issues.title,\n (\n users.username,\n user_profiles.avatar_url,\n user_profiles.banner_url,\n user_profiles.display_name,\n user_profiles.description\n ) AS \"user!: AssociatedUser\"\n FROM newsletter_issues\n JOIN users ON newsletter_issues.user_id = users.user_id\n JOIN user_profiles ON newsletter_issues.user_id = user_profiles.user_id\n WHERE newsletter_issues.published_at IS NOT NULL\n ORDER BY published_at DESC\n LIMIT 10\n ", 3 + "query": "\n SELECT\n newsletter_issues.description,\n newsletter_issues.published_at,\n newsletter_issues.slug,\n newsletter_issues.title,\n (\n user_profiles.avatar_url,\n user_profiles.banner_url,\n user_profiles.description,\n user_profiles.display_name,\n users.username\n ) AS \"user!: AssociatedUser\"\n FROM newsletter_issues\n JOIN users ON newsletter_issues.user_id = users.user_id\n JOIN user_profiles ON newsletter_issues.user_id = user_profiles.user_id\n WHERE newsletter_issues.published_at IS NOT NULL\n ORDER BY published_at DESC\n LIMIT 10\n ", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 40 40 null 41 41 ] 42 42 }, 43 - "hash": "ee6b8b1e87856f97194cd46467bad42d2f4671337c75d822435ef30a2e4b5afa" 43 + "hash": "9d3037e3bfef9b0c2b00f3e306fec3babfb52920048f7b32eb30dd1f028aff32" 44 44 }
+2 -2
.sqlx/query-ef5f2a02961a795d62468d8e132705e3e4cf1beaf5b45df0d2d9e91285c6761e.json .sqlx/query-874e988c402b4b673cf2c4a0d6bb580f09fdb6be66d340a3fcd36b97ff9b8943.json
··· 1 1 { 2 2 "db_name": "PostgreSQL", 3 - "query": "\n SELECT\n newsletter_issues.description,\n newsletter_issues.published_at,\n newsletter_issues.slug,\n newsletter_issues.title,\n (\n users.username,\n user_profiles.avatar_url,\n user_profiles.banner_url,\n user_profiles.display_name,\n user_profiles.description\n ) AS \"user!: AssociatedUser\"\n FROM newsletter_issues\n JOIN users ON newsletter_issues.user_id = users.user_id\n JOIN user_profiles ON newsletter_issues.user_id = user_profiles.user_id\n WHERE newsletter_issues.published_at IS NOT NULL\n AND users.username = $1\n ORDER BY published_at DESC\n LIMIT 10\n ", 3 + "query": "\n SELECT\n newsletter_issues.description,\n newsletter_issues.published_at,\n newsletter_issues.slug,\n newsletter_issues.title,\n (\n user_profiles.avatar_url,\n user_profiles.banner_url,\n user_profiles.description,\n user_profiles.display_name,\n users.username\n ) AS \"user!: AssociatedUser\"\n FROM newsletter_issues\n JOIN users ON newsletter_issues.user_id = users.user_id\n JOIN user_profiles ON newsletter_issues.user_id = user_profiles.user_id\n WHERE newsletter_issues.published_at IS NOT NULL\n AND users.username = $1\n ORDER BY published_at DESC\n LIMIT 10\n ", 4 4 "describe": { 5 5 "columns": [ 6 6 { ··· 42 42 null 43 43 ] 44 44 }, 45 - "hash": "ef5f2a02961a795d62468d8e132705e3e4cf1beaf5b45df0d2d9e91285c6761e" 45 + "hash": "874e988c402b4b673cf2c4a0d6bb580f09fdb6be66d340a3fcd36b97ff9b8943" 46 46 }
+11 -6
src/models/newsletter.rs
··· 244 244 Ok(self.newsletter_issue_id) 245 245 } 246 246 247 + // Note - For serializing nested records sqlx allows returning 248 + // sequence of values, which are then mapped to the key names in 249 + // order. This means the order of the columns in the query must 250 + // reflect the order in the struct definition. For casted values, 251 + // sqlx only checks that the column exists. 247 252 pub async fn find_public_newsletter( 248 253 username: String, 249 254 slug: String, ··· 259 264 newsletter_issues.slug, 260 265 newsletter_issues.title, 261 266 ( 262 - users.username, 263 267 user_profiles.avatar_url, 264 268 user_profiles.banner_url, 269 + user_profiles.description, 265 270 user_profiles.display_name, 266 - user_profiles.description 271 + users.username 267 272 ) AS "user!: AssociatedUser" 268 273 FROM newsletter_issues 269 274 JOIN users ON newsletter_issues.user_id = users.user_id ··· 292 297 newsletter_issues.slug, 293 298 newsletter_issues.title, 294 299 ( 295 - users.username, 296 300 user_profiles.avatar_url, 297 301 user_profiles.banner_url, 302 + user_profiles.description, 298 303 user_profiles.display_name, 299 - user_profiles.description 304 + users.username 300 305 ) AS "user!: AssociatedUser" 301 306 FROM newsletter_issues 302 307 JOIN users ON newsletter_issues.user_id = users.user_id ··· 323 328 newsletter_issues.slug, 324 329 newsletter_issues.title, 325 330 ( 326 - users.username, 327 331 user_profiles.avatar_url, 328 332 user_profiles.banner_url, 333 + user_profiles.description, 329 334 user_profiles.display_name, 330 - user_profiles.description 335 + users.username 331 336 ) AS "user!: AssociatedUser" 332 337 FROM newsletter_issues 333 338 JOIN users ON newsletter_issues.user_id = users.user_id
+54 -1
tests/api/newsletters/detail/index.rs
··· 1 1 use crate::helpers::spawn_app; 2 - use newsletter_api::models::{NewsletterIssueAPI, PublicNewsletter}; 2 + use newsletter_api::models::{NewsletterIssueAPI, PublicNewsletter, UserProfile}; 3 3 4 4 #[tokio::test] 5 5 async fn nonexistent_path_params_return_not_found() { ··· 50 50 assert_eq!(&response_body.description, "Newsletter description"); 51 51 assert_eq!(&response_body.content, "<h2>Newsletter body as HTML</h2>",); 52 52 } 53 + 54 + #[tokio::test] 55 + async fn public_newsletter_includes_associated_user() { 56 + // Arrange 57 + let app = spawn_app().await; 58 + app.test_user.login(&app).await; 59 + 60 + // Act 1 - Update user profile 61 + UserProfile { 62 + description: "Description".to_string(), 63 + display_name: "Display name".to_string(), 64 + bio: "Bio".to_string(), 65 + user_id: app.test_user.user_id.clone(), 66 + } 67 + .update(&app.db_pool) 68 + .await 69 + .expect("Failed to update profile"); 70 + 71 + // Act 2 - Publish newsletter issue 72 + app.post_admin_create_newsletter(&serde_json::json!({ 73 + "title": "Newsletter title", 74 + "description": "Newsletter description", 75 + "content": "## Newsletter body as HTML", 76 + "cover_image": "", 77 + })) 78 + .await; 79 + 80 + let response = app.get_admin_unpublished_newsletter_issues().await; 81 + let response_body: Vec<NewsletterIssueAPI> = response.json().await.unwrap(); 82 + let newsletter_issue_id = response_body[0].newsletter_issue_id; 83 + 84 + app.put_admin_publish_newsletter( 85 + &newsletter_issue_id, 86 + &serde_json::json!({ 87 + "idempotency_key": uuid::Uuid::new_v4().to_string() 88 + }), 89 + ) 90 + .await; 91 + 92 + // Act 3 - Retrieve endpoint for existing issue 93 + let response = app 94 + .get_public_newsletter(&app.test_user.username, &"newsletter-title".to_string()) 95 + .await; 96 + assert_eq!(200, response.status().as_u16()); 97 + 98 + let response_body: PublicNewsletter = response.json().await.unwrap(); 99 + assert_eq!(&response_body.title, "Newsletter title"); 100 + assert_eq!(&response_body.description, "Newsletter description"); 101 + assert_eq!(&response_body.content, "<h2>Newsletter body as HTML</h2>",); 102 + assert_eq!(&response_body.user.description, "Description"); 103 + assert_eq!(&response_body.user.display_name, "Display name"); 104 + assert_eq!(&response_body.user.username, &app.test_user.username); 105 + }