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.

Logging In

This application is already configured to handle password hashing and authorization using JWT. The entire application relies on Repository, in Repository.swift, for database operations.

Stop the app. Before you can enable logging in, you need to add the code for fetching users. Open User.swift and take note of the User type.

User has a static property containing the collection name for this model. This prevents you from mistyping the name.

_id holds the model’s identifier. MongoDB requires the use of this key. Unlike most databases, MongoDB does not support auto-incrementing for integers. The type of identifier used is ObjectId, which is both compact and scalable.

Next, you’ll see that MongoDB allows you to store information in the database exactly like your Swift structs. The profile and credentials fields contain grouped information.

Finally, the user stores an array of identifiers in following. Each identifier refers to another user that this user follows.

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