creating a parent child relationship in vapor 3 - swift

I am using Vapor 3 to try and create just a sample project where I have a dish, the parent, and the reviews for the dish, the child. All the tutorials that I have been seeing haven't been very clear on how to create the relationship or they are using it in conjecture with leaf. I do not want to use leaf for this, I just want to be able to show all the reviews when for the dish when I give it's id, and it seems that it is different than it was for vapor 2.
My 2 models are Dish and Review
Dish.swift: The parent,
import Foundation
import Vapor
import FluentSQLite
final class Dish: Content {
var id: Int?
var name: String
var course: String
var price: Double
var imageURL: String
var description: String
init(name: String, course: String, price: Double, imageURL: String, description: String) {
self.name = name
self.course = course
self.price = price
self.imageURL = imageURL
self.description = description
}
}
extension Dish {
var reviews: Children<Dish, Review> {
return children(\.dishId)
}
}
extension Dish: Parameter { }
extension Dish: SQLiteModel {
static let entity: String = "Dishes"
}
extension Dish: Migration { }
Review.swift, the child,
import Foundation
import Vapor
import FluentSQLite
final class Review: Content {
var id: Int?
var title: String
var body: String
var dishId: Dish.ID
init(title: String, body: String, dishId: Dish.ID) {
self.title = title
self.body = body
self.dishId = dishId
}
}
extension Review {
var dish: Parent<Review, Dish> {
return parent(\.dishId)
}
}
extension Review: Migration { }
extension Review: SQLiteModel {
static let entity: String = "Reviews"
}
extension Review: Parameter { }
the controller for Dish, DishController,
import Foundation
import Vapor
import FluentSQLite
class DishesController: RouteCollection {
func boot(router: Router) throws {
let dishesRoutes = router.grouped("api/dishes")
dishesRoutes.get("/", use: getAll)
dishesRoutes.get(Dish.parameter, use: getById)
dishesRoutes.post(Dish.self, at: "/", use: createDish)
dishesRoutes.delete(Dish.parameter, use: deleteDish)
}
func deleteDish(req: Request) throws -> Future<Dish> {
return try req.parameters.next(Dish.self).delete(on: req)
}
func createDish(req: Request, dish: Dish) -> Future<Dish> {
return dish.save(on: req)
}
func getAll(req: Request) -> Future<[Dish]> {
return Dish.query(on: req).all()
}
func getById(req: Request) throws -> Future<Dish> {
return try req.parameters.next(Dish.self)
}
}
and the controller for reviews. ReviewController,
import Foundation
import Vapor
import FluentSQLite
class ReviewController: RouteCollection {
func boot(router: Router) throws {
let reviewRoutes = router.grouped("api/reviews")
reviewRoutes.get("/", use: getAll)
reviewRoutes.get(Review.parameter, use: getById)
reviewRoutes.post(Review.self, at: "/", use: createReview)
reviewRoutes.delete(Review.parameter, use: deleteReview)
}
func deleteReview(req: Request) throws -> Future<Review> {
return try req.parameters.next(Review.self).delete(on: req)
}
func createReview(req: Request, review: Review) -> Future<Review> {
return review.save(on: req)
}
func getAll(req: Request) -> Future<[Review]> {
return Review.query(on: req).all()
}
func getById(req: Request) throws -> Future<Review> {
return try req.parameters.next(Review.self)
}
}
this is the routes.swift,
import Vapor
/// Register your application's routes here.
public func routes(_ router: Router) throws {
router.get("/reviews", Dish.parameter,"dish") { request -> Future<Dish> in
return try request.parameters.next(Review.self).flatMap(to: Dish.self) { review in
return review.dish.get(on: request)
}
}
let dishesController = DishesController()
try router.register(collection: dishesController)
let reviewController = ReviewController()
try router.register(collection: reviewController)
}
I just want a simple one to many relationship where one dish can have many reviews, but when I use postman to try and access the reviews for the particular dish, all I get is an error. I know that I used the correct syntax in postman because I can use all the other requests from the controllers just fine, just not any for the relationships. Please tell me what i am missing, because I am getting confused as to what I am doing wrong. If there is anything else I can add please ask.
Thank you very much

