This is a continuation from How to use Vapor
Tap enter
I know it seems sketchy that you have to start by tapping on a different database, in order to create a database.
Let's connect to the newly created database in Vapor
import Fluent
import FluentPostgresDriver
public func configure(_ app: Application) async throws {
app.databases.use(.postgres(hostname: "localhost", username: "postgres", password: "", database: "storiesdb"), as: .psql)
▶️ Run the app
If you encounter an issue connecting to the database, you can debug it by tapping on the database in the Postgres app, and then running this to view the users and their roles/capabilities:
\du
Make sure the user you are trying to sign in has the roles you need.
Attribute Meaning Use Case
Superuser Full access, no restrictions. DB administrators with ultimate authority.
Create role Create/alter roles. Manage user access and roles.
Create DB Create new databases. DB owners or dev teams managing databases.
Replication Set up data replication. Replication roles for backups/HA.
Bypass RLS Ignore Row-Level Security. System roles needing unrestricted access.
When you have a new team member, their database will be empty, but when they run Migrations it will add all the necessary tables.
import Fluent
import FluentPostgresDriver
/// Creates a database table called Stories
struct CreateStory: Migration {
private let storiesTableName: String = "stories"
/// Creates the table
func prepare(on database: Database) -> EventLoopFuture<Void> {
database
.schema(storiesTableName) // table name
.id()
.field("title", .string) // column name
.create()
}
/// Undos the table creation
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(storiesTableName).delete()
}
}
Now we can run our migration like so:
import NIOSSL
import Fluent
import FluentPostgresDriver
import Leaf
import Vapor
// configures your application
public func configure(_ app: Application) async throws {
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
app.databases.use(
.postgres(
hostname: "localhost",
username: "postgres",
password: "",
database: "storiesdb"
),
as: .psql
)
app.migrations.add(CreateStory()) // <---------
▶️ Run the app, Then
cd to your project in terminal. Then
vapor run migrate
This will take a moment, and eventually it will ask you to confirm running your migrations:
Then it should say “Migration successful”
If not, If there is an issue running vapor run migrate. You may want to check if another instance is blocking your migration. For this, you can run
Lsof -i :8080
Then you can see which pid is in use. You can call
kill <pid>
If that doesn’t work, you can call with force.
kill -9 <pid>
If that doesn’t work you can use the sudo command,
sudo kill -9 <pid>
see if it has a parent pid.
ps -o pid,ppid,stat,command -p <pid>
Then you can grab the ppid and kill that:
kill -9 <ppid>
Then run migrate again.
vapor run migrate
It may suggest swift run App
but I find vapor run migrate works better and more consistently.
Lets check out our new table. Open the Postico app.
Tap untitled server, then swap out the database field for your database, then tap connect.
This:
Should match up with:
app.databases.use(
.postgres(
configuration: .init(
hostname: "localhost", // Maps to "Host: localhost"
username: "postgres", // Maps to "User: scottlydon"
password: "", // Maps to "Password: "
database: "akintestdb", // Maps to Database: akintestdb"
tls: PostgresConnection.Configuration.TLS.disable
),
encodingContext: .iso8601,
decodingContext: .iso8601
),
as: .psql
you may be prompted with a permission request.
Should look somewhat like this:
If you don’t see your tables then make sure you follow these steps:
- Run your project
- Run the terminal command:
vapor run migration
- Then go to Postico and connect.
Tapping on the stories table, we see it has an id and title like we specified.
You can see the _fluent_migrations
which keeps track of the migrations run. Do not make changes to this. It should stay a source of truth for reads.
Don’t add columns via postico, only use the code and migrations so that it will be synced up with the code.
If there is a warning at top:
Then you can double click on your database in your postgress app and run:
SELECT current_user;
Then with the username it gives, you can grant access with the following, (swap out the username).
-- Grant permissions on existing tables
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO your_user;
-- Ensure permissions for future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO your_user;
-- Grant permissions on existing sequences
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO your_user;
-- Ensure permissions for future sequences
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO your_user;32. We insert data into the tables by creating Models
▶️ Run the app
33. Go to routes, then:
import Fluent
import Vapor
func routes(_ app: Application) throws {
app.post("stories") { req -> EventLoopFuture<Story> in
let story = try req.content.decode(Story.self) // content = body of request
return story.create(on: req.db).map { story }
}
▶️ Run the app
Now open up postman
You should see this in the postman response:
{
"title": "Christopher Robin",
"id": "F292972B-1CC4-4990-94D7-3BD459EC60D2"
}
Open Postico to see your entry. If you don’t see it at first, tap the refresh button.
See the entry is there!
33.1. How to add another property to a Model after you have already run the migration.
- If the app is not in production.
Then you can delete the table in Postico, and run the app and migration in terminal again.
- If the app is in production.
Then you can make a migration for the new property. For example:
import Fluent
struct AddDescriptionToGoals: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("goals")
.field("description", .string, .required) // Adjust the data type as needed
.update()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("goals")
.deleteField("description")
.update()
}
}
Remember to add it to the routes.
app.migrations.add(AddDescriptionToGoals())
34. How do we get all the stories from the database?
func routes(_ app: Application) throws {
let storiesPath: PathComponent = "stories"
app.get(storiesPath) { req in
Story.query(on: req.db).all()
}
▶️ Run the app
This will get all the stories in the table in the database. To
let storyId: String = "storyId"
app.get(storiesPath, ":\(storyId)") { req -> EventLoopFuture<Story> in
Story.find(req.parameters.get(storyId), on: req.db)
.unwrap(or: Abort(.notFound))
}
▶️ Run the app
To test it, copy the id of the entry you want to get from Postico. Then pass the following url into your browser:
http://127.0.0.1:8080/stories/<yourItemId>
You should get the object you requested back.
If you get “not found”
- Make sure the colon is in the get argument but not the find argument.
- Make sure you ran the app without breakpoints.
- Make sure you copy and pasted the whole id without leaving out a character.
35. Update an existing story
So we will create an endpoint that uses the Put method. With that, we don’t actually have to pass the id as an argument to the put method, or in the endpoint. We just need to include it in the body of the put request. Like so:
// /movies PUT
app.put(storiesPath) { req -> EventLoopFuture<HTTPStatus> in
let story = try req.content.decode(Story.self)
return Story.find(story.id, on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap {
$0.title = story.title
return $0.update(on: req.db).transform(to: .ok)
}
}
▶️ Run the app
Then go to postman and send the put request.
36. You can delete a row like so:
// /stories DELETE
app.delete(storiesPath, ":\(storyId)") { req -> EventLoopFuture<HTTPStatus> in
Story.find(req.parameters.get(storyId), on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { $0.delete(on: req.db) }
.transform(to: .ok)
}
▶️ Run the app
To test it, copy the id from postico, then you can go to postman and paste it in the url then set it to delete and tap send. Then check in Postico that it is gone with a refresh.
37. One to many relationships:
If you have stories and reviews, where each story can have multiple reviews, if you were to put the reviews in the story table, you would encounter an issue where you would have rows with repeating movie names, and uncertainty about what to put for the primary keys because you can’t have those repeat. However you can’t very well put the review ids into an array in a cell in the table because cells don’t accept lists.
The answer is to put the story ids (which there are one of per review) in a column in the review table.
38. Lets create the object for which there will be multiple (many) per story.
import Vapor
import Fluent
import FluentPostgresDriver
final class Review: Model, Content {
static let schema: String = "reviews"
@ID(key: .id)
var id: UUID?
@Field(key: "title")
var title: String
@Field(key: "body")
var body: String
@Parent(key: "story_id")
var story: Story
init() {}
init(
id: UUID? = nil,
title: String,
body: String,
storyId: UUID
) {
self.id = id
self.title = title
self.body = body
self.$story.id = storyId
}
}
▶️ Run the app
Warning: Make sure you don’t assign computed properties such as: @Children
, @OptionalChild
.
For example, the following will produce a run time error when called: Thread 21: Fatal error: Children relation Children<User, ContextCompatibilityMedians>(for: [user]) is get-only.
init(
id: UUID? = nil,
authId: String? = nil,
fullName: String,
email: String,
isIDVerified: Bool = false,
isEmailVerified: Bool = false,
hideStatus: HideStatus = .showing,
gender: String? = nil,
dob: Date? = nil,
profilePicture: String? = nil,
accessToken: String? = nil,
accessTokenExpiration: Date? = nil,
createdAt: Date = Date(),
updatedAt: Date = Date(),
latitude: Double? = nil,
longitude: Double? = nil,
allowedMetersForNearbyChecking: Double = 400
) {
self.id = id
self.fullName = fullName
self.email = email
self.isIDVerified = isIDVerified
self.isEmailVerified = isEmailVerified
self.hideStatus = hideStatus
self.gender = gender
self.dob = dob
self.profilePicture = profilePicture
self.accessToken = accessToken
self.createdAt = createdAt
self.updatedAt = updatedAt
if let latitude, let longitude {
self.update(location: Coordinates(latitude: latitude, longitude: longitude))
}
self.allowedMetersForNearbyChecking = allowedMetersForNearbyChecking
self.contextCompatibilityMedians = []
self.responseChoices = []
self.preferences = []
self.userQuestionImportances = []
}
The following are assigning @Children
property wrapper properties:
self.contextCompatibilityMedians = []
self.responseChoices = []
self.preferences = []
self.userQuestionImportances = []
Furthermore, you can can get better debug messages with the following in configure.swift
:
app.logger.logLevel = .debug
39. Create a Migration for the object:
import Vapor
import Fluent
import FluentPostgresDriver
/// Adds a Review table
struct CreateReview: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(Review.schema)
.id()
.field("title", .string)
.field("body", .string)
.field("story_id", .uuid, .references(Story.schema, "id"))
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(Review.schema).delete()
}
}
▶️ Run the app
Now add the migration in the configure file:
app.migrations.add(CreateReview())
▶️ Run the app
Open terminal and run the migration (vapor run migrate
):
➜ DeleteMe2 git:(main) ✗ vapor run migrate
Then open postico to check that the table was added.
40. Adding Model relationships:
The following demonstrates a one to many relationship
import Fluent
import Vapor
final class Story: Model, Content {
// ...
@Children(for: \.$story)
var reviews: [Review]
▶️ Run the app
And
import Vapor
import Fluent
import FluentPostgresDriver
final class Review: Model, Content {
// ...
@Parent(key: "story_id")
var story: Story
▶️ Run the app
41. How to save an entry (Review)
import Fluent
import Vapor
func routes(_ app: Application) throws {
// ...
app.post("\(Review.schema)") { req -> EventLoopFuture<Review> in
let review = try req.content.decode(Review.self)
return review.create(on: req.db).map { review }
}
▶️ Run the app
Change the URL to reviews.
Remember to run your project to update the instance!
Open up Postico to check, remember to refresh.
42 Retrieving all the reviews per story (all the many given a one)
import Fluent
import Vapor
func routes(_ app: Application) throws {
// ...
app.get("\(Story.schema)") { req in
// Gets all the stories and reviews associated with each story
Story.query(on: req.db).with(\.$reviews).all()
}
▶️ Run the app
Now if we change the call to a get and pass stories as the path, send it, we should receive a story with all of its reviews. In our case, there is only one.
— — — — — - side note
I’m taking a vapor course and I learned how to use with( for a many to many get call. For actors to movies, however, suppose I wanted to get an object property on a movie type as well, for example i want actors, and for each actor I want a list of each movie they’ve been in, and for each movie I want the directors that directed each movie, what would the route code look like?
— — — — — answer
Vapor, a server-side web framework for Swift, leverages Swift’s strong type safety and the Fluent ORM for handling database interactions. However, this question is quite complex, as it involves multiple layers of relationships (many-to-many and one-to-many). The following code snippet is an abstracted example of how you might achieve this with Vapor. This will need to be adapted to your specific database schema and model relationships.
router.get("actors", use: { req -> Future<[Actor]> in
return Actor.query(on: req)
.with(\.$movies) { movie in
movie.with(\.$directors)
}
.all()
}
)
▶️ Run the app
This route handler will return all actors along with their associated movies and the directors of each movie.
The .with(\.$movies)
function is used to fetch all the associated movies for each actor, while the inner .with(\.$directors)
function fetches all the directors associated with each movie.
Please note that you need to have defined the appropriate relationships in your models. In this case, you would have a pivot between `Actor
` and `Movie
`, and a one-to-many relationship from `Movie
` to `Director
`.
The code above is written assuming Vapor 4's syntax, which had some changes from Vapor 3, especially around the database and routing APIs.
Remember to handle all potential errors and edge cases in your production code, as this example is just a simplified scenario. Always test your code thoroughly, considering different potential inputs and edge cases to ensure it behaves as expected.
43. Many to Many
In order to manage a many to many relationship we must create a table purely dedicated to joining two tables, also known as a “Join” or “Pivot.”
Because a story can be “co-authored”, while an author can write multiple stories. We will use authors and stories to demonstrate many to many relationships.
44. Adding Author Model
import Vapor
import Fluent
import FluentPostgresDriver
final class Author: Model, Content {
static let schema: String = "authors"
@ID(key: .id)
var id: UUID?
@Field(key: "name")
var name: String
init() {}
init(id: UUID? = nil, name: String) {
self.id = id
self.name = name
}
}
▶️ Run the app
45. Adding pivot Model
Notice the pivot just has the id and reference to the first and the second model via their ids.
import Vapor
import Fluent
import FluentPostgresDriver
final class StoryAuthor: Model {
static let schema: String = "story_actors"
@ID(key: .id)
var id: UUID?
@Parent(key: "story_id")
var story: Story
@Parent(key: "author_id")
var author: Author
init() {}
init(storyId: UUID, authorId: UUID) {
self.$story.id = storyId
self.$author.id = authorId
}
}
▶️ Run the app
46. Create Migrations
import Fluent
import FluentPostgresDriver
struct CreateAuthor: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(Author.schema)
.id()
.field("name", .string)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(Author.schema).delete()
}
}
And the join/pivot migration:
import Fluent
import FluentPostgresDriver
struct CreateStoryAuthor: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database
.schema(StoryAuthor.schema)
.id()
.field("story_id", .uuid, .required, .references("stories", "id"))
.field("author_id", .uuid, .required, .references("actors", "id"))
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(StoryAuthor.schema).delete()
}
}
Then call the migrations, WARNING: make sure you call the create the tables that the joins reference before calling the Join migration!
app.migrations.add(CreateAuthor())
app.migrations.add(CreateStoryAuthor())
▶️ Run the app
Then go to terminal cd to your project, then
vapor run migrate
And say y
to the prompt.
We can open up Postico to see that they were created.
47. Adding relationships in Story and Author Models
Siblings is used for many to many relationships.
import Fluent
import Vapor
final class Story: Model, Content {
@Siblings(through: StoryAuthor.self, from: \.$story, to: \.$author)
var authors: [Author]
And Author:
import Vapor
import Fluent
import FluentPostgresDriver
final class Author: Model, Content {
@Siblings(through: StoryAuthor.self, from: \.$author, to: \.$story)
var stories: [Story]
▶️ Run the app
48. Creating a new Story and Author:
import Fluent
import Vapor
func routes(_ app: Application) throws {
app.post("\(Author.schema)") { req -> EventLoopFuture<Author> in
let author = try req.content.decode(Author.self)
return author.create(on: req.db).map { author }
}
▶️ Run the app
49. Attaching authors to stories, many to many relationships.
import Fluent
import Vapor
func routes(_ app: Application) throws {
// story/:storyId/author/:authorId
app.post("story", ":storyId", "author", ":authorId") { req -> EventLoopFuture<HTTPStatus> in
// get the story
let story = Story.find(req.parameters.get("storyId"), on: req.db)
.unwrap(or: Abort(.notFound)) // returns a promise that it will be returned eventually.
// get the author
let author = Author.find(req.parameters.get("authorId"), on: req.db)
.unwrap(or: Abort(.notFound))
return story.and(author).flatMap { story, author in
story.$authors.attach(author, on: req.db)
}.transform(to: .ok)
}
▶️ Run the app
Open up Postico and grab a valid author id and a valid story id.
Then open up Postman and send the post:
Then check Postico to see the entry
Tada! its there. 🎊
— — — — — — — — — - side note, question to GpT
So I made a pivot table, however, I’m not sure how the pivot table links to or associates with given properties. For example suppose I had two properties on a Movie: leadActors: [Actor] and backupActors: [Actor] how would my pivot table differentiate between the actors associated to a movie for one property vs another?
— — — — — — — — — GpT answer
To differentiate between different types of relationships (e.g., leadActors and backupActors), you would typically create two separate pivot tables. Each pivot table would represent a distinct many-to-many relationship.
In Swift Vapor, each pivot table is represented by a model. For example, you might have a `LeadActorMoviePivot` and a `BackupActorMoviePivot`. Both models would contain foreign keys referencing the `Actor` and `Movie` models, but would be used in different contexts according to the relationship they represent.
Your `Author` model might look something like this:
final class Actor: Model {
static let schema = "actors"
// Properties…
var leadMovies: Siblings<Actor, Movie, LeadActorMoviePivot> {
siblings(through: LeadActorMoviePivot.self)
}
var backupMovies: Siblings<Actor, Movie, BackupActorMoviePivot> {
siblings(through: BackupActorMoviePivot.self)
}
}
▶️ Run the app
And your `Story` model:
final class Movie: Model {
static let schema = "movies"
// Properties…
var leadActors: Siblings<Movie, Actor, LeadActorMoviePivot> {
siblings(through: LeadActorMoviePivot.self)
}
var backupActors: Siblings<Movie, Actor, BackupActorMoviePivot> {
siblings(through: BackupActorMoviePivot.self)
}
}
▶️ Run the app
Finally, you would define your pivot models like this:
final class LeadActorMoviePivot: Model {
static let schema = "lead_actor_movie"
// Properties…
}
final class BackupActorMoviePivot: Model {
static let schema = "backup_actor_movie"
// Properties…
}
▶️ Run the app
This way, you can differentiate between lead and backup actors in your database and in your Vapor code. Just remember to adjust this sample code according to your actual model properties and database schema.
Remember, it’s important to maintain good database practices such as enforcing referential integrity and understanding how to use indexes for better performance. Ensure your database schema is designed properly to minimize redundancy and optimize performance.
50. Time to fetch the many to many relationship.
app.get("\(Author.schema)") { req in
Author.query(on: req.db).with(\.$stories).all()
}
▶️ Run the app
pThat shows us stories associated with each author.
If we want to get authors associated with all the stories, we can add the same with(
call to the return for the stories route.
app.get("\(Story.schema)") { req in
// Gets all the stories and reviews associated with each story
Story.query(on: req.db).with(\.$authors).with(\.$reviews).all()
}
▶️ Run the app
See the authors included?