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

feat: add support for Struct in NestJS #762

Merged
merged 9 commits into from
Jan 31, 2023
Merged

Conversation

ZimGil
Copy link

@ZimGil ZimGil commented Jan 23, 2023

This PR is related to #69

After investigation of how Timestamp was solved with wrappers I was able to solve the issue we're seeing with Struct.

Notice that I'm considering that values of Stract are of type Value which is what's actually stated in the Docs

Field name Type Description
fields map<string, Value> Map of dynamically typed values.

While researching the wrappers solution I noticed that when wrapping google.protobuf.Struct we can wrap and unwrap but google.protobuf.Value can not be wrapped as protobufjs is not reading this value of wrappers. But due to the fact that Struct maps to Value I could use it's wrap and unwrap methods in order to wrap and unwrap Struct itself.

Same goes for ListValue

Had to bump @grpc/proto-loader to support NestJS useage of fromJSON()

Copy link
Owner

@stephenh stephenh left a comment

Choose a reason for hiding this comment

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

Hey @ZimGil , thanks for the PR! Apologies I just haven't had a time to get to it yet. I think it looks good. Neither NestJS or structs are things I've personally used so I really appreciate contributions to make them work!

I made a few small comments, and it looks like there is a failing test in CI, but overall this looks great, and would be great to get this merged. Thanks!

