diff --git a/dev/src/timestamp.ts b/dev/src/timestamp.ts index 80c67339a..35396eab6 100644 --- a/dev/src/timestamp.ts +++ b/dev/src/timestamp.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import * as firestore from '@google-cloud/firestore'; - -import {google} from '../protos/firestore_v1_proto_api'; -import {validateInteger} from './validate'; +import * as firestore from "@google-cloud/firestore"; +import { google } from "../protos/firestore_v1_proto_api"; +import { validateInteger } from "./validate"; import api = google.firestore.v1; /*! @@ -216,7 +215,7 @@ export class Timestamp implements firestore.Timestamp { */ toDate(): Date { return new Date( - this._seconds * 1000 + Math.round(this._nanoseconds / MS_TO_NANOS) + this._seconds * 1000 + Math.floor(this._nanoseconds / MS_TO_NANOS) ); } @@ -308,4 +307,24 @@ export class Timestamp implements firestore.Timestamp { const formattedNanoseconds = String(this.nanoseconds).padStart(9, '0'); return formattedSeconds + '.' + formattedNanoseconds; } + + static reviver(this: any, key: string, value: any): any { + if (key === '') { + const match = value.match(/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d)\.(\d+)([+-][0-2]\d:[0-5]\d|Z)/); + return new Timestamp( + Math.floor(Date.parse(match[1] + 'Z') / 1000), + Number(match[2].padEnd(9, '0')) + ); + } + } + + static replacer(this: any, key: string, value: any): any { + if (key === '') { + const t: Timestamp = value; + const isoDateSeconds: string = new Date(t._seconds * 1000) + .toISOString() + .split('.')[0]; + return `${isoDateSeconds}.${String(t._nanoseconds).padStart(9, '0')}Z`; + } + } } diff --git a/dev/src/write-batch.ts b/dev/src/write-batch.ts index 33536a275..9141efd7c 100644 --- a/dev/src/write-batch.ts +++ b/dev/src/write-batch.ts @@ -96,6 +96,23 @@ export class WriteResult implements firestore.WriteResult { this._writeTime.isEqual(other._writeTime)) ); } + + static reviver(this: any, key: string, value: any): any { + if (key === 'writeTime') { + return JSON.parse(value, Timestamp.reviver); + } + } + + static replacer(this: any, key: string, value: any): any { + switch (key) { + case '': + return { + writeTime: value._writeTime, + }; + case 'writeTime': + return JSON.stringify(value, Timestamp.replacer); + } + } } /** diff --git a/dev/test/timestamp.ts b/dev/test/timestamp.ts index 34a619841..5b5ae2e65 100644 --- a/dev/test/timestamp.ts +++ b/dev/test/timestamp.ts @@ -12,19 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import * as through2 from 'through2'; +import { describe, it } from "mocha"; +import { expect } from "chai"; +import * as through2 from "through2"; -import {google} from '../protos/firestore_v1_proto_api'; - -import * as Firestore from '../src/index'; -import { - ApiOverride, - createInstance as createInstanceHelper, - document, -} from '../test/util/helpers'; +import { google } from "../protos/firestore_v1_proto_api"; +import * as Firestore from "../src/index"; +import { ApiOverride, createInstance as createInstanceHelper, document } from "../test/util/helpers"; import api = google.firestore.v1; function createInstance(opts: {}, document: api.IDocument) { @@ -54,7 +49,7 @@ const DOCUMENT_WITH_EMPTY_TIMESTAMP = document('documentId', 'moonLanding', { timestampValue: {}, }); -describe('timestamps', () => { +describe.only('timestamps', () => { it('returned by default', () => { return createInstance({}, DOCUMENT_WITH_TIMESTAMP).then(firestore => { const expected = new Firestore.Timestamp(-14182920, 123000123); @@ -221,4 +216,42 @@ describe('timestamps', () => { expect(t1 > t2).to.be.false; expect(t1 >= t2).to.be.false; }); + + it('JSON.stringify() on the smallest Timestamp object', () => { + const timestamp = new Firestore.Timestamp(-62135596800, 0); + const writeResult = new Firestore.WriteResult(timestamp); + expect(JSON.stringify(timestamp, Firestore.Timestamp.replacer)).to.equal('"0001-01-01T00:00:00.000000000Z"'); + expect(JSON.stringify(writeResult, Firestore.WriteResult.replacer)).to.equal('{"writeTime":"\\"0001-01-01T00:00:00.000000000Z\\""}'); + }); + + it('JSON.parse() on the smallest Timestamp object', () => { + const timestamp = new Firestore.Timestamp(-62135596800, 0); + expect(JSON.parse('"0001-01-01T00:00:00.000000000Z"', Firestore.Timestamp.reviver)).to.deep.equal(timestamp); + }); + + it('JSON.stringify() on the largest Timestamp object', () => { + const timestamp = new Firestore.Timestamp(253402300799, 999999999); + expect(JSON.parse('"9999-12-31T23:59:59.999999999Z"', Firestore.Timestamp.reviver)).to.deep.equal(timestamp); + }); + + it('JSON.parse() on the largest Timestamp object', () => { + const timestamp = new Firestore.Timestamp(253402300799, 999999999); + expect(JSON.stringify(timestamp, Firestore.Timestamp.replacer)).to.equal('"9999-12-31T23:59:59.999999999Z"'); + }); + + it('JSON.stringify() on a Timestamp object whose date is in 2023', () => { + const timestamp = new Firestore.Timestamp(1680272000, 840000000); + expect(JSON.parse('"2023-03-31T14:13:20.840000000Z"', Firestore.Timestamp.reviver)).to.deep.equal(timestamp); + }); + + it('JSON.parse() on a Timestamp object whose date is in 2023', () => { + const timestamp = new Firestore.Timestamp(1680272000, 840000000); + expect(JSON.stringify(timestamp, Firestore.Timestamp.replacer)).to.equal('"2023-03-31T14:13:20.840000000Z"'); + }); + + it.only('JSON.stringify() on the smallest Timestamp object', () => { + const timestamp = new Firestore.Timestamp(-62135596800, 0); + const writeResult = new Firestore.WriteResult(timestamp); + expect(JSON.stringify(writeResult, Firestore.WriteResult.replacer)).to.equal('{"writeTime":"\\"0001-01-01T00:00:00.000000000Z\\""}'); + }); }); diff --git a/types/firestore.d.ts b/types/firestore.d.ts index 18d240a44..4a650e04a 100644 --- a/types/firestore.d.ts +++ b/types/firestore.d.ts @@ -1206,6 +1206,9 @@ declare namespace FirebaseFirestore { export class WriteResult { private constructor(); + static reviver(this: any, key: string, value: any): any; + static replacer(this: any, key: string, value: any): any; + /** * The write time as set by the Firestore servers. */