Skip to content

Adding Timestamp class #220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 26, 2018
48 changes: 29 additions & 19 deletions src/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ const DeleteTransform = fieldValue.DeleteTransform;
*/
const ServerTimestampTransform = fieldValue.ServerTimestampTransform;

/*!
* @see {Timestamp}
*/
const Timestamp = require('./timestamp');

/*!
* Injected.
*
Expand All @@ -67,13 +72,6 @@ let validate;
*/
const MAX_DEPTH = 20;

/*!
* Number of nanoseconds in a millisecond.
*
* @type {number}
*/
const MS_TO_NANOS = 1000000;

/**
* An immutable object representing a geographic location in Firestore. The
* location is represented as a latitude/longitude pair.
Expand All @@ -98,8 +96,8 @@ class GeoPoint {
* });
*/
constructor(latitude, longitude) {
validate.isNumber('latitude', latitude);
validate.isNumber('longitude', longitude);
validate.isNumber('latitude', latitude, -90, 90);
validate.isNumber('longitude', longitude, -180, 180);

this._latitude = latitude;
this._longitude = longitude;
Expand Down Expand Up @@ -532,6 +530,9 @@ class DocumentSnapshot {
* @returns {*} The converted JS type.
*/
_decodeValue(proto) {
const timestampsInSnapshotsEnabled = this._ref.firestore
._timestampsInSnapshotsEnabled;

switch (proto.valueType) {
case 'stringValue': {
return proto.stringValue;
Expand All @@ -546,10 +547,11 @@ class DocumentSnapshot {
return parseFloat(proto.doubleValue, 10);
}
case 'timestampValue': {
return new Date(
(proto.timestampValue.seconds || 0) * 1000 +
(proto.timestampValue.nanos || 0) / MS_TO_NANOS
const timestamp = new Timestamp(
Number(proto.timestampValue.seconds || 0),
Number(proto.timestampValue.nanos || 0)
);
return timestampsInSnapshotsEnabled ? timestamp : timestamp.toDate();
}
case 'referenceValue': {
return new DocumentReference(
Expand Down Expand Up @@ -733,15 +735,21 @@ class DocumentSnapshot {
};
}

if (is.date(val)) {
let epochSeconds = Math.floor(val.getTime() / 1000);
let timestamp = {
seconds: epochSeconds,
nanos: (val.getTime() - epochSeconds * 1000) * MS_TO_NANOS,
if (is.instance(val, Timestamp)) {
return {
valueType: 'timestampValue',
timestampValue: {seconds: val.seconds, nanos: val.nanoseconds},
};
}

if (is.date(val)) {
let timestamp = Timestamp.fromDate(val);
return {
valueType: 'timestampValue',
timestampValue: timestamp,
timestampValue: {
seconds: timestamp.seconds,
nanos: timestamp.nanoseconds,
},
};
}

Expand Down Expand Up @@ -1357,7 +1365,7 @@ class DocumentTransform {
* Returns the array of fields in this DocumentTransform.
*
* @private
* @type {Array.<FieldPath>} The fields specified in this DocumentTransform.
* @type {Array.<FieldPath>}
* @readonly
*/
get fields() {
Expand Down Expand Up @@ -1550,6 +1558,8 @@ function validateFieldValue(val, options, depth) {
return true;
} else if (is.instanceof(val, GeoPoint)) {
return true;
} else if (is.instanceof(val, Timestamp)) {
return true;
} else if (is.instanceof(val, FieldPath)) {
throw new Error(
'Cannot use object of type "FieldPath" as a Firestore value.'
Expand Down
57 changes: 57 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,26 @@ const GRPC_UNAVAILABLE = 14;
class Firestore {
/**
* @param {Object=} options - [Configuration object](#/docs).
* @param {string=} options.projectId The Firestore Project ID. Can be
* omitted in environments that support `Application Default Credentials`
* {@see https://cloud.google.com/docs/authentication}
* @param {string=} options.keyFilename Local file containing the Service
* Account credentials. Can be omitted in environments that support
* `Application Default Credentials`
* {@see https://cloud.google.com/docs/authentication}
* @param {boolean=} options.timestampsInSnapshots Enables the use of
* `Timestamp`s for timestamp fields in `DocumentSnapshots`.<br/>
* Currently, Firestore returns timestamp fields as `Date` but `Date` only
* supports millisecond precision, which leads to truncation and causes
* unexpected behavior when using a timestamp from a snapshot as a part
* of a subsequent query.
* <br/>Setting `timestampsInSnapshots` to true will cause Firestore to return
* `Timestamp` values instead of `Date` avoiding this kind of problem. To
* make this work you must also change any code that uses `Date` to use
* `Timestamp` instead.
* <br/>NOTE: in the future `timestampsInSnapshots: true` will become the
* default and this option will be removed so you should change your code to
* use `Timestamp` now and opt-in to this new behavior as soon as you can.
*/
constructor(options) {
options = extend({}, options, {
Expand Down Expand Up @@ -240,6 +260,34 @@ class Firestore {
Firestore.log('Firestore', 'Detected GCF environment');
}

this._timestampsInSnapshotsEnabled = !!options.timestampsInSnapshots;

if (!this._timestampsInSnapshotsEnabled) {
// eslint-disable-next-line no-console
console.error(`
The behavior for Date objects stored in Firestore is going to change
AND YOUR APP MAY BREAK.
To hide this warning and ensure your app does not break, you need to add the
following code to your app before calling any other Cloud Firestore methods:

const settings = {/* your settings... */ timestampsInSnapshots: true};
const firestore = new Firestore(settings);

With this change, timestamps stored in Cloud Firestore will be read back as
Firebase Timestamp objects instead of as system Date objects. So you will also
need to update code expecting a Date to instead expect a Timestamp. For example:

// Old:
const date = snapshot.get('created_at');
// New:
const timestamp = snapshot.get('created_at');
const date = timestamp.toDate();

Please audit all existing usages of Date when you enable the new behavior. In a
future release, the behavior will change to the new behavior, so if you do not
follow these steps, YOUR APP MAY BREAK.`);
}

if (options && options.projectId) {
validate.isString('options.projectId', options.projectId);
this._referencePath = new ResourcePath(options.projectId, '(default)');
Expand Down Expand Up @@ -1317,3 +1365,12 @@ module.exports.FieldValue = FieldValue;
* @type {Constructor}
*/
module.exports.FieldPath = FieldPath;

/**
* {@link Timestamp} class.
*
* @name Firestore.Timestamp
* @see Timestamp
* @type Timestamp
*/
module.exports.Timestamp = require('./timestamp');
214 changes: 214 additions & 0 deletions src/timestamp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*!
* Copyright 2018 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

const is = require('is');
const validate = require('./validate')();

This comment was marked as spam.

This comment was marked as spam.


/*!
* Number of nanoseconds in a millisecond.
*
* @type {number}
*/
const MS_TO_NANOS = 1000000;

/**
* A Timestamp represents a point in time independent of any time zone or
* calendar, represented as seconds and fractions of seconds at nanosecond
* resolution in UTC Epoch time. It is encoded using the Proleptic Gregorian
* Calendar which extends the Gregorian calendar backwards to year one. It is
* encoded assuming all minutes are 60 seconds long, i.e. leap seconds are
* "smeared" so that no leap second table is needed for interpretation. Range is
* from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z.
*
* @see https://github.com/google/protobuf/blob/master/src/google/protobuf/timestamp.proto
*/
class Timestamp {
/**
* Creates a new timestamp with the current date, with millisecond precision.
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* documentRef.set({ updateTime:Firestore.Timestamp.now() });
*
* @return {Timestamp} A new `Timestamp` representing the current date.
*/
static now() {
return Timestamp.fromMillis(Date.now());
}

/**
* Creates a new timestamp from the given date.
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* let date = Date.parse('01 Jan 2000 00:00:00 GMT');
* documentRef.set({ startTime:Firestore.Timestamp.fromDate(date) });
*
* @param {Date} date The date to initialize the `Timestamp` from.
* @return {Timestamp} A new `Timestamp` representing the same point in time
* as the given date.
*/
static fromDate(date) {
return Timestamp.fromMillis(date.getTime());
}

/**
* Creates a new timestamp from the given number of milliseconds.
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* documentRef.set({ startTime:Firestore.Timestamp.fromMillis(42) });
*
* @param {number} milliseconds Number of milliseconds since Unix epoch
* 1970-01-01T00:00:00Z.
* @return {Timestamp} A new `Timestamp` representing the same point in time
* as the given number of milliseconds.
*/
static fromMillis(milliseconds) {
const seconds = Math.floor(milliseconds / 1000);
const nanos = (milliseconds - seconds * 1000) * MS_TO_NANOS;
return new Timestamp(seconds, nanos);
}

/**
* Creates a new timestamp.
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* documentRef.set({ startTime:new Firestore.Timestamp(42, 0) });
*
* @param {number} seconds The number of seconds of UTC time since Unix epoch
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
* 9999-12-31T23:59:59Z inclusive.
* @param {number} nanoseconds The non-negative fractions of a second at
* nanosecond resolution. Negative second values with fractions must still
* have non-negative nanoseconds values that count forward in time. Must be
* from 0 to 999,999,999 inclusive.
*/
constructor(seconds, nanoseconds) {
validate.isInteger('seconds', seconds);
validate.isInteger('nanoseconds', nanoseconds, 0, 999999999);

this._seconds = seconds;
this._nanoseconds = nanoseconds;
}

/**
* The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z.
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* documentRef.get().then(snap => {
* let updatedAt = snap.updateTime;
* console.log(`Updated at ${updated.seconds}s ${updated.nanoseconds}ns`);
* });
*
* @type {number}
*/
get seconds() {
return this._seconds;
}

/**
* The non-negative fractions of a second at nanosecond resolution.
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* documentRef.get().then(snap => {
* let updated = snap.updateTime;
* console.log(`Updated at ${updated.seconds}s ${updated.nanoseconds}ns`);
* });
*
* @type {number}
*/
get nanoseconds() {
return this._nanoseconds;
}

/**
* Returns a new `Date` corresponding to this timestamp. This may lose
* precision.
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* documentRef.get().then(snap => {
* console.log(`Document updated at: ${snap.updateTime.toDate()}`);
* });
*
* @return {Date} JavaScript `Date` object representing the same point in time
* as this `Timestamp`, with millisecond precision.
*/
toDate() {
return new Date(
this._seconds * 1000 + Math.round(this._nanoseconds / MS_TO_NANOS)
);
}

/**
* Returns the number of milliseconds since Unix epoch 1970-01-01T00:00:00Z.
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* documentRef.get().then(snap => {
* let startTime = snap.get('startTime');
* let endTime = snap.get('endTime');
* console.log(`Duration: ${endTime - startTime}`);
* });
*
* @return {number} The point in time corresponding to this timestamp,
* represented as the number of milliseconds since Unix epoch
* 1970-01-01T00:00:00Z.
*/
toMillis() {
return this._seconds * 1000 + Math.floor(this._nanoseconds / MS_TO_NANOS);
}

/**
* Returns 'true' if this `Timestamp` is equal to the provided one.
*
* @example
* let documentRef = firestore.doc('col/doc');
*
* documentRef.get().then(snap => {
* if (snap.createTime.isEqual(snap.updateTime)) {
* console.log('Document is in its initial state.');
* }
* });
*
* @param {any} other The `Timestamp` to compare against.
* @return {boolean} 'true' if this `Timestamp` is equal to the provided one.
*/
isEqual(other) {
return (
this === other ||

This comment was marked as spam.

This comment was marked as spam.

(is.instanceof(other, Timestamp) &&
this._seconds === other.seconds &&
this._nanoseconds === other.nanoseconds)
);
}
}

module.exports = Timestamp;
Loading