if (nested) {
res.structValue!.fields[field] = wrapStruct(value[field], true);
} else {
res.fields![field] = wrapStruct(value[field], true);
Copy link
Owner

Choose a reason for hiding this comment

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

Question: Should this true be a false? Just thinking that we are in the nested = false branch of the it, so should we pass along false as well?

Copy link
Author

Choose a reason for hiding this comment

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

The only case we actually need for nested to be false is the base object, we should only get here initially and from here it should be always true

Copy link
Owner

Choose a reason for hiding this comment

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

Gotcha, thanks!

src/main.ts Outdated
@@ -1867,15 +1925,20 @@ function generateUnwrap(ctx: Context, fullProtoTypeName: string, fieldNames: Str
chunks.push(code`unwrap(message: Struct): {[key: string]: any} {
const object: { [key: string]: any } = {};
[...message.fields.keys()].forEach((key) => {
object[key] = message.fields.get(key);
const unwrappedValue = Value.unwrap(message.fields.get(key));
object[key] = unwrappedValue !== undefined ? unwrappedValue : message.fields.get(key);
Copy link
Owner

Choose a reason for hiding this comment

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

Question: Just curious, but instead of : message.fields.get(key), should this be just : undefined? Like if Value.unwrap can't unwrap the value, is there really a potentially valid-but-not-unwrappable value we need to get?

Copy link
Author

Choose a reason for hiding this comment

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

Can't recall why I put it there but there was something - I'll look into what happens if we remove it

Copy link
Author

Choose a reason for hiding this comment

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

The reason we have that here is for the case where the result of message.fields.get(key) is not necessarily actually wrapped and this cause the result of Value.unwrap() to return undefined

Returning the actual value instead help us avoid these.
Removing this broke the tests

Copy link
Owner

Choose a reason for hiding this comment

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

Ah okay, happy to keep it then.

Out of curiosity, is it expected that we'll end up with not-actually-wrapped values in should-be-wrapped cases, or was an existing test just being particularly ocd?

Copy link
Author

Choose a reason for hiding this comment

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

The second part :)

Copy link
Owner

Choose a reason for hiding this comment

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

Hm, I think I realized what was happening ...

In the findOneHero test, the server was only invoking Struct.wrap(...) once, but never invoking ListValue.wrap or Value.wrap on any of the inner contents.

Even after adding wrapper entries for them:

wrappers[".google.protobuf.Struct"] = { fromObject: Struct.wrap, toObject: Struct.unwrap } as any;
wrappers[".google.protobuf.Value"] = { fromObject: Value.wrap, toObject: Value.unwrap } as any;
wrappers[".google.protobuf.ListValue"] = { fromObject: ListValue.wrap, toObject: ListValue.unwrap } as any;

Value.wrap / ListValue.wrap are never invoked.

So I think that's why, inside the client .unwraps, we saw things that were "hey, wait a sec, not wrapped".

I was able to fix this by making Struct.wrap deeply wrap, and Struct.unwrap deeply unwrap...which AFAICT fixed the issue.

But it broke ts-proto's non-NestJS Struct.encode / Struct.decode methods, which specifically assumed that wrap/unwrap were shallow.

So now I need to go back and teach the encode/decode methods about the deepness of wrap/unwrap...or maybe for now we just have two versions of wrap/unwrap; one that is deep for NestJS, and one that is shallow for not-NestJS...

I'm going to go ahead and push up the deep-wrap/unwrap changes up to this PR, which will break some non-NestJS tests, but AFAICT that's the right path forward for NestJS's Struct support.

Does that seem right?

Copy link
Author

Choose a reason for hiding this comment

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

I agree that deeply wrapping and unwrapping is the way to go

Don't want to break the tests.

Copy link
Owner

Choose a reason for hiding this comment

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

Well, I cheated and just copy/pasted both deep & shallow versions of the wrappers, but it seems to be working for now. :-)

I really haven't personally used the Struct/List/ListValue types, and helping on this PR was the closest I've been to the code... I feel like something is still a little off...like how in the regular/non-NestJs output, Struct.encode takes a Struct but it should take an any...

Anyway, I'm going to punt on that for now. Tests are passing so I'll merge and 🤞 seems like it should work.

Thanks for the PR!

}

if (options.outputEncodeMethods || options.outputJsonMethods || options.outputTypeRegistry) {
if (options.outputEncodeMethods || options.outputJsonMethods || options.outputTypeRegistry || options.nestJs) {
Copy link
Owner

Choose a reason for hiding this comment

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

Suggestion: Ah okay, we add the || options.nestJs b/c we need it to generate any wrap/unwrap methods...

I wonder if we could make this a little more nuanced, by doing something like:

const wrap = this.generateWrap(...);
const unwrap = this.generateUnwrap(...);
if (options.outputEncodeMethods || ... || wrap.length > 0 || unwrap.length > 0)

Pretty minor, but I think it'd avoid outputting some of the empty export const Hero = {} lines that are unnecessary.

Copy link
Author

Choose a reason for hiding this comment

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

I'll look into that

Copy link
Author

Choose a reason for hiding this comment

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

We can't seem to do that as props of generateWrap() and generateUnwrap() are in the scope of the callback of visit inside that if

Any other suggestion to clean up the output?

Copy link
Owner

Choose a reason for hiding this comment

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

Ah shoot, you're right. I had to poke around a little bit, but I think pushed up a few more conditionals that cleaned up some of the unnecessary output.

@ZimGil
Copy link
Author

ZimGil commented Jan 30, 2023

@stephenh I've pushed a fix the the tests - replied to the comments after playing around.

src/main.ts Outdated
chunks.push(code`unwrap(message: Value): string | number | boolean | Object | null | Array<any> | undefined {
if (message?.${fieldNames.stringValue} !== undefined) {
chunks.push(code`unwrap(message: any): string | number | boolean | Object | null | Array<any> | undefined {
if (message?.hasOwnProperty('${fieldNames.stringValue}') && message?.${fieldNames.stringValue} !== undefined) {
Copy link
Owner

Choose a reason for hiding this comment

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

Are the hasOwnProperty checks necessary here? Just curious if they are actually fixing something or just being extra.

Copy link
Author

@ZimGil ZimGil Jan 30, 2023

Choose a reason for hiding this comment

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

Without it, it's looking at the prototype which actually has all other props set to null for some reason

Copy link
Author

Choose a reason for hiding this comment

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

At least in some cases

@stephenh stephenh merged commit e8c6d8b into stephenh:main Jan 31, 2023
stephenh pushed a commit that referenced this pull request Jan 31, 2023
# [1.139.0](v1.138.0...v1.139.0) (2023-01-31)

### Features

* add support for Struct in NestJS ([#762](#762)) ([e8c6d8b](e8c6d8b))

### Performance Improvements

* generate switch statement for oneof union encode ([#767](#767)) ([c3fd1e3](c3fd1e3))
@stephenh
Copy link
Owner

🎉 This PR is included in version 1.139.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Successfully merging this pull request may close these issues.

2 participants