Skip to content

Commit

Permalink
feat(timestamp): create new date/timestamp classes (#517)
Browse files Browse the repository at this point in the history
  • Loading branch information
callmehiphop authored and JustinBeckwith committed Feb 25, 2019
1 parent d712018 commit c8130ef
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 136 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/batch-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -161,7 +162,7 @@ class BatchTransaction extends Snapshot {

if (readTimestamp) {
this.readTimestampProto = readTimestamp;
this.readTimestamp = codec.convertProtoTimestampToDate(readTimestamp);
this.readTimestamp = new PreciseDate(readTimestamp);
}
}

Expand Down
91 changes: 72 additions & 19 deletions src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<string|number|undefined>) {
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.
*
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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',
Expand Down
68 changes: 58 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 <caption>With a `google.protobuf.Timestamp` object</caption>
* const [seconds, nanos] = process.hrtime();
* const timestamp = Spanner.timestamp({seconds, nanos});
*
* @example <caption>With a Date timestamp</caption>
* 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');
Expand All @@ -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');
Expand Down Expand Up @@ -853,6 +900,7 @@ promisifyAll(Spanner, {
'instance',
'int',
'operation',
'timestamp',
],
});

Expand Down
40 changes: 20 additions & 20 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -33,9 +34,9 @@ export type Rows = Array<Row|Json>;

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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -1079,7 +1079,7 @@ promisifyAll(Dml);
* });
*/
export class Transaction extends Dml {
commitTimestamp?: Date;
commitTimestamp?: PreciseDate;
commitTimestampProto?: p.ITimestamp;
private _queuedMutations: s.Mutation[];

Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit c8130ef

Please sign in to comment.