Kevin Renskers
Kevin Renskers

Reputation: 5960

Vapor 3: when returning a model, how to easily also return child objects

I have a model Campaign, that has multiple Months:

final class Campaign: Content, SQLiteModel {
  var id: Int?
  var name: String
  var months: Children<Campaign, Month> {
    return children(\.campaignID)
  }
}

When I want to return a Campaign in the most basic way, it doesn't include the Months, as computed properties are not Codable as I understand.

func getOneHandler(_ req: Request) throws -> Future<Campaign> {
  return try req.parameters.next(Campaign.self)
}

So, I made a new struct to hold the full object I want to return

struct FullCampaignData: Content {
  let id: Int
  let name: String
  var months: [Month]?
}

And then modified my route handler like this:

func getOneHandler(_ req: Request) throws -> Future<FullCampaignData> {
  return try req.parameters.next(Campaign.self).flatMap(to: FullCampaignData.self) { campaign in
    return try campaign.months.query(on: req).all().map(to: FullCampaignData.self) { months in
      return try FullCampaignData(id: campaign.requireID(), name: campaign.name, months: months)
    }
  }
}

This does work. However, it seems like a lot of work and a lot of boilerplate to do this. What's the normal Swifty "Vapory" way to deal with child objects, or other computed objects in general? Right now it seems to come down to a bunch of different versions of your models (for creation, for returning, the actual full internal one) and then converting between them, but I am hoping there is something I am missing here? Because it's so easy to forget to add a newly added model property also to that special public model.

Or at the very least, if converting between models in indeed the recommended way, is there a way doesn't need all these nested maps/flatmaps in the route handler?

Upvotes: 3

Views: 967

Answers (1)

imike
imike

Reputation: 5656

You could use SwifQL lib for complex queries

I'm not sure that with SQLite it is possible to subquery Months, but with PostgreSQL it is really easy cause it supports JSON

So for PostgreSQL your query may look like

func getOneHandler(_ req: Request) throws -> Future<FullCampaignData> {
    let monthsSubquery = SwifQL.select(Fn.array_agg(Fn.to_jsonb(Month.table)))
    let query = SwifQL.select(Campaign.table.*, |monthsSubquery| => "months")
        .from(Campaign.table)
        .join(.leftOuter, Month.table, on: \Month.campaignID == \Campaign.id)
        .execute(on: req, as: .psql)
        .all(decoding: FullCampaignData.self)
}

or

func getOneHandler(_ req: Request) throws -> Future<FullCampaignData> {
    let monthsSubquery = SwifQL.select(Fn.array_agg(Fn.to_jsonb(Month.table)))
        .from(Month.table)
        .where(\Month.campaignID == \Campaign.id)
    let query = SwifQL.select(Campaign.table.*, |monthsSubquery| => "months")
        .from(Campaign.table)
        .execute(on: req, as: .psql)
        .all(decoding: FullCampaignData.self)
}

Upvotes: 3

Related Questions