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

Firestore customizable encoding for where clauses and update methods #607

Open
wants to merge 29 commits into
base: master
Choose a base branch
from

Conversation

Daeda88
Copy link
Contributor

@Daeda88 Daeda88 commented Aug 30, 2024

Fixes #604 and trumps #605

This solution extends all update methods in Firestore to support more advanced update methods. String fields and FieldPaths can now be used interchangably and custom serializer can be passed.

documentRef.update("field" to "value, "otherField" to 1)

becomes

documentRef.update {
     // specifying build settings is optional
    buildSettings = {}

    "field" to "value"
    FieldPath("otherField") to 1
    "customField".to(CustomValueSerializer(), customValue)
}

Note that the existing vararg pairs can still be used.

Similar support has been added to where clauses as well as startAt/endAt/etc:

query.startAt {
    add(value)
    addWithStrategy(CustomValueSerializer(), customValue)
 }

There is a small regression on the existing update methods in that

public fun update(documentRef: DocumentReference, vararg fieldsAndValues: Pair<String, Any?>, buildSettings: EncodeSettings.Builder.() -> Unit)changed to public fun update(documentRef: DocumentReference, buildSettings: EncodeSettings.Builder.() -> Unit, vararg fieldsAndValues: Pair<String, Any?>) due to Kotlin not being able to figure out which update method to use.

Furthermore, I've split the Firestore tests into a few different files and extended them so they clean up all data after completion.

Lastly, I discovered bug #613 and fixed it

Daeda88 and others added 5 commits August 29, 2024 10:39
It had become part of commons-internal which kind of defeats the purpose.

Also made FirebaseEncoder/FirebaseDecoder an interface in the public API. Useful for writing custom Serializers that have custom behaviour on Firebase
@Daeda88
Copy link
Contributor Author

Daeda88 commented Aug 30, 2024

I still need to add Readme/some tests, but feedback would already be appreciated @nbransby as I wont be able to continue until next week

@Daeda88
Copy link
Contributor Author

Daeda88 commented Sep 2, 2024

Now fully ready for review @nbransby / @Reedyuk . See description above for details of changes.

@Daeda88
Copy link
Contributor Author

Daeda88 commented Sep 29, 2024

Will anyone ever review this @nbransby 😅

@nbransby
Copy link
Member

Sorry been a crazy month, will hopefully get to it soon

@nbransby
Copy link
Member

nbransby commented Oct 5, 2024

There is a small regression on the existing update methods in that

Does that make this a breaking change?

@nbransby
Copy link
Member

nbransby commented Oct 5, 2024

I still need to add Readme/some tests, but feedback would already be appreciated @nbransby as I wont be able to continue until next week

looks great to me, I would like to see more comprehensive documentation in the README that would also help me understand the intended usage.

@Daeda88
Copy link
Contributor Author

Daeda88 commented Oct 5, 2024

There is a small regression on the existing update methods in that

Does that make this a breaking change?

Yeah, its breaking. I went from:

update(docRef, "path" to value) { encodeDefaults = false }

to

update(docRef, { encodeDefaults = false }, "path" to value)

The reason I had to do something is because the new update method has the signature:

update(docRef) {
  "path" to value
}

and since the original method takes a vararg, it can be called as update(docRef) { encodeDefaults = false }, which means the compiler wont know wich method to call. I considered alternatives:

  1. Change the original code to always take at least one path (i.e. fun update(docRef, firstValue: Pair<String, Any>, vararg others: Pair<String, Any>) but that would mean you woundnt be able to call it with update(docRef, *listOfPairs.toTypedArray()) which I assume is a common usecase.
  2. Name the new update function something else, e.g. updateWith, but I though the downside of that was that it sort of breaks the agreement that we name stuff like it is named in the native libraries
  3. I do this approach, knowing that the encodeSettings closure is relatively new and will thus probably be not used that much yet.
  4. Keep the old signature and the new one as is now, but that means you have to use named arguments for the closure which sucks.

I think 1 is a definitive no-go. If you prefer we do 2, Im fine with that as well. Its always gonna be a tradeoff so its pick your poison basically.

As for documentation, Ill change that as soon as we agree on this point :)

@nbransby
Copy link
Member

nbransby commented Oct 5, 2024

I spent long enough trying to think of additional alternatives without joy so, I favour 2 and calling it updateFields

@Daeda88
Copy link
Contributor Author

Daeda88 commented Oct 7, 2024

Done @nbransby

@nbransby
Copy link
Member

nbransby commented Oct 9, 2024

Can you add a little more documentation to the readme?

@Daeda88
Copy link
Contributor Author

Daeda88 commented Oct 21, 2024

Sorry for the slow response, hope its clearer now @nbransby

Copy link
Collaborator

@Reedyuk Reedyuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me, will let @nbransby have the final say

README.md Outdated
"otherField".to(IntAsStringSerializer(), 1)

// Overwrite build settings. All fields added after this will have these build settings applied
buildSettings = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Daeda88 can you suggest a better name for this? I find buildSettings confusing in this context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I've refactored it to a method called 'encodeNextWith'.

Usage:

documentRef.updateFields {
    "field" to "value"
    // Set the value of otherField to "1" using a custom Serializer
    "otherField".to(IntAsStringSerializer(), 1)
    
    // Overwrite build settings. All fields added after this will have these build settings applied
    encodeNextWith {
        encodeDefaults = true
        serializersModule = module
    }
    "city" to abstractCity
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just add encodeWith as an optional arg to the to function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because then you'd have to add the same block to every call to 'to':

"field".to("value") { encodeDefaults = true }
"otherField".to("otherValue") { encodeDefaults = true }

Just looks less clean in my opinion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes thats not great either. But the blockThatAffectsEverythingBelowIt {} pattern doesn't feel that intuitive to me.

What about updateFields taking the encode settings as optional arguments in addition to being able to pass them into the to function, so it would become:

documentRef.updateFields(encodeDefaults = true) {
    "field" to "value"
    "otherField".to(IntAsStringSerializer(), 1)
    
    "city".to(abstractCity) { serializersModule = module }
}

Although that does beg the question why not just have the encode settings as optional arguments in the to function also, making it simply:

documentRef.updateFields(encodeDefaults = true) {
    "field" to "value"
    "otherField".to(IntAsStringSerializer(), 1)
    
    "city".to(abstractCity, serializersModule = module)
}

thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that now, you can set the encodeSettings anywhere in the block:

serializersModule = module
"city" to abstractCity

should be the same as

"city" to abstractCity
serializersModule = module

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so withEncodeSettings is just to restrict the settings to certain properties otherwise you can just set the encodeSettings at the root level? e.g:

documentRef.updateFields {
    "field" to "value"
    "otherField".to(IntAsStringSerializer(), 1)
    encodeDefaults = true
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. You can set the encodeSettings directly in the root dsl as well. The withEncodeSettings block allows for setting custom settings that apply only to the values added within the block. Seems like the nicest way to me. Its flexible and relatively readable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's much better - I even think it's not necessary to also accept the settings as optional arguments to updateFields, so:

documentRef.updateFields(encodeDefaults = true) {
    "field" to "value"
    "otherField".to(IntAsStringSerializer(), 1)
    
    withEncodeSettings {
        serializersModule = module
        "city" to abstractCity
    }
}

becomes:

documentRef.updateFields {
    encodeDefaults = true
    "field" to "value"
    "otherField".to(IntAsStringSerializer(), 1)
    
    withEncodeSettings {
        serializersModule = module
        "city" to abstractCity
    }
}

Copy link
Contributor Author

@Daeda88 Daeda88 Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was already like that :)

Edit: I see I wrote the wrong pseudo code earlier, but it works like your last example now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Encoder is no longer accessible?
3 participants