Fetching Pages with Fluent

Fetching pages with Fluent in Vapor, with a bit of Leaf and fun with Futures.

What are we trying to do?

One of the fairly obvious things to do in Cheer (the app that powers this site) is to display a page based on the URL slug. Plenty of RESTful example exist that abstract away some of the complexity of fetching and returning a matching object from the database, but we wanted to experiment with more complex queries for use in other apps. It also seemed like a good opportunity to experiment with Futures, and flatMaps.

It also seemed like a good opportunity to wonder what would have happened if WebObjects had continued to evolve. Seriously—we had the most amazing rapid application development back in 1996, so sometimes it is amusing to see 'new' frameworks (re-)implementing what was in place over twenty years ago. But at least the new frameworks don't start out with that pesky $50,000 price tag...

Leaf context

We're going to return an HTML page using Leaf. In order to populate the Leaf template the easiest approach seemed to be to create a PageContext struct within our PageController with the information necessary to display:

    struct PageContext: Encodable {
        let page: Page
        let previousPage: Page?
        let nextPage: Page?
        let author: Future<Author>
        let tags: Future<[Tag]>?
    }

Earlier experiments passed a dictionary of labels and values, with the values being represented as Strings—such as a string for the page title, the page introduction, and so on. However it became easier to let Leaf accept a full object—such as a Page—and simply use key paths to get to the required fields.

In this stuct we're passing three Pages—the page that is to be shown, and also the newer and older page so that the navigation links work. We're also passing the Author and an array of Tags but for reasons that may become apparent in the future (yeah, that's a bad pun) they're passed as Futures.

Routing

Also in the PageController we should add our routes or our app would literally be going nowhere:

    func addRoutes(router: Router) {
        router.get("", use: displayHome)
        router.get(String.parameter, use: displayPage)
    }

If there are no parameters on the URL then we display the home page, if there's a String parameter then we use our displayPage function.

Displaying the page

Now we get to the fun part—displaying the page.

    func displayPage(_ request: Request) throws -> Future<View> {
        let slug = try request.parameters.next(String.self)
        print("display page = \(slug)")
        
        return Page.query(on: request)
            .filter(\.slug == slug)
            .first()
            .unwrap(or: Abort(.notFound))
            .flatMap { page -> Future<View> in
                return Page.query(on: request)
                        .filter(\.created > page.created)
                        .sort(\.created, .ascending)
                        .first()
                    .and(Page.query(on: request)
                        .filter(\.created < page.created)
                        .sort(\.created, .descending)
                        .first())
                    .flatMap { nextPage, previousPage -> Future<View> in
                        let context = PageContext(
                            page: page, 
                            previousPage: previousPage, 
                            nextPage: nextPage, 
                            author: page.toAuthor.get(on: request), 
                            tags: try? page.toTags.query(on: request).all()
                        )
                        return try request.view().render(Templates.page.rawValue, context)
                }
        }
    }

The first part is fairly straightforward—get the slug from the incoming URL. This page's URL should be https://cocoacheerleaders.com/fetching_pages_with_fluent, so the slug should be fetching_pages_with_fluent.

First we get Page to perform a query, which requires a database connection on which to perform; here we can simply use the incoming Request.

The .filter(\.slug == slug) ought to only return one object since the slug should be unique, but we'll need to do .first() to get a single result. But what if we didn't get a matching result? The .unwrap(or: Abort(.notFound)) takes care of this—if we fail to get a result we return .notFound instead.

Assuming we did get the page...

Summary

We've found the page, the previous page, and the next page, and we've passed this information to the Leaf template to render. Or we will have done, depending on whether the futures caught up.

Resources

Vapor's website.

Discuss?

Haven't added a commenting system to this app, and probably won't. So if you have any feedback—positive or negative—either say it via email to david@cocoacheerleaders.com or on Twitter at @CocoaCheer.