Skip to content

Commit 9cd77fc

Browse files
AVaksmancrwilcox
authored andcommitted
feat!: throw error with out of bounds integer values, optionally wrap into DsInt or provide a custom 'integerValue' type cast options #516
1 parent 2b00f9b commit 9cd77fc

5 files changed

Lines changed: 894 additions & 67 deletions

File tree

handwritten/datastore/src/entity.ts

Lines changed: 145 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import arrify = require('arrify');
1818
import * as extend from 'extend';
1919
import * as is from 'is';
20-
import {Query, QueryProto} from './query';
20+
import {Query, QueryProto, IntegerTypeCastOptions} from './query';
2121
import {PathType} from '.';
2222
import * as Protobuf from 'protobufjs';
2323
import * as path from 'path';
@@ -111,16 +111,36 @@ export namespace entity {
111111
*
112112
* @class
113113
* @param {number|string} value The integer value.
114+
* @param {object} [typeCastOptions] Configuration to convert
115+
* values of `integerValue` type to a custom value. Must provide an
116+
* `integerTypeCastFunction` to handle `integerValue` conversion.
117+
* @param {function} typeCastOptions.integerTypeCastFunction A custom user
118+
* provided function to convert `integerValue`.
119+
* @param {sting|string[]} [typeCastOptions.properties] `Entity` property
120+
* names to be converted using `integerTypeCastFunction`.
114121
*
115122
* @example
116123
* const {Datastore} = require('@google-cloud/datastore');
117124
* const datastore = new Datastore();
118125
* const anInt = datastore.int(7);
119126
*/
120-
export class Int {
127+
export class Int extends Number {
121128
type: string;
122129
value: string;
123-
constructor(value: number | string) {
130+
typeCastFunction?: Function;
131+
typeCastProperties?: string[];
132+
private _entityPropertyName: string | undefined;
133+
constructor(
134+
value: number | string | ValueProto,
135+
typeCastOptions?: IntegerTypeCastOptions
136+
) {
137+
super(typeof value === 'object' ? value.integerValue : value);
138+
this._entityPropertyName =
139+
typeof value === 'object' ? value.propertyName : undefined;
140+
this.value =
141+
typeof value === 'object'
142+
? value.integerValue.toString()
143+
: value.toString();
124144
/**
125145
* @name Int#type
126146
* @type {string}
@@ -130,7 +150,46 @@ export namespace entity {
130150
* @name Int#value
131151
* @type {string}
132152
*/
133-
this.value = value.toString();
153+
if (typeCastOptions) {
154+
this.typeCastFunction = typeCastOptions.integerTypeCastFunction;
155+
if (typeof typeCastOptions.integerTypeCastFunction !== 'function') {
156+
throw new Error(
157+
`integerTypeCastFunction is not a function or was not provided.`
158+
);
159+
}
160+
161+
this.typeCastProperties = typeCastOptions.properties
162+
? arrify(typeCastOptions.properties)
163+
: undefined;
164+
}
165+
}
166+
// tslint:disable-next-line no-any
167+
valueOf(): any {
168+
let shouldCustomCast = this.typeCastFunction ? true : false;
169+
if (
170+
this.typeCastProperties &&
171+
!this.typeCastProperties.includes(this._entityPropertyName!)
172+
) {
173+
shouldCustomCast = false;
174+
}
175+
176+
if (shouldCustomCast) {
177+
try {
178+
return this.typeCastFunction!(this.value);
179+
} catch (error) {
180+
error.message = `integerTypeCastFunction threw an error:\n\n - ${error.message}`;
181+
throw error;
182+
}
183+
} else {
184+
return decodeIntegerValue({
185+
integerValue: this.value,
186+
propertyName: this._entityPropertyName,
187+
});
188+
}
189+
}
190+
191+
toJSON(): Json {
192+
return {type: this.type, value: this.value};
134193
}
135194
}
136195

@@ -376,11 +435,52 @@ export namespace entity {
376435
return value instanceof entity.Key;
377436
}
378437

438+
/**
439+
* Convert a protobuf `integerValue`.
440+
*
441+
* @private
442+
* @param {object} value The `integerValue` to convert.
443+
*/
444+
function decodeIntegerValue(value: ValueProto) {
445+
const num = Number(value.integerValue);
446+
if (!Number.isSafeInteger(num)) {
447+
throw new Error(
448+
'We attempted to return all of the numeric values, but ' +
449+
(value.propertyName ? value.propertyName + ' ' : '') +
450+
'value ' +
451+
value.integerValue +
452+
" is out of bounds of 'Number.MAX_SAFE_INTEGER'.\n" +
453+
"To prevent this error, please consider passing 'options.wrapNumbers=true' or\n" +
454+
"'options.wrapNumbers' as\n" +
455+
'{\n' +
456+
' integerTypeCastFunction: provide <your_custom_function>\n' +
457+
' properties: optionally specify property name(s) to be cutom casted' +
458+
'}\n'
459+
);
460+
}
461+
return num;
462+
}
463+
464+
/**
465+
* @typedef {object} IntegerTypeCastOptions Configuration to convert
466+
* values of `integerValue` type to a custom value. Must provide an
467+
* `integerTypeCastFunction` to handle `integerValue` conversion.
468+
* @property {function} integerTypeCastFunction A custom user
469+
* provided function to convert `integerValue`.
470+
* @property {string | string[]} [properties] `Entity` property
471+
* names to be converted using `integerTypeCastFunction`.
472+
*/
379473
/**
380474
* Convert a protobuf Value message to its native value.
381475
*
382476
* @private
383477
* @param {object} valueProto The protobuf Value message to convert.
478+
* @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in
479+
* {@link Datastore#Int} objects.
480+
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
481+
* If an `object`, this will return a value returned by
482+
* `wrapNumbers.integerTypeCastFunction`.
483+
* Please see {@link IntegerTypeCastOptions} for options descriptions.
384484
* @returns {*}
385485
*
386486
* @example
@@ -399,13 +499,19 @@ export namespace entity {
399499
* });
400500
* // <Buffer 68 65 6c 6c 6f>
401501
*/
402-
export function decodeValueProto(valueProto: ValueProto) {
502+
export function decodeValueProto(
503+
valueProto: ValueProto,
504+
wrapNumbers?: boolean | IntegerTypeCastOptions
505+
) {
403506
const valueType = valueProto.valueType!;
404507
const value = valueProto[valueType];
405508

406509
switch (valueType) {
407510
case 'arrayValue': {
408-
return value.values.map(entity.decodeValueProto);
511+
// tslint:disable-next-line no-any
512+
return value.values.map((val: any) =>
513+
entity.decodeValueProto(val, wrapNumbers)
514+
);
409515
}
410516

411517
case 'blobValue': {
@@ -421,11 +527,15 @@ export namespace entity {
421527
}
422528

423529
case 'integerValue': {
424-
return Number(value);
530+
return wrapNumbers
531+
? typeof wrapNumbers === 'object'
532+
? new entity.Int(valueProto, wrapNumbers).valueOf()
533+
: new entity.Int(valueProto, undefined)
534+
: decodeIntegerValue(valueProto);
425535
}
426536

427537
case 'entityValue': {
428-
return entity.entityFromEntityProto(value);
538+
return entity.entityFromEntityProto(value, wrapNumbers);
429539
}
430540

431541
case 'keyValue': {
@@ -554,6 +664,12 @@ export namespace entity {
554664
*
555665
* @private
556666
* @param {object} entityProto The protocol entity object to convert.
667+
* @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in
668+
* {@link Datastore#Int} objects.
669+
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
670+
* If an `object`, this will return a value returned by
671+
* `wrapNumbers.integerTypeCastFunction`.
672+
* Please see {@link IntegerTypeCastOptions} for options descriptions.
557673
* @returns {object}
558674
*
559675
* @example
@@ -574,15 +690,18 @@ export namespace entity {
574690
* // }
575691
*/
576692
// tslint:disable-next-line no-any
577-
export function entityFromEntityProto(entityProto: EntityProto): any {
693+
export function entityFromEntityProto(
694+
entityProto: EntityProto,
695+
wrapNumbers?: boolean | IntegerTypeCastOptions
696+
) {
578697
// tslint:disable-next-line no-any
579698
const entityObject: any = {};
580699
const properties = entityProto.properties || {};
581700

582701
// tslint:disable-next-line forin
583702
for (const property in properties) {
584703
const value = properties[property];
585-
entityObject[property] = entity.decodeValueProto(value);
704+
entityObject[property] = entity.decodeValueProto(value, wrapNumbers);
586705
}
587706

588707
return entityObject;
@@ -768,7 +887,12 @@ export namespace entity {
768887
* @param {object[]} results The response array.
769888
* @param {object} results.entity An entity object.
770889
* @param {object} results.entity.key The entity's key.
771-
* @returns {object[]}
890+
* @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in
891+
* {@link Datastore#Int} objects.
892+
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
893+
* If an `object`, this will return a value returned by
894+
* `wrapNumbers.integerTypeCastFunction`.
895+
* Please see {@link IntegerTypeCastOptions} for options descriptions.
772896
*
773897
* @example
774898
* request_('runQuery', {}, (err, response) => {
@@ -782,9 +906,12 @@ export namespace entity {
782906
* //
783907
* });
784908
*/
785-
export function formatArray(results: ResponseResult[]) {
909+
export function formatArray(
910+
results: ResponseResult[],
911+
wrapNumbers?: boolean | IntegerTypeCastOptions
912+
) {
786913
return results.map(result => {
787-
const ent = entity.entityFromEntityProto(result.entity!);
914+
const ent = entity.entityFromEntityProto(result.entity!, wrapNumbers);
788915
ent[entity.KEY_SYMBOL] = entity.keyFromKeyProto(result.entity!.key!);
789916
return ent;
790917
});
@@ -1274,6 +1401,7 @@ export interface ValueProto {
12741401
values?: ValueProto[];
12751402
// tslint:disable-next-line no-any
12761403
value?: any;
1404+
propertyName?: string;
12771405
}
12781406

12791407
export interface EntityProto {
@@ -1305,3 +1433,7 @@ export interface EntityObject {
13051433
data: {[k: string]: Entity};
13061434
excludeFromIndexes: string[];
13071435
}
1436+
1437+
export interface Json {
1438+
[field: string]: string;
1439+
}

handwritten/datastore/src/query.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,14 @@ class Query {
397397
* If not specified, default values are chosen by Datastore for the
398398
* operation. Learn more about strong and eventual consistency
399399
* [here](https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore).
400+
* @param {object} [options.gaxOptions] Request configuration options, outlined
401+
* here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
402+
* @param {boolean | IntegerTypeCastOptions} [options.wrapNumbers=false]
403+
* Wrap values of integerValue type in {@link Datastore#Int} objects.
404+
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
405+
* If an `object`, this will return a value returned by
406+
* `wrapNumbers.integerTypeCastFunction`.
407+
* Please see {@link IntegerTypeCastOptions} for options descriptions.
400408
* @param {function} [callback] The callback function. If omitted, a readable
401409
* stream instance is returned.
402410
* @param {?error} callback.err An error returned while making this request
@@ -517,9 +525,15 @@ export interface QueryProto {
517525
*/
518526
export {Query};
519527

528+
export interface IntegerTypeCastOptions {
529+
integerTypeCastFunction: Function;
530+
properties?: string | string[];
531+
}
532+
520533
export interface RunQueryOptions {
521534
consistency?: 'strong' | 'eventual';
522535
gaxOptions?: CallOptions;
536+
wrapNumbers?: boolean | IntegerTypeCastOptions;
523537
}
524538

525539
export interface RunQueryCallback {

handwritten/datastore/src/request.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,10 @@ class DatastoreRequest {
290290
return;
291291
}
292292

293-
const entities = entity.formatArray(resp!.found! as ResponseResult[]);
293+
const entities = entity.formatArray(
294+
resp!.found! as ResponseResult[],
295+
options.wrapNumbers
296+
);
294297
const nextKeys = (resp!.deferred || [])
295298
.map(entity.keyFromKeyProto)
296299
.map(entity.keyToKeyProto);
@@ -432,6 +435,12 @@ class DatastoreRequest {
432435
* [here](https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore).
433436
* @param {object} [options.gaxOptions] Request configuration options, outlined
434437
* here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
438+
* @param {boolean | IntegerTypeCastOptions} [options.wrapNumbers=false]
439+
* Wrap values of integerValue type in {@link Datastore#Int} objects.
440+
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
441+
* If an `object`, this will return a value returned by
442+
* `wrapNumbers.integerTypeCastFunction`.
443+
* Please see {@link IntegerTypeCastOptions} for options descriptions.
435444
* @param {function} callback The callback function.
436445
* @param {?error} callback.err An error returned while making this request
437446
* @param {object|object[]} callback.entity The entity object(s) which match
@@ -571,7 +580,6 @@ class DatastoreRequest {
571580
* that uses the end cursor from the previous query as the starting cursor for
572581
* the next query. You can pass that object back to this method to see if more
573582
* results exist.
574-
*
575583
* @param {Query} query Query object.
576584
* @param {object} [options] Optional configuration.
577585
* @param {string} [options.consistency] Specify either `strong` or `eventual`.
@@ -580,6 +588,12 @@ class DatastoreRequest {
580588
* [here](https://cloud.google.com/datastore/docs/articles/balancing-strong-and-eventual-consistency-with-google-cloud-datastore).
581589
* @param {object} [options.gaxOptions] Request configuration options, outlined
582590
* here: https://googleapis.github.io/gax-nodejs/global.html#CallOptions.
591+
* @param {boolean | IntegerTypeCastOptions} [options.wrapNumbers=false]
592+
* Wrap values of integerValue type in {@link Datastore#Int} objects.
593+
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
594+
* If an `object`, this will return a value returned by
595+
* `wrapNumbers.integerTypeCastFunction`.
596+
* Please see {@link IntegerTypeCastOptions} for options descriptions.
583597
* @param {function} [callback] The callback function. If omitted, a readable
584598
* stream instance is returned.
585599
* @param {?error} callback.err An error returned while making this request
@@ -764,7 +778,10 @@ class DatastoreRequest {
764778
let entities: Entity[] = [];
765779

766780
if (resp.batch.entityResults) {
767-
entities = entity.formatArray(resp.batch.entityResults);
781+
entities = entity.formatArray(
782+
resp.batch.entityResults,
783+
options.wrapNumbers
784+
);
768785
}
769786

770787
// Emit each result right away, then get the rest if necessary.
@@ -1400,10 +1417,7 @@ export interface AllocateIdsOptions {
14001417
allocations?: number;
14011418
gaxOptions?: CallOptions;
14021419
}
1403-
export interface CreateReadStreamOptions {
1404-
consistency?: string;
1405-
gaxOptions?: CallOptions;
1406-
}
1420+
export interface CreateReadStreamOptions extends RunQueryOptions {}
14071421
export interface GetCallback {
14081422
(err?: Error | null, entity?: Entities): void;
14091423
}

0 commit comments

Comments
 (0)