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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Server-Side Swift with MongoDB: Getting Started
20 mins
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"}
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:
- Get
users
collection. - Find
user
withid
. - 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:
- First, you create an aggregate pipeline based on function builders.
-
Then, you filter the timeline posts to match
followingUsersQuery
. - Next, you sort the timeline posts by creation date, so that recent posts are on top.
- And you limit the results to the first 10 posts, leaving the 10 most recent posts.
-
Now, you look up the creators of this post.
localField
refers to the field insideTimelinePost
. AndforeignField
refers to the field insideUser
. This operation returns all users that match this filter. Finally, this puts an array with results insidecreator
. -
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 increator
and then replaces the contents of creator with a single entity. -
You decode each result as a
ResolvedTimelinePost
. - 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!