Using vapor-fluent to upsert models - swift

I am currently struggling with doing an upsert with vapor/fluent. I have a model something like this:
struct DeviceToken: PostgreSQLModel {
var id: Int?
var token: String
var updatedAt: Date = Date()
init(id: Int? = nil, token: String, updatedAt: Date = Date()) {
self.id = id
self.token = token
self.updatedAt = updatedAt
}
}
struct Account: PostgreSQLModel {
var id: Int?
let username: String
let service: String
...
let deviceTokenId: DeviceToken.ID
init(id: Int? = nil, service: String, username: String, ..., deviceTokenId: DeviceToken.ID) {
self.id = id
self.username = username
....
self.deviceTokenId = deviceTokenId
}
}
From the client something like
{
"deviceToken": {
"token": "ab123",
"updatedAt": "01-01-2019 10:10:10"
},
"account": {
"username": "user1",
"service": "some service"
}
}
is send.
What I'd like to do is to insert the new models if they do not exist else update them. I saw the create(orUpdate:) method however this will only update if the id is the same (in my understanding). Since the client does not send the id i am not quite sure how to handle this.
Also I can't decode the model since the account is send without the deviceTokenId and therefore the decoding will fail. I guess I can address the latter problem by overriding NodeCovertible or by using two different models (one for decoding the json without the id and the actual model from above). However the first problem still remains.
What I exactly want to do is:
Update a DeviceToken if an entry with token already exists else create it
If an account with the combination of username and service already exists update its username, service and deviceTokenId else create it. DeviceTokenId is the id returned from 1.
Any chance you can help me out here ?

For everyone who is interested:
I solved it by writing an extension on PostgreSQLModel to supply an upsert method. I added a gist for you to have a look at: here.
Since these kind of links sometimes are broken when you need the information here a quick overview:
Actual upsert implementation:
extension QueryBuilder
where Result: PostgreSQLModel, Result.Database == Database {
/// Creates the model or updates it depending on whether a model
/// with the same ID already exists.
internal func upsert(_ model: Result,
columns: [PostgreSQLColumnIdentifier]) -> Future<Result> {
let row = SQLQueryEncoder(PostgreSQLExpression.self).encode(model)
/// remove id from row if not available
/// otherwise the not-null constraint will break
row = row.filter { (key, value) -> Bool in
if key == "id" && value.isNull { return false }
return true
}
let values = row
.map { row -> (PostgreSQLIdentifier, PostgreSQLExpression) in
return (.identifier(row.key), row.value)
}
self.query.upsert = .upsert(columns, values)
return create(model)
}
}
Convenience methods
extension PostgreSQLModel {
/// Creates the model or updates it depending on whether a model
/// with the same ID already exists.
internal func upsert(on connection: DatabaseConnectable) -> Future<Self> {
return Self
.query(on: connection)
.upsert(self, columns: [.keyPath(Self.idKey)])
}
internal func upsert<U>(on connection: DatabaseConnectable,
onConflict keyPath: KeyPath<Self, U>) -> Future<Self> {
return Self
.query(on: connection)
.upsert(self, columns: [.keyPath(keyPath)])
}
....
}
I solved the other problem I had that my database model could not be decoded since the id was not send from the client, by using a inner struct which would hold only the properties the client would send. The id and other database generated properties are in the outer struct. Something like:
struct DatabaseModel: PostgreSQLModel {
var id: Int?
var someProperty: String
init(id: Int? = nil, form: DatabaseModelForm) {
self.id = id
self.someProperty = form.someProperty
}
struct DatabaseModelForm: Content {
let someProperty: String
}
}

Related

How can I fetch a json file using Vapor for my leaf template to show the data?

