Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When using beforeSave on object with a Pointer column - "error: ParseError code=111 error=schema mismatch for Test.test1; expected Pointer<Test1> but got Object" #65

Open
jaysonng opened this issue Feb 17, 2024 · 25 comments
Labels
bug Something isn't working

Comments

@jaysonng
Copy link
Contributor

jaysonng commented Feb 17, 2024

When using beforeSave trigger on an object that has a pointer to another object like so:

struct Test: ParseObject {

    //: These are required for `ParseObject`.
    var originalData: Data?
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?

    var name: String?
    var test1: Test1?
}

struct Test1: ParseObject {

    //: These are required for `ParseObject`.
    var originalData: Data?
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?

    var description: String?
    var test2: Test2?
    var test3: Test3?
}

Doing nothing but just having the beforeSave trigger on produces a schema mismatch error where it expects a Pointer to the sub-object.

func beforeSaveTest(req: Request) async throws -> HookResponse<Test> {

        if let error: HookResponse<Test> = checkHeaders(req) { return error }
        
        var parseRequest = try req.content.decode(HookTrigObjReq<User, Test>.self)
        let options      = try parseRequest.options(req)
        
        if parseRequest.user != nil {
            parseRequest = try await parseRequest.hydrateUser(options: options, request: req)
        }
        
        guard let object = parseRequest.object else {
            return ParseHookResponse(error: .init(code: .missingObjectId,
                                                  message: "Object not sent in request."))
        }
        
        return HookResponse(success: object)
        
    }

I encountered this as I was trying to save a complex object with multiple pointers and tested with these Test objects getting the same result.

error: ParseError code=111 error=schema mismatch for Test.test1; expected Pointer<Test1> but got Object
@jaysonng jaysonng changed the title When using beforeSave - error: "schema mismatch for Test.test1; expected Pointer<Test1> but got Object" When using beforeSave on object with a Pointer column - "error: ParseError code=111 error=schema mismatch for Test.test1; expected Pointer<Test1> but got Object" Feb 17, 2024
@cbaker6 cbaker6 added the wontfix This will not be worked on label Feb 17, 2024
@cbaker6
Copy link
Member

cbaker6 commented Feb 17, 2024

See my response here: netreconlab/Parse-Swift#151 (comment)

@jaysonng
Copy link
Contributor Author

jaysonng commented Feb 17, 2024

See my response here: netreconlab/Parse-Swift#151 (comment)

I think this is a different problem. it wasn't a matter of it having the same schema or anything like that.

I was just passing an already existing object, which has pointed to another object (already existing)

it didn't have to create anything. But passing the object through Parse-Server-Swift BeforeSave causes this error.

i just tested beforeSave on JS cloudcode and it updated fine.

This has something to do with Pointer and XXXXX object and how ParseSwift handles it. I'm just not sure exactly where it goes wrong.

@jaysonng
Copy link
Contributor Author

Screenshot 2024-02-17 at 11 36 49 PM

As an example. I just tried to update "wohoo" to "wohoo1"

and again got this error:

error: ParseError code=111 error=schema mismatch for Test.test1; expected Pointer<Test1> but got Object

Even when I try it on Dashboard.

Screenshot 2024-02-17 at 11 39 58 PM

@cbaker6
Copy link
Member

cbaker6 commented Feb 17, 2024

I don’t see a “beforeSave” trigger in the code you provided. I do see “beforeSaveTest” but that’s not a beforeSave trigger

@jaysonng
Copy link
Contributor Author

jaysonng commented Feb 17, 2024

I don’t see a “beforeSave” trigger in the code you provided. I do see “beforeSaveTest” but that’s not a beforeSave trigger

sorry, I call beforeSaveTest using the boot routes builder like so.

    func boot(routes: RoutesBuilder) throws {
        
        let lokalityGroup = routes.grouped("lokality")
        //: Triggers
        lokalityGroup.post("save", "before", object: Lokality.self,
                           trigger: .beforeSave, use: beforeSave)
        lokalityGroup.post("save", "before", object: Test.self,
                           trigger: .beforeSave, use: beforeSaveTest)
   }

with logs

[ INFO ] POST /lokality/save/before [request-id: 2049B8FB-60B5-49E9-AC65-8A80302F75DA]

so it does get called on .save()

@cbaker6
Copy link
Member

cbaker6 commented Feb 17, 2024

It still looks like the same problem as #65 (comment) to me. ParseServerSwift uses ParseSwift, the beforeSave webhook is called before an object is created on the server so it will run into the same issue. I think you can improve on your systems design to circumvent this, like not saving empty items for no reason. You should save objects when they actually have data. I gave some examples in the previous links. If you insist on keeping your design, you may need to remove your “beforeSave” code using ParseServerSwift, and us the JS Cloud Code for that part.

You could also never create empty objects on the client, use ParseServerSwift beforeSave to create them if they are nil

@jaysonng
Copy link
Contributor Author

It's not empty though.

They're already there and existing.

@jaysonng
Copy link
Contributor Author

jaysonng commented Feb 17, 2024

