Skip to content

Commit facf2ee

Browse files
authored
SWIFT-876 Implement more complex example Vapor application (#493)
1 parent 7e77a3d commit facf2ee

File tree

10 files changed

+444
-0
lines changed

10 files changed

+444
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// swift-tools-version:5.2
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "ComplexVaporExample",
6+
platforms: [
7+
.macOS(.v10_15)
8+
],
9+
dependencies: [
10+
.package(url: "https://github.com/vapor/vapor", .upToNextMajor(from: "4.7.0")),
11+
.package(url: "https://github.com/vapor/leaf", .exact("4.0.0-rc.1.2")),
12+
.package(url: "https://github.com/mongodb/mongo-swift-driver", .upToNextMajor(from: "1.0.0"))
13+
],
14+
targets: [
15+
.target(
16+
name: "App",
17+
dependencies: [
18+
.product(name: "Vapor", package: "vapor"),
19+
.product(name: "Leaf", package: "leaf"),
20+
.product(name: "MongoSwift", package: "mongo-swift-driver")
21+
]
22+
),
23+
.target(name: "Run", dependencies: [
24+
.target(name: "App"),
25+
.product(name: "MongoSwift", package: "mongo-swift-driver")
26+
])
27+
]
28+
)
15 KB
Binary file not shown.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# MongoDB + Vapor Example Application
2+
3+
This repository contains an example application built using [Vapor 4](vapor.codes) along with version 1.0 of the [MongoDB Swift driver](https://github.com/mongodb/mongo-swift-driver).
4+
5+
This application is intended to demonstrate best practices for integrating the driver into your backend. It is **not** production-ready, and does not necessarily follow best HTML or Javascript practices. The frontend implementation is a minimal amount of code built with Vapor's templating language [Leaf](https://github.com/vapor/leaf) to allow you to interact with all of the application's HTTP endpoints.
6+
7+
This application require Swift 5.2 and MongoDB 3.6+. It will run on Linux as well as macOS 10.15+.
8+
9+
## Building and Running the Application
10+
1. Install MongoDB on your system if you haven't already. Downloads are available [here](https://www.mongodb.com/download-center/community).
11+
1. Start up MongoDB running locally: `mongod --dbpath some-directory-here`. You may need to specify a `dbpath` directory for the database to use.
12+
1. Run `./loadData.sh` to load example application data into the database.
13+
1. Install Swift 5.2 on your system if you haven't already. You can download Swift and find instructions for installing it [here](https://swift.org/download/).
14+
1. From the root directory of the project, run `swift build`. This will likely take a while the first time you do so.
15+
1. Once building has completed, run `swift run` from the root directory. You should get a message that the server has started running on `http://127.0.0.1:8080`.
16+
1. Open up your browser and visit `http://127.0.0.1:8080`. You should see the application and be able to test out adding, deleting, and editing data in the collection.
17+
18+
## Application Architecture
19+
20+
This is a fully asynchronous application. At its core is [SwiftNIO](https://github.com/apple/swift-nio), which is used to implement both Vapor and the MongoDB driver.
21+
22+
The application is a basic HTTP server combined with a minimal frontend, which supports storing a list of kittens and details about them. The server will handle the following types of requests:
23+
1. A GET request at the root URL `/` loads the main index page containing a list of kittens.
24+
1. A POST request at the root URL `/` adds a new kitten.
25+
1. A GET request at the URL `/kittens/{name}` loads information about the kitten with the specified name.
26+
1. A PATCH request at the URL `/kittens/{name}` edits the `favoriteFood` property for the kitten with the specified name.
27+
1. A DELETE request at the URL `/kittens/{name}` deletes the kitten with the specified name.
28+
29+
### MongoDB Usage
30+
This application connects to a local standalone MongoDB server. It uses the collection "kittens" in the database "home". The "kittens" collection has a [unique index](https://docs.mongodb.com/manual/core/index-unique/) on the "name" field, ensuring that no two kittens in the collection can have the same name.
31+
32+
If you'd like to point the application to a MongoDB server elsewhere (e.g. on [MongoDB Atlas](https://www.mongodb.com/cloud/atlas)) or running on a different port, or change any configuration options for the client, you can edit the code where the client is created in `Sources/App/configure.swift`.
33+
34+
This application uses a single `MongoClient` for the entire application. `MongoClient` is implemented with that approach in mind: it is safe to use across threads, and is backed by a [connection pool](https://en.wikipedia.org/wiki/Connection_pool) which enables sharing resources throughout the application.
35+
36+
We recommend storing the client in `Application.storage` and adding a computed property to access it in an extension of `Application`, to allow easy shared access throughout the application. You can see an example of how to do this in `Sources/App/configure.swift`.
37+
38+
The application also uses a single shared `MongoCollection` object for the entire application, defined in `routes.swift`. `MongoCollection` is also thread-safe, and is essentially a wrapper around a `MongoClient` specifying a namespace and providing access to collection-specific API methods.
39+
40+
#### Important Note on `EventLoop` hopping
41+
Anywhere we call a MongoDB API method returning an `EventLoopFuture`, we use `hop(to: req.eventLoop)` after to return to the request's `EventLoop`. As `MongoClient` is backed by an `EventLoopGroup`, the `EventLoopFuture`s it (as well as its child `MongoDatabase`s and `MongoCollection`s) returns may fire on any `EventLoop` in the group. However, per Vapor's [documentation](https://docs.vapor.codes/4.0/async/):
42+
> Vapor expects that route closures will stay on `req.eventLoop`. If you hop threads, you must ensure access to `Request` and the final response future all happen on the request's event loop.
43+
44+
Therefore, we must always `hop` when we are done calling a driver API. Please see the `EventLoopFuture` [documentation](https://apple.github.io/swift-nio/docs/current/NIO/Classes/EventLoopFuture.html) for more details on this method.
45+
46+
### Codable Usage
47+
Throughout the application, we frequently use [`Codable`](https://developer.apple.com/documentation/swift/codable) Swift types. These are very useful as they allow us to convert seamlessly from BSON, the format MongoDB stores data in, to Swift types used in the server, to JSON to send to the client. The same is true for the opposite direction.
48+
49+
Note that Vapor's[`Content`](https://api.vapor.codes/vapor/master/Vapor/Protocols/Content.html) protocol, which specifies types that can be initialized from HTTP requests and serialized to HTTP responses, inherits from `Codable`.
50+
51+
When creating a `MongoCollection` object in the driver, you can pass in the name of a `Codable` type:
52+
```swift
53+
let collection = client.db("home").collection("kittens", withType: Kitten.self)
54+
```
55+
56+
This will instantiate a `MongoCollection<Kitten>`. You can then use `Kitten` directly with many API methods -- for example, `insertOne` will directly accept a `Kitten` instance, and `findOne` will return an `EventLoopFuture<Kitten>`.
57+
58+
Sometimes you may need to work with the `BSONDocument` type as well, for example when providing a query filter. If you want to construct these documents from `Codable` types you may do so using `BSONEncoder`, as we do with the `updateDocument` in our PATCH handler for `/kittens/{name}`.
59+
60+
The driver also exposes a `BSONDecoder` for initializing `Decodable` types from `BSONDocument`s if you need to do the reverse.
61+
62+
Please see our [BSON guide](https://mongodb.github.io/mongo-swift-driver/MongoSwift/bson.html) for more details on the BSON library.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Kittens</title>
6+
<style type="text/css">
7+
table, th, td {
8+
border: 1px solid black;
9+
}
10+
th, td {
11+
padding: 8px;
12+
}
13+
h1, h2, th, td, label {
14+
font-family: "Trebuchet MS", Helvetica, sans-serif;
15+
}
16+
</style>
17+
</head>
18+
<body>
19+
20+
<h1>Kittens 🐱🐱</h1>
21+
<table>
22+
<tr>
23+
<th>Name</th>
24+
<th>Color</th>
25+
<th>Favorite Food</th>
26+
</tr>
27+
#for(kitten in kittens):
28+
<tr>
29+
<td><a href="/kittens/#(kitten.name)">#(kitten.name)</a></td>
30+
<td>#(kitten.color)</td>
31+
<td>#(kitten.favoriteFood)</td>
32+
</tr>
33+
#endfor
34+
</table>
35+
<br>
36+
<br>
37+
<h2>Add a new kitten</h2>
38+
<form method="POST">
39+
<div>
40+
<label for="name">Name</label>
41+
<input name="name">
42+
</div>
43+
<div>
44+
<label for="color">Color</label>
45+
<input name="color">
46+
<div>
47+
<label for="favoriteFood">Favorite Food</label>
48+
<select id="favoriteFood" name="favoriteFood">
49+
<option value="" selected disabled hidden>Select</option>
50+
<option value="salmon">salmon</option>
51+
<option value="turkey">turkey</option>
52+
<option value="chicken">chicken</option>
53+
<option value="tuna">tuna</option>
54+
<option value="beef">beef</option>
55+
</select>
56+
</div>
57+
<div>
58+
<button>Add</button>
59+
</div>
60+
</form>
61+
</body>
62+
</html>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>Kittens</title>
6+
<style type="text/css">
7+
table, th, td {
8+
border: 1px solid black;
9+
}
10+
th, td {
11+
padding: 8px;
12+
}
13+
h1, h2, th, td, label {
14+
font-family: "Trebuchet MS", Helvetica, sans-serif;
15+
}
16+
</style>
17+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
18+
</head>
19+
<body>
20+
<h1>#(name) 🐱</h1>
21+
<table>
22+
<tr>
23+
<th>Color</th>
24+
<td>#(color)</td>
25+
</tr>
26+
<tr>
27+
<th>Favorite Food</th>
28+
<td>#(favoriteFood)
29+
</td>
30+
</tr>
31+
</table>
32+
<br><br>
33+
<h2>Edit</h2>
34+
<label for="favoriteFood">New Favorite Food</label>
35+
<select id="favoriteFood" name="favoriteFood">
36+
<option value="" selected disabled hidden>Select</option>
37+
<option value="salmon">salmon</option>
38+
<option value="turkey">turkey</option>
39+
<option value="chicken">chicken</option>
40+
<option value="tuna">tuna</option>
41+
<option value="beef">beef</option>
42+
</select>
43+
</div>
44+
<div>
45+
<button id="save-btn">Save</button>
46+
<br>
47+
<br>
48+
</div>
49+
<br><br><br>
50+
<button id="delete-btn">Delete kitten</button>
51+
<br>
52+
<br>
53+
<a href="/">Home</a>
54+
<div id="error-info"></div>
55+
56+
<script type="text/javascript">
57+
$(document).ready(() => {
58+
$("\#delete-btn").click(() => {
59+
$.ajax({
60+
type:'DELETE',
61+
url:'/kittens/#(name)',
62+
success: () => {
63+
window.location.href = '/';
64+
},
65+
error: (req, status, message) => {
66+
alert("Error: " + message);
67+
}
68+
});
69+
});
70+
71+
$("\#save-btn").click(() => {
72+
var newFood = $("\#favoriteFood").val();
73+
if (newFood === null) return;
74+
$.ajax({
75+
type:'PATCH',
76+
contentType : 'application/json',
77+
url:'/kittens/#(name)',
78+
data: JSON.stringify({"favoriteFood": newFood}),
79+
success: () => {
80+
window.location.href = '/kittens/#(name)';
81+
},
82+
error: (req, status, message) => {
83+
alert("Error: " + message);
84+
}
85+
});
86+
});
87+
});
88+
</script>
89+
90+
</body>
91+
</html>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
import MongoSwift
3+
import Vapor
4+
5+
/// Possible cat food choices.
6+
enum CatFood: String, Codable {
7+
case salmon,
8+
tuna,
9+
chicken,
10+
turkey,
11+
beef
12+
}
13+
14+
/// The structure of a food update request.
15+
struct FoodUpdate: Codable {
16+
let favoriteFood: CatFood
17+
}
18+
19+
/// Represents a kitten.
20+
struct Kitten: Content {
21+
/// Unique identifier.
22+
var _id: BSONObjectID?
23+
/// Name.
24+
let name: String
25+
/// Fur color.
26+
let color: String
27+
/// Favorite food.
28+
let favoriteFood: CatFood
29+
}
30+
31+
/// Context struct for the index page.
32+
struct IndexContext: Encodable {
33+
let kittens: [Kitten]
34+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Leaf
2+
import MongoSwift
3+
import Vapor
4+
5+
extension Application {
6+
/// A global `MongoClient` for use throughout the application. The client is thread-safe
7+
/// and backed by a pool of connections so it should be shared across event loops.
8+
public var mongoClient: MongoClient {
9+
get {
10+
self.storage[MongoClientKey.self]!
11+
}
12+
set {
13+
self.storage[MongoClientKey.self] = newValue
14+
}
15+
}
16+
17+
private struct MongoClientKey: StorageKey {
18+
typealias Value = MongoClient
19+
}
20+
}
21+
22+
/// Configures the application.
23+
public func configure(_ app: Application) throws {
24+
// serve files from /Public folder
25+
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
26+
27+
// Initialize a client using the application's `EventLoopGroup`.
28+
let client = try MongoClient("mongodb://localhost:27017", using: app.eventLoopGroup)
29+
app.mongoClient = client
30+
31+
// Use LeafRenderer for views.
32+
app.views.use(.leaf)
33+
34+
// register routes
35+
try routes(app)
36+
}

0 commit comments

Comments
 (0)