I have a JSON hosted somewhere and I want to fetch the content, put it in a context for my leaf template to read.
However, I cannot make it work. I get the code to compile, but I get an error in the localhost
{"error":true,"reason":"Unsupported Media Type"}
Can somebody help me please! Happy holidays for all.
struct WebsiteController: RouteCollection {
func boot(routes: RoutesBuilder) throws {
routes.get(use: indexHandler)
}
func indexHandler(_ req: Request) -> EventLoopFuture<View> {
return req.client.get("https://streeteasydaily.s3.us-west-2.amazonaws.com/streeteasy1.json").flatMap { res in
do {
let json = try res.content.decode([Listing].self)
print(json[0].photos[0])
let context = IndexContext(title: "Homepage", listings: json)
return try req.view.render("index", context)
} catch {
// Handle error
print("cayo en error")
return req.eventLoop.makeFailedFuture(error)
}
}
}
}
struct IndexContext: Encodable {
let title: String
let listings: [Listing]
}
Model
final class Listing: Model {
static let schema = "listings" //basically the table name
#ID
var id: UUID?
#Field(key: "address")
var address: String
#Field(key: "description")
var description: String
#Field(key: "photos")
var photos: [String]
init() {}
//to initialize the db
init(id: UUID? = nil, address: String, description: String, photos: [String]) {
self.id = id
self.address = address
self.description = description
self.photos = photos
}
}
//to make acronym conform to CONTENT, and use it in Vapor
extension Listing: Content {}
This error is because the decode is failing to identify all the fields in your JSON to match against those defined in Listing and/or the array of such objects. The filenames must match those in the JSON exactly - i.e. case-sensitive and every field in the structure/model must exist in the JSON. Additional fields in the JSON that are not needed/included in the structure/model are fine.

Creation of instance set id, although it should be nil

I am working on a small vapor project with fluent. Right now I should be able to create a user, create a token for said created user and return the user with the session token. Afterwards I should be able to login and create a new token.
The problem right now is that whenever I create a token, the id value of said token is set to 1, even though I set it to nil. This does not happen when creating a user, then the auto increment is working as intended.
The database I am using is MySQL.
My migration CreateTokens.swift:
import Fluent
struct CreateTokens: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(Token.schema)
.field("id", .int, .identifier(auto: true))
.unique(on: "id")
.field("user_id", .int, .references("users", "id"))
.field("value", .string, .required)
.unique(on: "value")
.field("created_at", .datetime, .required)
.field("expires_at", .datetime)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(Token.schema).delete()
}
}
My Token (Token.swift):
import Vapor
import Fluent
final class Token: Model {
static let schema = "tokens"
#ID(custom: "id", generatedBy: .database)
var id: Int?
#Parent(key: "id")
var user: User
#Field(key: "value")
var value: String
#Field(key: "expires_at")
var expiresAt: Date?
#Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() {}
init(id: Int? = nil,
userId: User.IDValue,
token: String,
expiresAt: Date?
) {
self.id = id
self.$user.id = userId
self.value = token
self.expiresAt = expiresAt
}
}
extension Token: ModelTokenAuthenticatable {
static let valueKey = \Token.$value
static let userKey = \Token.$user
var isValid: Bool {
guard let expiryDate = expiresAt else {
return true
}
return expiryDate > Date()
}
}
In my User struct, when I create a token:
func createToken() throws -> Token {
let calendar = Calendar(identifier: .gregorian)
let expiryDate = calendar.date(byAdding: .year, value: 1, to: Date())
let token = try Token(userId: requireID(), token: [UInt8].random(count: 16).base64, expiresAt: expiryDate)
return token
}
If I set a breakpoint on the return token and print out token, I get the following:
(lldb) po token
Token(input: [expires_at: Optional(2021-11-11 21:01:10 +0000), value: "wn8kvw/GYSfqL280RLCDbQ==", id: 1])
The ID value is set to 1, however in the code it is (to me) clear that I've set it to nil. Even if I add token.id = nil before the return, it is still set to 1.
What am I doing wrong here? Or did I stumble upon a fluent bug?
As you mentioned, the key "id" seems to be used for both the id and the user properties, which is a common type of mistake in this kind of stringly-typed API.
One way I tried to mitigate this is to extend FieldKey and force myself to always use these static properties instead of string, that way the IDE's auto-complete provides an opportunity for me to think about the right field key to use—something that is missing when you type these manually.
extension FieldKey {
static var userID: FieldKey { "user_id" }
}
// And in the model
#Parent(key: .userID)
var user: User
Hope this helps in the future!
I've found the issue which I've spend at least 1.5h on, and it is a stupid mistake.
The problem lies in:
#Parent(key: "id")
var user: User
This should have been user_id as key, not id. Now it re-writes the id to whatever userId I've send in.

