-
Notifications
You must be signed in to change notification settings - Fork 349
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: support mapping ObjectId message as mongodb.ObjectId #467
Conversation
fcd7835
to
4c81e8a
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.
Cool.
Honestly I'm a little torn to add yet-another-flag into the mix, but I get the value-add and boilerplate reduction you're going after. And this sort of "just hack in your own use cases" is one of the benefits ts-proto tries to provide.
Just brainstorming, it seems like ideally this sort of "bring your own custom types" would be great to somehow be achievable via config settings instead of code changes...
Like basically what you're doing is setting up "for protobuf types that match ...this name..., then use ...this JS type... as the field, with ...this function for encode mapping..., ...this function for decode mapping..., and ...this function for from json handling...". Etc.
Each of those "...this function..." snippets could probably be ts-poet imp
strings that would resolve to non-ts-proto-generated files that users would just ship / write themselves...
Dunno @webmaster128 / @boukeversteegh / @aikoven see ^ musing if any of you are particularly excited about running with / would benefit applications you're using ts-proto in.
But @davearata-snorack , unless you're really jazzed to explore the config-based approach (which would be great! :-)), I'm good with the current approach as well. Maybe we could eventually port it over to the config-based approach if/when that gets implemented.
I had a few minor naming questions, want to remove a few integration tests, but otherwise I'm good approving + shipping this after that.
src/main.ts
Outdated
@@ -75,6 +76,10 @@ import { generateGenericServiceDefinition } from './generate-generic-service-def | |||
export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [string, Code] { | |||
const { options, utils } = ctx; | |||
|
|||
if (options.useObjectId) { | |||
imp('mongodb*mongodb'); |
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.
In general, this is not how imp
works...like just calling it won't really do anything, you need have it used in a code...
string that is actually output for the "auto-import" logic to kick in. I.e. you can probably just remove this line b/c it shouldn't be doing anything.
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.
removed this line
src/main.ts
Outdated
const fromObjectId = conditionalOutput( | ||
'fromObjectId', | ||
code` | ||
function fromObjectId(oid: ObjectId): ${mongodb}.ObjectId { |
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.
Maybe call this fromProtoObjectId
?
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.
changed this to fromProtoObjectId
src/main.ts
Outdated
const toObjectId = conditionalOutput( | ||
'toObjectId', | ||
code` | ||
function toObjectId(oid: ${mongodb}.ObjectId): ObjectId { |
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.
Same, maybe toProtoObjectId
?
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.
changed this to toProtoObjectId
src/options.ts
Outdated
@@ -34,6 +34,7 @@ export type Options = { | |||
forceLong: LongOption; | |||
useOptionals: boolean | 'none' | 'messages' | 'all'; // boolean is deprecated | |||
useDate: DateOption; | |||
useObjectId: boolean; |
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.
Lets call this useMongoObjectId
? Given it's got mongodb specific imports/etc, I think that will be more clear.
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.
changed this to useMongoObjectId
@@ -0,0 +1 @@ | |||
useObjectId=false |
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 appreciate the comprehensiveness, but let's just remove the use-object-id-false
test, I think.
The other tests essentially test the "false" codepath, and each new integration/*
test we add adds a bit of non-zero overhead (due to the extra proto codegen/etc. steps each one has), so I'd prefer to just remove it for now.
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.
removed this test
README.markdown
Outdated
@@ -671,6 +678,20 @@ The representation of `google.protobuf.Timestamp` is configurable by the `useDat | |||
| --------------------------- | ---------------------- | ------------------------------------ | ---------------- | | |||
| `google.protobuf.Timestamp` | `Date` | `{ seconds: number, nanos: number }` | `string` | | |||
|
|||
## ObjectId |
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.
Hm, maybe just drop this section? I think the - With ...
bullet point up above is good enough.
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.
removed this part of the doc
@@ -333,6 +338,8 @@ Generated code will be placed in the Gradle build directory. | |||
|
|||
- With `--ts_proto_opt=useDate=false`, fields of type `google.protobuf.Timestamp` will not be mapped to type `Date` in the generated types. See [Timestamp](#timestamp) for more details. | |||
|
|||
- With `--ts_proto_opt=useObjectId=true`, fields of a type called ObjectId where the message is constructed to have on field called value that is a string will be mapped to type `mongodb.ObjectId` in the generated types. This will require your project to install the mongodb npm package. See [ObjectId](#objectid) for more details. |
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.
Pedantically, afaiu we don't actually look inside of the ObjectId
protobuf type to see that it only has a single value: string
field; currently we're just matching on the type name and calling that good enough.
Which is fine, but let's update the docs to match that, and just say "protobuf fields of a type ObjectId
will be mapped to mongodb.ObjectId
in the generated types."
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.
The code relies on the stucture of the objectid protobuf message has that field value that's type string. that's how we convert between a mongodb.ObjectId and a protoObjectId
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 yeah, agreed we rely on .value
but technically the isObjectId
function is only checking the name of the message type:
Just the way this is worded, "With ...this setting..., fields called ObjectId where the message ... has field called value" made me think that isObjectId
really was going to check both message type name & like message.fields.length === 1 & message.fields[0].name === value && message.fields[0].type === string
.
string value = 1; | ||
} | ||
|
||
message Todo { |
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.
In the same vein of reducing our integration/*
sprawl, I'd be tempted to make your use-objectid-true-external-import
be the only integration test this PR adds.
Having ObjectId
in a separate proto file I think is a good / important boundary case to test, and given that is covered by the other test, personally I'm pretty fine with trusting that the same-file use case works as well, w/o an explicit test (unless for some reason we end up having a bug / regression b/c of it).
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.
removed this test
I pretty much share @stephenh's concerns here. Maintaining well known types that are shred across the protobuf industry probably makes sense. Adding software specific code will hardly scale. I think it would be relatively straight forward to create a plugin-system where you map a fully qualified protobuf type name to a plugin file that implements the methods from main.ts. Is there any official proto definition of an |
@stephenh @webmaster128 Thanks for the feedback! This is my first time working with some of the modules like ts-poet so something like that auto import functionality i wasn't fully aware of. I hear what you mean when you compare my ObjectId specific implementation vs a generic extensible implementation. I am not sure if I would be the best person to take on that implementation as that seems to be a bit larger of a task and I am not sure I have the availability to take that on, but I do see the value in it. For now, I will work on the feedback you provided and update the PR. |
4c81e8a
to
2baeb3e
Compare
I would give this plugin system a shot based on this particular use case. It think it's very helpful for some of the things we are doing as well. In a recent use case, we need to convert decimal types and I'd also love to convert But what I would need for this is 👇. Timestamp can be customized via
|
No I don't believe there is an official proto definition of an ObjectId. I went with a simple
Because that was how I was converting ObjectId's. My understanding is that it is safe to call
|
Well, this does kind of point to the need for custom serialization / deserialization logic, but I'm also not in favor of putting application specific transformation logic within ts-proto. A hook would be preferable. Also in this case I would probably go for a generic transformer function that replaces all ObjectIds with strings. For example (not tested): fix(message: any) {
if (typeof(message) === 'object' && message != null) {
if (message.objectId) {
message.objectId = message.objectId.toString();
}
Object.values(message).forEach(([key, value]) => fix(value));
}
} Or another approach, that can be used without modifying ts-proto (this only works for encode/decode):
message User {
google.protobuf.Value id= 1;
}
const _wrap = Value.wrap;
Value.wrap = (value) => {
return isMongoObjectId(value) ? _wrap(value.toString()) : _wrap(value);
}; Now you can call expect(User.decode(User.encode({id: someMongoObjectId}).finish())).toEqual({id: "38f7gw3478fw9457gt9w457t"});
You can even do this with |
Hm, @boukeversteegh I think the Also I would assume that the rest of the services within @davearata-snorack 's org already use a protobuf type of Per the plugin system, I agree we should definitely head that way; i.e. I could see something like: {
"somePackage.ObjectId": {
"type": "MongoDb@mongodb",
"encode": "[email protected]",
"decode": "[email protected]",
"fromJson": "[email protected]"
}
} And But, for now I'm going to merge this PR as-is, because if anything I like to implement things the super-simple / super-hard-coded way first, as @davearata-snorack has done, and then use that to guide making fancier abstractions like the custom types solution (for whoever wants to take it on; I personally probably won't have time soon). Then, if/when the custom types are implemented, I'd also be fine making a breaking change of removing (If anything, if we don't like it, the existence of |
# [1.100.0](v1.99.0...v1.100.0) (2022-01-09) ### Features * support mapping ObjectId message as mongodb.ObjectId ([#467](#467)) ([8b23897](8b23897))
🎉 This PR is included in version 1.100.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
I like your approach! Premature abstraction often leads to bad design, but to avoid it we have to accept over-specification at first. Thanks for that insight! |
@webmaster128 @boukeversteegh @stephenh thanks for the feedback, help and merging the PR! If you decide to go down the path of a more configurable abstraction, I would love to help. |
MongoDB is a popular database for JS / TypeScript projects and the default type for a MongoDB id is an ObjectId. When building protobuf messages from mongo documents it becomes tedious to convert ObjectIds to strings. For example if we have a simple mongo document that has the fields
_id
andname
like so:and a protobuf definition like this:
You would need to call
_id.toString()
any time you want to send the document as a protobuf message and you would need to callnew mongodb.ObjectId(message._id)
any time you want to convert a message to a mongo document. If it was just one field it would not be so bad. But if you have a more complex document with arrays of ObjectIds or nested objects with ObjectIds you can run into making large functions just for iterating over fields to convert ObjectIds to strings and vice versa.This change adds an optional flag
useObjectId
. The flag is set to false by default so if the flag is not set then there will be no behavior changes. Similar to theuseDate
flag when theuseObjectId
is set to true any messages with the nameObjectId
will be mapped to mongodb.ObjectId so that the conversion is handled for the client. This does have an assumption that theObjectId
message has one field calledvalue
that is astring
. So the protobuf definition would look like this:This also requires the client installs the
mongodb
package from npm. There are two tests one where theObjectId
message is defined in the same proto file as the message that has anObjectId
field. A second test where theObjectId
message is defined in an external file.