ml-archive/submissions

Model with property not present in Request

Opened this issue · 3 comments

I have a simple Post model

// Post.swift
final class Post: MySQLModel, Content, Parameter {

    var id: Int?
    var title: String
    var body: String
    var userId: User.ID
}
extension Post: Submittable {
    convenience init(_ create: Post.Create) throws {
        self.init(id: nil, title: create.title, body: create.body, userId: create.userId)
    }
    
    func update(_ update: Post.Submission) throws {
        self.title = update.title ?? self.title
        self.body = update.body ?? self.body
    }
    
    
    struct Submission: SubmissionType {
        let title: String?
        let body: String?
    }
    
    struct Create: Decodable {
        let title: String
        let body: String
        let userId: User.ID
    }
}
extension Post.Submission {
    func fieldEntries() throws -> [FieldEntry<Post>] {
        return try [
            makeFieldEntry(keyPath: \.title, label: "Title", validators: [.count(5...)], isRequired: true),
            makeFieldEntry(keyPath: \.body, label: "Body", validators: [.count(10...)], isRequired: true),
        ]
    }
    
    init(_ post: Post?) {
        title = post?.title
        body = post?.body
    }
}
// PostController.swift
...
  func create(_ req: Request) throws -> Future<Either<Post, SubmissionValidationError>> {       
        let user = try req.authenticated(User.self)
        let userId = user?.id
        return try req.content.decode(Post.Submission.self)
            .createValid(on: req)
            .save(on: req)
            .promoteErrors()
    }

...

userId property needs to be added after a User obtained from token. It can not be sent in request parameters. It was the code before using Submissions

    func create(_ req: Request) throws -> Future<Post> {        
        let user = try req.authenticated(User.self)
        let userId = user?.id
        let postData = try req.content.syncDecode(Post.CreatePost.self)
        return Post(title: postData.title, body: postData.body, userId: userId!).save(on: req)
    }

Not all the properties of a model are sent via the request, some of them have to be calculated or fetched from DB and then use to create/save the model. Something like willCreate or willSave hook will be useful for these kind of situations.

Your situation seems like one that will be common. What if the Request would be passed into the init(_ create: Create) and update(_ update: Update) methods? Then you could authenticate there (or do any other database lookup) and set the user id. Would that solve your problem?

@siemensikkema Thanks, that would solve the problem.
But I think it will cause some inconsistency in codes, as the Request would pass to init, all the code that should be written in Controller section, have to be written in Model, or some duplicate code will be present in Model.
Is it possible to append optional parameters to create and update methods?
Like this:

 init(_ create: Post.Create, userId: userId) 

or pass a dictionary like in NotificationCenter's userInfo

How about the following:

let userID = try req.authenticated(User.self).requireID()
return try req.content
    .decode(Post.Submission.self)
    .validate(inContext: .create, on: req)
    .map { submission in
        Create(submission: submission, userId: userID)
    }
    .map(T.S.init)

For update you can simply change the user after updateValid and before save.