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.
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.