If you want to access all reviews for the particular dish, try the following code.
router.get("/dish", Dish.parameter,"reviews") { request -> Future<[Review]> in
return try request.parameters.next(Dish.self).flatMap(to: [Review].self) { (dish) in
return try dish.reviews.query(on: request).all()
}
}
Now In postman, pass a dish id as below:
GET: http://localhost:8080/dish/1/reviews

Related

Published Object Not Working - Combine Swift

Anyone who understands please help me, I want to retrieve data from Firestore and it wants to be read in realtime whenever there is a change from the database, so I use addSnapshotListener to read it, it works every time there is a change from Firestore, but it's still in object form NewsResponse. Because the final result I want to change to a NewsDomainModel form, then I continue the results from NewsResponse to _mapper.transformerResponseToDomain to be converted into NewsDomainModel, but every latest data generated using addSnapshotListener is not updated to the _mapper.transformerResponseToDomain, _mapper only reads 1 time the data sent only.
GetNewsRepository
import SwiftUI
import Core
import Combine
public class GetNewsRepository<
NewsLocaleDataSource: LocaleDataSource,
RemoteDataSource: DataSource,
Transformer: Mapper>: ObservableObject, Repository
where
NewsLocaleDataSource.Request == String,
NewsLocaleDataSource.Response == NewsModuleEntity,
RemoteDataSource.Request == String,
RemoteDataSource.Response == [NewsResponse],
Transformer.Request == String,
Transformer.Response == [NewsResponse],
Transformer.Entity == [NewsModuleEntity],
Transformer.Domain == [NewsDomainModel] {
public typealias Request = String
#Published public var Response: [NewsDomainModel] = [NewsDomainModel]()
private let _localeDataSource: NewsLocaleDataSource
#Published public var _remoteDataSource: RemoteDataSource
#Published public var _mapper: Transformer
public init(
localeDataSource: NewsLocaleDataSource,
remoteDataSource: RemoteDataSource,
mapper: Transformer) {
_localeDataSource = localeDataSource
_remoteDataSource = remoteDataSource
_mapper = mapper
}
public func execute(request: String?) -> AnyPublisher<[NewsDomainModel], Error> {
return self._remoteDataSource.execute(request: request)
.map { self._mapper.transformerResponseToDomain(response: $0) }
.eraseToAnyPublisher()
}
}
GetNewsRemoteDataSource
import Core
import Combine
import FirebaseFirestore
import FirebaseFirestoreSwift
import Foundation
public class GetNewsRemoteDataSource: ObservableObject, DataSource {
public typealias Request = String
#Published public var Response: [NewsResponse] = [NewsResponse]()
private let _endPoint: String
public init(endPoint: String) {
_endPoint = endPoint
}
public func execute(request: String?) -> AnyPublisher<[NewsResponse], Error> {
return Future<[NewsResponse], Error> { completion in
let ref = Firestore.firestore()
ref.collection("news").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("Document not found")
return
}
let dataJson = documents.compactMap { queryDocumentSnapshot in
try? queryDocumentSnapshot.data(as: NewsResponse.self)
} // THIS DATA WILL BE AUTOMATIC UPDATE IF DATA FROM FIRESTORE UPDATED
completion(.success(dataJson))
}
}.eraseToAnyPublisher()
}
}
NewsTransformer
import Core
import Combine
public class NewsTransformer<NewsMapper: Mapper>: ObservableObject, Mapper
where
NewsMapper.Request == String,
NewsMapper.Response == NewsResponse,
NewsMapper.Entity == NewsModuleEntity,
NewsMapper.Domain == NewsDomainModel {
#Published public var Request = String()
#Published public var Response = [NewsResponse]()
#Published public var Entity = [NewsModuleEntity]()
#Published public var Domain = [NewsDomainModel]()
private let _newsMapper: NewsMapper
public init(newsMapper: NewsMapper) {
_newsMapper = newsMapper
}
public func transformerResponseToEntity(request: String?, response: [NewsResponse]) -> [NewsModuleEntity] {
return response.map { result in
_newsMapper.transformerResponseToEntity(request: request, response: result)
}
}
public func transformerResponseToDomain(response: [NewsResponse]) -> [NewsDomainModel] {
print("\(response) DATA RESPONSE") // NOT UPDATING
return response.map { results in
_newsMapper.transformerResponseToDomain(response: results)
}
}
public func transformerEntityToDomain(entity: [NewsModuleEntity]) -> [NewsDomainModel] {
return entity.map { result in
_newsMapper.transformerEntityToDomain(entity: result)
}
}
public func transformerDomainToEntities(domain: [NewsDomainModel]) -> [NewsModuleEntity] {
return domain.map { result in
_newsMapper.transformerDomainToEntities(domain: result)
}
}
}
The sequence of images above is GetNewsRepository (Repository), GetNewsRemoteDataSource (Get Data From Firebase), NewsTransformer (Transform from NewsResponse to NewsDomainModel)
Sorry if the title I ask is wrong.
Your Future in GetNewsRemoteDataSource completes. Once a publisher has completed it won't send any further values. Instead of using a Future, return a PassthroughSubject. And use the .send method to pass values through the publisher.
https://developer.apple.com/documentation/combine/passthroughsubject

