Server-Side Swift with MongoDB: Getting Started

In this Server-Side Swift tutorial you will learn how to setup MongoDB and use MongoKitten to run basic queries, build Aggregate Pipelines and store files with GridFS. By Joannis Orlandos.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Fetching a User

To log into the application, the login route in Routes.swift makes a call to findUser(byUsername:inDatabase:) in Repository.swift. Within each of the repository’s methods, you have access to the MongoDB database.

First, select the collection containing users, using a subscript. In Repository.swift within Repository, add the following line to findUser(byUsername:inDatabase:), before the return statement:

let usersCollection = database[User.collection]

Here, you’re subscripting the database with a string to access a collection. Also, notice that you don’t need to create collections. MongoDB creates them for you when you need them.

To query the collection for users, you’ll use a MongoCollection find operation. And because the repository is looking for a specific user, use findOne(_:as:).

Find operations can accept an argument for the filters that the result has to match. In MongoKitten, you can represent the query as a Document or as a MongoKittenQuery. MongoKittenQuery is more readable but does not support all features.

To create a document query, replace the return statement below the usersCollection line with the following code:

return usersCollection.findOne(
  "credentials.username" == username,
  as: User.self
)

To query the username, you first refer to the value by the whole path separated by a dot ( . ). Next, you use this keypath with the == operator to create an equality filter. Finally, you provide the value to compare to.

If a document matches the provided filter, MongoKitten will attempt to decode it as a User. After this, you return the resulting User.

To check that it works, build and run the application again.

You already have an example user from the first time the application launched. The username is me and the password is opensesame.

Visit the app in your browser and log in using the credentials above.

If you see the following error, you’re logged in!

{"error":true,"reason":"unimplemented"}

The thrown error is formatted as json

Stop the app.

Loading the Feed

Now that you can log in, it’s time to to generate the feed for the current user.

To do so, find the users that this user is following. For this use case, you’ll use the Repository method findUser(byId:inDatabase:). The implementation is similar to what you did in findUser(byUsername:inDatabase:), so give this a try first.

How’d you do? Does your findUser(byId:inDatabase:) read like this?

let users = database[User.collection] // 1
return users
  .findOne("_id" == userId, as: User.self) // 2
  .flatMapThrowing { user in // 3
    guard let user = user else {
      throw Abort(.notFound)
    }
    return user
}

In the code above, you:

  1. Get users collection.
  2. Find user with id.
  3. Unwrap the user or throw an error if nil.

To build up the feed, you add this list of user identifiers. Next, you add the user’s own identifier so that users see their own posts.

Replace the return statement in getFeed(forUser:inDatabase:) with the following:

return findUser(byId: userId, inDatabase: database)
  .flatMap { user in
    // Users you're following and yourself
    var feedUserIds = user.following
    feedUserIds.append(userId)
    
    let followingUsersQuery: Document = [
      "creator": [
        "$in": feedUserIds
      ]
    ]
    
    // More code coming. Ignore error message about return.
}

The $in filter tests if the creator field exists in feedUserIds.

With a find query, you can retrieve a list of all posts. Because most users are only interested in recent posts, you need to set a limit. A simple find would be perfect for returning an array of TimelinePost objects. But in this function, you need an array of ResolvedTimelinePost objects.

The difference between both models is the creator key. The entire user model is present in ResolvedTimelinePost. Leaf uses this information to present the author of the post.

A lookup aggregate stage is a perfect fit for this.

Creating Aggregate Pipelines

An aggregate pipeline is one of the best features of MongoDB. It works like a Unix pipe, where each operation’s output becomes the input of the next. The entire collection functions as the initial dataset.

To create an aggregate query, add the following code to getFeed(forUser:inDatabase:) under the comment More code coming.:

return database[TimelinePost.collection].buildAggregate { // 1
  match(followingUsersQuery) // 2
  sort([
    "creationDate": .descending
  ]) // 3
  limit(10) // 4
  lookup(
    from: User.collection,
    localField: "creator",
    foreignField: "_id",
    as: "creator"
  ) // 5
  unwind(fieldPath: "$creator") // 6
}
  .decode(ResolvedTimelinePost.self) // 7
  .allResults() // 8

Here’s what you’re doing:

  1. First, you create an aggregate pipeline based on function builders.
  2. Then, you filter the timeline posts to match followingUsersQuery.
  3. Next, you sort the timeline posts by creation date, so that recent posts are on top.
  4. And you limit the results to the first 10 posts, leaving the 10 most recent posts.
  5. Now, you look up the creators of this post. localField refers to the field inside TimelinePost. And foreignField refers to the field inside User. This operation returns all users that match this filter. Finally, this puts an array with results inside creator.
  6. Next, you limit creator to one user. As an array, creator can contain zero, one or many users. But for this project, it must always contain exactly one user. To accomplish this, unwind(fieldPath:) outputs one timeline post for each value in creator and then replaces the contents of creator with a single entity.
  7. You decode each result as a ResolvedTimelinePost.
  8. Finally, you execute the query and return all results.

The homepage will not render without suggesting users to follow. To verify that the feed works, replace the return statement of findSuggestedUsers(forUser:inDatabase:) with the following:

return database.eventLoop.makeSucceededFuture([])

Build and run, load the website, and you’ll see the first feed!


Default data is displayedYour first feed

Suggesting Users

The homepage only shows the posts by Ray Wenderlich and yourself. That happens because you’re only following Ray Wenderlich. To discover other users, you’ll create another pipeline in findSuggestedUsers(forUser:inDatabase:).

This pipeline will fill the sidebar with users you’re not following yet. To do this, you’ll create a filter that only shows people you’re not following. You’ll use the same filter as above, but reversed. Replace the return statement in findSuggestedUsers(forUser:inDatabase:) with the following:

let users = database[User.collection] // 1
return findUser(byId: userId, inDatabase: database).flatMap { user in // 2
  
  // 3
  var feedUserIds = user.following
  feedUserIds.append(userId)
  
  // 4
  let otherUsersQuery: Document = [
    "_id": [
      "$nin": feedUserIds
    ]
  ]
  
  // Continue writing here
  
}

In the code above, you:

  1. Get users collection.
  2. Find user by userId.
  3. List the user identifiers whose profiles are not shown.
  4. Construct a filter that looks for users where their identifier is Not IN the array.

If you use a simple filter, users will always see the same suggestions. But you’ll do better than that! Instead of a simple filter, you’ll suggest users based on the people you’re following, using a Graph Lookup. This works in the same way as a lookup stage, but recursively.

While a graph lookup provides more relevant results, it doesn’t show suggestions when a user doesn’t follow anyone. Therefore, use a sample stage instead. sample(_:) creates a random set of entities from the input results.

Add this code below the Continue writing here comment:

return users.buildAggregate {
  match(otherUsersQuery) // 1
  sample(5) // 2
  sort([
    "profile.firstName": .ascending,
    "profile.lastName": .ascending
  ]) // 3
}.decode(User.self).allResults() // 4

In this code, you:

  1. Find all users excluding yourself and those you’re following.
  2. Select up to five random users.
  3. Sort them by firstName, then lastName.
  4. Decode the results as a User and return all results.

Build and run the application, reload the page and ta-da! Your sidebar now contains users that you can follow.

Sidebar has users you can follow.