vapor / fluent

Vapor ORM (queries, models, and relations) for NoSQL and SQL databases
https://docs.vapor.codes/4.0/fluent/overview/
MIT License
1.32k stars 172 forks source link

Adds eTag related methods for Response creation and verification #667

Closed grosch closed 4 years ago

grosch commented 4 years ago

@tanner0101 This has to be deployed at the same time as https://github.com/vapor/vapor/pull/2246

In order to prevent lost updates, REST APIs should send back an ETag header when querying an object, and subsequent DELETE/PATCH calls should include that ETag in an If-Match HTTP header.

If you are returning your Model directly (i.e. it also conforms to Content) you should now write your controller methods as follows. Note that they all return a Response now.

func get(req: Request) throws -> EventLoopFuture<Response> {
    Todo.find(req.parameters.get("id"), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMapThrowing { try .withETag($0) }
}

func patch(req: Request) throws -> EventLoopFuture<Response> {
    return Todo.find(req.parameters.get("id"), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMapThrowing { try $0.verifyETag(on: req) }
        .flatMap { todo in
            // Update all your properties here

            return todo.update(on: req.db).flatMapThrowing {
                // If your model update will modify related tables, you'll
                // want to req-query the object here, before you create
                // the response DTO.
                return try .withETag(todo, includeBody: false)
            }
    }
}

func create(req: Request) throws -> EventLoopFuture<Response> {
    let todo = try Todo(from: req.content.decode(TodoCreateDTO.self))

    return todo.create(on: req.db)
        .flatMapThrowing { try Response.created(todo, for: req) }
}

func delete(req: Request) throws -> EventLoopFuture<HTTPStatus> {
    return Todo.find(req.parameters.get("id"), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMapThrowing { try $0.verifyETag(on: req) }
        .flatMap { $0.delete(on: req.db).transform(to: .noContent) }
}

If, however, you are using a DTO instead of directly returning the model, then conform your response object to ModelContent (instead of Content) and then write them like so.

func getDTO(req: Request) throws -> EventLoopFuture<Response> {
    Todo.find(req.parameters.get("id"), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMapThrowing { return try .withETag(TodoDTO(model: $0)) }
}

func patchDTO(req: Request) throws -> EventLoopFuture<Response> {
    return Todo.find(req.parameters.get("id"), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMapThrowing { try $0.verifyETag(TodoDTO.self, on: req)}
        .flatMap { todo in
            // Update all your properties here

            return todo.update(on: req.db).flatMapThrowing {
                // If your model update will modify related tables, you'll
                // want to req-query the object here, before you create
                // the response DTO.
                return try .withETag(TodoDTO(model: todo), includeBody: false)
            }
    }
}

func createDTO(req: Request) throws -> EventLoopFuture<Response> {
    let todo = try Todo(from: req.content.decode(TodoCreateDTO.self))

    return todo.create(on: req.db)
        .flatMapThrowing {
            let dto = try TodoDTO(model: todo)
            return try .created(dto, for: req, id: todo.requireID())
    }
}

func deleteTDO(req: Request) throws -> EventLoopFuture<HTTPStatus> {
    return Todo.find(req.parameters.get("id"), on: req.db)
        .unwrap(or: Abort(.notFound))
        .flatMapThrowing { try $0.verifyETag(TodoDTO.self, on: req) }
        .flatMap { $0.delete(on: req.db).transform(to: .noContent) }
}
grosch commented 4 years ago

Closing this and reworking entirely.