How to save a model with a specific id in Vapor 3

I'm trying to add some seed data to a table in a migration. For this table I don't want the id value to be generated automatically, but I want to set it manually for each record. How can this be done? My current code looks like the bellow, but the record isn't inserted to the database and no error is thrown when the migration runs.
Model class:
import Foundation
import FluentPostgreSQL
import Vapor
final class PropertyType: PostgreSQLModel {
var id: Int?
var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
extension PropertyType: Migration { }
extension PropertyType: Content { }
extension PropertyType: Parameter { }
Migration class:
import FluentPostgreSQL
import Vapor
struct AddPropertyTypes: Migration {
typealias Database = PostgreSQLDatabase
static func prepare(on conn: PostgreSQLConnection) -> Future<Void> {
let propertyType1 = PropertyType(id: 1, name: "Einfamilienhaus")
return propertyType.save(on: conn).transform(to: ())
}
static func revert(on conn: PostgreSQLConnection) -> Future<Void> {
let futures = [1].map { id in
return CodePropertyType.query(on: conn).filter(\CodePropertyType.id == id)
.delete()
}
return futures.flatten(on: conn)
}
}
Just replace
propertyType.save(on: conn)
with
propertyType.create(on: conn)

Handle different kinds of json on the same route with vapor 3

I'm trying to add a POST endpoint on my Vapor 3 server to create a new resource, but I want to have 3 different JSON formats that can create that resource. So I have 3 different Content structs that are each fairly similar.
struct IDJson: Content, DeckConvertible {
var name: String?
var format: Int
var hero: Int
var cards: [Int]
}
struct NameJson: Content, DeckConvertible {
var name: String?
var format: Int
var hero: String
var cards: [String]
}
struct DeckstringJson: Content, DeckConvertible {
var name: String?
var deckstring: String
}
And I would like to add them all to my router like this
class DeckRouteController: RouteCollection {
func boot(router: Router) throws {
router.post(IDJson.self, at: "user", "collection", use: createDeckHandler)
router.post(NameJson.self, at: "user", "collection", use: createDeckHandler)
router.post(DeckstringJson.self, at: "user", "collection", use: createDeckHandler)
}
}
...
private extension DeckRouteController {
func createDeckHandler(_ request: Request, container: DeckConvertible) throws -> Future<Response> {
// Create deck
}
}
Is this possible? at the moment only the last one gets used but is there a way I can do this with some middleware handler or something?
You can handle the decoding in the function itself, ie:
router.post("user", "collection", use: createDeckHandler)
func createDeckHandler(_ request: Request) throws -> String {
let asId = try? request.content.syncDecode(IDJson.self)
let asName = try? request.content.syncDecode(NameJson.self)
if let asId = asId {
// do stuff
} else if let asName = asName {
// do something else
}
}

Vapor 2, One to Many relation

Do you have any example of how to create One to Many relation using Vapor 2?
There are some examples of how to do this, but they use the old version of Vapor.
Thank you for all suggestions.
I have found a solution. Here is simple example of an owner having many cars, maybe will be helpful for someone.
Owner:
final class Owner: Model {
static let idKey = "id"
static let nameKey = "name"
static let carsKey = "cars"
var name: String
let storage = Storage()
var cars: Children<Owner, Car> {
return children()
}
init(name: String) {
self.name = name
}
init(row: Row) throws {
name = try row.get(Owner.nameKey)
}
func makeRow() throws -> Row {
var row = Row()
try row.set(Owner.nameKey, name)
return row
}
}
extension Owner: Preparation {
static func prepare(_ database: Database) throws {
try database.create(self) { builder in
builder.id()
builder.string(Owner.nameKey)
}
}
static func revert(_ database: Database) throws {
try database.delete(self)
}
}
extension Owner: JSONConvertible {
convenience init(json: JSON) throws {
try self.init(
name: json.get(Owner.nameKey)
)
}
func makeJSON() throws -> JSON {
var json = JSON()
try json.set(Owner.idKey, id)
try json.set(Owner.nameKey, name)
try json.set(Owner.carsKey, try cars.all())
return json
}
}
extension Owner: ResponseRepresentable { }
extension Owner: Updateable {
public static var updateableKeys: [UpdateableKey<Owner>] {
return [
UpdateableKey(Owner.nameKey, String.self) { owner, text in
owner.name = name
}
]
}
}
Car:
final class Car: Model {
static let idKey = "id"
static let makeKey = "make"
static let modelKey = "model"
static let ownerIdKey = "owner_id"
var make: String
var model: String
var ownerId: Identifier
let storage = Storage()
var owner: Parent<Car, Owner> {
return parent(id: ownerId)
}
init(make: String, model: String, ownerId: Identifier) {
self.make = make
self.model = model
self.ownerId = ownerId
}
init(row: Row) throws {
make = try row.get(Car.makeKey)
model = try row.get(Car.modelKey)
ownerId = try row.get(Car.ownerIdKey)
}
func makeRow() throws -> Row {
var row = Row()
try row.set(Car.makeKey, make)
try row.set(Car.modelKey, model)
try row.set(Car.ownerIdKey, ownerId)
return row
}
}
extension Car: JSONConvertible {
convenience init(json: JSON) throws {
try self.init(
make: json.get(Car.makeKey),
model: json.get(Car.modelKey),
ownerId: json.get(Car.ownerIdKey)
)
}
func makeJSON() throws -> JSON {
var json = JSON()
try json.set(Car.idKey, id)
try json.set(Car.makeKey, make)
try json.set(Car.modelKey, model)
try json.set(Car.ownerIdKey, ownerId)
return json
}
}
extension Car: ResponseRepresentable {}
extension Car: Preparation {
static func prepare(_ database: Database) throws {
try database.create(self) { builder in
builder.id()
builder.string(Car.makeKey)
builder.string(Car.modelKey)
builder.foreignId(for: Owner.self)
}
}
static func revert(_ database: Database) throws {
try database.delete(self)
}
}
extension Car: Updateable {
public static var updateableKeys: [UpdateableKey<Car>] {
return [
UpdateableKey(Car.makeKey, String.self) { car, make in
car.make = make
}
]
}
}

What's the best way to return a collection of response representable objects in Swift Vapor?

Context:
Recently, I've decided to take up Swift server side development because I think the Vapor framework is extremely cool. I've gotten a bit stuck while experimenting and would like some advice on templating with leaf and vapor.
I've reviewed the documentation several times when it comes to rendering views. Rendering a templated view with variables requires the name of the leaf template and a Response Representable node object containing the variables.
Trying to work out a scenario with templating and the framework itself (because that's how I learn best), I tried to mock a blog format. This is my class/get request:
// MARK: Blog Post Object
final class BlogPost: NodeRepresentable {
var postId: Int
var postTitle: String
var postContent: String
var postPreview: String
func makeNode(context: Context) throws -> Node {
return try Node(node: [
"postId":self.postId,
"postTitle":self.postTitle,
"postContent":self.postContent,
"postPreview":self.postPreview
])
}
init(_ postId: Int, _ postTitle: String, _ postContent: String) {
self.postId = postId
self.postTitle = postTitle
self.postContent = postContent
self.postPreview = postContent.trunc(100)
}
}
// MARK: Blog view request; iterate over blog objects
drop.get("blog") { request in
let result = try drop.database?.driver.raw("SELECT * FROM Posts;")
guard let posts = result?.nodeArray else {
throw Abort.serverError
}
var postCollection = [BlogPost]()
for post in posts {
guard let postId = post["postId"]?.int,
let postTitle = post["postTitle"]?.string,
let postContent = post["postPreview"]?.string else {
throw Abort.serverError
}
let post = BlogPost(postId, postTitle, postContent)
postCollection.append(post)
}
// Pass posts to be tokenized
/* THIS CODE DOESN'T WORK BECAUSE "CANNOT CONVERT VALUE OF TYPE
* '[BLOGPOST]' TO EXPECTED DICTIONARY VALUE OF TYPE "NODE"
* LOOKING FOR THE BEST METHOD TO PASS THIS LIST OF OBJECTS
*/
drop.view.make("blog", [
"posts":postCollection
])
}
and this is my blog.leaf file:
#extend("base")
#export("head") {
<title>Blog</title>
}
#export("body") {
<h1 class="page-header">Blog Posts</h1>
<div class="page-content-container">
#loop(posts, "posts") {
<div class="post-container">
<h3 style="post-title">#(posts["postTitle"])</h3>
<p style="post-preview">#(posts["postPreview"])</h3>
</div>
}
</div>
}
Problem:
As you can see, I'm a bit stuck on finding the best method for iterating over objects and templating their properties into the leaf file. Anyone have any suggestions? Sorry for the bad programming conventions, by the way. I'm fairly new in Object/Protocol Oriented Programming.
What I ended up doing is, making the Post model conform to the Model protocol.
import Foundation
import HTTP
import Vapor
// MARK: Post Class
final class Post: Model {
var id: Node?
var title: String
var content: String
var date: Date
var isVisible: Bool
// TODO: Implement truncate extension for String and set preview
// to content truncated to 100 characters
var preview = "placeholder"
var exists: Bool = false
init(title: String, content: String, isVisible: Bool = true) {
self.title = title
self.content = content
self.date = Date()
self.isVisible = isVisible
}
init(node: Node, in context: Context) throws {
let dateInt: Int = try node.extract("date")
let isVisibleInt: Int = try node.extract("isVisible")
id = try node.extract("id")
title = try node.extract("title")
content = try node.extract("content")
date = Date(timeIntervalSinceNow: TimeInterval(dateInt))
isVisible = Bool(isVisibleInt as NSNumber)
exists = false
}
func makeNode(context: Context) throws -> Node {
return try Node(node: [
"id": id,
"title": title,
"content": content,
"date": Int(date.timeIntervalSince1970),
"isVisible": Int(isVisible as NSNumber)
])
}
static func prepare(_ database: Database) throws {
try database.create("Posts") { posts in
posts.id()
posts.string("title", optional: false)
posts.string("content", optional: false)
posts.int("date", optional: false)
posts.int("isVisible", optional: false)
}
}
static func revert(_ database: Database) throws {
try database.delete("posts")
}
}
Then to return/create instances of the Post object:
import Vapor
import Foundation
import HTTP
final class BlogController {
func addRoutes(_ drop: Droplet) {
let blogRouter = drop.grouped("blog")
let blogAPIRouter = drop.grouped("api","blog")
blogRouter.get("posts", handler: getPostsView)
blogAPIRouter.get("posts", handler: getPosts)
blogAPIRouter.post("newPost", handler: newPost)
}
// MARK: Get Posts
func getPosts(_ request: Request) throws -> ResponseRepresentable {
let posts = try Post.all().makeNode()
return try JSON(node: [
"Posts":posts
])
}
// Mark: New Post
func newPost(_ request: Request) throws -> ResponseRepresentable {
guard let title = request.data["title"]?.string,
let content = request.data["content"]?.string else {
throw Abort.badRequest
}
var post = Post(title: title, content: content)
try post.save()
return "success"
}
// Mark: Get Posts Rendered
func getPostsView(_ request: Request) throws -> ResponseRepresentable {
return try getPosts(request)
}
}
I'm not an expert on Vapor yet, but I think you need to use .makeNode() so your postCollection object get converted to something you can later use on the template.
Something like this:
drop.view.make("blog", ["posts":postCollection.makeNode()])
func list(_ req: Request) throws -> ResponseRepresentable {
let list = try User.all()
let node = try list.makeNode(in: nil)
let json = try JSON(node: [ "list":node ])
return json
}