-
Notifications
You must be signed in to change notification settings - Fork 106
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!: throw error with out of bounds integer values, optionally wrap into DsInt or provide a custom 'integerValue' type cast options #516
feat!: throw error with out of bounds integer values, optionally wrap into DsInt or provide a custom 'integerValue' type cast options #516
Conversation
…unds integer values instead of truncating, add custom 'integerValue' type cast option
Codecov Report
@@ Coverage Diff @@
## master #516 +/- ##
========================================
+ Coverage 93.81% 94% +0.19%
========================================
Files 12 12
Lines 889 918 +29
Branches 175 189 +14
========================================
+ Hits 834 863 +29
Misses 44 44
Partials 11 11
Continue to review full report at Codecov.
|
@bcoe please take a look. |
Should we think about forcing a user to opt-in or opt-out of the new throwing behavior? In Spanner, by default, the user will receive the Int objects. They can call The user also has the choice to provide an option, In Datastore terms, we could:
I like this approach, as it would make this breaking change more practical for users in both camps-- where out of bounds integers aren't relevant (no errors), as well as where they are (useful errors). It's almost a fix instead of a breaking change at that point, but I won't argue for that :) |
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 looking good to me, my only ask would be a few more targeted samples that demonstrate the new API; updating the existing conceps.js
is great start.
samples/concepts.js
Outdated
@@ -1018,11 +1018,11 @@ class Transaction extends TestHelper { | |||
// Restore `datastore` to the mock API. | |||
datastore = datastoreMock; | |||
assert.strictEqual( | |||
accounts[0].balance, | |||
accounts[0].balance.valueOf(), |
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.
Could we add a couple examples in:
https://github.com/googleapis/nodejs-datastore/blob/master/samples/concepts.js#L272
That demonstrate:
- how to save an entry with an integer value (I believe we already have this).
- how to fetch an entry with an integer value.
- potentially how we'd JSON stringify an entry we fetched?
I know we're already showing the conversion with accounts[0].balance.valueOf()
, but I think a more self contained example might be nice.
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 am not sure this can be done without looking odd. This sample exists alongside other language ones that are this simple.
I would be fine making a future work issue for this.
/cc: @BenWhitehead
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.
Since we don't currently have any section in the docs discussing how to work with "large" numbers, and I'm not sure when we will. I think it'd be good to at least have a commented sample in the repo that can be discovered and referred to independent from this development PR. Once we have the sample we can add a work item to get it published into the docs.
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 am capturing this in an internal bug b/143094618.
If you feel strongly this is needed, it may belong in a doc. comment with the function being called?
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.
As long as there is something (either in samples, or in the reference docs alongside the code) so that someone can learn about this, my main concern is satisfied. Since this is a bit of an odd compatibility case that not all of the supported languages necessitate I think it'll take some design/work to incorporate into the docs based on how they're structured right 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.
Just wanted to hold this to see if #516 (comment) can turn into a discussion :)
} | ||
} | ||
// tslint:disable-next-line no-any | ||
valueOf(): 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.
We can make this type-safe for typescript users by making this class generic on the return type of valueOf()
:
class Int<T = number> extends Number ...
We'll also be able to make integerTypeCastFunction
type-safe by making IntegerTypeCastOptions
generic:
export interface IntegerTypeCastOptions<T> {
integerTypeCastFunction: (value: string) => T;
properties?: string | string[];
}
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.
Nevermind folks.. It's not possible to override the return type of valueOf()
to the generic type since Int
inherits from Number
. Typescript really dislike type-hacking..
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 also means Typescript users would have to coerce the type returned by valueOf()
:
// assume integerTypeCastFunction returns a BigInt
const bigInt = myInt.valueOf() as BigInt;
); | ||
} | ||
this.typeCastFunction = typeCastOptions.integerTypeCastFunction; | ||
this.typeCastProperties = typeCastOptions.properties |
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 it's fair to just do this.typeCastProperties = arrify(typeCastOptions.properties)
, even if it's empty 🤷♂
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.
it won't work. arrify
will always return an array (empty) even if typeCastOptions.properties
is undefined
A few lined down the logic is
- only custom cast those properties whose names have been specified by user. As such an empty array will signal to "not to custom cast anything as it is not provided by user".
- an
undefined
typeCastOptions.properties
will result in custom casting everyintegerType
entity.property
src/entity.ts
Outdated
} | ||
// tslint:disable-next-line no-any | ||
valueOf(): any { | ||
let customCast = this.typeCastFunction ? true : 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.
It might be a bit more readable to ask exactly what we're wondering:
let shouldCustomCast = typeof this.typeCastFunction === 'function';
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.
shouldCustomCast
is a better name- we already throwing an error above if
this.typeCastFunction
is not a function.
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 first reason I thought to suggest this change was due to the this.typeCastFunction ? true : false
, which is somewhat unusual-- that is coercing the type based on if it's not undefined/null. For readability, I would rather see it say exactly what we're wondering: "We should custom cast the integer if the user gave us a function", vs "We should custom cast the integer if the user gave us a non-null argument".
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.
Got it.
My line of thought was, since we already weeded out anything but a function
, at this point the value of this.typeCastFunction
can only be a function
.
But for code readability purposes it makes sense.
Fixed it.
src/entity.ts
Outdated
try { | ||
return this.typeCastFunction!(this.value); | ||
} catch (error) { | ||
error.message = `integerTypeCastFunction threw an error - ${error.message}`; |
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.
Up for debate, but newlining here might print better:
error.message = `integerTypeCastFunction returned an error:\n\n ${error.message}`;
src/query.ts
Outdated
* @param {boolean} [options.wrapNumbers=false] | ||
* Indicates if the numbers should be wrapped in Int wrapper. | ||
* @param {object} [options.integerTypeCastOptions] Configurations to | ||
* optionally wrap `integerValue` in Datastore Int object and optionally |
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 the first sentence can be changed to.
Configuration to convert values of integerValue
type to a custom value.
src/query.ts
Outdated
* @param {object} [options.integerTypeCastOptions] Configurations to | ||
* optionally wrap `integerValue` in Datastore Int object and optionally | ||
* provide an `integerTypeCastFunction` to handle `integerValue` conversion. | ||
* Note: `integerTypeCastingOptions` values will be ingnored |
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.
Note: integerTypeCastOptions
is ignored when options.wrapNumbers
is set.
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.
Done
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.
src/request.ts
Outdated
@@ -432,6 +436,17 @@ class DatastoreRequest { | |||
* [here](https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore). | |||
* @param {object} [options.gaxOptions] Request configuration options, outlined | |||
* here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions. | |||
* @param {boolean} [options.wrapNumbers=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.
Same notes as above throughout this documentation block.
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.
Done
src/request.ts
Outdated
@@ -580,6 +594,17 @@ class DatastoreRequest { | |||
* [here](https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore). | |||
* @param {object} [options.gaxOptions] Request configuration options, outlined | |||
* here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions. | |||
* @param {boolean} [options.wrapNumbers=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.
Same notes about documentation for this and the properties that follow.
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.
Done
@stephenplusplus ptal. |
@stephenplusplus combine With new changes user can
PTAL |
src/entity.ts
Outdated
'value ' + | ||
value.integerValue + | ||
" is out of bounds of 'Number.MAX_SAFE_INTEGER'.\n" + | ||
"Please consider passing 'options.wrapNumbersOptions=true' or\n" + |
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 like that you went with a dual-purpose option, where it can be a boolean, for simple mode, or an object for an advanced use case. I would rather have wrapNumbers
be the name, however, so that wrapNumbers: true
is possible, compared to wrapNumbersOptions: true
. When the object is used, wrapNumbers = {integerTypeCastFunction: () => {}}
still seems fine.
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.
Renamed
* @typedef {object} IntegerTypeCastOptions Configuration to convert | ||
* values of `integerValue` type to a custom value. Must provide an | ||
* `integerTypeCastFunction` to handle `integerValue` conversion. | ||
* @property {function} integerTypeCastFunction A custom user |
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 users will be surprised when they learn integerValue
s are not converted to what they returned from their function until "valueOf()" is called. Imagining a scenario where a user has their own Int implementation, AppBigInt
, I think they would expect instanceof entity.data.myNumber === AppBigInt
rather than our DatastoreInt
-- which they probably assumed they were opting out of.
If you still think having it work the way it does in this PR now would be best, let's beef up the docs to over-explain how their return value will come into play.
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.
Using the logic flow you wrote above, this is my proposal:
- Default -
query.run(q)
- API will try to convert to
Number
- Will throw on overflow
- API will try to convert to
- Pass options.wrapNumbers as boolean (just wrap numbers, no custom cast) -
query.run(q, {wrapNumbers: true})
- API will return a DsInt obj in which
- #valueOf will act the same way as default behavior, i.e. try to convert to Number, throw on overflow. However now, in case of an exception with #valueOf user can still retrieve a string representation of the numerical value with
#value
and does not need to make another network call.
- #valueOf will act the same way as default behavior, i.e. try to convert to Number, throw on overflow. However now, in case of an exception with #valueOf user can still retrieve a string representation of the numerical value with
- API will return a DsInt obj in which
- Pass options.wrapNumbers as object -
query.run(q, {wrapNumbers: {integerTypeCastFunction: = myFunction}})
- API will return the return value from user-provided
wrapNumbers.integerTypeCastFunction
- API will return the return value from user-provided
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.
Implemented
src/query.ts
Outdated
@@ -397,6 +397,10 @@ class Query { | |||
* If not specified, default values are chosen by Datastore for the | |||
* operation. Learn more about strong and eventual consistency | |||
* [here](https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore). | |||
* @param {object} [options.gaxOptions] Request configuration options, outlined | |||
* here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions. | |||
* @param {boolean | IntegerTypeCastOptions} [options.wrapNumbersOptions=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.
The description should explain "If a boolean, this will wrap values... If an object, you can customize the behavior..."
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.
Does below sound ok?
* @param {boolean | IntegerTypeCastOptions} [options.wrapNumbers=false]
* Wrap values of integerValue type in {@link Datastore#Int} object.
* If a `boolean`, this will wrap values in {@link Datastore#Int}.
* If an `object`, this will return a value returned by
* `wrapNumbers.integerTypeCastFunction`.
* Please see {@link IntegerTypeCastOptions} for options descriptions.
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.
Pushed the changes 👍
src/entity.ts
Outdated
'value ' + | ||
value.integerValue + | ||
" is out of bounds of 'Number.MAX_SAFE_INTEGER'.\n" + | ||
"Please consider passing 'options.wrapNumbers=true' or\n" + |
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.
Could you put the “To prevent this error, please...”. That way, it doesn’t float on its own line after the demonstration object.
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.
Done
src/entity.ts
Outdated
try { | ||
return this.typeCastFunction!(this.value); | ||
} catch (error) { | ||
error.message = `integerTypeCastFunction threw an error:\n\n - ${error.message}`; |
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.
Could you do two spaces in front of the hyphen? I think that’s the standard newline+indent formatting we use in other places.
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.
Done
src/entity.ts
Outdated
/** | ||
* Convert a protobuf Value message to its native value. | ||
* | ||
* @private | ||
* @param {object} valueProto The protobuf Value message to convert. | ||
* @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in | ||
* {@link Datastore#Int} object. |
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.
“objects.”
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.
Fixed
src/entity.ts
Outdated
/** | ||
* Convert a protobuf Value message to its native value. | ||
* | ||
* @private | ||
* @param {object} valueProto The protobuf Value message to convert. | ||
* @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in | ||
* {@link Datastore#Int} object. | ||
* If a `boolean`, this will wrap values in {@link Datastore#Int}. |
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.
“objects.” at the end there please!
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.
Fixed!
test/entity.ts
Outdated
const wrapNumbers = {integerTypeCastFunction: stub}; | ||
|
||
entity.decodeValueProto(valueProto, wrapNumbers); | ||
assert.strictEqual(stub.called, true); |
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.
Let’s also check that the value returned is what the iTCF returned.
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.
added functionality to integerTypeCastFunction
added an assert to verify entity.decodeValueProto
returnes the value returned by integerTypeCastFunction
src/entity.ts
Outdated
this.typeCastFunction = typeCastOptions.integerTypeCastFunction; | ||
if (typeof typeCastOptions.integerTypeCastFunction !== 'function') { | ||
throw new Error( | ||
`integerTypeCastFunction is not a function or is not provided.` |
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.
“was not provided.”
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.
FIxed!
test/entity.ts
Outdated
assert.strictEqual(entity.decodeValueProto(valueProto), expectedValue); | ||
}); | ||
|
||
it('should wrap nubers with an option', () => { |
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.
“numbers”
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.
Fixed!
test/entity.ts
Outdated
assert.strictEqual(entity.decodeValueProto(valueProto), expectedValue); | ||
}); | ||
|
||
it('should not wrap nubers by default', () => { |
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.
“numbers”
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.
Fixed as well!
test/request.ts
Outdated
@@ -456,6 +456,53 @@ describe('Request', () => { | |||
.emit('reading'); | |||
}); | |||
|
|||
it('should pass `wrapNumbersOprions` to formatArray', done => { |
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 these tests should be rewritten to assure all the asserts are called. If necessary, break them apart into multiple tests for each value type.
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.
Split all these tests into single test per each value.
@stephenplusplus |
Thank you very much for your patience with my reviews! It looks great to me! |
Fixes #147
integerValue
is now decoded intodatastore.Int
objectentity.Int#valueof()
nowNumber
Number
typeCastFunction
to be used to convert integer valuestypeCastFunction