Skip to content

Commit

Permalink
feat!: throw error with out of bounds integer values, optionally wrap…
Browse files Browse the repository at this point in the history
… into DsInt or provide a custom 'integerValue' type cast options #516
  • Loading branch information
AVaksman authored and crwilcox committed Nov 14, 2019
1 parent 53ddc21 commit 6c8cc74
Show file tree
Hide file tree
Showing 5 changed files with 894 additions and 67 deletions.
158 changes: 145 additions & 13 deletions src/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import arrify = require('arrify');
import * as extend from 'extend';
import * as is from 'is';
import {Query, QueryProto} from './query';
import {Query, QueryProto, IntegerTypeCastOptions} from './query';
import {PathType} from '.';
import * as Protobuf from 'protobufjs';
import * as path from 'path';
Expand Down Expand Up @@ -111,16 +111,36 @@ export namespace entity {
*
* @class
* @param {number|string} value The integer value.
* @param {object} [typeCastOptions] Configuration to convert
* values of `integerValue` type to a custom value. Must provide an
* `integerTypeCastFunction` to handle `integerValue` conversion.
* @param {function} typeCastOptions.integerTypeCastFunction A custom user
* provided function to convert `integerValue`.
* @param {sting|string[]} [typeCastOptions.properties] `Entity` property
* names to be converted using `integerTypeCastFunction`.
*
* @example
* const {Datastore} = require('@google-cloud/datastore');
* const datastore = new Datastore();
* const anInt = datastore.int(7);
*/
export class Int {
export class Int extends Number {
type: string;
value: string;
constructor(value: number | string) {
typeCastFunction?: Function;
typeCastProperties?: string[];
private _entityPropertyName: string | undefined;
constructor(
value: number | string | ValueProto,
typeCastOptions?: IntegerTypeCastOptions
) {
super(typeof value === 'object' ? value.integerValue : value);
this._entityPropertyName =
typeof value === 'object' ? value.propertyName : undefined;
this.value =
typeof value === 'object'
? value.integerValue.toString()
: value.toString();
/**
* @name Int#type
* @type {string}
Expand All @@ -130,7 +150,46 @@ export namespace entity {
* @name Int#value
* @type {string}
*/
this.value = value.toString();
if (typeCastOptions) {
this.typeCastFunction = typeCastOptions.integerTypeCastFunction;
if (typeof typeCastOptions.integerTypeCastFunction !== 'function') {
throw new Error(
`integerTypeCastFunction is not a function or was not provided.`
);
}

this.typeCastProperties = typeCastOptions.properties
? arrify(typeCastOptions.properties)
: undefined;
}
}
// tslint:disable-next-line no-any
valueOf(): any {
let shouldCustomCast = this.typeCastFunction ? true : false;
if (
this.typeCastProperties &&
!this.typeCastProperties.includes(this._entityPropertyName!)
) {
shouldCustomCast = false;
}

if (shouldCustomCast) {
try {
return this.typeCastFunction!(this.value);
} catch (error) {
error.message = `integerTypeCastFunction threw an error:\n\n - ${error.message}`;
throw error;
}
} else {
return decodeIntegerValue({
integerValue: this.value,
propertyName: this._entityPropertyName,
});
}
}

toJSON(): Json {
return {type: this.type, value: this.value};
}
}

Expand Down Expand Up @@ -376,11 +435,52 @@ export namespace entity {
return value instanceof entity.Key;
}

/**
* Convert a protobuf `integerValue`.
*
* @private
* @param {object} value The `integerValue` to convert.
*/
function decodeIntegerValue(value: ValueProto) {
const num = Number(value.integerValue);
if (!Number.isSafeInteger(num)) {
throw new Error(
'We attempted to return all of the numeric values, but ' +
(value.propertyName ? value.propertyName + ' ' : '') +
'value ' +
value.integerValue +
" is out of bounds of 'Number.MAX_SAFE_INTEGER'.\n" +
"To prevent this error, please consider passing 'options.wrapNumbers=true' or\n" +
"'options.wrapNumbers' as\n" +
'{\n' +
' integerTypeCastFunction: provide <your_custom_function>\n' +
' properties: optionally specify property name(s) to be cutom casted' +
'}\n'
);
}
return num;
}

/**
* @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
* provided function to convert `integerValue`.
* @property {string | string[]} [properties] `Entity` property
* names to be converted using `integerTypeCastFunction`.
*/
/**
* 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} objects.
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
* If an `object`, this will return a value returned by
* `wrapNumbers.integerTypeCastFunction`.
* Please see {@link IntegerTypeCastOptions} for options descriptions.
* @returns {*}
*
* @example
Expand All @@ -399,13 +499,19 @@ export namespace entity {
* });
* // <Buffer 68 65 6c 6c 6f>
*/
export function decodeValueProto(valueProto: ValueProto) {
export function decodeValueProto(
valueProto: ValueProto,
wrapNumbers?: boolean | IntegerTypeCastOptions
) {
const valueType = valueProto.valueType!;
const value = valueProto[valueType];

switch (valueType) {
case 'arrayValue': {
return value.values.map(entity.decodeValueProto);
// tslint:disable-next-line no-any
return value.values.map((val: any) =>
entity.decodeValueProto(val, wrapNumbers)
);
}

case 'blobValue': {
Expand All @@ -421,11 +527,15 @@ export namespace entity {
}

case 'integerValue': {
return Number(value);
return wrapNumbers
? typeof wrapNumbers === 'object'
? new entity.Int(valueProto, wrapNumbers).valueOf()
: new entity.Int(valueProto, undefined)
: decodeIntegerValue(valueProto);
}

case 'entityValue': {
return entity.entityFromEntityProto(value);
return entity.entityFromEntityProto(value, wrapNumbers);
}

case 'keyValue': {
Expand Down Expand Up @@ -554,6 +664,12 @@ export namespace entity {
*
* @private
* @param {object} entityProto The protocol entity object to convert.
* @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in
* {@link Datastore#Int} objects.
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
* If an `object`, this will return a value returned by
* `wrapNumbers.integerTypeCastFunction`.
* Please see {@link IntegerTypeCastOptions} for options descriptions.
* @returns {object}
*
* @example
Expand All @@ -574,15 +690,18 @@ export namespace entity {
* // }
*/
// tslint:disable-next-line no-any
export function entityFromEntityProto(entityProto: EntityProto): any {
export function entityFromEntityProto(
entityProto: EntityProto,
wrapNumbers?: boolean | IntegerTypeCastOptions
) {
// tslint:disable-next-line no-any
const entityObject: any = {};
const properties = entityProto.properties || {};

// tslint:disable-next-line forin
for (const property in properties) {
const value = properties[property];
entityObject[property] = entity.decodeValueProto(value);
entityObject[property] = entity.decodeValueProto(value, wrapNumbers);
}

return entityObject;
Expand Down Expand Up @@ -768,7 +887,12 @@ export namespace entity {
* @param {object[]} results The response array.
* @param {object} results.entity An entity object.
* @param {object} results.entity.key The entity's key.
* @returns {object[]}
* @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in
* {@link Datastore#Int} objects.
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
* If an `object`, this will return a value returned by
* `wrapNumbers.integerTypeCastFunction`.
* Please see {@link IntegerTypeCastOptions} for options descriptions.
*
* @example
* request_('runQuery', {}, (err, response) => {
Expand All @@ -782,9 +906,12 @@ export namespace entity {
* //
* });
*/
export function formatArray(results: ResponseResult[]) {
export function formatArray(
results: ResponseResult[],
wrapNumbers?: boolean | IntegerTypeCastOptions
) {
return results.map(result => {
const ent = entity.entityFromEntityProto(result.entity!);
const ent = entity.entityFromEntityProto(result.entity!, wrapNumbers);
ent[entity.KEY_SYMBOL] = entity.keyFromKeyProto(result.entity!.key!);
return ent;
});
Expand Down Expand Up @@ -1274,6 +1401,7 @@ export interface ValueProto {
values?: ValueProto[];
// tslint:disable-next-line no-any
value?: any;
propertyName?: string;
}

export interface EntityProto {
Expand Down Expand Up @@ -1305,3 +1433,7 @@ export interface EntityObject {
data: {[k: string]: Entity};
excludeFromIndexes: string[];
}

export interface Json {
[field: string]: string;
}
14 changes: 14 additions & 0 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,14 @@ 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.wrapNumbers=false]
* Wrap values of integerValue type in {@link Datastore#Int} objects.
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
* If an `object`, this will return a value returned by
* `wrapNumbers.integerTypeCastFunction`.
* Please see {@link IntegerTypeCastOptions} for options descriptions.
* @param {function} [callback] The callback function. If omitted, a readable
* stream instance is returned.
* @param {?error} callback.err An error returned while making this request
Expand Down Expand Up @@ -517,9 +525,15 @@ export interface QueryProto {
*/
export {Query};

export interface IntegerTypeCastOptions {
integerTypeCastFunction: Function;
properties?: string | string[];
}

export interface RunQueryOptions {
consistency?: 'strong' | 'eventual';
gaxOptions?: CallOptions;
wrapNumbers?: boolean | IntegerTypeCastOptions;
}

export interface RunQueryCallback {
Expand Down
28 changes: 21 additions & 7 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,10 @@ class DatastoreRequest {
return;
}

const entities = entity.formatArray(resp!.found! as ResponseResult[]);
const entities = entity.formatArray(
resp!.found! as ResponseResult[],
options.wrapNumbers
);
const nextKeys = (resp!.deferred || [])
.map(entity.keyFromKeyProto)
.map(entity.keyToKeyProto);
Expand Down Expand Up @@ -432,6 +435,12 @@ 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 | IntegerTypeCastOptions} [options.wrapNumbers=false]
* Wrap values of integerValue type in {@link Datastore#Int} objects.
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
* If an `object`, this will return a value returned by
* `wrapNumbers.integerTypeCastFunction`.
* Please see {@link IntegerTypeCastOptions} for options descriptions.
* @param {function} callback The callback function.
* @param {?error} callback.err An error returned while making this request
* @param {object|object[]} callback.entity The entity object(s) which match
Expand Down Expand Up @@ -571,7 +580,6 @@ class DatastoreRequest {
* that uses the end cursor from the previous query as the starting cursor for
* the next query. You can pass that object back to this method to see if more
* results exist.
*
* @param {Query} query Query object.
* @param {object} [options] Optional configuration.
* @param {string} [options.consistency] Specify either `strong` or `eventual`.
Expand All @@ -580,6 +588,12 @@ 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 | IntegerTypeCastOptions} [options.wrapNumbers=false]
* Wrap values of integerValue type in {@link Datastore#Int} objects.
* If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
* If an `object`, this will return a value returned by
* `wrapNumbers.integerTypeCastFunction`.
* Please see {@link IntegerTypeCastOptions} for options descriptions.
* @param {function} [callback] The callback function. If omitted, a readable
* stream instance is returned.
* @param {?error} callback.err An error returned while making this request
Expand Down Expand Up @@ -764,7 +778,10 @@ class DatastoreRequest {
let entities: Entity[] = [];

if (resp.batch.entityResults) {
entities = entity.formatArray(resp.batch.entityResults);
entities = entity.formatArray(
resp.batch.entityResults,
options.wrapNumbers
);
}

// Emit each result right away, then get the rest if necessary.
Expand Down Expand Up @@ -1400,10 +1417,7 @@ export interface AllocateIdsOptions {
allocations?: number;
gaxOptions?: CallOptions;
}
export interface CreateReadStreamOptions {
consistency?: string;
gaxOptions?: CallOptions;
}
export interface CreateReadStreamOptions extends RunQueryOptions {}
export interface GetCallback {
(err?: Error | null, entity?: Entities): void;
}
Expand Down
Loading

0 comments on commit 6c8cc74

Please sign in to comment.