Reputation: 5960
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
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
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