Screenshot 2024-02-17 at 11 36 49 PM

See this. The info is already there on the dashboard. I'm just trying to update the row with objectId DrhQdVNrhs

"Wohoo" is a string that's already existing.

The Test1 is existing and pointing to an existing object.

When i try to update "wohoo" to "wohoo1" I get the error.

@jaysonng
Copy link
Contributor Author

So it's not a matter of creating empty objects this time around but how ParseServerSwift handles saving a Pointer but receiving an object in beforeSave.

Somehow it sees the struct and thinks it's a 'Pointer' but really it's getting a 'Test1' - in beforeSave.

@jaysonng
Copy link
Contributor Author

I'll give it a try on more of my other classes that have the same setup of pointers a do a beforeSave that already has existing data and see if i get it saving.

The Test classes I use are for posting here on Github but I initially encountered the error on my other classes.

@jaysonng
Copy link
Contributor Author

jaysonng commented Feb 18, 2024

I got it to save just now by changing var test1: Test1? on my Test struct by changing it to a Pointer<Test1>? and it saved as expected on ParseDashboard.

The save was just updating an existing object.

struct Test: ParseObject {

    //: These are required for `ParseObject`.
    var originalData: Data?
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?

    var name: String?
    var test1: Test1?
}

to

var test1: Pointer<Test1>?

If that gives you an idea of where we can look?

This "fix" isn't workable as I'd have to toPointer() every time I use the object in my code.

@cbaker6
Copy link
Member

cbaker6 commented Feb 18, 2024

I recommend looking at the unit tests and try to create a PR which replicates your scenario. You should be able to do it in the ParseSwift repo as it supports all of the hooks. The test case will most likely go into this file: https://github.com/netreconlab/Parse-Swift/blob/main/Tests/ParseSwiftTests/ParseHookTriggerTests.swift.

@jaysonng
Copy link
Contributor Author

jaysonng commented Feb 18, 2024

I recommend looking at the unit tests and try to create a PR which replicates your scenario. You should be able to do it in the ParseSwift repo as it supports all of the hooks. The test case will most likely go into this file: https://github.com/netreconlab/Parse-Swift/blob/main/Tests/ParseSwiftTests/ParseHookTriggerTests.swift.

My coding skills haven't gotten that far yet. 😄 I mean I've done a few. It's how I test the methods. but not sure how to test the hooks yet directly like in that ParseHookTriggerTest.swift. I'll look into it though.

thanks

@cbaker6
Copy link
Member

cbaker6 commented Feb 18, 2024

What happens when you try the following?

test1.set(\.name, “woohoo1”) and then save? When updating individual properties on an object that’s already created, it’s better to use set anyways: https://github.com/netreconlab/Parse-Swift/blob/b3169f2b438df9bb1d14935246be0a462c97c42f/ParseSwift.playground/Pages/1%20-%20Your%20first%20Object.xcplaygroundpage/Contents.swift#L241-L261

@jaysonng
Copy link
Contributor Author

What happens when you try the following?

test1.set(\.name, “woohoo1”) and then save? When updating individual properties on an object that’s already created, it’s better to use set anyways: https://github.com/netreconlab/Parse-Swift/blob/b3169f2b438df9bb1d14935246be0a462c97c42f/ParseSwift.playground/Pages/1%20-%20Your%20first%20Object.xcplaygroundpage/Contents.swift#L241-L261

func testTest() async {
        do {

            let testQuery = Test.query()
            var result = try await testQuery.first(options: [.usePrimaryKey])
//            result.name = "wohoo1234"
            result.set(\.name, to: "wohoo4321")
            try await result.save(options: [.usePrimaryKey])

            print("Successfully updated: \(result.objectId.logable)")

        } catch {
            print("error: \(error.localizedDescription)")
            XCTAssertNil("Update Test failed")
        }

    }

still the same error
error: ParseError code=111 error=schema mismatch for Test.test1; expected Pointer<Test1> but got Object

@jaysonng
Copy link
Contributor Author

I'm still trying to figure out how to do a Unit Test without an actual database though - for the PR.

@cbaker6
Copy link
Member

cbaker6 commented Feb 19, 2024

I'm still trying to figure out how to do a Unit Test without an actual database though - for the PR.

The test file I referenced before shows how to do this:

Server response: https://github.com/netreconlab/Parse-Swift/blob/b3169f2b438df9bb1d14935246be0a462c97c42f/Tests/ParseSwiftTests/ParseHookTriggerTests.swift#L188-L201

Expected JSON: https://github.com/netreconlab/Parse-Swift/blob/b3169f2b438df9bb1d14935246be0a462c97c42f/Tests/ParseSwiftTests/ParseHookTriggerTests.swift#L72-L82

There are 1000+ examples in the current unit tests. Look for one that seems more intuitive, then go to more complex

@jaysonng
Copy link
Contributor Author

How do I actually trigger the beforeSave Trigger in a test? I can't seem to find an existing test that actually calls it or runs it. everything seems to be just creating / updating hooks.

@jaysonng
Copy link
Contributor Author

@cbaker6

