Skip to content
48 changes: 34 additions & 14 deletions handwritten/firestore/api-report/firestore.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { google } from '../protos/firestore_v1_proto_api';
import { google as google_2 } from '../../protos/firestore_v1_proto_api';
import { GoogleError } from 'google-gax';
import * as proto from '../protos/firestore_v1_proto_api';
import * as protos from '../../protos/firestore_v1_proto_api';
import { protos } from '@google-cloud/firestore-api';
import * as protos_2 from '../../protos/firestore_v1_proto_api';
Comment thread
MarkDuckworth marked this conversation as resolved.
import { Readable } from 'stream';
import { Span as Span_2 } from '@opentelemetry/api';

Expand Down Expand Up @@ -1414,6 +1415,16 @@ export class FieldValue implements firestore.FieldValue {
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
isEqual(other: firestore.FieldValue): boolean;
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@return" is not defined in this configuration
static maximum(n: number): FieldValue;
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@return" is not defined in this configuration
static minimum(n: number): FieldValue;
Comment thread
MarkDuckworth marked this conversation as resolved.
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
static serverTimestamp(): FieldValue;
Expand Down Expand Up @@ -2333,6 +2344,8 @@ function pow(base: string, exponent: Expression): FunctionExpression;
// @public
function pow(base: string, exponent: number): FunctionExpression;

export { protos }

// Warning: (tsdoc-undefined-tag) The TSDoc tag "@class" is not defined in this configuration
//
// @public
Expand Down Expand Up @@ -2492,7 +2505,7 @@ export class Query<AppModelType = firestore.DocumentData, DbModelType extends fi
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
//
// @internal
_toBundledQuery(): protos.firestore.IBundledQuery;
_toBundledQuery(): protos_2.firestore.IBundledQuery;
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
//
// @internal
Expand Down Expand Up @@ -2813,6 +2826,10 @@ export class Timestamp implements firestore.Timestamp {
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
static fromInstant(instant: Temporal.Instant): Timestamp;
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
static fromMillis(milliseconds: number): Timestamp;
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
Expand All @@ -2839,6 +2856,9 @@ export class Timestamp implements firestore.Timestamp {
toDate(): Date;
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
toInstant(): Temporal.Instant;
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
toMillis(): number;
// Warning: (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
Expand Down Expand Up @@ -3266,18 +3286,18 @@ function xor(first: BooleanExpression, second: BooleanExpression, ...additionalC
// build/types/src/bundle.d.ts:19:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/filter.d.ts:120:4 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/filter.d.ts:155:4 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:299:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:319:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:326:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:347:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:354:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:363:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:371:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:378:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:387:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:885:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:904:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:919:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:301:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:321:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:328:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:349:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:356:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:365:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:373:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:380:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:389:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:887:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:906:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/index.d.ts:921:8 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/path.d.ts:29:4 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
// build/types/src/path.d.ts:31:4 - (tsdoc-undefined-tag) The TSDoc tag "@class" is not defined in this configuration
// build/types/src/path.d.ts:146:4 - (tsdoc-undefined-tag) The TSDoc tag "@private" is not defined in this configuration
Expand Down
34 changes: 34 additions & 0 deletions handwritten/firestore/dev/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ export class Serializer {
};
}

if (isTemporalInstant(val)) {
return Timestamp.fromInstant(val).toProto();
}

if (val === null) {
return {
nullValue: 'NULL_VALUE',
Expand Down Expand Up @@ -606,6 +610,8 @@ export function validateUserInput(
// Ok.
} else if (isMomentJsType(value)) {
// Ok.
} else if (isTemporalInstant(value)) {
// Ok.
} else if (value instanceof Buffer || value instanceof Uint8Array) {
// Ok.
} else if (value === null) {
Expand All @@ -617,6 +623,34 @@ export function validateUserInput(
}
}

/**
* Checks if the given value is an instance of `Temporal.Instant`.
* Checks both `instanceof` against runtime `Temporal.Instant` (if available)
* and duck typing via `[Symbol.toStringTag]` and `epochNanoseconds` bigint property
* to support polyfills or cross-realm instances.
*/
export function isTemporalInstant(value: unknown): value is Temporal.Instant {
if (typeof value !== 'object' || value === null) {
return false;
}
if (
typeof Temporal !== 'undefined' &&
typeof (Temporal as Record<string, unknown>).Instant === 'function'
) {
const instantCtor = (Temporal as Record<string, unknown>).Instant as new (
...args: unknown[]
) => unknown;
if (value instanceof instantCtor) {
return true;
}
}
const instant = value as Partial<Temporal.Instant>;
return (
instant[Symbol.toStringTag] === 'Temporal.Instant' &&
typeof instant.epochNanoseconds === 'bigint'
);
}

/**
* Returns true if value is a MomentJs date object.
* @private
Expand Down
35 changes: 35 additions & 0 deletions handwritten/firestore/dev/src/temporal.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*!
* Copyright 2026 Google LLC
*
* 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.
*/

// Type definitions for ECMAScript Temporal API

declare global {
namespace Temporal {
interface Instant {
readonly [Symbol.toStringTag]?: string;
readonly epochMilliseconds: number;
readonly epochNanoseconds: bigint;
toString(): string;
}

const Instant: {
fromEpochNanoseconds(epochNanoseconds: bigint): Instant;
fromEpochMilliseconds(epochMilliseconds: number): Instant;
};
}
}

export {};
67 changes: 67 additions & 0 deletions handwritten/firestore/dev/src/timestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,47 @@ export class Timestamp implements firestore.Timestamp {
return new Timestamp(seconds, nanos);
}

/**
* Creates a new timestamp from the given `Temporal.Instant`.
*
* @example
* ```
* let documentRef = firestore.doc('col/doc');
*
* let instant = Temporal.Now.instant();
* documentRef.set({ startTime:Firestore.Timestamp.fromInstant(instant) });
*
* ```
* @param {Temporal.Instant} instant The `Temporal.Instant` to initialize the `Timestamp` from.
* @returns {Timestamp} A new `Timestamp` representing the same point in time
Comment thread
MarkDuckworth marked this conversation as resolved.
* as the given instant.
*/
static fromInstant(instant: Temporal.Instant): Timestamp {
if (!instant || typeof instant.epochNanoseconds !== 'bigint') {
throw new Error('Invalid Temporal.Instant object provided.');
}
return Timestamp._fromEpochNanoseconds(instant.epochNanoseconds);
}

private static _fromEpochNanoseconds(nanos: bigint): Timestamp {
let seconds: number;
let nanoseconds: number;
if (nanos >= 0n) {
seconds = Number(nanos / 1000000000n);
nanoseconds = Number(nanos % 1000000000n);
} else {
const rem = nanos % 1000000000n;
if (rem === 0n) {
seconds = Number(nanos / 1000000000n);
nanoseconds = 0;
} else {
seconds = Number(nanos / 1000000000n - 1n);
nanoseconds = Number(rem + 1000000000n);
}
}
return new Timestamp(seconds, nanoseconds);
}

/**
* Generates a `Timestamp` object from a Timestamp proto.
*
Expand Down Expand Up @@ -198,6 +239,32 @@ export class Timestamp implements firestore.Timestamp {
return this._nanoseconds;
}

/**
* Converts a `Timestamp` to a `Temporal.Instant` object.
*
* @example
* ```
* let documentRef = firestore.doc('col/doc');
*
* documentRef.get().then(snap => {
* console.log(`Document updated at: ${snap.updateTime.toInstant()}`);
* });
*
* ```
* @returns {Temporal.Instant} `Temporal.Instant` object representing the same point in time
Comment thread
MarkDuckworth marked this conversation as resolved.
* as this `Timestamp`, with nanosecond precision.
*/
toInstant(): Temporal.Instant {
if (typeof Temporal === 'undefined' || !Temporal.Instant) {
throw new Error(
'The Temporal object is not available in the current environment.',
);
}
const nanos =
BigInt(this._seconds) * 1000000000n + BigInt(this._nanoseconds);
return Temporal.Instant.fromEpochNanoseconds(nanos);
}

/**
* Returns a new `Date` corresponding to this timestamp. This may lose
* precision.
Expand Down
30 changes: 30 additions & 0 deletions handwritten/firestore/dev/test/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
let documentRef: DocumentReference;

beforeEach(() => {
return createInstance().then(firestoreInstance => {

Check failure on line 70 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw
firestore = firestoreInstance;
documentRef = firestore.doc('collectionId/documentId');
});
Expand Down Expand Up @@ -112,7 +112,7 @@
let firestore: Firestore;

beforeEach(() => {
return createInstance().then(firestoreInstance => {

Check failure on line 115 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw
firestore = firestoreInstance;
});
});
Expand Down Expand Up @@ -300,6 +300,36 @@
});
});

it('supports Temporal.Instant', async () => {
const Temporal =
(globalThis as Record<string, unknown>).Temporal ||
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('@js-temporal/polyfill').Temporal;
const instant = Temporal.Instant.fromEpochNanoseconds(1488872578916123456n);

const overrides: ApiOverride = {
commit: request => {
requestEquals(
request,
set({
document: document('documentId', 'temporalInstant', {
timestampValue: {
nanos: 916123456,
seconds: '1488872578',
},
}),
}),
);
return response(writeResult(1));
},
};

const firestore = await createInstance(overrides);
await firestore.doc('collectionId/documentId').set({
temporalInstant: instant,
});
});

it('supports BigInt', async () => {
const overrides: ApiOverride = {
commit: request => {
Expand Down Expand Up @@ -772,13 +802,13 @@
},
};

void createInstance(overrides).then(firestore => {

Check failure on line 805 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw
firestore
.doc('collectionId/documentId')
.get()
.catch(err => {

Check warning on line 809 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Avoid nesting promises
expect(err.message).to.equal('RPC Error');
done();

Check warning on line 811 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Avoid calling back inside of a promise
});
});
});
Expand Down Expand Up @@ -814,7 +844,7 @@
let firestore: Firestore;

beforeEach(() => {
return createInstance().then(firestoreClient => {

Check failure on line 847 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw
firestore = firestoreClient;
});
});
Expand Down Expand Up @@ -926,7 +956,7 @@
let firestore: Firestore;

beforeEach(() => {
return createInstance().then(firestoreClient => {

Check failure on line 959 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw
firestore = firestoreClient;
});
});
Expand Down Expand Up @@ -1386,7 +1416,7 @@
let firestore: Firestore;

beforeEach(() => {
return createInstance().then(firestoreClient => {

Check failure on line 1419 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw
firestore = firestoreClient;
});
});
Expand Down Expand Up @@ -1507,7 +1537,7 @@
let firestore: Firestore;

beforeEach(() => {
return createInstance().then(firestoreClient => {

Check failure on line 1540 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw
firestore = firestoreClient;
});
});
Expand Down Expand Up @@ -2131,7 +2161,7 @@
return firestore
.doc('coll/doc')
.listCollections()
.then(collections => {

Check failure on line 2164 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw

Check warning on line 2164 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Avoid nesting promises
expect(collections[0].path).to.equal('coll/doc/first');
expect(collections[1].path).to.equal('coll/doc/second');
});
Expand All @@ -2143,7 +2173,7 @@
let firestore: Firestore;

beforeEach(() => {
return createInstance().then(firestoreInstance => {

Check failure on line 2176 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw
firestore = firestoreInstance;
});
});
Expand Down Expand Up @@ -2172,7 +2202,7 @@
},
};

return createInstance(overrides).then(async firestore => {

Check failure on line 2205 in handwritten/firestore/dev/test/document.ts

View workflow job for this annotation

GitHub Actions / lint

Each then() should return a value or throw
const docRef = firestore
.collection('collectionId')
.doc('documentId')
Expand Down
Loading
Loading