This workshop will be retired on May 31, 2020.
Creating and Reading Reviews16:34 with Pasan Premaratne
With courses out of the way we can define routes for review operations starting with POST and GET
Provided below are two download links for Postman collections to test the routes built in the course. Treehouse Review Routes contain Postman API calls to just the routes built for the Reviews model. The second link, Treehouse Reviews, includes API calls for all routes. You can use either. Just select import at the top of Postman and locate the JSON file to add them.
With courses out of the way, we can define routes for review operations, and 0:00 we'll continue to do this in routes.swift. 0:04 So we'll add in MARK here. 0:07 Remember, if you're not in Xcode, you don't need to do this. 0:08 Review Routes, and now it should be separated. 0:11 Okay, the big difference here is that since reviews are attached to courses, 0:16 the URLs through review operations should also include a course parameter in them. 0:21 Which means we have to do some additional work for each operation. 0:26 We'll start by creating a review. 0:29 Technically, we shouldn't be able to create a review 0:32 without an associated course. 0:35 So we're not going to define an api/reviews route in order 0:37 to prevent that from happening. 0:41 Instead, we'll need to do it through a course as a POST request. 0:43 And this is going to be at /api/courses/:courseId/ /reviews, okay. 0:48 And this will create a new review for a course. 0:54 And then we're going to include the review information in the JSON body. 0:59 The URL for the post operation is going to be api/courses followed by 1:07 the courseID to specify the course and then the reviews. 1:12 Just like with the post operation on course, 1:16 that info we needed will be sent in the JSON body so we'll need to decode it. 1:18 The URL will be at router.post("api", 1:22 "courses", Course.parameter, "reviews"). 1:27 As always, we'll supply a request to the closure and 1:34 we're going to get back a Future>Review<. 1:38 Inside the closure, we need to carry out two operations. 1:44 First, we need to get the course whose ID is specified as a path in the URL and 1:47 then decode the JSON into a review object. 1:53 This way we can assert that the course specified in the URL and 1:57 the ID specified in the JSON body are the same, and there's no mismatch. 2:01 Now before we can do any of this, we need to make review conformed to all the right 2:05 protocols to let Vapor handle our decoding magic. 2:09 So we'll navigate to review.swift. 2:12 And at the bottom here we'll conform to the content protocol and 2:15 the parameter protocol. 2:21 Back in routes.swift, we'll start by using a flat map to execute two 2:25 future generating operations that resolve into one value. 2:30 So return try flatMap. 2:34 And we want to eventually return a review, so 2:38 we are going to specify the type here as Review.self. 2:40 The first operation we want to execute is to get a course from the ID specified and 2:44 this is the same, like we did in the last video. 2:48 So, req.parameters.next (Course.self). 2:50 The second is to decode the JSON body containing the review 2:54 information into a review object. 2:57 But now since review conforms to content, 3:00 again we can let Vapor handle this as well. 3:02 So we'll say req.content.decode(Review.self). 3:05 Both of these values are passed into the callbacks, so 3:10 here we have a course object and a review object. 3:14 We haven't defined or seen the JSON body yet, but 3:17 based on how we've defined our model. 3:20 And you can switch back to Review.swift to check it out. 3:22 We've specified that the course ID property is required and 3:25 cannot be optional. 3:29 This means that for Vapor to decode into review from the JSON body, 3:30 we need to supply a course ID in the JSON. 3:35 This is obviously duplicated effort, 3:38 because there's already a course specified in the URL. 3:40 So we technically know which course we're attaching this review to, 3:43 and we could assign the ID property during initialization. 3:47 But we don't want to specify that the property's optional because it 3:51 really isn't. 3:54 And that just makes this video longer. 3:55 Because it's so easy to get both the course from the parameter and 3:57 to decode the JSON, we're going to ignore the fact that we're repeating some logic. 4:01 Instead we'll make sure that the JSON is assigned to the right course for 4:05 the sake of this content by checking the course ID in the URL and 4:10 the JSON body to see if they match up. 4:14 If they don't then we'll abort this operation early and send a failure back. 4:16 So here we can say guard course.id == review.courseID. 4:21 Else if this doesn't match, then we'll throw, remember that this is 4:28 a throwing operation at the very top, right here, so we can throw from inside. 4:33 And we're gonna say abort which is an error defined in Vapor and 4:38 the type is badRequest. 4:44 And we can provide a text reason or a string, and 4:47 here we'll say Course ID specified in JSON does not match. 4:51 Another way to handle this would be to allow reviews to be created using 4:57 the URL API Reviews /reviews much like the URL for courses. 5:02 But this isn't good API design, though. 5:06 Since reviews are modeled as part of a course, 5:08 we want the API endpoints to reflect this. 5:11 Allowing for a direct reviews endpoint would signify that reviews can be created 5:13 without an associated course, which is incorrect. 5:18 Okay, so if the Course ID and reviews course ID match, we can go ahead and 5:21 save it. 5:25 So we'll return review.save(on: req). 5:26 That's all for this post operation. 5:31 Now for the review routes, instead of defining requests and testing it in 5:33 Postman individually like we did in the last one, I've done all the work for you. 5:37 So in the teacher's notes, look for a JSON file name Treehouse Review Routes. 5:40 Download this and import it into Postman, and 5:45 every time we add review just run the entire collection. 5:48 And make sure the relevant request return the right status codes. 5:52 I'll leave that up to you now. 5:54 Okay, so next is the GET route. 5:56 First is the one to return all the reviews for a single course, and 6:01 this is going to be at /api/courses /:courseId/reviews. 6:05 So, same as last time, this is our get request. 6:10 So Return all reviews for a course. 6:14 Since this is the same, I'm going to go up here and just copy this and 6:20 make a few changes. 6:25 So this is a get request. 6:28 And then this is going to return an array of reviews inside a future 6:30 because we are asking for all the reviews. 6:35 Now inside the body, we need to figure out which course is specified in the URL. 6:38 We did that for the previous route, right up top, but 6:43 this time we only need to resolve one future before specifying our logic. 6:46 So rather than starting with a call to flatMap, 6:50 we'll start by fetching the relevant course. 6:52 Return try req.parameters.next (Course.self) and 6:55 you know that this returns a future that resolves into a course. 7:00 We then need to return a future that resolves into an array of review objects, 7:06 so we're going to call flatMap on this. 7:11 The ultimate type is review Array of [Review].self. 7:13 Now be careful here. 7:16 It's important that you specify array of [Review].self so 7:18 array of [Review] is the type, rather than array of [Review].self. 7:21 Right, because that would resolve it to something different and 7:25 you'd get an error back. 7:29 The call back contains, as an argument, the course, from the first future we 7:31 called, and now we need to fetch all the reviews for this individual course. 7:36 One way we could do that would be to query all reviews and 7:41 filter out those whose course ID property matches the course ID value. 7:44 But there's actually an easier way. 7:48 In our model, reviews belong to courses. 7:51 And in fact, we've already done the work to setup a parent-child relationship 7:53 because every review object links to its parent via the course ID property. 7:58 To fetch all reviews for a given course, we need a way to query this relationship. 8:02 And Vapor makes this really simple with its generic children property. 8:07 So navigate to course.swift. 8:11 And in here we're going to add a computed property. 8:14 We'll do it above all these extensions, extension, 8:16 we'll do this in an extension as well, extension Course. 8:19 And the property here, we'll call it reviews. 8:23 The type for the property is Fluent's children type and 8:27 this is a generic type that reflects the many aspect of a one-to-many relationship. 8:31 It is modeled in the parent. 8:37 So, course is the parent here, review is the child. 8:39 And it returns all the models that contain a reference to the parents identifier. 8:43 When specifying the type, we need to provide concrete types for 8:48 two generic parameters. 8:52 So first the parent, the Course, and then the child type, Review. 8:53 Now inside the body of the property, we'll use a function, 8:58 Children defined in Fluent to handle the actual query for us. 9:02 So, we'll say return children. 9:06 As you can see here the function takes a key path on the child 9:08 reference to retrieve all the children. 9:13 Since we specified the child as review, when we define the generic parameters, 9:16 Fluent already knows which model to query. 9:21 So all we need to do is specify a key path to the property that references 9:24 the parent's identifier. 9:28 And here we'll say, so \.courseId. 9:30 And if you go to the review type, so go to review.swift, that's the model or 9:36 that's the property that models the parent relationship, cool. 9:41 Now Fluent should be able to return all the reviews for a given course. 9:45 Aren't ORMs the best? 9:49 So back in routes.swift, 9:51 we can use this computed property to return all reviews for a course. 9:53 So we'll say try, course.reviews.query(on: req). 9:58 Remember this is what we did before, sort of. 10:04 So this calling query doesn't actually return anything. 10:07 It creates a query object, but 10:10 here we're creating a query builder object on the reviews property on course. 10:12 And to return all of them, but before we just call all. 10:17 If you build in run and then run the route collection in Postman, 10:20 the first four requests should return a status code of 200. 10:24 You can also inspect the response body on the get all reviews route to see 10:27 which reviews we created and make sure that they're there, all right? 10:32 So everything looks good. 10:37 Next up, let's add an individual review for a given course, or 10:39 let's get an individual review for a given course. 10:42 So this will be GET, and we'll copy this path here. 10:45 Cuz it's more or less the same, 10:52 except at the end we're gonna tack on a reviewId to get that individual review. 10:54 So here we'll return a single review. 10:59 Now the URL is getting rather long here. 11:03 In addition to the previous path we also have this reviewId as an argument and 11:06 we can work this out in one of two ways. 11:10 So like we specified a course parameter directly in the path, 11:13 we could also list the path component as review.parameter. 11:17 So for example, we'll grab that here and 11:21 then I'm going to paste it in because this is the same method, same path. 11:23 And we could, so over here we could say review.parameter and 11:27 then we could decode the database from the database. 11:31 The review object that matches the ID specified. 11:35 But this seems like unnecessary work really. 11:38 If we're decoding a review here or we're fetching a review directly 11:40 from a database, well presumably we can go ahead and return it. 11:44 We don't need to do anything with this course object. 11:48 But instead let's do it the right way. 11:50 We'll assume that the last path component is a plain integer value. 11:53 So we'll say int.parameter. 11:56 Since we're returning a single review object over here, 11:59 the type that we're returning is Future<Review>. 12:03 And then inside the body, 12:07 the strategy we're going to take is we're going to decode the course. 12:09 We're going to fetch all of the associated reviews and 12:12 then filter it out to get one that matches this ID parameter specified, all right? 12:16 And that makes a bit more sense in my opinion because if we're just going to 12:21 fetch the review directly using that review.parameter why not just go ahead and 12:25 return it. 12:29 But this way I get to show you some other stuff. 12:30 All right, so the first thing we need to do is fetch from the database the course 12:32 specified as a path component. 12:36 So return try req.parameters.next. 12:38 And obviously I'm just going to kind of blur through the typing here. 12:43 Because you should be familiar with this. 12:44 And then using this course we want to fetch all the reviews on it first. 12:47 And then return one of those. 12:51 Since we're returning a single review we'll flatMap(to: Review.self). 12:53 In the callback, we have access to that course that is resolved. 12:58 And now in here, we can decode the int parameter so 13:02 that we can use that as an ID that we filter on. 13:06 So let's say let reviewId = try req.parameters.next. 13:09 So the same thing we did earlier. 13:16 Here we'll say Int.self. 13:18 Next we want to fetch all reviews associated with a course so 13:20 that we can filter on them. 13:24 We've done this before in the previous route handler. 13:26 So let's just copy that path in, or part, rather. 13:29 So here I'm going to add a return statement and then paste that. 13:33 So return. 13:36 Instead of all reviews though we're going to do something different. 13:38 So we create the query builder object, but this time we're going to call filter. 13:41 We'll say .filter. 13:46 Filter takes a key path that we're going to filter against on the review model and 13:47 then match that up with a value. 13:52 So here we'll say the key path that we want to query is Review.id. 13:55 And we want to get the object whose Review.id property matches 14:00 the reviewId parameter in the path. 14:05 In theory, this filter operation could return more than one review. 14:08 So the return type here when calling filter is actually an array of values. 14:12 And we are going to call first on this to get a first match. 14:15 But you'll see an error here, and that's because to actually use the filter 14:20 method we need to import inside this file a Fluent framework. 14:24 Otherwise, you'll get this weird error back that there's no types and 14:28 a bunch of confusing stuff. 14:32 Okay, all right, so if we call first now, we'll still get an error back. 14:34 And the issue is that first returns an optional review object since one may 14:39 not exist at all. 14:44 Calling first on any array in Swift returns an optional value. 14:45 So to account for this, 14:50 we need to actually map this potential value to a non-optional review. 14:51 Remember that we use Map when we want to return anything other than a Future. 14:56 We're going to just map to a review object here, but 15:01 this outer flatMap that we've called, right, at the top here, 15:04 will take that single review object and wrap it back up in a fuutre. 15:08 A bit confusing, I know. 15:13 Took me a while to figure it out. 15:14 So I'll say map (to: Review.self). 15:15 And to the closure here, we're supplying the result from this filter.first call. 15:22 Which is a filteredReview object. 15:30 filteredReview, the value that is passed into our innermost closure when 15:34 the operations resolve, is an optional value. 15:39 So inside the body let's check if it's an optional and return the value if it's not. 15:42 So I'll say guard let filteredReview = filteredReview else. 15:46 At the bottom, we can go ahead and return filteredReview, the non optional version. 15:53 And in the else clause again we'll throw an Abort, we'll say object notFound. 16:00 So a notFound error and the reason here is reason: 16:05 "No review available for specified ID". 16:12 Okay, open up Postman and rerun the collection. 16:19 You should now see that the get single review for 16:23 course request succeeded with a 200. 16:25 Let's stop here. 16:28 In the next video, let's add a put and 16:29 delete handler to finish up all our routes. 16:31
You need to sign up for Treehouse in order to download course files.Sign up