Kevin Renskers
Kevin Renskers

Reputation: 5960

Vapor 4: how to map an eagerly loaded parent relation into a different format?

I am struggling a bit with how to return a model that contains a parent relationship, while mapping that eagerly loaded model into a different form.

Let's consider the following 2 models: Course and User.

final class Course: Model, Content {
  static let schema = "courses"

  @ID(key: .id)
  var id: UUID?

  @Field(key: "name")
  var name: String

  @Parent(key: "teacher_id")
  var teacher: User

  init() { }
}

final class User: Model, Content {
  static let schema = "users"

  @ID(key: .id)
  var id: UUID?

  @OptionalField(key: "avatar")
  var avatar: String?

  @Field(key: "name")
  var name: String

  @Field(key: "private")
  var somePrivateField: String

  init() { }
}

I have a route like this, which returns an array of courses:

func list(req: Request) throws -> EventLoopFuture<[Course]> {
  return Course
    .query(on: req.db)
    .all()
}

The resulting JSON looks something like this:

[
  {
    "id": 1,
    "name": "Course 1",
    "teacher": {
      "id": 1
    }
]

What I want instead is that the teacher object is returned, which is easy enough by adding .with(\.$teacher) to the query. Vapor 4 does make this very easy!

[
  {
    "id": 1,
    "name": "Course 1",
    "teacher": {
      "id": 1,
      "name": "User 1",
      "avatar": "https://www.example.com/avatar.jpg",
      "somePrivateField": "super secret internal info"
    }
]

And there's my problem: the entire User object is returned, with literally all fields, even ones I don't want to make public.

What is the easiest way to transform the teacher info a different version of the User model, like PublicUser? Does that mean I have to make a DTO for the Course, map my array from [Course] to [PublicCourse], copy all properties, keep them in sync when the Course model changes, etc?

That seems like a lot of boilerplate with lots of room for mistakes in the future. Would love to hear if there are better options.

Upvotes: 2

Views: 1031

Answers (2)

Nick
Nick

Reputation: 5200

Okay, what about this approach? Create a second Model, called Teacher, say, that is defined as the subset of fields from User that you want to expose in your API/JSON and has the same schema/table name as User:

final class Teacher: Model, Content {
  static let schema = "users"

  // public fields
}

Then change your relation in Course to:

@Parent(key: "teacher_id")
var teacher: Teacher

Your original query will work unchanged, but just returning the reduced field-set. It will work certainly if you are using Teacher read-only. No need to create Migrations for Teacher as the underlying table exists. I can't see a way to avoid including the ID, but that may not be a problem.

Upvotes: 1

Nick
Nick

Reputation: 5200

You can do this by first encoding the original model and then decoding it into a structure with fewer fields. So, for an instance of Course stored in course to convert to PublicCourse you would do:

struct PublicCourse: Decodable {
    //...
    let teacher: PublicUser
    //...
}

let course:Course = // result of Course.query including `with(\.$teacher)`
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(course)
let publicCourse = try decoder.decode(PublicCourse.self, from: data)

Notice the PublicUser field in the structure. If this is the cut-down version, you can generate your minimal JSON in one go.

Upvotes: 2

Related Questions