Skip to content

Commit 4945ba3

Browse files
Adding Timestamp class (#220)
1 parent 5a4abc4 commit 4945ba3

16 files changed

Lines changed: 774 additions & 48 deletions

File tree

handwritten/firestore/src/document.js

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ const DeleteTransform = fieldValue.DeleteTransform;
5050
*/
5151
const ServerTimestampTransform = fieldValue.ServerTimestampTransform;
5252

53+
/*!
54+
* @see {Timestamp}
55+
*/
56+
const Timestamp = require('./timestamp');
57+
5358
/*!
5459
* Injected.
5560
*
@@ -67,13 +72,6 @@ let validate;
6772
*/
6873
const MAX_DEPTH = 20;
6974

70-
/*!
71-
* Number of nanoseconds in a millisecond.
72-
*
73-
* @type {number}
74-
*/
75-
const MS_TO_NANOS = 1000000;
76-
7775
/**
7876
* An immutable object representing a geographic location in Firestore. The
7977
* location is represented as a latitude/longitude pair.
@@ -98,8 +96,8 @@ class GeoPoint {
9896
* });
9997
*/
10098
constructor(latitude, longitude) {
101-
validate.isNumber('latitude', latitude);
102-
validate.isNumber('longitude', longitude);
99+
validate.isNumber('latitude', latitude, -90, 90);
100+
validate.isNumber('longitude', longitude, -180, 180);
103101

104102
this._latitude = latitude;
105103
this._longitude = longitude;
@@ -532,6 +530,9 @@ class DocumentSnapshot {
532530
* @returns {*} The converted JS type.
533531
*/
534532
_decodeValue(proto) {
533+
const timestampsInSnapshotsEnabled = this._ref.firestore
534+
._timestampsInSnapshotsEnabled;
535+
535536
switch (proto.valueType) {
536537
case 'stringValue': {
537538
return proto.stringValue;
@@ -546,10 +547,11 @@ class DocumentSnapshot {
546547
return parseFloat(proto.doubleValue, 10);
547548
}
548549
case 'timestampValue': {
549-
return new Date(
550-
(proto.timestampValue.seconds || 0) * 1000 +
551-
(proto.timestampValue.nanos || 0) / MS_TO_NANOS
550+
const timestamp = new Timestamp(
551+
Number(proto.timestampValue.seconds || 0),
552+
Number(proto.timestampValue.nanos || 0)
552553
);
554+
return timestampsInSnapshotsEnabled ? timestamp : timestamp.toDate();
553555
}
554556
case 'referenceValue': {
555557
return new DocumentReference(
@@ -733,15 +735,21 @@ class DocumentSnapshot {
733735
};
734736
}
735737

736-
if (is.date(val)) {
737-
let epochSeconds = Math.floor(val.getTime() / 1000);
738-
let timestamp = {
739-
seconds: epochSeconds,
740-
nanos: (val.getTime() - epochSeconds * 1000) * MS_TO_NANOS,
738+
if (is.instance(val, Timestamp)) {
739+
return {
740+
valueType: 'timestampValue',
741+
timestampValue: {seconds: val.seconds, nanos: val.nanoseconds},
741742
};
743+
}
744+
745+
if (is.date(val)) {
746+
let timestamp = Timestamp.fromDate(val);
742747
return {
743748
valueType: 'timestampValue',
744-
timestampValue: timestamp,
749+
timestampValue: {
750+
seconds: timestamp.seconds,
751+
nanos: timestamp.nanoseconds,
752+
},
745753
};
746754
}
747755

@@ -1357,7 +1365,7 @@ class DocumentTransform {
13571365
* Returns the array of fields in this DocumentTransform.
13581366
*
13591367
* @private
1360-
* @type {Array.<FieldPath>} The fields specified in this DocumentTransform.
1368+
* @type {Array.<FieldPath>}
13611369
* @readonly
13621370
*/
13631371
get fields() {
@@ -1550,6 +1558,8 @@ function validateFieldValue(val, options, depth) {
15501558
return true;
15511559
} else if (is.instanceof(val, GeoPoint)) {
15521560
return true;
1561+
} else if (is.instanceof(val, Timestamp)) {
1562+
return true;
15531563
} else if (is.instanceof(val, FieldPath)) {
15541564
throw new Error(
15551565
'Cannot use object of type "FieldPath" as a Firestore value.'

handwritten/firestore/src/index.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,26 @@ const GRPC_UNAVAILABLE = 14;
197197
class Firestore {
198198
/**
199199
* @param {Object=} options - [Configuration object](#/docs).
200+
* @param {string=} options.projectId The Firestore Project ID. Can be
201+
* omitted in environments that support `Application Default Credentials`
202+
* {@see https://cloud.google.com/docs/authentication}
203+
* @param {string=} options.keyFilename Local file containing the Service
204+
* Account credentials. Can be omitted in environments that support
205+
* `Application Default Credentials`
206+
* {@see https://cloud.google.com/docs/authentication}
207+
* @param {boolean=} options.timestampsInSnapshots Enables the use of
208+
* `Timestamp`s for timestamp fields in `DocumentSnapshots`.<br/>
209+
* Currently, Firestore returns timestamp fields as `Date` but `Date` only
210+
* supports millisecond precision, which leads to truncation and causes
211+
* unexpected behavior when using a timestamp from a snapshot as a part
212+
* of a subsequent query.
213+
* <br/>Setting `timestampsInSnapshots` to true will cause Firestore to return
214+
* `Timestamp` values instead of `Date` avoiding this kind of problem. To
215+
* make this work you must also change any code that uses `Date` to use
216+
* `Timestamp` instead.
217+
* <br/>NOTE: in the future `timestampsInSnapshots: true` will become the
218+
* default and this option will be removed so you should change your code to
219+
* use `Timestamp` now and opt-in to this new behavior as soon as you can.
200220
*/
201221
constructor(options) {
202222
options = extend({}, options, {
@@ -240,6 +260,34 @@ class Firestore {
240260
Firestore.log('Firestore', 'Detected GCF environment');
241261
}
242262

263+
this._timestampsInSnapshotsEnabled = !!options.timestampsInSnapshots;
264+
265+
if (!this._timestampsInSnapshotsEnabled) {
266+
// eslint-disable-next-line no-console
267+
console.error(`
268+
The behavior for Date objects stored in Firestore is going to change
269+
AND YOUR APP MAY BREAK.
270+
To hide this warning and ensure your app does not break, you need to add the
271+
following code to your app before calling any other Cloud Firestore methods:
272+
273+
const settings = {/* your settings... */ timestampsInSnapshots: true};
274+
const firestore = new Firestore(settings);
275+
276+
With this change, timestamps stored in Cloud Firestore will be read back as
277+
Firebase Timestamp objects instead of as system Date objects. So you will also
278+
need to update code expecting a Date to instead expect a Timestamp. For example:
279+
280+
// Old:
281+
const date = snapshot.get('created_at');
282+
// New:
283+
const timestamp = snapshot.get('created_at');
284+
const date = timestamp.toDate();
285+
286+
Please audit all existing usages of Date when you enable the new behavior. In a
287+
future release, the behavior will change to the new behavior, so if you do not
288+
follow these steps, YOUR APP MAY BREAK.`);
289+
}
290+
243291
if (options && options.projectId) {
244292
validate.isString('options.projectId', options.projectId);
245293
this._referencePath = new ResourcePath(options.projectId, '(default)');
@@ -1317,3 +1365,12 @@ module.exports.FieldValue = FieldValue;
13171365
* @type {Constructor}
13181366
*/
13191367
module.exports.FieldPath = FieldPath;
1368+
1369+
/**
1370+
* {@link Timestamp} class.
1371+
*
1372+
* @name Firestore.Timestamp
1373+
* @see Timestamp
1374+
* @type Timestamp
1375+
*/
1376+
module.exports.Timestamp = require('./timestamp');
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*!
2+
* Copyright 2018 Google Inc. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
'use strict';
18+
19+
const is = require('is');
20+
const validate = require('./validate')();
21+
22+
/*!
23+
* Number of nanoseconds in a millisecond.
24+
*
25+
* @type {number}
26+
*/
27+
const MS_TO_NANOS = 1000000;
28+
29+
/**
30+
* A Timestamp represents a point in time independent of any time zone or
31+
* calendar, represented as seconds and fractions of seconds at nanosecond
32+
* resolution in UTC Epoch time. It is encoded using the Proleptic Gregorian
33+
* Calendar which extends the Gregorian calendar backwards to year one. It is
34+
* encoded assuming all minutes are 60 seconds long, i.e. leap seconds are
35+
* "smeared" so that no leap second table is needed for interpretation. Range is
36+
* from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z.
37+
*
38+
* @see https://github.com/google/protobuf/blob/master/src/google/protobuf/timestamp.proto
39+
*/
40+
class Timestamp {
41+
/**
42+
* Creates a new timestamp with the current date, with millisecond precision.
43+
*
44+
* @example
45+
* let documentRef = firestore.doc('col/doc');
46+
*
47+
* documentRef.set({ updateTime:Firestore.Timestamp.now() });
48+
*
49+
* @return {Timestamp} A new `Timestamp` representing the current date.
50+
*/
51+
static now() {
52+
return Timestamp.fromMillis(Date.now());
53+
}
54+
55+
/**
56+
* Creates a new timestamp from the given date.
57+
*
58+
* @example
59+
* let documentRef = firestore.doc('col/doc');
60+
*
61+
* let date = Date.parse('01 Jan 2000 00:00:00 GMT');
62+
* documentRef.set({ startTime:Firestore.Timestamp.fromDate(date) });
63+
*
64+
* @param {Date} date The date to initialize the `Timestamp` from.
65+
* @return {Timestamp} A new `Timestamp` representing the same point in time
66+
* as the given date.
67+
*/
68+
static fromDate(date) {
69+
return Timestamp.fromMillis(date.getTime());
70+
}
71+
72+
/**
73+
* Creates a new timestamp from the given number of milliseconds.
74+
*
75+
* @example
76+
* let documentRef = firestore.doc('col/doc');
77+
*
78+
* documentRef.set({ startTime:Firestore.Timestamp.fromMillis(42) });
79+
*
80+
* @param {number} milliseconds Number of milliseconds since Unix epoch
81+
* 1970-01-01T00:00:00Z.
82+
* @return {Timestamp} A new `Timestamp` representing the same point in time
83+
* as the given number of milliseconds.
84+
*/
85+
static fromMillis(milliseconds) {
86+
const seconds = Math.floor(milliseconds / 1000);
87+
const nanos = (milliseconds - seconds * 1000) * MS_TO_NANOS;
88+
return new Timestamp(seconds, nanos);
89+
}
90+
91+
/**
92+
* Creates a new timestamp.
93+
*
94+
* @example
95+
* let documentRef = firestore.doc('col/doc');
96+
*
97+
* documentRef.set({ startTime:new Firestore.Timestamp(42, 0) });
98+
*
99+
* @param {number} seconds The number of seconds of UTC time since Unix epoch
100+
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
101+
* 9999-12-31T23:59:59Z inclusive.
102+
* @param {number} nanoseconds The non-negative fractions of a second at
103+
* nanosecond resolution. Negative second values with fractions must still
104+
* have non-negative nanoseconds values that count forward in time. Must be
105+
* from 0 to 999,999,999 inclusive.
106+
*/
107+
constructor(seconds, nanoseconds) {
108+
validate.isInteger('seconds', seconds);
109+
validate.isInteger('nanoseconds', nanoseconds, 0, 999999999);
110+
111+
this._seconds = seconds;
112+
this._nanoseconds = nanoseconds;
113+
}
114+
115+
/**
116+
* The number of seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z.
117+
*
118+
* @example
119+
* let documentRef = firestore.doc('col/doc');
120+
*
121+
* documentRef.get().then(snap => {
122+
* let updatedAt = snap.updateTime;
123+
* console.log(`Updated at ${updated.seconds}s ${updated.nanoseconds}ns`);
124+
* });
125+
*
126+
* @type {number}
127+
*/
128+
get seconds() {
129+
return this._seconds;
130+
}
131+
132+
/**
133+
* The non-negative fractions of a second at nanosecond resolution.
134+
*
135+
* @example
136+
* let documentRef = firestore.doc('col/doc');
137+
*
138+
* documentRef.get().then(snap => {
139+
* let updated = snap.updateTime;
140+
* console.log(`Updated at ${updated.seconds}s ${updated.nanoseconds}ns`);
141+
* });
142+
*
143+
* @type {number}
144+
*/
145+
get nanoseconds() {
146+
return this._nanoseconds;
147+
}
148+
149+
/**
150+
* Returns a new `Date` corresponding to this timestamp. This may lose
151+
* precision.
152+
*
153+
* @example
154+
* let documentRef = firestore.doc('col/doc');
155+
*
156+
* documentRef.get().then(snap => {
157+
* console.log(`Document updated at: ${snap.updateTime.toDate()}`);
158+
* });
159+
*
160+
* @return {Date} JavaScript `Date` object representing the same point in time
161+
* as this `Timestamp`, with millisecond precision.
162+
*/
163+
toDate() {
164+
return new Date(
165+
this._seconds * 1000 + Math.round(this._nanoseconds / MS_TO_NANOS)
166+
);
167+
}
168+
169+
/**
170+
* Returns the number of milliseconds since Unix epoch 1970-01-01T00:00:00Z.
171+
*
172+
* @example
173+
* let documentRef = firestore.doc('col/doc');
174+
*
175+
* documentRef.get().then(snap => {
176+
* let startTime = snap.get('startTime');
177+
* let endTime = snap.get('endTime');
178+
* console.log(`Duration: ${endTime - startTime}`);
179+
* });
180+
*
181+
* @return {number} The point in time corresponding to this timestamp,
182+
* represented as the number of milliseconds since Unix epoch
183+
* 1970-01-01T00:00:00Z.
184+
*/
185+
toMillis() {
186+
return this._seconds * 1000 + Math.floor(this._nanoseconds / MS_TO_NANOS);
187+
}
188+
189+
/**
190+
* Returns 'true' if this `Timestamp` is equal to the provided one.
191+
*
192+
* @example
193+
* let documentRef = firestore.doc('col/doc');
194+
*
195+
* documentRef.get().then(snap => {
196+
* if (snap.createTime.isEqual(snap.updateTime)) {
197+
* console.log('Document is in its initial state.');
198+
* }
199+
* });
200+
*
201+
* @param {any} other The `Timestamp` to compare against.
202+
* @return {boolean} 'true' if this `Timestamp` is equal to the provided one.
203+
*/
204+
isEqual(other) {
205+
return (
206+
this === other ||
207+
(is.instanceof(other, Timestamp) &&
208+
this._seconds === other.seconds &&
209+
this._nanoseconds === other.nanoseconds)
210+
);
211+
}
212+
}
213+
214+
module.exports = Timestamp;

0 commit comments

Comments
 (0)