diff --git a/package.json b/package.json index 2b31aa187..227d2392c 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "dependencies": { "@google-cloud/common-grpc": "^0.10.0", "@google-cloud/paginator": "^0.1.0", + "@google-cloud/precise-date": "^0.1.0", "@google-cloud/projectify": "^0.3.0", "@google-cloud/promisify": "^0.4.0", "arrify": "^1.0.1", diff --git a/src/batch-transaction.ts b/src/batch-transaction.ts index 597c06d8c..d9e542b17 100644 --- a/src/batch-transaction.ts +++ b/src/batch-transaction.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {PreciseDate} from '@google-cloud/precise-date'; import {promisifyAll} from '@google-cloud/promisify'; import * as extend from 'extend'; import * as is from 'is'; @@ -161,7 +162,7 @@ class BatchTransaction extends Snapshot { if (readTimestamp) { this.readTimestampProto = readTimestamp; - this.readTimestamp = codec.convertProtoTimestampToDate(readTimestamp); + this.readTimestamp = new PreciseDate(readTimestamp); } } diff --git a/src/codec.ts b/src/codec.ts index ab1a3ca53..82b99229e 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import {Service} from '@google-cloud/common-grpc'; +import {DateStruct, PreciseDate} from '@google-cloud/precise-date'; import * as arrify from 'arrify'; import {CallOptions} from 'google-gax'; import * as is from 'is'; @@ -38,26 +39,76 @@ export interface JSONOptions { wrapStructs?: boolean; } +// https://github.com/Microsoft/TypeScript/issues/27920 +type DateFields = [number, number, number]; + /** - * @typedef SpannerDate + * Date-like object used to represent Cloud Spanner Dates. DATE types represent + * a logical calendar date, independent of time zone. DATE values do not + * represent a specific 24-hour period. Rather, a given DATE value represents a + * different 24-hour period when interpreted in a different time zone. Because + * of this, all values passed to {@link Spanner.date} will be interpreted as + * local time. + * + * To represent an absolute point in time, use {@link Spanner.timestamp}. + * * @see Spanner.date + * @see https://cloud.google.com/spanner/docs/data-types#date-type + * + * @class + * @extends Date + * + * @param {string|number} [date] String representing the date or number + * representing the year. + * @param {number} [month] Number representing the month. + * @param {number} [date] Number representing the date. + * + * @example + * Spanner.date('3-3-1933'); */ -export class SpannerDate { - value: string; - constructor(value?: string|number|Date) { - if (arguments.length > 1) { - throw new TypeError([ - 'The spanner.date function accepts a Date object or a', - 'single argument parseable by Date\'s constructor.', - ].join(' ')); +export class SpannerDate extends Date { + constructor(dateString?: string); + constructor(year: number, month: number, date: number); + constructor(...dateFields: Array) { + const yearOrDateString = dateFields[0]; + + if (!yearOrDateString) { + dateFields[0] = new Date().toDateString(); } - if (is.undefined(value)) { - value = new Date(); + + // JavaScript Date objects will interpret ISO date strings as Zulu time, + // but by formatting it, we can infer local time. + if (/^\d{4}-\d{1,2}-\d{1,2}/.test(yearOrDateString as string)) { + const [year, month, date] = (yearOrDateString as string).split(/-|T/); + dateFields = [`${month}-${date}-${year}`]; } - this.value = new Date(value!).toJSON().replace(/T.+/, ''); + + super(...dateFields.slice(0, 3) as DateFields); + } + /** + * Returns the date in ISO date format. + * `YYYY-[M]M-[D]D` + * + * @returns {string} + */ + toJSON(): string { + const year = this.getFullYear(); + let month = (this.getMonth() + 1).toString(); + let date = this.getDate().toString(); + + if (month.length === 1) { + month = `0${month}`; + } + + if (date.length === 1) { + date = `0${date}`; + } + + return `${year}-${month}-${date}`; } } + /** * Using an abstract class to simplify checking for wrapped numbers. * @@ -245,9 +296,11 @@ function decode(value: Value, type: s.Type): Value { case s.TypeCode.INT64: decoded = new Int(decoded); break; - case s.TypeCode.TIMESTAMP: // falls through + case s.TypeCode.TIMESTAMP: + decoded = new PreciseDate(decoded); + break; case s.TypeCode.DATE: - decoded = new Date(decoded); + decoded = new SpannerDate(decoded); break; case s.TypeCode.ARRAY: decoded = decoded.map(value => { @@ -299,7 +352,7 @@ function encodeValue(value: Value): Value { return value.toJSON(); } - if (value instanceof WrappedNumber || value instanceof SpannerDate) { + if (value instanceof WrappedNumber) { return value.value; } @@ -410,14 +463,14 @@ function getType(value: Value): Type { return {type: 'bytes'}; } - if (is.date(value)) { - return {type: 'timestamp'}; - } - if (value instanceof SpannerDate) { return {type: 'date'}; } + if (is.date(value)) { + return {type: 'timestamp'}; + } + if (value instanceof Struct) { return { type: 'struct', diff --git a/src/index.ts b/src/index.ts index 224ea7a66..684f0baaf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,12 +18,14 @@ import {Service, Operation} from '@google-cloud/common-grpc'; import {paginator} from '@google-cloud/paginator'; +import {PreciseDate} from '@google-cloud/precise-date'; import {replaceProjectIdToken} from '@google-cloud/projectify'; import {promisifyAll} from '@google-cloud/promisify'; import * as extend from 'extend'; import {GoogleAuth, GoogleAuthOptions} from 'google-auth-library'; import * as is from 'is'; import * as path from 'path'; +import {common as p} from 'protobufjs'; import * as streamEvents from 'stream-events'; import * as through from 'through2'; import {GrpcServiceConfig} from '@google-cloud/common-grpc/build/src/service'; @@ -770,29 +772,76 @@ class Spanner extends Service { return stream; } + static date(dateString?: string); + static date(year: number, month: number, date: number); /** * Helper function to get a Cloud Spanner Date object. * - * @method Spanner.date - * @param {string|date} value The date as a string or Date object. - * @returns {object} - * @see {@link Spanner#date} + * DATE types represent a logical calendar date, independent of time zone. + * DATE values do not represent a specific 24-hour period. Rather, a given + * DATE value represents a different 24-hour period when interpreted in a + * different time zone. Because of this, all values passed to + * {@link Spanner.date} will be interpreted as local time. + * + * To represent an absolute point in time, use {@link Spanner.timestamp}. + * + * @param {string|number} [date] String representing the date or number + * representing the year. + * @param {number} [month] Number representing the month. + * @param {number} [date] Number representing the date. + * @returns {SpannerDate} * * @example * const {Spanner} = require('@google-cloud/spanner'); * const date = Spanner.date('08-20-1969'); */ - static date(value?) { - return new codec.SpannerDate(value); + // tslint:disable-next-line no-any + static date(...dateFields: any[]) { + return new codec.SpannerDate(...dateFields); + } + + /** + * Date object with nanosecond precision. Supports all standard Date arguments + * in addition to several custom types. + * @external PreciseDate + * @see {@link https://github.com/googleapis/nodejs-precise-date|PreciseDate} + */ + /** + * Helper function to get a Cloud Spanner Timestamp object. + * + * String timestamps should have a canonical format of + * `YYYY-[M]M-[D]D[( |T)[H]H:[M]M:[S]S[.DDDDDDDDD]]Z` + * + * **Timestamp values must be expressed in Zulu time and cannot include a UTC + * offset.** + * + * @see https://cloud.google.com/spanner/docs/data-types#timestamp-type + * + * @param {string|number|google.protobuf.Timestamp} [timestamp] Either a + * RFC 3339 timestamp formatted string or + * {@link google.protobuf.Timestamp} object. + * @returns {external:PreciseDate} + * + * @example + * const timestamp = Spanner.timestamp('2019-02-08T10:34:29.481145231Z'); + * + * @example With a `google.protobuf.Timestamp` object + * const [seconds, nanos] = process.hrtime(); + * const timestamp = Spanner.timestamp({seconds, nanos}); + * + * @example With a Date timestamp + * const timestamp = Spanner.timestamp(Date.now()); + */ + static timestamp(value?: string|number|p.ITimestamp): PreciseDate { + value = value || Date.now(); + return new PreciseDate(value as number); } /** * Helper function to get a Cloud Spanner Float64 object. * - * @method Spanner.float * @param {string|number} value The float as a number or string. * @returns {object} - * @see {@link Spanner#float} * * @example * const {Spanner} = require('@google-cloud/spanner'); @@ -805,10 +854,8 @@ class Spanner extends Service { /** * Helper function to get a Cloud Spanner Int64 object. * - * @method Spanner.int * @param {string|number} value The int as a number or string. * @returns {object} - * @see {@link Spanner#int} * * @example * const {Spanner} = require('@google-cloud/spanner'); @@ -853,6 +900,7 @@ promisifyAll(Spanner, { 'instance', 'int', 'operation', + 'timestamp', ], }); diff --git a/src/transaction.ts b/src/transaction.ts index 2c3259324..89ed8a92b 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {DateStruct, PreciseDate} from '@google-cloud/precise-date'; import {promisifyAll} from '@google-cloud/promisify'; import * as arrify from 'arrify'; import {EventEmitter} from 'events'; @@ -33,9 +34,9 @@ export type Rows = Array; export interface TimestampBounds { strong?: boolean; - minReadTimestamp?: Date|p.ITimestamp; + minReadTimestamp?: PreciseDate|p.ITimestamp; maxStaleness?: number|p.IDuration; - readTimestamp?: Date|p.ITimestamp; + readTimestamp?: PreciseDate|p.ITimestamp; exactStaleness?: number|p.IDuration; returnReadTimestamp?: boolean; } @@ -96,12 +97,12 @@ export interface RunUpdateCallback { * @typedef {object} TimestampBounds * @property {boolean} [strong=true] Read at a timestamp where all previously * committed transactions are visible. - * @property {Date|google.protobuf.Timestamp} [minReadTimestamp] Executes all - * reads at a `timestamp >= minReadTimestamp`. + * @property {external:PreciseDate|google.protobuf.Timestamp} [minReadTimestamp] + * Executes all reads at a `timestamp >= minReadTimestamp`. * @property {number|google.protobuf.Timestamp} [maxStaleness] Read data at a * `timestamp >= NOW - maxStaleness` (milliseconds). - * @property {Date|google.protobuf.Timestamp} [readTimestamp] Executes all - * reads at the given timestamp. + * @property {external:PreciseDate|google.protobuf.Timestamp} [readTimestamp] + * Executes all reads at the given timestamp. * @property {number|google.protobuf.Timestamp} [exactStaleness] Executes all * reads at a timestamp that is `exactStaleness` (milliseconds) old. * @property {boolean} [returnReadTimestamp=true] When true, @@ -149,7 +150,7 @@ export class Snapshot extends EventEmitter { id?: string|Uint8Array; ended: boolean; metadata?: s.Transaction; - readTimestamp?: Date; + readTimestamp?: PreciseDate; readTimestampProto?: p.ITimestamp; request: (config: {}, callback: Function) => void; requestStream: (config: {}) => Readable; @@ -180,7 +181,7 @@ export class Snapshot extends EventEmitter { * The timestamp at which all reads will be performed. * * @name Snapshot#readTimestamp - * @type {?Date} + * @type {?external:PreciseDate} */ /** * **Snapshot only** @@ -272,8 +273,7 @@ export class Snapshot extends EventEmitter { if (readTimestamp) { this.readTimestampProto = readTimestamp; - this.readTimestamp = - codec.convertProtoTimestampToDate(readTimestamp); + this.readTimestamp = new PreciseDate(readTimestamp as DateStruct); } callback!(null, resp); @@ -888,17 +888,17 @@ export class Snapshot extends EventEmitter { * @returns {object} */ static encodeTimestampBounds(options: TimestampBounds): s.ReadOnly { - const {returnReadTimestamp = true} = options; const readOnly: s.ReadOnly = {}; + const {returnReadTimestamp = true} = options; - if (is.date(options.minReadTimestamp)) { - const timestamp = (options.minReadTimestamp as Date).getTime(); - readOnly.minReadTimestamp = codec.convertMsToProtoTimestamp(timestamp); + if (options.minReadTimestamp instanceof PreciseDate) { + readOnly.minReadTimestamp = + (options.minReadTimestamp as PreciseDate).toStruct(); } - if (is.date(options.readTimestamp)) { - const timestamp = (options.readTimestamp as Date).getTime(); - readOnly.readTimestamp = codec.convertMsToProtoTimestamp(timestamp); + if (options.readTimestamp instanceof PreciseDate) { + readOnly.readTimestamp = + (options.readTimestamp as PreciseDate).toStruct(); } if (is.number(options.maxStaleness)) { @@ -1079,7 +1079,7 @@ promisifyAll(Dml); * }); */ export class Transaction extends Dml { - commitTimestamp?: Date; + commitTimestamp?: PreciseDate; commitTimestampProto?: p.ITimestamp; private _queuedMutations: s.Mutation[]; @@ -1088,7 +1088,7 @@ export class Transaction extends Dml { * {@link Transaction#commit} is called. * * @name Transaction#commitTimestamp - * @type {?Date} + * @type {?external:PreciseDate} */ /** * The protobuf version of {@link Transaction#commitTimestamp}. This is useful @@ -1199,7 +1199,7 @@ export class Transaction extends Dml { if (resp && resp.commitTimestamp) { this.commitTimestampProto = resp.commitTimestamp; this.commitTimestamp = - codec.convertProtoTimestampToDate(resp.commitTimestamp); + new PreciseDate(resp.commitTimestamp as DateStruct); } callback!(err, resp); diff --git a/system-test/spanner.ts b/system-test/spanner.ts index eeaa9f4fc..47cd6966c 100644 --- a/system-test/spanner.ts +++ b/system-test/spanner.ts @@ -518,7 +518,7 @@ describe('Spanner', () => { describe('timestamps', () => { it('should write timestamp values', done => { - const date = new Date(); + const date = Spanner.timestamp(); insert({TimestampValue: date}, (err, row) => { assert.ifError(err); @@ -553,7 +553,7 @@ describe('Spanner', () => { }); it('should write timestamp array values', done => { - const values = [new Date(), new Date('3-3-1933')]; + const values = [Spanner.timestamp(), Spanner.timestamp('3-3-1933')]; insert({TimestampArray: values}, (err, row) => { assert.ifError(err); @@ -603,10 +603,8 @@ describe('Spanner', () => { insert({DateArray: values}, (err, row) => { assert.ifError(err); - - const returnedValues = row.toJSON().DateArray.map(Spanner.date); - assert.deepStrictEqual(returnedValues, values); - + const {DateArray} = row.toJSON(); + assert.deepStrictEqual(DateArray, values); done(); }); }); @@ -616,11 +614,10 @@ describe('Spanner', () => { it('should accept the commit timestamp placeholder', done => { const data = {CommitTimestamp: Spanner.COMMIT_TIMESTAMP}; - insert(data, (err, row, commitResponse) => { + insert(data, (err, row, {commitTimestamp}) => { assert.ifError(err); - const timestampFromCommit = - fromProtoToDate(commitResponse.commitTimestamp); + const timestampFromCommit = Spanner.timestamp(commitTimestamp); const timestampFromRead = row.toJSON().CommitTimestamp; assert.deepStrictEqual(timestampFromCommit, timestampFromRead); @@ -1311,15 +1308,13 @@ describe('Spanner', () => { }); describe('insert & query', () => { - const DATE = new Date('1969-08-20'); - const ID = generateName('id'); const NAME = generateName('name'); const FLOAT = 8.2; const INT = 2; const INFO = Buffer.from(generateName('info')); - const CREATED = new Date(); - const DOB = Spanner.date(DATE); + const CREATED = Spanner.timestamp(); + const DOB = Spanner.date('1969-08-20'); const ACCENTS = ['jamaican']; const PHONE_NUMBERS = [123123123, 234234234]; const HAS_GEAR = true; @@ -1338,10 +1333,6 @@ describe('Spanner', () => { }; const EXPECTED_ROW = extend(true, {}, INSERT_ROW); - EXPECTED_ROW.DOB = DATE; - EXPECTED_ROW.Float = FLOAT; - EXPECTED_ROW.Int = INT; - EXPECTED_ROW.PhoneNumbers = PHONE_NUMBERS; before(() => { return table.insert(INSERT_ROW); @@ -2022,7 +2013,7 @@ describe('Spanner', () => { describe('timestamp', () => { it('should bind the value', done => { - const timestamp = new Date(); + const timestamp = Spanner.timestamp(); const query = { sql: 'SELECT @v', @@ -2057,7 +2048,8 @@ describe('Spanner', () => { }); it('should bind arrays', done => { - const values = [new Date(), new Date('3-3-1999'), null]; + const values = + [Spanner.timestamp(), Spanner.timestamp('3-3-1999'), null]; const query = { sql: 'SELECT @v', @@ -2160,7 +2152,7 @@ describe('Spanner', () => { it('should bind arrays', done => { const values = [ Spanner.date(), - Spanner.date(new Date('3-3-1999')), + Spanner.date('3-3-1999'), null, ]; diff --git a/test/batch-transaction.ts b/test/batch-transaction.ts index 32aab3abf..978dd9f3b 100644 --- a/test/batch-transaction.ts +++ b/test/batch-transaction.ts @@ -42,9 +42,17 @@ const fakePfy = extend({}, pfy, { }, }); +class FakeTimestamp { + calledWith_: IArguments; + constructor() { + this.calledWith_ = arguments; + } +} + // tslint:disable-next-line no-any const fakeCodec: any = { encode: util.noop, + Timestamp: FakeTimestamp, Int() {}, Float() {}, SpannerDate() {}, @@ -79,11 +87,13 @@ describe('BatchTransaction', () => { const SESSION: any = {}; before(() => { - BatchTransaction = proxyquire('../src/batch-transaction.js', { - '@google-cloud/promisify': fakePfy, - './codec.js': {codec: fakeCodec}, - './transaction.js': {Snapshot: FakeTransaction}, - }).BatchTransaction; + BatchTransaction = + proxyquire('../src/batch-transaction.js', { + '@google-cloud/precise-date': {PreciseDate: FakeTimestamp}, + '@google-cloud/promisify': fakePfy, + './codec.js': {codec: fakeCodec}, + './transaction.js': {Snapshot: FakeTransaction}, + }).BatchTransaction; }); beforeEach(() => { @@ -210,7 +220,6 @@ describe('BatchTransaction', () => { }); it('should update the transaction with returned metadata', done => { - const fakeTimestamp = new Date(); const response = extend({}, RESPONSE, { transaction: { id: ID, @@ -219,15 +228,17 @@ describe('BatchTransaction', () => { }); REQUEST.callsFake((_, callback) => callback(null, response)); - sandbox.stub(fakeCodec, 'convertProtoTimestampToDate') - .withArgs(TIMESTAMP) - .returns(fakeTimestamp); batchTransaction.createPartitions_(CONFIG, (err, parts, resp) => { assert.strictEqual(resp, response); assert.strictEqual(batchTransaction.id, ID); - assert.strictEqual(batchTransaction.readTimestamp, fakeTimestamp); assert.strictEqual(batchTransaction.readTimestampProto, TIMESTAMP); + + const timestamp = + batchTransaction.readTimestamp as unknown as FakeTimestamp; + assert(timestamp instanceof FakeTimestamp); + assert.strictEqual(timestamp.calledWith_[0], TIMESTAMP); + done(); }); }); diff --git a/test/codec.ts b/test/codec.ts index c79550b21..94a1cd550 100644 --- a/test/codec.ts +++ b/test/codec.ts @@ -19,6 +19,7 @@ import * as assert from 'assert'; import * as proxyquire from 'proxyquire'; import * as sinon from 'sinon'; +import {PreciseDate} from '@google-cloud/precise-date'; import {Service} from '@google-cloud/common-grpc'; import {SpannerClient as s} from '../src/v1'; @@ -42,26 +43,73 @@ describe('codec', () => { afterEach(() => sandbox.restore()); describe('SpannerDate', () => { - it('should choke on multiple arguments', () => { - const expectedErrorMessage = [ - 'The spanner.date function accepts a Date object or a', - 'single argument parseable by Date\'s constructor.', - ].join(' '); + describe('instantiation', () => { + it('should accept date strings', () => { + const date = new codec.SpannerDate('3-22-1986'); + const json = date.toJSON(); - assert.throws(() => { - const x = new codec.SpannerDate(2012, 3, 21); - }, new RegExp(expectedErrorMessage)); - }); + assert.strictEqual(json, '1986-03-22'); + }); + + it('should default to the current local date', () => { + const date = new codec.SpannerDate(); + const today = new Date(); + const year = today.getFullYear(); + const month = today.getMonth(); + const day = today.getDate(); + const expected = new codec.SpannerDate(year, month, day); + + assert.deepStrictEqual(date, expected); + }); + + it('should interpret ISO date strings as local time', () => { + const date = new codec.SpannerDate('1986-03-22'); + const json = date.toJSON(); + + assert.strictEqual(json, '1986-03-22'); + }); + + it('should accept y/m/d number values', () => { + const date = new codec.SpannerDate(1986, 2, 22); + const json = date.toJSON(); - it('should create an instance from a string', () => { - const spannerDate = new codec.SpannerDate('08-20-1969'); - assert.strictEqual(spannerDate.value, '1969-08-20'); + assert.strictEqual(json, '1986-03-22'); + }); + + it('should truncate additional date fields', () => { + const truncated = new codec.SpannerDate(1986, 2, 22, 4, 8, 10); + const expected = new codec.SpannerDate(1986, 2, 22); + + assert.deepStrictEqual(truncated, expected); + }); }); - it('should create an instance from a Date object', () => { - const date = new Date(); - const spannerDate = new codec.SpannerDate(date); - assert.strictEqual(spannerDate.value, date.toJSON().replace(/T.+/, '')); + describe('toJSON', () => { + let date: Date; + + beforeEach(() => { + date = new codec.SpannerDate(); + sandbox.stub(date, 'getFullYear').returns(1999); + sandbox.stub(date, 'getMonth').returns(11); + sandbox.stub(date, 'getDate').returns(31); + }); + + it('should return the spanner date string', () => { + const json = date.toJSON(); + assert.strictEqual(json, '1999-12-31'); + }); + + it('should pad single digit months', () => { + (date.getMonth as sinon.SinonStub).returns(8); + const json = date.toJSON(); + assert.strictEqual(json, '1999-09-31'); + }); + + it('should pad single digit dates', () => { + (date.getDate as sinon.SinonStub).returns(3); + const json = date.toJSON(); + assert.strictEqual(json, '1999-12-03'); + }); }); }); @@ -360,22 +408,22 @@ describe('codec', () => { it('should decode TIMESTAMP', () => { const value = new Date(); - + const expected = new PreciseDate(value.getTime()); const decoded = codec.decode(value.toJSON(), { code: s.TypeCode.TIMESTAMP, }); - assert.deepStrictEqual(decoded, value); + assert.deepStrictEqual(decoded, expected); }); it('should decode DATE', () => { const value = new Date(); - + const expected = new codec.SpannerDate(value.toISOString()); const decoded = codec.decode(value.toJSON(), { code: s.TypeCode.DATE, }); - assert.deepStrictEqual(decoded, value); + assert.deepStrictEqual(decoded, expected); }); it('should decode ARRAY and inner members', () => { @@ -488,7 +536,7 @@ describe('codec', () => { }); it('should encode TIMESTAMP', () => { - const value = new Date(); + const value = new PreciseDate(); const encoded = codec.encode(value); @@ -500,7 +548,7 @@ describe('codec', () => { const encoded = codec.encode(value); - assert.strictEqual(encoded, value.value); + assert.strictEqual(encoded, value.toJSON()); }); it('should encode INT64', () => { @@ -548,15 +596,20 @@ describe('codec', () => { codec.getType(Buffer.from('abc')), {type: 'bytes'}); }); - it('should determine if the value is a timestamp', () => { - assert.deepStrictEqual(codec.getType(new Date()), {type: 'timestamp'}); - }); - it('should determine if the value is a date', () => { assert.deepStrictEqual( codec.getType(new codec.SpannerDate()), {type: 'date'}); }); + it('should determine if the value is a timestamp', () => { + assert.deepStrictEqual( + codec.getType(new PreciseDate()), {type: 'timestamp'}); + }); + + it('should accept a plain date object as a timestamp', () => { + assert.deepStrictEqual(codec.getType(new Date()), {type: 'timestamp'}); + }); + it('should determine if the value is a struct', () => { const struct = codec.Struct.fromJSON({a: 'b'}); const type = codec.getType(struct); diff --git a/test/index.ts b/test/index.ts index 06cd59b00..ffae4da7f 100644 --- a/test/index.ts +++ b/test/index.ts @@ -22,6 +22,7 @@ import * as path from 'path'; import * as proxyquire from 'proxyquire'; import * as through from 'through2'; import {util} from '@google-cloud/common-grpc'; +import {PreciseDate} from '@google-cloud/precise-date'; import {replaceProjectIdToken} from '@google-cloud/projectify'; import * as pfy from '@google-cloud/promisify'; import * as sinon from 'sinon'; @@ -68,6 +69,7 @@ const fakePfy = extend({}, pfy, { 'instance', 'int', 'operation', + 'timestamp', ]); }, }); @@ -255,7 +257,7 @@ describe('Spanner', () => { describe('date', () => { it('should create a SpannerDate instance', () => { - const value = {}; + const value = '1999-1-1'; const customValue = {}; fakeCodec.SpannerDate = class { @@ -270,6 +272,13 @@ describe('Spanner', () => { }); }); + describe('timestamp', () => { + it('should create a PreciseDate instance', () => { + const date = Spanner.timestamp(); + assert(date instanceof PreciseDate); + }); + }); + describe('float', () => { it('should create a SpannerDate instance', () => { const value = {}; diff --git a/test/transaction.ts b/test/transaction.ts index 89b9ee55e..917e74784 100644 --- a/test/transaction.ts +++ b/test/transaction.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import {PreciseDate} from '@google-cloud/precise-date'; import * as assert from 'assert'; import {EventEmitter} from 'events'; import {common as p} from 'protobufjs'; @@ -169,18 +170,15 @@ describe('Transaction', () => { }); it('should localize `readTimestamp` if present', done => { - const convertedTimestamp = new Date(); - const readTimestamp = {}; + const expectedTimestamp = new PreciseDate(0); + const readTimestamp = {seconds: 0, nanos: 0}; const response = Object.assign({readTimestamp}, BEGIN_RESPONSE); REQUEST.callsFake((_, callback) => callback(null, response)); - sandbox.stub(codec, 'convertProtoTimestampToDate') - .withArgs(readTimestamp) - .returns(convertedTimestamp); snapshot.begin(err => { assert.ifError(err); - assert.strictEqual(snapshot.readTimestamp, convertedTimestamp); + assert.deepStrictEqual(snapshot.readTimestamp, expectedTimestamp); assert.strictEqual(snapshot.readTimestampProto, readTimestamp); done(); }); @@ -697,31 +695,25 @@ describe('Transaction', () => { }); it('should convert `minReadTimestamp` Date to proto', () => { - const fakeTimestamp = Date.now(); - const fakeDate = new Date(); + const fakeTimestamp = new PreciseDate(); - sandbox.stub(fakeDate, 'getTime').returns(fakeTimestamp); - sandbox.stub(codec, 'convertMsToProtoTimestamp') - .withArgs(fakeTimestamp) - .returns(PROTO_TIMESTAMP); + sandbox.stub(fakeTimestamp, 'toStruct').returns(PROTO_TIMESTAMP); - const options = - Snapshot.encodeTimestampBounds({minReadTimestamp: fakeDate}); + const options = Snapshot.encodeTimestampBounds({ + minReadTimestamp: fakeTimestamp, + }); assert.strictEqual(options.minReadTimestamp, PROTO_TIMESTAMP); }); it('should convert `readTimestamp` Date to proto', () => { - const fakeTimestamp = Date.now(); - const fakeDate = new Date(); + const fakeTimestamp = new PreciseDate(); - sandbox.stub(fakeDate, 'getTime').returns(fakeTimestamp); - sandbox.stub(codec, 'convertMsToProtoTimestamp') - .withArgs(fakeTimestamp) - .returns(PROTO_TIMESTAMP); + sandbox.stub(fakeTimestamp, 'toStruct').returns(PROTO_TIMESTAMP); - const options = - Snapshot.encodeTimestampBounds({readTimestamp: fakeDate}); + const options = Snapshot.encodeTimestampBounds({ + readTimestamp: fakeTimestamp, + }); assert.strictEqual(options.readTimestamp, PROTO_TIMESTAMP); }); @@ -1013,19 +1005,15 @@ describe('Transaction', () => { it('should set the `commitTimestamp` if in response', () => { const requestStub = sandbox.stub(transaction, 'request'); - const fakeTimestamp = {}; - const formattedTimestamp = new Date(); - - sandbox.stub(codec, 'convertProtoTimestampToDate') - .withArgs(fakeTimestamp) - .returns(formattedTimestamp); + const expectedTimestamp = new PreciseDate(0); + const fakeTimestamp = {seconds: 0, nanos: 0}; transaction.commit(() => {}); const requestCallback = requestStub.lastCall.args[1]; requestCallback(null, {commitTimestamp: fakeTimestamp}); - assert.strictEqual(transaction.commitTimestamp, formattedTimestamp); + assert.deepStrictEqual(transaction.commitTimestamp, expectedTimestamp); assert.strictEqual(transaction.commitTimestampProto, fakeTimestamp); });