Vapor 4: Children relation not eager loaded, use $ prefix to acces

Currently I am working on a school assignment where we have to build an API using Vapor. I have a few basic API calls working and I am trying a bit more advanced API calls now but I can't get this to work.
I have this function addToParty that is being called when the URL /party/join/:partyID is called with a body
{
"id": "CC1FAC6B-A2B3-471C-A488-147300196981",
"username": "string",
"is_ready": true
}
I am trying to find a party by the partyId and add the user to the list of users of the party.
func addToParty (req: Request) throws -> EventLoopFuture<Party.Output> {
guard let id = req.parameters.get("partyID", as: UUID.self) else {
throw Abort(.badRequest)
}
let input = try req.content.decode(Party.JoinParty.self)
return Party.find(id, on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { element in
element.users.append(User(id: UUID(input.id), username: input.username, is_ready: input.is_ready))
return element.save(on: req.db)
.map{ Party.Output(code: "200") }
}
}
When I try the code above I get the error Fatal error: Children relation not eager loaded, use $ prefix to access: Children<Party, User>(for: [party_id]): file FluentKit/Children.swift, line 33 from the line
element.users.append(User(id: UUID(input.id), username: input.username, is_ready: input.is_ready))
When I comment this line the code runs and I get a return code.
I tried adding the prefix to element.$users and $User but then it complains about not being able to find element.$users and $User in scope.
Party model
import Fluent
import Vapor
final class Party: Model, Content {
static let schema = "parties"
struct JoinParty: Content {
let id: String
let username: String
let is_ready: Bool
}
struct Output: Content {
let code: String
}
#ID(key: .id)
var id: UUID?
#Field(key: "party_code")
var party_code: String
#Field(key: "host_id")
var host_id: UUID
#Field(key: "is_active")
var is_active: Bool
// change to Game when model is made
#Field(key: "selected_games")
var selected_games: [String]?
// change to Setting when model is made
#Field(key: "settings")
var settings: String
#Field(key: "results")
var results: Array<GameResult>?
#Children(for: \.$party)
var users: [User]
init() { }
init(id: UUID? = nil,
party_code: String,
host_id: UUID,
is_active: Bool,
selected_games: [String]? = nil,
settings: String,
results: Array<GameResult>? = nil) {
self.id = id
self.party_code = party_code
self.host_id = host_id
self.is_active = is_active
self.selected_games = selected_games
self.settings = settings
self.results = results
}
}
User model
import Fluent
import Vapor
final class User: Model, Content {
static let schema = "users"
struct Input: Content {
let id: UUID
let username: String
}
struct Output: Content {
let id: String
let username: String
}
#ID(key: .id)
var id: UUID?
#Field(key: "username")
var username: String
#Field(key: "is_ready")
var is_ready: Bool
#OptionalParent(key: "party_id")
var party: Party?
#Children(for: \.$user)
var gameResults: [GameResult]
init() {}
init(id: UUID? = nil, username: String, is_ready: Bool, partyID: UUID? = nil) {
self.id = id
self.username = username
self.is_ready = is_ready
self.$party.id = partyID
}
}
I have a similar function to update a username from a user already working which is almost the same thing.
func update(req: Request) throws -> EventLoopFuture<User.Output> {
let input = try req.content.decode(User.Input.self)
return User.find(input.id, on: req.db)
.unwrap(or: Abort(.notFound))
.flatMap { user in
user.username = input.username
return user.save(on: req.db)
.map { User.Output(id: user.id!.uuidString, username: user.username) }
}
}
Any help would be really appreciated.
Thank you in advance.
Unfortunately, adding children to a parent model is not that intuitive yet. I hope that someday we can get that added, but it's not there yet. Fluent 5 maybe?
Anyway, what you'll need to do instead is create your new User model, passing in the party's ID value to the partyID initializer parameter, and then save the User model.
let user = User(id: UUID(input.id), username: input.username, is_ready: input.is_ready, partyID: element.id)
return user.save(on: request.db)
So your method should end up looking like this:
func addToParty(req: Request) throws -> EventLoopFuture<Party.Output> {
guard let id = req.parameters.get("partyID", as: UUID.self) else {
throw Abort(.badRequest)
}
let input = try req.content.decode(Party.JoinParty.self)
return Party.find(id, on: req.db).unwrap(or: Abort(.notFound)).flatMap { element in
return User(
id: UUID(input.id),
username: input.username,
is_ready: input.is_ready,
partyID: element.id
).save(on: req.db)
}.transform(to: Party.Output(code: "200"))
}

How do I add an additional property to a Model?

I have a Model which conforms to Content:
import Vapor
import Fluent
final class User: Model, Content {
static let schema = "user"
#ID(key: .id)
var id: UUID?
#Field(key: "email")
var email: String
init() { }
init(id: UUID? = nil, email: String) {
self.id = id
self.email = email
}
}
This can be fetched from the database and directly returned from a route, so the client receives the users as JSON.
Now I want to add an additional property to the user, which isn't stored in the database but a fixed value or just added after the user is loaded from the database:
final class User: Model, Content {
// ...
var someOther: String = "hello"
// ...
}
or
final class User: Model, Content {
// ...
var someOther: String? // set later
// ...
}
Now the problem is that this property is never added to the JSON, so the client only gets the user's id and email and NOT the someProperty. How do I fix this?
When you want to send or receive data from your client that has a different representation from what your model has, it's common practice to define a custom type that you can then convert to and from the model type.
Given this, you would define a new User.Response type that conforms to Content instead of the User model, and return that from your route instead.
extension User {
struct Response: Content {
let id: UUID
let email: String
let otherValue = "Hello World"
}
}
func user(request: Request) throws -> EventLoopFuture<User.Response> {
return User.find(UUID(), on: request.db).flatMapThrowing { user in
guard let id = user.id else {
throw Abort(.internalServerError, "User is missing ID")
}
return User.Response(id: id, email: user.email)
}
}
The reason the additional value that you added wasn't encoded from your model is because the default encoder for model types iterates over the field properties and encodes those properties, so if you have a property that doesn't use one of the ID, Field, Parent, Children, or other field property wrappers, it won't be encoded.

Manually modifying model property values in vapor 4 response

I have a vapor 4 application. I do a query from database for getting some items and I want to perform some manual calculation based on the returned values before finishing the request. here a sample code of what I am trying to achieve.
final class Todo: Model, Content {
static var schema: String = "todos"
#ID(custom: .id)
var id: Int?
#Field(key: "title")
var title: String
var someValue: Int?
}
/// Allows `Todo` to be used as a dynamic migration.
struct CreateTodo: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(Todo.schema)
.field("id", .int, .identifier(auto: true))
.field("title", .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(Todo.schema).delete()
}
}
final class TodoController:RouteCollection{
func boot(routes: RoutesBuilder) throws {
routes.get("tmp", use: temp)
}
func temp(_ req:Request) throws -> EventLoopFuture<[Todo]> {
Todo.query(on: req.db).all().map { todos in
todos.map {
$0.someValue = (0...10).randomElement()!
return $0
}
}
}
}
The problem is that those manual changes, aren't available in response. In this case someValue property.
Thanks.
[
{
"title": "item 1",
"id": 1
},
{
"title": "item 2",
"id": 2
}
]
The problem you're hitting is that Models override the Codable implementations. This allows you to do things like passing around parents and not adding children etc.
However, this breaks your case. What you should do, is create a new type if you want to return a Todo with another field that isn't stored in the database, something like:
struct TodoResponse: Content {
let id: Int
let title: String
let someValue: Int
}
And then convert from your database type to your response type in your route handler (this is a pretty common pattern and the recommended way to do it in Vapor)