From 79f7855b3f6df59afe322dd578ed65f408a2fc2b Mon Sep 17 00:00:00 2001 From: Dave Gramlich Date: Tue, 8 Jan 2019 06:40:13 -0800 Subject: [PATCH] refactor(codec): improve typescript defs (#490) --- src/batch-transaction.ts | 8 +- src/codec.ts | 741 +++++++++++++++++++-------------- src/database.ts | 3 +- src/row-builder.ts | 29 +- src/transaction-request.ts | 10 +- src/transaction.ts | 3 - src/v1/spanner_client.d.ts | 44 +- system-test/spanner.ts | 28 +- test/batch-transaction.ts | 36 -- test/codec.ts | 803 +++++++++++++++--------------------- test/row-builder.ts | 134 +----- test/transaction-request.ts | 125 ++---- 12 files changed, 870 insertions(+), 1094 deletions(-) diff --git a/src/batch-transaction.ts b/src/batch-transaction.ts index 2b1d979da..cfee7f974 100644 --- a/src/batch-transaction.ts +++ b/src/batch-transaction.ts @@ -119,9 +119,7 @@ class BatchTransaction extends Transaction { } const reqOpts = codec.encodeQuery(query); const gaxOpts = query.gaxOptions; - if (gaxOpts) { - delete reqOpts.gaxOptions; - } + this.createPartitions_( { client: 'SpannerClient', @@ -194,9 +192,7 @@ class BatchTransaction extends Transaction { createReadPartitions(options, callback) { const reqOpts = codec.encodeRead(options); const gaxOpts = options.gaxOptions; - if (gaxOpts) { - delete reqOpts.gaxOptions; - } + this.createPartitions_( { client: 'SpannerClient', diff --git a/src/codec.ts b/src/codec.ts index 56d3bcdc4..c81a48d62 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -13,15 +13,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import {Service} from '@google-cloud/common-grpc'; import * as arrify from 'arrify'; -import * as extend from 'extend'; +import {CallOptions} from 'google-gax'; import * as is from 'is'; +import {common as p} from 'protobufjs'; + +import {SpannerClient as s} from './v1'; + +// tslint:disable-next-line no-any +type Value = any; + +interface Field { + name: string; + value: Value; +} + +interface Json { + [field: string]: Value; +} + +/** + * @typedef JsonOptions + * @property {boolean} [wrapNumbers=false] Indicates if the numbers should be + * wrapped in Int/Float wrappers. + * @property {boolean} [wrapStructs=false] Indicates if the structs should be + * wrapped in Struct wrapper. + */ +interface JsonOptions { + wrapNumbers?: boolean; + wrapStructs?: boolean; +} +/** + * @typedef SpannerDate + * @see Spanner.date + */ export class SpannerDate { - value; - constructor(value) { + value: string; + constructor(value?: string|number|Date) { if (arguments.length > 1) { throw new TypeError([ 'The spanner.date function accepts a Date object or a', @@ -31,156 +61,162 @@ export class SpannerDate { if (is.undefined(value)) { value = new Date(); } - this.value = new Date(value).toJSON().replace(/T.+/, ''); + this.value = new Date(value!).toJSON().replace(/T.+/, ''); } } -export class Float { - value; - constructor(value) { +/** + * Using an abstract class to simplify checking for wrapped numbers. + * + * @private + */ +abstract class WrappedNumber { + value!: string|number; + abstract valueOf(): number; +} + +/** + * @typedef Float + * @see Spanner.float + */ +export class Float extends WrappedNumber { + value: number; + constructor(value: number) { + super(); this.value = value; } - valueOf() { + valueOf(): number { return Number(this.value); } } -export class Int { - value; - constructor(value) { +/** + * @typedef Int + * @see Spanner.int + */ +export class Int extends WrappedNumber { + value: string; + constructor(value: string) { + super(); this.value = value.toString(); } - valueOf() { + valueOf(): number { const num = Number(this.value); if (num > Number.MAX_SAFE_INTEGER) { - throw new Error('Integer ' + this.value + ' is out of bounds.'); + throw new Error(`Integer ${this.value} is out of bounds.`); } return num; } } /** - * We use this symbol as a means to identify if an array is actually a struct. - * We need to do this because changing Structs from an object to an array would - * be a major breaking change. - * - * @private - * - * @example - * const struct = []; - * struct[TYPE] = 'struct'; - */ -const TYPE = Symbol(); - -/** - * Struct wrapper. This returns an array, but will decorate the array to give it - * struct characteristics. - * - * @private - * - * @returns {array} + * @typedef Struct + * @see Spanner.struct */ -export class Struct extends Array { - constructor() { - super(); - this[TYPE] = Struct.TYPE; - Object.defineProperty( - this, 'toJSON', - {enumerable: false, value: codec.generateToJSONFromRow(this)}); - } - +export class Struct extends Array { /** - * Use this to assign/check the type when dealing with structs. + * Converts struct into a pojo (plain old JavaScript object). * - * @private + * @param {JsonOptions} [options] JSON options. + * @returns {object} */ - static TYPE = 'struct'; - + toJSON(options?: JsonOptions): Json { + return codec.convertFieldsToJson(this, options); + } /** - * Converts an array of objects to a struct array. + * Converts an array of fields to a struct. * * @private * - * @param {object[]} arr Struct array. + * @param {object[]} fields List of struct fields. * @return {Struct} */ - static fromArray(arr) { - const struct = new Struct(); - struct.push.apply(struct, arr); - return struct; + static fromArray(fields: Field[]): Struct { + return new Struct(...fields); } - /** - * Converts a JSON object to a struct array. + * Converts a JSON object to a struct. * * @private * * @param {object} json Struct JSON. * @return {Struct} */ - static fromJSON(json: {}) { - const struct = new Struct(); - Object.keys(json || {}).forEach(name => { + static fromJSON(json: Json): Struct { + const fields = Object.keys(json || {}).map(name => { const value = json[name]; - struct.push({name, value}); + return {name, value}; }); - return struct; + return Struct.fromArray(fields); } +} - /** - * Checks to see if the provided object is a Struct. - * - * @private - * - * @param {*} thing The object to check. - * @returns {boolean} - */ - static isStruct(thing: {}): thing is Struct { - return !!(thing && thing[TYPE] === Struct.TYPE); +/** + * Wherever a row or struct object is returned, it is assigned a "toJSON" + * function. This function will generate the JSON for that row. + * + * @private + * + * @param {array} row The row to generate JSON for. + * @param {JsonOptions} [options] JSON options. + * @returns {object} + */ +function convertFieldsToJson(fields: Field[], options?: JsonOptions): Json { + const json: Json = {}; + + const defaultOptions = {wrapNumbers: false, wrapStructs: false}; + + options = Object.assign(defaultOptions, options); + + for (const {name, value} of fields) { + if (!name) { + continue; + } + + try { + json[name] = convertValueToJson(value, options); + } catch (e) { + e.message = [ + `Serializing column "${name}" encountered an error: ${e.message}`, + 'Call row.toJSON({ wrapNumbers: true }) to receive a custom type.', + ].join(' '); + throw e; + } } + + return json; } /** - * Wherever a row object is returned, it is assigned a "toJSON" function. This - * function will create that function in a consistent format. + * Attempts to convert a wrapped or nested value into a native JavaScript type. * - * @param {array} row The row to generate JSON for. - * @returns {function} + * @private + * + * @param {*} value The value to convert. + * @param {JsonOptions} options JSON options. + * @return {*} */ -function generateToJSONFromRow(row) { - return (options) => { - options = extend( - { - wrapNumbers: false, - }, - options); - - return row.reduce((serializedRow, keyVal) => { - const name = keyVal.name; - let value = keyVal.value; - - if (!name) { - return serializedRow; - } +function convertValueToJson(value: Value, options: JsonOptions): Value { + if (!options.wrapNumbers && value instanceof WrappedNumber) { + return value.valueOf(); + } - const isNumber = value instanceof Float || value instanceof Int; - if (!options.wrapNumbers && isNumber) { - try { - value = value.valueOf(); - } catch (e) { - e.message = [ - `Serializing column "${name}" encountered an error: ${e.message}`, - 'Call row.toJSON({ wrapNumbers: true }) to receive a custom type.', - ].join(' '); - throw e; - } - } + if (value instanceof Struct) { + if (!options.wrapStructs) { + return value.toJSON(options); + } - serializedRow[name] = value; + return value.map(({name, value}) => { + value = convertValueToJson(value, options); + return {name, value}; + }); + } - return serializedRow; - }, {}); - }; + if (Array.isArray(value)) { + return value.map(child => convertValueToJson(child, options)); + } + + return value; } /** @@ -189,53 +225,47 @@ function generateToJSONFromRow(row) { * @private * * @param {*} value Value to decode - * @param {object[]} field Struct fields + * @param {object[]} type Value type object. * @returns {*} */ -function decode(value, field) { - function decodeValue_(decoded, type) { - if (is.null(decoded)) { - return null; - } - switch (type.code) { - case 'BYTES': - decoded = Buffer.from(decoded, 'base64'); - break; - case 'FLOAT64': - decoded = new codec.Float(decoded); - break; - case 'INT64': - decoded = new codec.Int(decoded); - break; - case 'TIMESTAMP': // falls through - case 'DATE': - decoded = new Date(decoded); - break; - case 'ARRAY': - decoded = decoded.map(value => { - return decodeValue_(value, type.arrayElementType); - }); - break; - case 'STRUCT': - // tslint:disable-next-line no-any - const struct = new (Struct as any)(); - const fields = type.structType.fields; - fields.forEach((field, index) => { - const name = field.name; - let value = decoded[name] || decoded[index]; - value = decodeValue_(value, field.type); - struct.push({name, value}); - }); - decoded = struct; - break; - default: - break; - } +function decode(value: Value, type: s.Type): Value { + if (is.null(value)) { + return null; + } - return decoded; + let decoded = value; + + switch (type.code) { + case s.TypeCode.BYTES: + decoded = Buffer.from(decoded, 'base64'); + break; + case s.TypeCode.FLOAT64: + decoded = new Float(decoded); + break; + case s.TypeCode.INT64: + decoded = new Int(decoded); + break; + case s.TypeCode.TIMESTAMP: // falls through + case s.TypeCode.DATE: + decoded = new Date(decoded); + break; + case s.TypeCode.ARRAY: + decoded = decoded.map(value => { + return decode(value, type.arrayElementType!); + }); + break; + case s.TypeCode.STRUCT: + const fields = type.structType!.fields.map(({name, type}, index) => { + const value = decode(decoded[name] || decoded[index], type!); + return {name, value}; + }); + decoded = Struct.fromArray(fields); + break; + default: + break; } - return decodeValue_(value, field.type); + return decoded; } /** @@ -243,108 +273,144 @@ function decode(value, field) { * * @private * - * @param {*} value The value to be encoded + * @param {*} value The value to be encoded. + * @returns {object} google.protobuf.Value + */ +function encode(value: Value): p.IValue { + return Service.encodeValue_(encodeValue(value)); +} + +/** + * Formats values into expected format of google.protobuf.Value. The actual + * conversion to a google.protobuf.Value object happens via + * `Service.encodeValue_` + * + * @private + * + * @param {*} value The value to be encoded. * @returns {*} */ -function encode(value) { - function preEncode(value) { - const numberShouldBeStringified = - (!(value instanceof Float) && is.integer(value)) || - value instanceof Int || is.infinite(value) || Number.isNaN(value); - - if (is.date(value)) { - value = value.toJSON(); - } else if ( - value instanceof SpannerDate || value instanceof Float || - value instanceof Int) { - value = value.value; - } else if (Buffer.isBuffer(value)) { - value = value.toString('base64'); - } else if (Struct.isStruct(value)) { - value = value.map(field => preEncode(field.value)); - } else if (is.array(value)) { - value = value.map(preEncode); - } else if (is.object(value) && is.fn(value.hasOwnProperty)) { - for (const prop in value) { - if (value.hasOwnProperty(prop)) { - value[prop] = preEncode(value[prop]); - } - } - } +function encodeValue(value: Value): Value { + if (is.number(value) && !is.decimal(value)) { + return value.toString(); + } - if (numberShouldBeStringified) { - value = value.toString(); - } + if (is.date(value)) { + return value.toJSON(); + } + + if (value instanceof WrappedNumber || value instanceof SpannerDate) { + return value.value; + } + + if (Buffer.isBuffer(value)) { + return value.toString('base64'); + } - return value; + if (value instanceof Struct) { + return Array.from(value).map(field => encodeValue(field.value)); } - // tslint:disable-next-line no-any - return (Service as any).encodeValue_(preEncode(value)); + + if (is.array(value)) { + return value.map(encodeValue); + } + + return value; +} + +/** + * Just a map with friendlier names for the types. + * + * @private + * @enum {string} + */ +enum TypeCode { + unspecified = s.TypeCode.TYPE_CODE_UNSPECIFIED, + bool = s.TypeCode.BOOL, + int64 = s.TypeCode.INT64, + float64 = s.TypeCode.FLOAT64, + timestamp = s.TypeCode.TIMESTAMP, + date = s.TypeCode.DATE, + string = s.TypeCode.STRING, + bytes = s.TypeCode.BYTES, + array = s.TypeCode.ARRAY, + struct = s.TypeCode.STRUCT +} + +/** + * Conveniece Type object that simplifies specifying the data type, the array + * child type and/or struct fields. + * + * @private + */ +interface Type { + type: string; + fields?: FieldType[]; + child?: Type; +} + +interface FieldType extends Type { + name: string; } /** - * Get the corresponding Spanner data type. + * Get the corresponding Spanner data type for the provided value. * * @private * - * @param {*} field - The field value. - * @returns {string} + * @param {*} value - The value. + * @returns {object} * * @example - * Database.getType_(NaN); - * // 'float64' + * codec.getType(NaN); + * // {type: 'float64'} */ -function getType(field) { - if (is.boolean(field)) { - return 'bool'; - } - - const isSpecialNumber = is.infinite(field) || - (is.number(field) && isNaN(field)); +function getType(value: Value): Type { + const isSpecialNumber = is.infinite(value) || + (is.number(value) && isNaN(value)); - if (is.decimal(field) || isSpecialNumber || field instanceof Float) { - return 'float64'; + if (is.decimal(value) || isSpecialNumber || value instanceof Float) { + return {type: 'float64'}; } - if (is.number(field) || field instanceof Int) { - return 'int64'; + if (is.number(value) || value instanceof Int) { + return {type: 'int64'}; } - if (is.string(field)) { - return 'string'; + if (is.boolean(value)) { + return {type: 'bool'}; } - if (Buffer.isBuffer(field)) { - return 'bytes'; + if (is.string(value)) { + return {type: 'string'}; } - if (is.date(field)) { - return 'timestamp'; + if (Buffer.isBuffer(value)) { + return {type: 'bytes'}; } - if (field instanceof SpannerDate) { - return 'date'; + if (is.date(value)) { + return {type: 'timestamp'}; } - if (Struct.isStruct(field)) { - const fields = field.map(field => { - return { - name: field.name, - type: getType(field.value), - }; - }); + if (value instanceof SpannerDate) { + return {type: 'date'}; + } + if (value instanceof Struct) { return { type: 'struct', - fields, + fields: Array.from(value).map(({name, value}) => { + return Object.assign({name}, getType(value)); + }), }; } - if (is.array(field)) { + if (is.array(value)) { let child; - for (let i = 0; i < field.length; i++) { - child = field[i]; + for (let i = 0; i < value.length; i++) { + child = value[i]; if (!is.null(child)) { break; @@ -357,40 +423,51 @@ function getType(field) { }; } - return 'unspecified'; + return {type: 'unspecified'}; } /** - * A list of available Spanner types. The index of said type in Array aligns - * with the type code that query params require. + * Generic request options. * * @private */ -const TYPES = [ - 'unspecified', - 'bool', - 'int64', - 'float64', - 'timestamp', - 'date', - 'string', - 'bytes', - 'array', - 'struct', -]; +interface RequestOptions { + json?: boolean; + jsonOptions?: JsonOptions; + gaxOptions?: CallOptions; +} + +/** + * ExecuteSql request options. This includes all standard ExecuteSqlRequest + * options as well as several convenience properties. + * + * @see [Query Syntax](https://cloud.google.com/spanner/docs/query-syntax) + * @see [ExecuteSql API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.ExecuteSql) + * + * @typedef ExecuteSqlRequest + * @property {object} [params] A map of parameter names to values. + * @property {object} [types] A map of parameter names to types. If omitted the + * client will attempt to guess for all non-null values. + * @property {boolean} [json=false] Receive the rows as serialized objects. This + * is the equivalent of calling `toJSON()` on each row. + * @property {JsonOptions} [jsonOptions] Configuration options for the + * serialized objects. + */ +export interface ExecuteSqlRequest extends s.ExecuteSqlRequest, RequestOptions { + params?: {[field: string]: Value}; + types?: {[field: string]: string|Type}; +} /** * Encodes a ExecuteSqlRequest object into the correct format. * * @private * - * @param {object} query The query object. - * @param {object} [query.params] A map of parameter name to values. - * @param {object} [query.types] A map of parameter types. + * @param {ExecuteSqlRequest} query The request object. * @returns {object} */ -function encodeQuery(query) { - query = extend({}, query); +function encodeQuery(query: ExecuteSqlRequest): s.ExecuteSqlRequest { + query = Object.assign({}, query); if (query.params) { const fields = {}; @@ -399,77 +476,151 @@ function encodeQuery(query) { query.types = {}; } - // tslint:disable-next-line forin - for (const prop in query.params) { - const field = query.params[prop]; - if (!query.types[prop]) { - query.types[prop] = codec.getType(field); + const types = query.types!; + + Object.keys(query.params).forEach(param => { + const value = query.params![param]; + if (!types[param]) { + types[param] = codec.getType(value); } - fields[prop] = codec.encode(field); - } + fields[param] = codec.encode(value); + }); query.params = {fields}; } if (query.types) { - const formattedTypes = {}; - // tslint:disable-next-line forin - for (const field in query.types) { - formattedTypes[field] = codec.createTypeObject(query.types[field]); - } + const paramTypes = {}; + + Object.keys(query.types).forEach(param => { + paramTypes[param] = codec.createTypeObject(query.types![param]); + }); + + query.paramTypes = paramTypes; delete query.types; - query.paramTypes = formattedTypes; + } + + if (query.json) { + delete query.json; + delete query.jsonOptions; + } + + if (query.gaxOptions) { + delete query.gaxOptions; } return query; } +/** + * A KeyRange represents a range of rows in a table or index. + * + * A range has a start key and an end key. These keys can be open or closed, + * indicating if the range includes rows with that key. + * + * Keys are represented by an array of strings where the nth value in the list + * corresponds to the nth component of the table or index primary key. + * + * @typedef KeyRange + * @property {string[]} [startClosed] If the start is closed, then the range + * includes all rows whose first key columns exactly match. + * @property {string[]} [startOpen] If the start is open, then the range + * excludes rows whose first key columns exactly match. + * @property {string[]} [endClosed] If the end is closed, then the range + * includes all rows whose first key columns exactly match. + * @property {string[]} [endOpen] If the end is open, then the range excludes + * rows whose first key columns exactly match. + */ +interface KeyRange { + startClosed?: Value[]; + startOpen?: Value[]; + endClosed?: Value[]; + endOpen?: Value[]; +} + +/** + * Read request options. This includes all standard ReadRequest options as well + * as several convenience properties. + * + * @see [StreamingRead API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.Spanner.StreamingRead) + * @see [ReadRequest API Documentation](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest) + * + * @typedef ReadRequest + * @property {string[]} [keys] The primary keys of the rows in this table to be + * yielded. If using a composite key, provide an array within this array. + * See the example below. + * @property {KeyRange[]} [ranges] An alternative to the keys property, this can + * be used to define a range of keys to be yielded. + * @property {boolean} [json=false] Receive the rows as serialized objects. This + * is the equivalent of calling `toJSON()` on each row. + * @property {JsonOptions} [jsonOptions] Configuration options for the + * serialized objects. + */ +export interface ReadRequest extends s.ReadRequest, RequestOptions { + keys?: string[]; + ranges?: KeyRange[]; +} + /** * Encodes a ReadRequest into the correct format. * * @private * - * @param {object|string|string[]} query The query + * @param {ReadRequest} query The query * @returns {object} */ -function encodeRead(query) { - if (is.array(query) || is.string(query)) { - query = { - keys: query, - }; - } +export function encodeRead(query: ReadRequest): s.ReadRequest { + query = Object.assign({}, query); - const encoded = extend({}, query); + if (!query.keySet) { + query.keySet = {}; - if (query.keys || query.ranges) { - encoded.keySet = {}; + if (!query.keys && !query.ranges) { + query.keySet.all = true; + } } if (query.keys) { - encoded.keySet.keys = arrify(query.keys).map(key => { - return { - values: arrify(key).map(codec.encode), - }; - }); - delete encoded.keys; + query.keySet.keys = arrify(query.keys).map(convertToListValue); + delete query.keys; } if (query.ranges) { - encoded.keySet.ranges = arrify(query.ranges).map(rawRange => { - const range = extend({}, rawRange); - // tslint:disable-next-line forin - for (const bound in range) { - range[bound] = { - values: arrify(range[bound]).map(codec.encode), - }; - } + query.keySet.ranges = arrify(query.ranges).map(keyRange => { + const range: s.KeyRange = {}; + + Object.keys(keyRange).forEach(bound => { + range[bound] = convertToListValue(keyRange[bound]); + }); return range; }); - delete encoded.ranges; + delete query.ranges; + } + + if (query.json) { + delete query.json; + delete query.jsonOptions; } - return encoded; + if (query.gaxOptions) { + delete query.gaxOptions; + } + + return query; +} + +/** + * Converts a value to google.protobuf.ListValue + * + * @private + * + * @param {*} value The value to convert. + * @returns {object} + */ +function convertToListValue(value: T): p.IListValue { + const values = arrify(value).map(codec.encode); + return {values}; } /** @@ -480,40 +631,32 @@ function encodeRead(query) { * @param {object|string} [config='unspecified'] Type config. * @return {object} */ -function createTypeObject(config) { - config = config || 'unspecified'; - - if (is.string(config)) { - config = {type: config}; +function createTypeObject(friendlyType?: string|Type): s.Type { + if (!friendlyType) { + friendlyType = 'unspecified'; } - const type = config.type; - let code = TYPES.indexOf(type); - - if (code === -1) { - code = 0; // unspecified + if (is.string(friendlyType)) { + friendlyType = {type: friendlyType} as Type; } - // tslint:disable-next-line no-any - const typeObject: any = {code}; + const config: Type = (friendlyType as Type); + const code: s.TypeCode = TypeCode[config.type] || TypeCode.unspecified; + const type: s.Type = {code}; - if (type === 'array') { - typeObject.arrayElementType = createTypeObject(config.child); + if (code === s.TypeCode.ARRAY) { + type.arrayElementType = codec.createTypeObject(config.child); } - if (type === 'struct') { - typeObject.structType = {}; - typeObject.structType.fields = arrify(config.fields).map(field => { - const fieldConfig = is.object(field.type) ? field.type : field; - - return { - name: field.name, - type: createTypeObject(fieldConfig), - }; - }); + if (code === s.TypeCode.STRUCT) { + type.structType = { + fields: arrify(config.fields).map(field => { + return {name: field.name, type: codec.createTypeObject(field)}; + }) + }; } - return typeObject; + return type; } export const codec = { @@ -521,13 +664,11 @@ export const codec = { SpannerDate, Float, Int, - TYPE, - generateToJSONFromRow, + convertFieldsToJson, decode, encode, getType, encodeQuery, - TYPES, encodeRead, Struct }; diff --git a/src/database.ts b/src/database.ts index 9012129d4..7c4ff8d7c 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1332,8 +1332,7 @@ class Database extends ServiceObject { }, }; } - delete reqOpts.json; - delete reqOpts.jsonOptions; + function makeRequest(resumeToken) { return self.makePooledStreamingRequest_({ client: 'SpannerClient', diff --git a/src/row-builder.ts b/src/row-builder.ts index c20a27f81..cfd319239 100644 --- a/src/row-builder.ts +++ b/src/row-builder.ts @@ -113,12 +113,12 @@ class RowBuilder { const field = this.fields[index]; return { name: field.name, - value: RowBuilder.formatValue(field, value), + value: codec.decode(value, field.type), }; }); Object.defineProperty(formattedRow, 'toJSON', { enumerable: false, - value: codec.generateToJSONFromRow(formattedRow), + value: options => codec.convertFieldsToJson(formattedRow, options), }); return formattedRow; }); @@ -140,31 +140,6 @@ class RowBuilder { } return value; } - /** - * Format a value into the expected structure, e.g. turn struct values into an - * object. - * - * @param {object} field Field object - * @param {*} value Field value - * @returns {*} - */ - static formatValue(field, value) { - if (value === 'NULL_VALUE') { - return null; - } - if (field.code === 'ARRAY') { - return value.map(value => { - return RowBuilder.formatValue(field.arrayElementType, value); - }); - } - if (field.code !== 'STRUCT') { - return codec.decode(value, field); - } - return field.structType.fields.reduce((struct, field, index) => { - struct[field.name] = RowBuilder.formatValue(field, value[index]); - return struct; - }, {}); - } /** * Merge chunk values. * diff --git a/src/transaction-request.ts b/src/transaction-request.ts index e0b1bc1e5..dc5649db7 100644 --- a/src/transaction-request.ts +++ b/src/transaction-request.ts @@ -209,18 +209,16 @@ class TransactionRequest { */ createReadStream(table, query) { const reqOpts = codec.encodeRead(query); + const gaxOptions = query.gaxOptions; + reqOpts.table = table; - delete reqOpts.json; - delete reqOpts.jsonOptions; + if (this.transaction && this.id) { reqOpts.transaction = { id: this.id, }; } - const gaxOptions = query.gaxOptions; - if (gaxOptions) { - delete reqOpts.gaxOptions; - } + const makeRequest = resumeToken => { return this.requestStream({ client: 'SpannerClient', diff --git a/src/transaction.ts b/src/transaction.ts index 422e1c554..208970825 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -724,9 +724,6 @@ class Transaction extends TransactionRequest { }, codec.encodeQuery(query)); - delete reqOpts.json; - delete reqOpts.jsonOptions; - if (this.id) { reqOpts.transaction.id = this.id; } else { diff --git a/src/v1/spanner_client.d.ts b/src/v1/spanner_client.d.ts index e2fc13ebc..4d02bfac8 100644 --- a/src/v1/spanner_client.d.ts +++ b/src/v1/spanner_client.d.ts @@ -167,7 +167,7 @@ declare namespace SpannerClient { transaction?: TransactionSelector; sql: string; params?: protobuf.IStruct; - paramTypes: Array<[string, Type]>; + paramTypes?: {[field: string]: Type}; resumeToken: Uint8Array | string; queryMode: QueryMode; partitionToken: Uint8Array | string; @@ -219,12 +219,12 @@ declare namespace SpannerClient { session: string; transaction?: TransactionSelector; table: string; - index: string; - columns: string[]; - keySet?: KeySet; - limit: number; - resumeToken: Uint8Array | string; - partitionToken: Uint8Array | string; + index?: string; + columns?: string[]; + keySet: KeySet; + limit?: number; + resumeToken?: Uint8Array | string; + partitionToken?: Uint8Array | string; } interface ReadCallback { @@ -272,9 +272,9 @@ declare namespace SpannerClient { } interface KeySet { - keys: protobuf.IListValue[]; - ranges: KeyRange[]; - all: boolean; + keys?: protobuf.IListValue[]; + ranges?: KeyRange[]; + all?: boolean; } interface Write { @@ -375,21 +375,21 @@ declare namespace SpannerClient { interface TransactionSelector { singleUse?: TransactionOptions; - id: Uint8Array | string; + id?: Uint8Array | string; begin?: TransactionOptions; } - enum TypeCode { - TYPE_CODE_UNSPECIFIED, - BOOL, - INT64, - FLOAT64, - TIMESTAMP, - DATE, - STRING, - BYTES, - ARRAY, - STRUCT + const enum TypeCode { + TYPE_CODE_UNSPECIFIED = 'TYPE_CODE_UNSPECIFIED', + BOOL = 'BOOL', + INT64 = 'INT64', + FLOAT64 = 'FLOAT64', + TIMESTAMP = 'TIMESTAMP', + DATE = 'DATE', + STRING = 'STRING', + BYTES = 'BYTES', + ARRAY = 'ARRAY', + STRUCT = 'STRUCT' } interface Type { diff --git a/system-test/spanner.ts b/system-test/spanner.ts index 76d046228..2cfe25204 100644 --- a/system-test/spanner.ts +++ b/system-test/spanner.ts @@ -164,10 +164,6 @@ describe('Spanner', () => { database.run(query, (err, rows) => { assert.ifError(err); - const symbols = Object.getOwnPropertySymbols(rows[0][0].value[0]); - assert.strictEqual(symbols.length, 1); - assert.strictEqual(rows[0][0].value[0][symbols[0]], 'struct'); - const expected = [ { name: '', @@ -206,10 +202,6 @@ describe('Spanner', () => { database.run(query, (err, rows) => { assert.ifError(err); - const symbols = Object.getOwnPropertySymbols(rows[0][1].value[0]); - assert.strictEqual(symbols.length, 1); - assert.strictEqual(rows[0][1].value[0][symbols[0]], 'struct'); - const expected = [ { name: 'id', @@ -328,8 +320,9 @@ describe('Spanner', () => { insert({IntValue: value}, (err, row) => { assert.ifError(err); - const intValue = row.toJSON({wrapNumbers: true}).IntValue.value; - assert.strictEqual(intValue, value); + const expected = Spanner.int(value); + const actual = row.toJSON({wrapNumbers: true}).IntValue; + assert.deepStrictEqual(actual, expected); done(); }); }); @@ -355,9 +348,7 @@ describe('Spanner', () => { insert({IntArray: values}, (err, row) => { assert.ifError(err); - - const expected = values.map(Spanner.int); - assert.deepStrictEqual(row.toJSON().IntArray, expected); + assert.deepStrictEqual(row.toJSON().IntArray, values); done(); }); }); @@ -365,7 +356,7 @@ describe('Spanner', () => { describe('float64s', () => { it('should write float64 values', done => { - insert({FloatValue: Spanner.float(8.2)}, (err, row) => { + insert({FloatValue: 8.2}, (err, row) => { assert.ifError(err); assert.deepStrictEqual(row.toJSON().FloatValue, 8.2); done(); @@ -433,9 +424,7 @@ describe('Spanner', () => { insert({FloatArray: values}, (err, row) => { assert.ifError(err); - - const expected = values.map(Spanner.float); - assert.deepStrictEqual(row.toJSON().FloatArray, expected); + assert.deepStrictEqual(row.toJSON().FloatArray, values); done(); }); }); @@ -1352,10 +1341,7 @@ describe('Spanner', () => { EXPECTED_ROW.DOB = DATE; EXPECTED_ROW.Float = FLOAT; EXPECTED_ROW.Int = INT; - EXPECTED_ROW.PhoneNumbers = [ - Spanner.int(PHONE_NUMBERS[0]), - Spanner.int(PHONE_NUMBERS[1]), - ]; + EXPECTED_ROW.PhoneNumbers = PHONE_NUMBERS; before(() => { return table.insert(INSERT_ROW); diff --git a/test/batch-transaction.ts b/test/batch-transaction.ts index 086c51c0b..38fe9bf78 100644 --- a/test/batch-transaction.ts +++ b/test/batch-transaction.ts @@ -123,26 +123,6 @@ describe('BatchTransaction', () => { batchTransaction.createQueryPartitions(QUERY.sql, done); }); - - it('should remove gax options from the query', done => { - const fakeQuery = { - sql: QUERY.sql, - gaxOptions: GAX_OPTS, - }; - - fakeCodec.encodeQuery = (query) => { - assert.strictEqual(query, fakeQuery); - return extend({a: 'b'}, QUERY); - }; - - batchTransaction.createPartitions_ = (config, callback) => { - assert.deepStrictEqual(config.reqOpts, {sql: QUERY.sql, a: 'b'}); - assert.strictEqual(config.gaxOpts, GAX_OPTS); - callback(); // the done fn - }; - - batchTransaction.createQueryPartitions(fakeQuery, done); - }); }); describe('createPartitions_', () => { @@ -253,22 +233,6 @@ describe('BatchTransaction', () => { batchTransaction.createReadPartitions(query, done); }); - - it('should remove gax options from the query', done => { - const query = {gaxOptions: GAX_OPTS}; - - fakeCodec.encodeRead = () => { - return extend({}, QUERY); - }; - - batchTransaction.createPartitions_ = (config, callback) => { - assert.deepStrictEqual(config.reqOpts, {table: QUERY.table}); - assert.strictEqual(config.gaxOpts, GAX_OPTS); - callback(); // the done fn - }; - - batchTransaction.createReadPartitions(query, done); - }); }); describe('execute', () => { diff --git a/test/codec.ts b/test/codec.ts index 5a275c7bb..226e57adc 100644 --- a/test/codec.ts +++ b/test/codec.ts @@ -17,47 +17,30 @@ 'use strict'; import * as assert from 'assert'; -import * as extend from 'extend'; import * as proxyquire from 'proxyquire'; -import {util} from '@google-cloud/common-grpc'; +import * as sinon from 'sinon'; +import {Service} from '@google-cloud/common-grpc'; -class FakeGrpcService { - static encodeValue_ = util.noop; - static decodeValue_ = util.noop; -} +import {SpannerClient as s} from '../src/v1'; describe('codec', () => { - let codecCached; let codec; - const TYPES = [ - 'unspecified', - 'bool', - 'int64', - 'float64', - 'timestamp', - 'date', - 'string', - 'bytes', - 'array', - 'struct', - ]; + const sandbox = sinon.createSandbox(); before(() => { codec = proxyquire('../src/codec.js', { - '@google-cloud/common-grpc': { - Service: FakeGrpcService, - }, + '@google-cloud/common-grpc': {Service}, }).codec; - codecCached = extend({}, codec); }); beforeEach(() => { - extend(codec, codecCached); - FakeGrpcService.encodeValue_ = util.noop; - FakeGrpcService.decodeValue_ = util.noop; + sandbox.stub(Service, 'encodeValue_').callsFake(value => value); + sandbox.stub(Service, 'decodeValue_').callsFake(value => value); }); + afterEach(() => sandbox.restore()); + describe('SpannerDate', () => { it('should choke on multiple arguments', () => { const expectedErrorMessage = [ @@ -126,96 +109,49 @@ describe('codec', () => { }); describe('Struct', () => { - let generateToJSONFromRow_; - - before(() => { - generateToJSONFromRow_ = codec.generateToJSONFromRow; - }); - - afterEach(() => { - codec.generateToJSONFromRow = generateToJSONFromRow_; - }); - - describe('initialization', () => { - it('should create an array', () => { + describe('toJSON', () => { + it('should covert the struct to JSON', () => { const struct = new codec.Struct(); - assert(Array.isArray(struct)); - }); + const options = {}; + const fakeJson = {}; - it('should set the type', () => { - const struct = new codec.Struct(); - const type = struct[codec.TYPE]; + const stub = sandbox.stub(codec, 'convertFieldsToJson') + .withArgs(struct, options) + .returns(fakeJson); - assert.strictEqual(codec.Struct.TYPE, 'struct'); - assert.strictEqual(type, codec.Struct.TYPE); + assert.strictEqual(struct.toJSON(options), fakeJson); }); + }); - it('should create a toJSON property', () => { - const fakeJSON = {}; - let cachedStruct; - - codec.generateToJSONFromRow = (struct) => { - cachedStruct = struct; - return fakeJSON; - }; + describe('fromArray', () => { + it('should wrap the array in a struct', () => { + const fields = [{name: 'name', value: 'value'}]; + const struct = codec.Struct.fromArray(fields); - const struct = new codec.Struct(); + assert(struct instanceof codec.Struct); - assert.strictEqual(struct, cachedStruct); - assert.strictEqual(struct.toJSON, fakeJSON); + fields.forEach((field, i) => { + assert.strictEqual(struct[i], field); + }); }); }); describe('fromJSON', () => { - it('should capture the key value pairs', () => { + it('should covert json to a struct', () => { const json = {a: 'b', c: 'd'}; + const expected = [{name: 'a', value: 'b'}, {name: 'c', value: 'd'}]; const struct = codec.Struct.fromJSON(json); - const expected = new codec.Struct(); - expected.push.apply(expected, [ - {name: 'a', value: 'b'}, - {name: 'c', value: 'd'}, - ]); - assert.deepStrictEqual(struct, expected); - }); - }); + assert(struct instanceof codec.Struct); - describe('fromArray', () => { - it('should convert array to struct array', () => { - const arr = [{name: 'a', value: 1}, {name: 'b', value: 2}]; - const struct = codec.Struct.fromArray(arr); - - const expectedStruct = new codec.Struct(); - expectedStruct.push.apply(expectedStruct, arr); - - assert(codec.Struct.isStruct(struct)); - assert.deepStrictEqual(struct, expectedStruct); - }); - }); - - describe('isStruct', () => { - it('should return true for structs', () => { - const struct = new codec.Struct(); - const isStruct = codec.Struct.isStruct(struct); - - assert.strictEqual(isStruct, true); - }); - - it('should return false for arrays', () => { - const isStruct = codec.Struct.isStruct([]); - - assert.strictEqual(isStruct, false); - }); - - it('should return false for falsey values', () => { - const isStruct = codec.Struct.isStruct(null); - - assert.strictEqual(isStruct, false); + expected.forEach((field, i) => { + assert.deepStrictEqual(struct[i], field); + }); }); }); }); - describe('generateToJSONFromRow', () => { + describe('convertFieldsToJson', () => { const ROW = [ { name: 'name', @@ -223,26 +159,14 @@ describe('codec', () => { }, ]; - let toJSON; - - beforeEach(() => { - toJSON = codec.generateToJSONFromRow(ROW); - }); - - it('should return a function', () => { - assert.strictEqual(typeof toJSON, 'function'); - }); - it('should not require options', () => { - assert.doesNotThrow(() => { - toJSON(); - }); + assert.doesNotThrow(() => codec.convertFieldsToJson(ROW)); }); it('should return serialized rows', () => { - assert.deepStrictEqual(toJSON(), { - name: 'value', - }); + const json = codec.convertFieldsToJson(ROW); + + assert.deepStrictEqual(json, {name: 'value'}); }); it('should not return nameless values', () => { @@ -252,73 +176,142 @@ describe('codec', () => { }, ]; - const toJSON = codec.generateToJSONFromRow(row); - assert.deepStrictEqual(toJSON(), {}); + const json = codec.convertFieldsToJson(row); + assert.deepStrictEqual(json, {}); }); - it('should not wrap numbers by default', () => { - const row = [ - { - name: 'Number', - value: new codec.Int(3), - }, - ]; + describe('structs', () => { + it('should not wrap structs by default', () => { + const options = {wrapNumbers: false, wrapStructs: false}; + const fakeStructJson = {}; + + const struct = new codec.Struct(); + const stub = sandbox.stub(struct, 'toJSON').returns(fakeStructJson); + + const row = [ + {name: 'Struct', value: struct}, + ]; + + const json = codec.convertFieldsToJson(row, options); + + assert.strictEqual(json.Struct, fakeStructJson); + assert.deepStrictEqual(stub.lastCall.args[0], options); + }); + + it('should wrap structs with option', () => { + const value = 3.3; - const toJSON = codec.generateToJSONFromRow(row); - assert.strictEqual(typeof toJSON().Number, 'number'); - assert.strictEqual(toJSON().Number, 3); + const expectedStruct = codec.Struct.fromJSON({Number: value}); + const struct = codec.Struct.fromJSON({Number: new codec.Float(value)}); + + const row = [ + {name: 'Struct', value: struct}, + ]; + + const json = codec.convertFieldsToJson(row, {wrapStructs: true}); + assert.deepStrictEqual(json.Struct, expectedStruct); + }); }); - it('should wrap numbers with option', () => { - const int = new codec.Int(3); + describe('numbers', () => { + it('should not wrap numbers by default', () => { + const row = [ + { + name: 'Number', + value: new codec.Int(3), + }, + ]; - const row = [ - { - name: 'Number', - value: int, - }, - ]; + const json = codec.convertFieldsToJson(row); + assert.strictEqual(typeof json.Number, 'number'); + assert.strictEqual(json.Number, 3); + }); + + it('should wrap numbers with option', () => { + const int = new codec.Int(3); + + const row = [ + { + name: 'Number', + value: int, + }, + ]; + + const json = codec.convertFieldsToJson(row, {wrapNumbers: true}); - const toJSON = codec.generateToJSONFromRow(row); - const value = toJSON({wrapNumbers: true}).Number; + assert(json.Number instanceof codec.Int); + assert.deepStrictEqual(json.Number, int); + }); - assert(value instanceof codec.Int); - assert.deepStrictEqual(value, int); + it('should throw an error if number is out of bounds', () => { + const int = new codec.Int('9223372036854775807'); + + const row = [ + { + name: 'Number', + value: int, + }, + ]; + + assert.throws(() => { + codec.convertFieldsToJson(row); + }, new RegExp('Serializing column "Number" encountered an error')); + }); }); - it('should throw an error if number is out of bounds', () => { - const int = new codec.Int('9223372036854775807'); + describe('arrays', () => { + it('should not wrap numbers by default', () => { + const value = 3; - const row = [ - { - name: 'Number', - value: int, - }, - ]; + const row = [ + { + name: 'List', + value: [new codec.Int(value)], + }, + ]; - const toJSON = codec.generateToJSONFromRow(row); + const json = codec.convertFieldsToJson(row); + assert.deepStrictEqual(json.List, [value]); + }); - assert.throws(() => { - toJSON(); - }, new RegExp('Serializing column "Number" encountered an error')); + it('should wrap numbers with option', () => { + const value = new codec.Int(3); + + const row = [{name: 'List', value: [value]}]; + + const json = codec.convertFieldsToJson(row, {wrapNumbers: true}); + assert.deepStrictEqual(json.List, [value]); + }); + + it('should not wrap structs by default', () => { + const struct = new codec.Struct(); + const expectedStruct = {a: 'b', c: 'd'}; + + sandbox.stub(struct, 'toJSON').returns(expectedStruct); + + const row = [{name: 'List', value: [struct]}]; + + const json = codec.convertFieldsToJson(row); + assert.deepStrictEqual(json.List, [expectedStruct]); + }); + + it('should wrap structs with option', () => { + const expectedStruct = codec.Struct.fromJSON({a: 'b', c: 'd'}); + + const row = [{name: 'List', value: [expectedStruct]}]; + + const json = codec.convertFieldsToJson(row, {wrapStructs: true}); + assert.deepStrictEqual(json.List, [expectedStruct]); + }); }); }); describe('decode', () => { // Does not require any special decoding. const BYPASS_FIELD = { - type: { - code: 'not-real-code', - }, + code: 'not-real-code', }; - beforeEach(() => { - // tslint:disable-next-line no-any - (FakeGrpcService as any).decodeValue_ = (value) => { - return value; - }; - }); - it('should return the same value if not a special type', () => { const value = {}; @@ -327,31 +320,27 @@ describe('codec', () => { }); it('should return null values as null', () => { - FakeGrpcService.decodeValue_ = () => null; + (Service.decodeValue_ as sinon.SinonStub).returns(null); const decoded = codec.decode(null, BYPASS_FIELD); assert.strictEqual(decoded, null); }); it('should decode BYTES', () => { - const value = Buffer.from('bytes value'); + const expected = Buffer.from('bytes value'); + const encoded = expected.toString('base64'); - const decoded = codec.decode(value.toString('base64'), { - type: { - code: 'BYTES', - }, + const decoded = codec.decode(encoded, { + code: s.TypeCode.BYTES, }); - // tslint:disable-next-line: no-any - assert.deepStrictEqual(decoded, Buffer.from(value as any, 'base64')); + assert.deepStrictEqual(decoded, expected); }); it('should decode FLOAT64', () => { const value = 'Infinity'; const decoded = codec.decode(value, { - type: { - code: 'FLOAT64', - }, + code: s.TypeCode.FLOAT64, }); assert(decoded instanceof codec.Float); @@ -362,9 +351,7 @@ describe('codec', () => { const value = '64'; const decoded = codec.decode(value, { - type: { - code: 'INT64', - }, + code: s.TypeCode.INT64, }); assert(decoded instanceof codec.Int); @@ -375,9 +362,7 @@ describe('codec', () => { const value = new Date(); const decoded = codec.decode(value.toJSON(), { - type: { - code: 'TIMESTAMP', - }, + code: s.TypeCode.TIMESTAMP, }); assert.deepStrictEqual(decoded, value); @@ -387,9 +372,7 @@ describe('codec', () => { const value = new Date(); const decoded = codec.decode(value.toJSON(), { - type: { - code: 'DATE', - }, + code: s.TypeCode.DATE, }); assert.deepStrictEqual(decoded, value); @@ -399,11 +382,9 @@ describe('codec', () => { const value = ['1']; const decoded = codec.decode(value, { - type: { - code: 'ARRAY', - arrayElementType: { - code: 'INT64', - }, + code: s.TypeCode.ARRAY, + arrayElementType: { + code: s.TypeCode.INT64, }, }); @@ -415,58 +396,38 @@ describe('codec', () => { fieldName: '1', }; - const int = {int: true}; - codec.Int = class { - constructor(value_) { - assert.strictEqual(value_, value.fieldName); - return int; - } - }; - const decoded = codec.decode(value, { - type: { - code: 'STRUCT', - structType: { - fields: [ - { - name: 'fieldName', - type: { - code: 'INT64', - }, + code: s.TypeCode.STRUCT, + structType: { + fields: [ + { + name: 'fieldName', + type: { + code: s.TypeCode.INT64, }, - ], - }, + }, + ], }, }); - assert(codec.Struct.isStruct(decoded)); - const expectedStruct = new codec.Struct(); - expectedStruct.push.apply(expectedStruct, [ - { - name: 'fieldName', - value: int, - }, - ]); + const expectedStruct = new codec.Struct({ + name: 'fieldName', + value: new codec.Int(value.fieldName), + }); + assert(decoded instanceof codec.Struct); assert.deepStrictEqual(decoded, expectedStruct); }); }); describe('encode', () => { - beforeEach(() => { - // tslint:disable-next-line no-any - (FakeGrpcService as any).encodeValue_ = value => value; - }); - it('should return the value from the common encoder', () => { const value = {}; const defaultEncodedValue = {}; - // tslint:disable-next-line no-any - (FakeGrpcService as any).encodeValue_ = (value_) => { - assert.strictEqual(value_, value); - return defaultEncodedValue; - }; + (Service.encodeValue_ as sinon.SinonStub) + .withArgs(value) + .returns(defaultEncodedValue); const encoded = codec.encode(value); assert.strictEqual(encoded, defaultEncodedValue); @@ -483,7 +444,7 @@ describe('codec', () => { it('should encode structs', () => { const value = codec.Struct.fromJSON({a: 'b', c: 'd'}); const encoded = codec.encode(value); - assert.deepStrictEqual([].concat(encoded.slice()), ['b', 'd']); + assert.deepStrictEqual(encoded, ['b', 'd']); }); it('should stringify Infinity', () => { @@ -557,154 +518,105 @@ describe('codec', () => { assert.strictEqual(encoded, 10); }); - - it('should encode each key in a dictionary-like object', () => { - const obj = { - f: new codec.Float(10), - i: new codec.Int(10), - }; - const encoded = codec.encode(obj); - assert.deepStrictEqual(encoded, {f: 10, i: '10'}); - }); - - it('should only encode public properties of objects', () => { - const obj = { - hasOwnProperty(key) { - // jshint ignore:line - return key === 'public'; - }, - _private: new codec.Int(10), - public: new codec.Int(10), - }; - const encoded = codec.encode(obj); - assert.deepStrictEqual(encoded._private, obj._private); - assert.deepStrictEqual(encoded.public, '10'); - }); }); describe('getType', () => { it('should determine if the value is a boolean', () => { - assert.strictEqual(codec.getType(true), 'bool'); + assert.deepStrictEqual(codec.getType(true), {type: 'bool'}); }); it('should determine if the value is a float', () => { - assert.strictEqual(codec.getType(NaN), 'float64'); - assert.strictEqual(codec.getType(Infinity), 'float64'); - assert.strictEqual(codec.getType(-Infinity), 'float64'); - assert.strictEqual(codec.getType(2.2), 'float64'); - assert.strictEqual(codec.getType(new codec.Float(1.1)), 'float64'); + assert.deepStrictEqual(codec.getType(NaN), {type: 'float64'}); + assert.deepStrictEqual(codec.getType(Infinity), {type: 'float64'}); + assert.deepStrictEqual(codec.getType(-Infinity), {type: 'float64'}); + assert.deepStrictEqual(codec.getType(2.2), {type: 'float64'}); + assert.deepStrictEqual( + codec.getType(new codec.Float(1.1)), {type: 'float64'}); }); it('should determine if the value is an int', () => { - assert.strictEqual(codec.getType(1234), 'int64'); - assert.strictEqual(codec.getType(new codec.Int(1)), 'int64'); + assert.deepStrictEqual(codec.getType(1234), {type: 'int64'}); + assert.deepStrictEqual(codec.getType(new codec.Int(1)), {type: 'int64'}); }); it('should determine if the value is a string', () => { - assert.strictEqual(codec.getType('abc'), 'string'); + assert.deepStrictEqual(codec.getType('abc'), {type: 'string'}); }); it('should determine if the value is bytes', () => { - assert.strictEqual(codec.getType(Buffer.from('abc')), 'bytes'); + assert.deepStrictEqual( + codec.getType(Buffer.from('abc')), {type: 'bytes'}); }); it('should determine if the value is a timestamp', () => { - assert.strictEqual(codec.getType(new Date()), 'timestamp'); + assert.deepStrictEqual(codec.getType(new Date()), {type: 'timestamp'}); }); it('should determine if the value is a date', () => { - assert.strictEqual(codec.getType(new codec.SpannerDate()), 'date'); + assert.deepStrictEqual( + codec.getType(new codec.SpannerDate()), {type: 'date'}); }); it('should determine if the value is a struct', () => { const struct = codec.Struct.fromJSON({a: 'b'}); const type = codec.getType(struct); - assert.strictEqual(type.type, 'struct'); - assert.deepStrictEqual([].concat(type.fields.slice()), [ - { - name: 'a', - type: 'string', - }, - ]); + + assert.deepStrictEqual(type, { + type: 'struct', + fields: [ + {name: 'a', type: 'string'}, + ] + }); }); it('should attempt to determine arrays and their values', () => { assert.deepStrictEqual(codec.getType([Infinity]), { type: 'array', - child: 'float64', + child: { + type: 'float64', + } }); }); it('should return unspecified for unknown values', () => { - assert.strictEqual(codec.getType(null), 'unspecified'); + assert.deepStrictEqual(codec.getType(null), {type: 'unspecified'}); assert.deepStrictEqual(codec.getType([null]), { type: 'array', - child: 'unspecified', + child: { + type: 'unspecified', + } }); }); }); - describe('TYPES', () => { - it('should export types', () => { - assert.deepStrictEqual(codec.TYPES, TYPES); - }); - }); - describe('encodeQuery', () => { - let createTypeObject_; - + const SQL = 'SELECT * FROM table'; const QUERY = { - sql: 'SELECT * FROM table', - a: 'b', - c: 'd', + sql: SQL, }; - before(() => { - createTypeObject_ = codec.createTypeObject; - }); - - afterEach(() => { - codec.createTypeObject = createTypeObject_; - }); - it('should return the query', () => { - const fakeQuery = { - a: 'b', - c: 'd', - }; - - const encodedQuery = codec.encodeQuery(fakeQuery); + const encodedQuery = codec.encodeQuery(QUERY); - assert.deepStrictEqual(fakeQuery, encodedQuery); + assert.deepStrictEqual(QUERY, encodedQuery); }); it('should clone the query', () => { - const fakeQuery = { - a: 'b', - }; - - const encodedQuery = codec.encodeQuery(fakeQuery); - assert.notStrictEqual(fakeQuery, encodedQuery); + const encodedQuery = codec.encodeQuery(QUERY); + assert.notStrictEqual(QUERY, encodedQuery); - delete encodedQuery.a; - assert.strictEqual(fakeQuery.a, 'b'); + delete encodedQuery.sql; + assert.strictEqual(QUERY.sql, SQL); }); it('should encode query parameters', () => { - const fakeQuery = { - sql: QUERY, - params: { - test: 'value', - }, - }; - const encodedValue = {}; + const fakeQuery = Object.assign({}, QUERY, {params: {test: 'value'}}); - codec.encode = (field) => { - assert.strictEqual(field, fakeQuery.params.test); - return encodedValue; - }; + sandbox.stub(codec, 'encode') + .withArgs(fakeQuery.params.test) + .returns(encodedValue); const encodedQuery = codec.encodeQuery(fakeQuery); assert.strictEqual(encodedQuery.params.fields.test, encodedValue); @@ -722,86 +634,53 @@ describe('codec', () => { bytes: Buffer.from('abc'), }; - const types = Object.keys(params); - - const fakeQuery = { - sql: QUERY, - params, - }; - - let getTypeCallCount = 0; - - codec.getType = (field) => { - const type = types[getTypeCallCount++]; - - assert.strictEqual(params[type], field); - return type; - }; - + const fakeQuery = Object.assign({}, QUERY, {params}); const encodedQuery = codec.encodeQuery(fakeQuery); assert.deepStrictEqual(encodedQuery.paramTypes, { unspecified: { - code: 0, + code: s.TypeCode.TYPE_CODE_UNSPECIFIED, }, bool: { - code: 1, + code: s.TypeCode.BOOL, }, int64: { - code: 2, + code: s.TypeCode.INT64, }, float64: { - code: 3, + code: s.TypeCode.FLOAT64, }, timestamp: { - code: 4, + code: s.TypeCode.TIMESTAMP, }, date: { - code: 5, + code: s.TypeCode.DATE, }, string: { - code: 6, + code: s.TypeCode.STRING, }, bytes: { - code: 7, + code: s.TypeCode.BYTES, }, }); }); it('should not overwrite existing type definitions', () => { - const fakeQuery = { + const fakeQuery = Object.assign({}, QUERY, { params: { test: 123, }, types: { test: 'string', }, - }; - - codec.getType = () => { - throw new Error('Should not be called'); - }; - - codec.encodeQuery(fakeQuery); - }); - - it('should create type objects', () => { - const fakeQuery = { - types: { - test: 'string', - }, - }; - - const fakeTypeObject = {}; + }); - codec.createTypeObject = (type) => { - assert.strictEqual(type, 'string'); - return fakeTypeObject; - }; + sandbox.stub(codec, 'getType').throws(); const query = codec.encodeQuery(fakeQuery); - assert.deepStrictEqual(query.paramTypes, {test: fakeTypeObject}); + assert.deepStrictEqual( + query.paramTypes, {test: {code: s.TypeCode.STRING}}); }); it('should delete the type map from the request options', () => { @@ -820,41 +699,34 @@ describe('codec', () => { }); describe('encodeRead', () => { + it('should return all keys if ranges/keys are absent', () => { + const encoded = codec.encodeRead({}); + + assert.deepStrictEqual(encoded, {keySet: {all: true}}); + }); + describe('query.keys', () => { it('should encode and map input to keySet', () => { + const keyMap = {key: {}, composite: {}, key2: {}}; + const query = { - keys: ['key', ['composite', 'key']], + keys: [ + 'key', ['composite', 'key2'], // composite key + ], }; - const encodedValue = {}; - let numEncodeRequests = 0; - - codec.encode = (key) => { - numEncodeRequests++; - - switch (numEncodeRequests) { - case 1: - assert.strictEqual(key, query.keys[0]); - break; - case 2: - assert.strictEqual(key, query.keys[1][0]); - break; - case 3: - assert.strictEqual(key, query.keys[1][1]); - break; - default: - break; - } + const stub = sandbox.stub(codec, 'encode'); - return encodedValue; - }; + Object.keys(keyMap).forEach(key => { + stub.withArgs(key).returns(keyMap[key]); + }); const expectedKeys = [ { - values: [encodedValue], + values: [keyMap.key], }, { - values: [encodedValue, encodedValue], + values: [keyMap.composite, keyMap.key2], }, ]; @@ -862,47 +734,15 @@ describe('codec', () => { assert.deepStrictEqual(encoded.keySet.keys, expectedKeys); }); - it('should accept just a key', () => { - const query = 'key'; - - const encodedValue = {}; - codec.encode = (key) => { - assert.strictEqual(key, query); - return encodedValue; - }; - - const encoded = codec.encodeRead(query); - - assert.strictEqual(encoded.keySet.keys[0].values[0], encodedValue); - }); - - it('should accept just an array of keys', () => { - const query = ['key']; - - const encodedValue = {}; - codec.encode = (key) => { - assert.strictEqual(key, query[0]); - return encodedValue; - }; - - const encoded = codec.encodeRead(query); - - assert.strictEqual(encoded.keySet.keys[0].values[0], encodedValue); - }); - it('should arrify query.keys', () => { - const query = { - keys: 'key', - }; - + const query = {keys: 'key'}; const encodedValue = {}; - codec.encode = (key) => { - assert.strictEqual(key, query.keys); - return encodedValue; - }; - const encoded = codec.encodeRead(query); + sandbox.stub(codec, 'encode') + .withArgs(query.keys) + .returns(encodedValue); + const encoded = codec.encodeRead(query); assert.strictEqual(encoded.keySet.keys[0].values[0], encodedValue); }); @@ -912,50 +752,47 @@ describe('codec', () => { }; const encoded = codec.encodeRead(query); - assert.strictEqual(encoded.keys, undefined); }); }); describe('query.ranges', () => { it('should encode/map the inputs', () => { + const keyMap = {key: {}, composite: {}, key2: {}}; + const query = { ranges: [ { startOpen: 'key', - endClosed: ['composite', 'key'], + endClosed: ['composite', 'key2'], }, ], }; - const encodedValue = {}; - let numEncodeRequests = 0; - - codec.encode = (key) => { - const keys = ['key', 'composite', 'key']; + const stub = sandbox.stub(codec, 'encode'); - assert.strictEqual(key, keys[numEncodeRequests++]); - return encodedValue; - }; + Object.keys(keyMap).forEach(key => { + stub.withArgs(key).returns(keyMap[key]); + }); const expectedRanges = [ { startOpen: { - values: [encodedValue], + values: [keyMap.key], }, endClosed: { - values: [encodedValue, encodedValue], + values: [keyMap.composite, keyMap.key2], }, }, ]; const encoded = codec.encodeRead(query); - - assert.strictEqual(numEncodeRequests, 3); assert.deepStrictEqual(encoded.keySet.ranges, expectedRanges); }); it('should arrify query.ranges', () => { + const keyMap = {start: {}, end: {}}; + const query = { ranges: [ { @@ -965,28 +802,24 @@ describe('codec', () => { ], }; - const encodedValue = {}; - let numEncodeRequests = 0; + const stub = sandbox.stub(codec, 'encode'); - codec.encode = (key) => { - assert.strictEqual(key, ['start', 'end'][numEncodeRequests++]); - return encodedValue; - }; + Object.keys(keyMap).forEach(key => { + stub.withArgs(key).returns(keyMap[key]); + }); const expectedRanges = [ { startOpen: { - values: [encodedValue], + values: [keyMap.start], }, endClosed: { - values: [encodedValue], + values: [keyMap.end], }, }, ]; const encoded = codec.encodeRead(query); - - assert.strictEqual(numEncodeRequests, 2); assert.deepStrictEqual(encoded.keySet.ranges, expectedRanges); }); @@ -1008,18 +841,56 @@ describe('codec', () => { }); describe('createTypeObject', () => { - it('should convert the type to its int value', () => { - TYPES.forEach((typeName, i) => { - const type = codec.createTypeObject(typeName); + it('should convert strings to the corresponding type', () => { + const typeMap = { + unspecified: { + code: s.TypeCode.TYPE_CODE_UNSPECIFIED, + }, + bool: { + code: s.TypeCode.BOOL, + }, + int64: { + code: s.TypeCode.INT64, + }, + float64: { + code: s.TypeCode.FLOAT64, + }, + timestamp: { + code: s.TypeCode.TIMESTAMP, + }, + date: { + code: s.TypeCode.DATE, + }, + string: { + code: s.TypeCode.STRING, + }, + bytes: { + code: s.TypeCode.BYTES, + }, + array: { + code: s.TypeCode.ARRAY, + arrayElementType: { + code: s.TypeCode.TYPE_CODE_UNSPECIFIED, + } + }, + struct: { + code: s.TypeCode.STRUCT, + structType: {fields: []}, + } + }; - assert.deepStrictEqual(type.code, i); + Object.keys(typeMap).forEach(key => { + const type = codec.createTypeObject(key); + assert.deepStrictEqual(type, typeMap[key]); }); }); it('should default to unspecified for unknown types', () => { const type = codec.createTypeObject('unicorn'); - assert.deepStrictEqual(type, {code: TYPES.indexOf('unspecified')}); + assert.deepStrictEqual(type, { + code: s.TypeCode.TYPE_CODE_UNSPECIFIED, + }); }); it('should set the arrayElementType', () => { @@ -1029,9 +900,9 @@ describe('codec', () => { }); assert.deepStrictEqual(type, { - code: TYPES.indexOf('array'), + code: s.TypeCode.ARRAY, arrayElementType: { - code: TYPES.indexOf('bool'), + code: s.TypeCode.BOOL, }, }); }); @@ -1046,19 +917,19 @@ describe('codec', () => { }); assert.deepStrictEqual(type, { - code: TYPES.indexOf('struct'), + code: s.TypeCode.STRUCT, structType: { fields: [ { name: 'boolKey', type: { - code: TYPES.indexOf('bool'), + code: s.TypeCode.BOOL, }, }, { name: 'intKey', type: { - code: TYPES.indexOf('int64'), + code: s.TypeCode.INT64, }, }, ], @@ -1072,32 +943,32 @@ describe('codec', () => { fields: [ { name: 'nestedStruct', - type: { - type: 'struct', - fields: [ - { - type: 'bool', - name: 'boolKey', - }, - ], - }, + type: 'struct', + fields: [ + { + type: 'bool', + name: 'boolKey', + }, + ], }, ], }); assert.deepStrictEqual(type, { - code: TYPES.indexOf('struct'), + code: s.TypeCode.STRUCT, structType: { fields: [ { name: 'nestedStruct', type: { - code: TYPES.indexOf('struct'), + code: s.TypeCode.STRUCT, structType: { fields: [ { name: 'boolKey', - type: {code: TYPES.indexOf('bool')}, + type: { + code: s.TypeCode.BOOL, + }, }, ], }, diff --git a/test/row-builder.ts b/test/row-builder.ts index 6b3952871..e6d748d7c 100644 --- a/test/row-builder.ts +++ b/test/row-builder.ts @@ -18,21 +18,11 @@ import {util} from '@google-cloud/common-grpc'; import * as assert from 'assert'; import * as extend from 'extend'; import * as proxyquire from 'proxyquire'; +import * as sinon from 'sinon'; import {codec} from '../src/codec'; import * as rb from '../src/row-builder'; - -let decodeOverride; -let generateToJSONFromRowOverride; -const fakeCodec = { - decode() { - return (decodeOverride || codec.decode).apply(null, arguments); - }, - generateToJSONFromRow() { - return (generateToJSONFromRowOverride || codec.generateToJSONFromRow) - .apply(null, arguments); - }, -}; +import {SpannerClient as s} from '../src/v1'; class FakeGrpcService { static decodeValue_(value) {} @@ -45,6 +35,7 @@ describe('RowBuilder', () => { let RowBuilderCached: typeof rb.RowBuilder; let rowBuilder: rb.RowBuilder; + const sandbox = sinon.createSandbox(); const FIELDS = [{}, {}]; before(() => { @@ -52,19 +43,19 @@ describe('RowBuilder', () => { '@google-cloud/common-grpc': { Service: FakeGrpcService, }, - './codec.js': {codec: fakeCodec}, + './codec.js': {codec}, }).RowBuilder; RowBuilderCached = extend({}, RowBuilder); }); beforeEach(() => { FakeGrpcService.decodeValue_ = util.noop; - decodeOverride = null; - generateToJSONFromRowOverride = null; extend(RowBuilder, RowBuilderCached); rowBuilder = new RowBuilder(FIELDS); }); + afterEach(() => sandbox.restore()); + describe('acceptance tests', () => { const TESTS = require('../../test/data/streaming-read-acceptance-test.json').tests; @@ -169,85 +160,6 @@ describe('RowBuilder', () => { }); }); - describe('formatValue', () => { - it('should iterate an array', () => { - const field = { - code: 'ARRAY', - arrayElementType: 'type', - }; - - const value = [{}]; - const decodedValue = {}; - - decodeOverride = (value_, field_) => { - assert.strictEqual(value_, value[0]); - assert.strictEqual(field_, field.arrayElementType); - return decodedValue; - }; - - const formattedValue = RowBuilder.formatValue(field, value); - assert.deepStrictEqual(formattedValue, [decodedValue]); - }); - - it('should return null if value is NULL_VALUE', () => { - const field = { - code: 'ARRAY', - arrayElementType: 'type', - }; - - const value = 'NULL_VALUE'; - - const formattedValue = RowBuilder.formatValue(field, value); - assert.strictEqual(formattedValue, null); - }); - - it('should return decoded value if not an array or struct', () => { - const field = { - code: 'NOT_STRUCT_OR_ARRAY', - }; - - const value = [{}]; - const decodedValue = {}; - - decodeOverride = (value_, field_) => { - assert.strictEqual(value_, value); - assert.strictEqual(field_, field); - return decodedValue; - }; - - const formattedValue = RowBuilder.formatValue(field, value); - assert.strictEqual(formattedValue, decodedValue); - }); - - it('should iterate a struct', () => { - const field = { - code: 'STRUCT', - structType: { - fields: [ - { - name: 'fieldName', - type: 'NOT_STRUCT_OR_ARRAY', // so it returns original value - }, - ], - }, - }; - - const value = [{}]; - const decodedValue = {}; - - decodeOverride = (value_, field_) => { - assert.strictEqual(value_, value[0]); - assert.strictEqual(field_, field.structType.fields[0]); - return decodedValue; - }; - - const formattedValue = RowBuilder.formatValue(field, value); - assert.deepStrictEqual(formattedValue, { - fieldName: decodedValue, - }); - }); - }); - describe('merge', () => { it('should merge arrays', () => { const type = { @@ -523,15 +435,14 @@ describe('RowBuilder', () => { }); it('should format the values', () => { + const value = ROWS[0][0]; + const type = rowBuilder.fields[0].type; + const formattedValue = { formatted: true, }; - RowBuilder.formatValue = (field, value) => { - assert.strictEqual(field, rowBuilder.fields[0]); - assert.strictEqual(value, ROWS[0][0]); - return formattedValue; - }; + sinon.stub(codec, 'decode').withArgs(value, type).returns(formattedValue); const rows = rowBuilder.toJSON(ROWS); const row = rows[0]; @@ -548,28 +459,23 @@ describe('RowBuilder', () => { }); }); - describe('toJSON', () => { - const toJSONOverride = () => {}; + describe('Row#toJSON', () => { let FORMATTED_ROW; beforeEach(() => { - generateToJSONFromRowOverride = () => { - return toJSONOverride; - }; - - const formattedValue = {}; - - RowBuilder.formatValue = () => { - return formattedValue; - }; - - rowBuilder.rows = [[{}]]; - FORMATTED_ROW = rowBuilder.toJSON(ROWS)[0]; }); it('should assign a toJSON method', () => { - assert.strictEqual(FORMATTED_ROW.toJSON, toJSONOverride); + const fakeJson = {}; + const fakeOptions = {wrapNumbers: false}; + + sandbox.stub(codec, 'convertFieldsToJson') + .withArgs(FORMATTED_ROW, fakeOptions) + .returns(fakeJson); + + const json = FORMATTED_ROW.toJSON(fakeOptions); + assert.strictEqual(json, fakeJson); }); it('should not include toJSON when iterated', () => { diff --git a/test/transaction-request.ts b/test/transaction-request.ts index 66d13dffa..84914824f 100644 --- a/test/transaction-request.ts +++ b/test/transaction-request.ts @@ -19,6 +19,7 @@ import * as pfy from '@google-cloud/promisify'; import * as assert from 'assert'; import * as extend from 'extend'; import * as proxyquire from 'proxyquire'; +import * as sinon from 'sinon'; import {split} from 'split-array-stream'; import * as through from 'through2'; @@ -48,23 +49,20 @@ const fakePfy = extend({}, pfy, { }, }); -const fakeCodec: typeof codec = { - encode: util.noop - // tslint:disable-next-line no-any -} as any; - describe('TransactionRequest', () => { // tslint:disable-next-line variable-name let TransactionRequest: typeof tr.TransactionRequest; let transactionRequest: tr.TransactionRequest; + const sandbox = sinon.createSandbox(); + before(() => { TransactionRequest = proxyquire('../src/transaction-request', { '@google-cloud/common-grpc': { Service: FakeGrpcService, }, '@google-cloud/promisify': fakePfy, - './codec.js': {codec: fakeCodec}, + './codec.js': {codec}, './partial-result-stream': {partialResultStream: fakePartialResultStream}, }).TransactionRequest; @@ -72,12 +70,13 @@ describe('TransactionRequest', () => { beforeEach(() => { FakeGrpcService.encodeValue_ = util.noop; - fakeCodec.encode = util.noop; transactionRequest = new TransactionRequest(); transactionRequest.request = util.noop; transactionRequest.requestStream = util.noop; }); + afterEach(() => sandbox.restore()); + describe('instantiation', () => { let formatTimestamp; @@ -214,12 +213,12 @@ describe('TransactionRequest', () => { describe('createReadStream', () => { const TABLE = 'table-name'; - const QUERY = {e: 'f'}; + const QUERY = {session: 'a', table: 'b', keySet: {all: true}}; + + let stub: sinon.SinonStub; beforeEach(() => { - fakeCodec.encodeRead = () => { - return QUERY; - }; + stub = sandbox.stub(codec, 'encodeRead').returns(QUERY); }); it('should accept a query object', done => { @@ -232,12 +231,9 @@ describe('TransactionRequest', () => { table: TABLE, }); - fakeCodec.encodeRead = (readRequest) => { - assert.strictEqual(readRequest, query); - return QUERY; - }; - transactionRequest.requestStream = (options) => { + const [readRequest] = stub.lastCall.args; + assert.strictEqual(readRequest, query); assert.deepStrictEqual(options.reqOpts, expectedReqOpts); done(); }; @@ -366,9 +362,7 @@ describe('TransactionRequest', () => { const TABLE = 'table-name'; const KEYS = ['key', ['composite', 'key']]; - const ENCODED_VALUE = { - encoded: true, - }; + const ENCODED_VALUE = {kind: 'stringValue', stringValue: 'value'}; const EXPECTED_MUTATION = { delete: { @@ -386,12 +380,11 @@ describe('TransactionRequest', () => { }, }; + let stub: sinon.SinonStub; + beforeEach(() => { transactionRequest.transaction = true; - - fakeCodec.encode = () => { - return ENCODED_VALUE; - }; + stub = sandbox.stub(codec, 'encode').returns(ENCODED_VALUE); }); describe('non-transactional instance', () => { @@ -460,29 +453,14 @@ describe('TransactionRequest', () => { }); it('should correctly make and return the request', done => { - let numEncodeRequests = 0; - - fakeCodec.encode = (key) => { - numEncodeRequests++; - - switch (numEncodeRequests) { - case 1: - assert.strictEqual(key, KEYS[0]); - break; - case 2: - assert.strictEqual(key, KEYS[1][0]); - break; - case 3: - assert.strictEqual(key, KEYS[1][1]); - break; - default: - break; - } - - return ENCODED_VALUE; - }; + const expectedKeys = [KEYS[0], KEYS[1][0], KEYS[1][1]]; transactionRequest.queue_ = (mutation) => { + expectedKeys.forEach((expectedKey, i) => { + const [key] = stub.getCall(i).args; + assert.strictEqual(key, expectedKey); + }); + assert.deepStrictEqual(mutation, EXPECTED_MUTATION); done(); }; @@ -500,13 +478,6 @@ describe('TransactionRequest', () => { }); it('should accept just a key', done => { - const encodedValue = { - encoded: true, - }; - fakeCodec.encode = () => { - return encodedValue; - }; - transactionRequest.queue_ = (mutation) => { const expectedSingleMutation = extend(true, {}, EXPECTED_MUTATION); @@ -698,12 +669,11 @@ describe('TransactionRequest', () => { ], }; + let stub: sinon.SinonStub; + beforeEach(() => { transactionRequest.transaction = true; - - fakeCodec.encode = (value) => { - return value; - }; + stub = sandbox.stub(codec, 'encode').callsFake(value => value); }); describe('non-transactional instance', () => { @@ -773,44 +743,17 @@ describe('TransactionRequest', () => { }); it('should correctly make and return the request', done => { - let numEncodeRequests = 0; - - fakeCodec.encode = (value) => { - numEncodeRequests++; - - switch (numEncodeRequests) { - case 1: - assert.strictEqual(value, KEYVALS[0].anotherNullable); - break; - case 2: - assert.strictEqual(value, KEYVALS[0].key); - break; - case 3: - assert.strictEqual(value, KEYVALS[0].nonNullable); - break; - case 4: - assert.strictEqual(value, KEYVALS[0].nullable); - break; - case 5: - assert.strictEqual(value, KEYVALS[1].anotherNullable); - break; - case 6: - assert.strictEqual(value, KEYVALS[1].key); - break; - case 7: - assert.strictEqual(value, KEYVALS[1].nonNullable); - break; - case 8: - assert.strictEqual(value, KEYVALS[1].nullable); - break; - default: - break; - } - - return value; - }; + const expectedValues = [ + KEYVALS[0].anotherNullable, KEYVALS[0].key, KEYVALS[0].nonNullable, + KEYVALS[0].nullable, KEYVALS[1].anotherNullable, KEYVALS[1].key, + KEYVALS[1].nonNullable, KEYVALS[1].nullable + ]; transactionRequest.queue_ = mutation => { + expectedValues.forEach((expectedValue, i) => { + const [value] = stub.getCall(i).args; + assert.strictEqual(value, expectedValue); + }); assert.deepStrictEqual(mutation, EXPECTED_MUTATION); done(); };