-
Notifications
You must be signed in to change notification settings - Fork 122
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
Feat: Add custom body serializer support #103
Feat: Add custom body serializer support #103
Conversation
@paulpdaniels: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Meteor Contributor Agreement here: https://contribute.meteor.com/ |
CI seems to be reporting an issue when packaging the build, due to something in
|
Same issue as apollographql/apollo-link#601 (comment) ; check a2514eb for solve. CI would pass once #105 is merged, and this MR rebased. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great, however, can you please add documentation to docs/rest.md ?
Thanks @paulpdaniels!
f428e93
to
bf3f426
Compare
Codecov Report
@@ Coverage Diff @@
## master #103 +/- ##
==========================================
+ Coverage 90.64% 91.03% +0.39%
==========================================
Files 2 2
Lines 278 279 +1
Branches 89 87 -2
==========================================
+ Hits 252 254 +2
+ Misses 23 22 -1
Partials 3 3
Continue to review full report at Codecov.
|
@fbartho docs added. |
Actually don't merge (quite) yet, I'm fairly certain I put the docs in the wrong place. |
I noticed that the How about changing the signature to This would mean extending the default body serialization to set the header to (As a side note, the |
@marnusw I concur with your recommendations. -- Is there a better signature we can provide than that one though?. This feels a bit brittle, what if we want more parameters passed in down the line? -- Maybe we should make it accept options, and return |
@marnusw re: your comment about the docs: -- we have the docs mirrored in this repo, so any changes we make to release the next release of apollo-link-rest, we can mirror back into the official repo. |
@paulpdaniels please flag when you're ready for us to review things again after your comment that you weren't quite ready! |
src/restLink.ts
Outdated
|
||
/** | ||
* Optional field that prepares the request body for transport. | ||
* @default function that serializes from JSON. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: Should be "serializes to JSON"
That makes sense. I looked at it again, and right now I can't see any additional options that would be worth passing in, but changing the signature in this way will make that possible down the line: So for now I would recommend If I may, pending consensus on the signature above, these are the summarized recommendations we've discussed:
|
bf3f426
to
62f34fc
Compare
@marnusw I like your idea, wondering if we could tweak it though to keep const jsonSerializer = (body: args) => ({data: JSON.stringify(body), headers: {'ContentType': 'application/json'}})
// In restLink
const {data, headers: additionalHeaders} = body ? (bodySerializer || jsonSerializer).call(null, body) : {};
const mergedHeaders = headerMergePolicy(headers, additionalHeaders)
return await (customFetch || fetch)(`${uri}${pathWithParams}`, {
credentials,
method,
headers: mergedHeaders,
body: data
}) Would that work @fbartho ? |
@paulpdaniels That sounds like a pretty elegant solution. I didn't actually check now, but I'm guessing there would be no reason why the body serializer couldn't be called before the initial header merge, and then there would potentially be just one header merge step from the three different sources. I have to add, though, that I intended to come back and post saying it's interesting how your ideas mellow over a few days: It occurred to me that as far as actual usage of these features go the following makes pretty good sense: new ApolloLinkRest({
headers: {'Content-Type': 'multipart/form-data'},
bodySerializer = (body: args) => /* Make FormData */)
}) It's concise and does exactly what is needed and expected. The only other thing that would be required then is that the lib sets the |
@marnusw Do you mean for setting the default serializer and headers? Because I think we need to set the serializer per Something like: mutation Signup(
$formData: FormDataInput,
$loginData: LoginDataInput,
$formSerializer: any,
$jsonSerializer: any
) {
signup(input: $formData) @rest(type: "Signup", path: "/v1/signup", bodySerializer: $formSerializer) { ...SignedUpFragment }
login(input: $loginData) @rest(type: "User", path: "/v1/login", bodySerializer: $jsonSerializer) { ...UserFragment }
} We need to pass a serializer for each. You did get me thinking though that maybe we could support something like how the new RestLink({
...standardOptions,
additionalSerializers: {
// Don't need to include JSON unless we are overriding it, it will also be our default
form: (body: any) => ({data: buildForm(body), headers: {'Content-Type': 'multipart/form'}})
}
}) |
@paulpdaniels Got it, that makes perfect sense. I hadn't looked at the implementation in sufficient detail to notice you were doing it per I think defining the available serializers once in the initial config as you propose is a good idea since it could become repetitive otherwise. In this case each function setting its own headers is definitely a requirement as well, with either one of the two ways discussed. Returning the headers does keep the function pure which I like. I just noticed as far as naming things go we should probably pass in form: (data: any) => ({body: buildForm(data), headers: {'Content-Type': 'multipart/form'}}) Let's see what @fbartho thinks. |
Hey guys, I love this thread. Here are my first thoughts on the discussion:
Did I forget to address anything? haha didn't mean to throw a wall of text at you. /cc @marnusw |
Ok @fbartho @marnusw I tried to boil down what I think we are all saying. I refactored so that there are now a couple options for handling custom body serializers (I followed the precedent with All together it would look something like this (apologizes for the contrived example): const link = new RestLink({
// Pre-specify some additional serializers
additionalSerializers: {
json: data => ({body: JSON.stringify(data), headers: {'Content-Type': 'application/json'}})
},
// If not specified then always use xml
defaultSerializer: xmlSerializer
})
const mutation = gql`
mutation OneForAll($input: FormInput!, $customSerializer: any) {
# Use a custom one off serializer
postForm(input: $input) @rest(
path: "/v1/form",
type: "Response",
method: "POST",
bodySerializer: $customSerializer
)
{
id, timestamp
}
# Uses the default serializer
postDefault(input: $input) @rest(
path: "/v1/xml",
type: "Response",
method: "POST"
)
{ ...DefaultFragment }
# Use the serializer based on the passed in key
postJSON(input: $input) @rest(
path: "/v1/json",
type: "Response",
method: "POST",
bodySerializerKey: 'json'
) { id }
}
`;
client.mutate({
query: mutation,
variables: {
input: {hello: "world"},
// Arbitrary constant serializer
customSerializer: () => ({body: 42, headers: {'Content-Type': 'text/plain'}})
}
}) If we agree on these features, I'll fix the test and update the docs based on these options. |
src/restLink.ts
Outdated
headers, | ||
body: body && JSON.stringify(body), | ||
headers: additionalHeaders | ||
? headersMergePolicy(additionalHeaders, headers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This I'm actually not to sure about @fbartho, if I switch the order here, which I initially tried to do, it broke some other non-related tests. Doing this keeps all tests passing, but I guess we should decide what the expected behavior of this should be? I assume the serializer should in general get the final say here, but I think this might be doing the opposite no?
@fbartho per your point 2, I guess it would depend on how we handle header merging. I'm not certain how often a serializer would need the current header stack to conditionally determine what to do. On its surface it seems like it would be helpful to expose that to serializer, but can't think of a use case to actually justify it. |
@paulpdaniels, I think this is a good proposal. It's grown somewhat in complexity, but it does provide a lot of flexibility. I just have some opinions about how things are named: 1 - In the 2 - I think the serializer key format will be more common than a local custom serializer, so how about using the shorter label
I don't feel strongly about point 2 though, just a thought. In terms of passing the headers into the serializer function: I also can't think of a use case for that right now. It think the function will/should mainly be used to set the content-type, and maybe an authorization header for a particular API. Even the last seems like it's not quite the place for it, but at the moment modifying headers per |
@marnusw 👍 on the naming conventions, I'll update those. I'm going to leave the serializer signature as is for now, i.e. not passing in the headers to it, just merging the resulting headers, if only because neither of us have a clear reason why it'd be necessary to include that ability. I think it may be better to just defer that decision (since it would be backward compatible), to when someone comes forward with an articulated use case for this. Then we can have PR that specifically addresses that issue. |
How do we feel about bodySerializer being either a function or a string key? That prevents us from adding yet another potential parameter, while just making the link code a little harder to write, (but TypeScript covers that for us). The reason why we need to pass the headers in, is that headers are by REST spec allowed to have duplicates. But if there is already a content-type header, then we need to replace it with the new content-type coming out of the bodySerializer. Aka, the bodyserializer needs to be able to replace the headers wholesale, not just have its headers merged in. |
I’m ready to come to a final version and merge this today or tomorrow if you @paulpdaniels or @marnusw think we can come to a happy solution that is fully implemented by tomorrow? |
Ok @fbartho incorporated your feedback as well. |
A quick scan of the latest changes look like exactly what I had been thinking of @paulpdaniels, thanks! -- I'll try to carve out some time to test it tomorrow! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for finishing the implementation @paulpdaniels! I haven't looked at the code yet, but I have a few comments on the docs.
docs/rest.md
Outdated
@@ -90,6 +90,9 @@ Construction of `RestLink` takes an options object to customize the behavior of | |||
* `fieldNameNormalizer?: /function/`: _optional_ function that takes the response field name and converts it into a GraphQL compliant name. -- This is useful if your `REST` API returns fields that aren't representable as GraphQL, or if you want to convert between `snake_case` field names in JSON to `camelCase` keyed fields. | |||
* `fieldNameDenormalizer?: /function/`: _optional_ function that takes a GraphQL-compliant field name and converts it back into an endpoint-specific name. | |||
* `typePatcher: /map-of-functions/`: _optional_ Structure to allow you to specify the `__typename` when you have nested objects in your REST response! | |||
* `bodySerializers: /map-of-functions/`: _optional_ Structure to allow the definition of alternative serializers, which can then be specified by their key. | |||
* `defaultSerializer /function/`: _optional_ function that will be used by the `RestLink` as the default serializer when no `bodySerializer` is defined for a `@rest` call. Default is JSON. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I may nitpick, could defaultSerializer
and bodySerializers
be swapped around here? The way I read the narrative it would be easier to understand the intent with defaultSerializer
first and then discussing the even more advanced configuration.
docs/rest.md
Outdated
@@ -192,6 +195,13 @@ Here is one way you might customize `RestLink`: | |||
bodySnippet... | |||
} | |||
}, | |||
bodySerializer: (body: any) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this option should be defaultSerializer
now. Should it also be receiving the headers
as a second parameter and returning {body, headers}
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep you are right!
docs/rest.md
Outdated
@@ -263,6 +273,7 @@ An `@rest(…)` directive takes two required and several optional arguments: | |||
* _optional_ `endpoint?: string` key to use when looking up the endpoint in the (optional) `endpoints` table if provided to RestLink at creation time. | |||
* _optional_ `bodyKey?: string = "input"`: This is the name of the `variable` to use when looking to build a REST request-body for a `PUT` or `POST` request. It defaults to `input` if not supplied. | |||
* _optional_ `bodyBuilder?: /function/`: If provided, this is the name a `function` that you provided to `variables`, that is called when a request-body needs to be built. This lets you combine arguments or encode the body in some format other than JSON. | |||
* _optional_ `bodySerializer?: /function/`: function that takes the body of the request directly before it is passed to the fetch call. Defaults to `JSON.stringify`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be a function or a string now, for indexing into bodySerializers
. Perhaps:
- optional
bodySerializer?: /string | function/
: string key to look up a function inbodySerializers
or a custom serialization function for the body/headers of this request before it is passed to the fetch call. Defaults toJSON.stringify
and settingContent-Type: application/json
.
docs/rest.md
Outdated
```graphql | ||
mutation encryptedForm( | ||
$input: PublishablePostInput!, | ||
formSerializer: any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a question for my own sake: Why is there no $
in front of this variable? Does that tell GraphQL not to serialize it (since it's a function)? Would it still be passed in on the variables
same as the input value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also curious! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah lol, that would be a typo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
O, right... I just thought it mightn't be because it was like that twice. Copy & paste I guess. 😄
Just one more issue to solve including this PR (#102) |
a74a84f
to
015d824
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the build problems are your fault. I think they're something wrong between apollo-utilities and the graphql dependencies.
I'm going to merge this and try to fix those separately.
Adds support for custom body serializers which can be specified in a similar manner to
bodyBuilder
. Defaults toJSON
if none is specified in order to preserve default behaviour.cc/ @fbartho
fixes #96