Skip to content
This repository has been archived by the owner on Apr 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #4 from nodes-vapor/patch-model
Browse files Browse the repository at this point in the history
Patch model
  • Loading branch information
BrettRToomey authored Jan 17, 2017
2 parents 665f30b + 96fbb31 commit c0c97d9
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 5 deletions.
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,32 @@ Now that you have a conforming model, you can safely extract it from a `Request`
### Main.swift
```swift
drop.post("users") { req in
var user: User = try request.extractModel()
var user: User = try req.extractModel()
print(user.id == nil) // prints `true`
try user.save()
return user
}
```

## Validation 👌
## Updating/patching existing models 🖇
Just like model extraction, securely updating a model with data from a request is a trivial process.
```swift
drop.post("users", User.self) { req, user in
var updatedUser = try req.patchModel(user)
try updatedUser.save()
}
```

### Updating model with Id
If you don't have an instance of the model you wish to update you can have `Sanitize` fetch and update the model for you.
```swift
drop.post("users", Int.self) { req, userId in
var user: User = try req.patchModel(userId)
try user.save()
}
```

## Validation ✅
This package doesn't specifically provide any validation tools, but it is capable of running your validation suite for you. Thusly, simplifying the logic in your controllers. Sanitized has two ways of accomplishing this: pre and post validation.

### Pre-init validation
Expand All @@ -66,7 +84,7 @@ Create a `preValidation` check by overriding the default implementation in your
```swift
static func preValidate(data: JSON) throws {
// we only want to ensure that `name` exists/
guard data["name"]?.string != nil else {
guard data["name"] != nil else {
throw MyError.invalidRequest("Name not provided.")
}
}
Expand Down
59 changes: 58 additions & 1 deletion Sources/Sanitizable+Request.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import HTTP
import Node
import Vapor

extension Request {
Expand All @@ -7,7 +8,7 @@ extension Request {
/// - Throws:
/// - badRequest: Thrown when the request doesn't have a JSON body.
/// - updateErrorThrown: `Sanitizable` models have the ability to override
/// the error thrown for when a model fails to instantiate.
/// the error thrown when a model fails to instantiate.
///
/// - Returns: The extracted, sanitized `Model`.
public func extractModel<M: Model>() throws -> M where M: Sanitizable {
Expand All @@ -30,4 +31,60 @@ extension Request {
try model.postValidate()
return model
}

/// Updates the `Model` with the provided `id`, first stripping sensitive fields
///
/// - Parameters:
/// - id: id of the `Model` to fetch and then patch
///
/// - Throws:
/// - notFound: No entity found with the provided `id`.
/// - badRequest: Thrown when the request doesn't have a JSON body.
/// - updateErrorThrown: `Sanitizable` models have the ability to override
/// the error thrown when a model fails to instantiate.
///
/// - Returns: The updated `Model`.
public func patchModel<M: Model>(id: NodeRepresentable) throws -> M where M: Sanitizable {
guard let model = try M.find(id) else {
throw Abort.notFound
}

return try patchModel(model)
}

/// Updates the provided `Model`, first stripping sensitive fields
///
/// - Parameters:
/// - model: the `Model` to patch
///
/// - Throws:
/// - badRequest: Thrown when the request doesn't have a JSON body.
/// - updateErrorThrown: `Sanitizable` models have the ability to override
/// the error thrown when a model fails to instantiate.
///
/// - Returns: The updated `Model`.
public func patchModel<M: Model>(_ model: M) throws -> M where M: Sanitizable {
//consider making multiple lines
guard let requestJSON = self.json?.permit(M.permitted).makeNode().nodeObject else {
throw Abort.badRequest
}

var modelJSON = try model.makeNode()

requestJSON.forEach {
modelJSON[$0.key] = $0.value
}

var model: M
do {
model = try M(node: modelJSON)
} catch {
let error = M.updateThrownError(error)
throw error
}

model.exists = true
try model.postValidate()
return model
}
}
11 changes: 11 additions & 0 deletions Sources/Sanitizable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,14 @@ extension Sanitizable {

}
}

public enum CredentialScope {
case none
case user
case otherUser
case admin
}

extension CredentialScope: Context {

}
55 changes: 54 additions & 1 deletion Tests/SanitizedTests/SanitizedTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,55 @@ class SanitizedTests: XCTestCase {
XCTAssertNil(result["name"])
XCTAssertNil(result["email"])
}

func testPatchBasic() {
let model = try! TestModel(node: [
"id": 15,
"name": "Rylo Ken",
"email": "[email protected]"
])

let request = buildRequest(body: [
"id": 11, // this should be sanitized
"email": "[email protected]"
])

expectNoThrow() {
let model = try request.patchModel(model)
XCTAssertEqual(model.id?.int, 15)
XCTAssertEqual(model.name, "Rylo Ken")
XCTAssertEqual(model.email, "[email protected]")
}
}

func testPatchFailed() {
let model = try! TestModel(node: [
"id": 15,
"name": "Rylo Ken",
"email": "[email protected]"
])

let request = buildInvalidRequest()

expect(toThrow: Abort.badRequest) {
let _: TestModel = try request.patchModel(model)
}
}

func testPatchById() {
Database.default = Database(TestDriver())
let request = buildRequest(body: [
"id": 11, // this should be sanitized
"email": "[email protected]"
])

expectNoThrow() {
let model: TestModel = try request.patchModel(id: 1)
XCTAssertEqual(model.id?.int, 1, "Id shouldn't have changed")
XCTAssertEqual(model.name, "Jimmy", "Name shouldn't have changed")
XCTAssertEqual(model.email, "[email protected]", "email should've changed")
}
}
}

extension SanitizedTests {
Expand Down Expand Up @@ -127,7 +176,11 @@ struct TestModel: Model, Sanitizable {
}

func makeNode(context: Context) throws -> Node {
return .null
return try Node(node: [
"id": id,
"name": name,
"email": "email"
])
}

static func prepare(_ database: Database) throws {}
Expand Down
26 changes: 26 additions & 0 deletions Tests/SanitizedTests/Utilities/TestDatabase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Vapor
import Fluent

class TestDriver: Driver {
var idKey: String = "id"

func query<T : Entity>(_ query: Query<T>) throws -> Node {
switch query.action {
case .fetch:
return Node.array([Node.object([
"id": 1,
"name": "Jimmy",
"email": "[email protected]"
])])
default:
return nil
}
}

func schema(_ schema: Schema) throws {}

@discardableResult
public func raw(_ query: String, _ values: [Node] = []) throws -> Node {
return .null
}
}

0 comments on commit c0c97d9

Please sign in to comment.