I haven't been able to successfully do a unit test with my case but I have figured out what's causing the problem.

In JS CloudCode, the object I get out of the Request vs the paramsRequest.object (which was decoded) is structured differently.

In JS CloudCode, the object keeps the __type: Pointer on the object. In ParseServerSwift, the line

parseRequest = req.content.decode(...)

converts the Pointer to just an Object which is then what we pass to the ParseHookResponse at the end of the beforeSave

return ParseHookResponse(success: object)

Question is, how do I pull out the data I need from the Request (while in the beforeSave trigger function) without decoding it or decode it in a way that I keep the Pointer structure?

func beforeSaveTest(req: Request) async throws -> ParseHookResponse<Test> {
        
        if let error: HookResponse<Test> = checkHeaders(req) { return error }
        
// object structure is changed here which JS validation errors out later in SchemaController.js
        var parseRequest = try req.content.decode(ParseHookTriggerObjectRequest<User, Test>.self)
        let options      = try parseRequest.options(req)
        
        if parseRequest.user != nil {
            parseRequest = try await parseRequest.hydrateUser(options: options, request: req)
        }
        
        guard var object = parseRequest.object else {
            return ParseHookResponse(error: .init(code: .missingObjectId,
                                                  message: "Object not sent in request."))
        }

        return HookResponse(success: object)
    }

SchemaController.js in parse-server

@cbaker6
Copy link
Member

cbaker6 commented Feb 22, 2024

I think I may understand where the issue is at now, at this line:

// Parse uses tailored encoders/decoders. These can be retrieved from any ParseObject
ContentConfiguration.global.use(encoder: User.getJSONEncoder(), for: .json)

Change to:

// Parse uses tailored encoders/decoders. These can be retrieved from any ParseObject
ContentConfiguration.global.use(encoder: User.getEncoder() /* Change the encoder here */, for: .json)

Delete all of your hooks and functions before connecting the updated ParseServer and let me know if this works. It's possible this may introduce other issues that weren't originally there since parse-server-swift is only connecting to a ParseServer and not something that expects traditional JSON. If there are new issues, we will need to look further into how to better utilize Vapors encoder configuration.

@cbaker6 cbaker6 added bug Something isn't working and removed wontfix This will not be worked on labels Feb 22, 2024
@jaysonng
Copy link
Contributor Author

jaysonng commented Feb 23, 2024

Error when building Parse-Swift-Server:

Screenshot 2024-02-23 at 1 33 02 PM

needed to add as! ContentCoder because of this error:

Screenshot 2024-02-23 at 1 33 53 PM

@jaysonng
Copy link
Contributor Author

Commenting out line 50, allows me to build and run Parse-Server-Swift but I get the same schema error on calling beforeSave.

Screenshot 2024-02-23 at 5 13 44 PM

@jaysonng
Copy link
Contributor Author

I've been trying to pull the data out "as is" of the HTTP header Request but couldn't figure it out.

I'm not well versed in decoding yet. usually I just decode json.

@cbaker6
Copy link
Member

cbaker6 commented Feb 23, 2024

It doesn't look like it's a decoding issue, it looks more like an encoding issue because the parse-server expects Parse json to look a particular way. Objects that can be pointers should be sent as pointers which is why #65 (comment) works. I'll look into a fix when I get some time next week.

If you you really want to get it working now, create two objects for ParseObjects that contain pointers:

// This is the same as the original, but add the `convertForSending` method
struct Test: ParseObject {

    //: These are required for `ParseObject`.
    var originalData: Data?
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?

    var name: String?
    var test1: Test1?

     func convertForSending() -> TestForSendingOnly {
           var convertedObject = TestForSendingOnly()
           // Copy all properties manually
           convertedObject.createdAt = try? self.createdAt
           // ...
           convertedObject.test1 = try? self.test1.toPointer()

           return convertedObject
    }
}

// This almost looks like the original, but has pointers
struct TestForSendingOnly: ParseObject {

    //: These are required for `ParseObject`.
    var originalData: Data?
    var objectId: String?
    var createdAt: Date?
    var updatedAt: Date?
    var ACL: ParseACL?

    var name: String?
    var test1: Pointer<Test1>?
}

// Cloud Code
func beforeSaveTest(req: Request) async throws -> ParseHookResponse<TestForSendingOnly> {
        
        if let error: HookResponse<Test> = checkHeaders(req) { return error }
        
        var parseRequest = try req.content.decode(ParseHookTriggerObjectRequest<User, Test>.self)
        let options      = try parseRequest.options(req)
        
        if parseRequest.user != nil {
            parseRequest = try await parseRequest.hydrateUser(options: options, request: req)
        }
        
        guard var object = parseRequest.object else {
            return ParseHookResponse(error: .init(code: .missingObjectId,
                                                  message: "Object not sent in request."))
        }

        let convertedObject = object.convertForSending()
        return HookResponse(success: convertedObject)
}

@jaysonng
Copy link
Contributor Author

ah didn't think of that workaround. but given that I have lots of objects with lots of pointers, I think I'll wait for your fix for this then.

thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants