Skip to content

Commit 70be6ac

Browse files
Add ProtoJS types to generated proto types (#2749)
1 parent 2d825a5 commit 70be6ac

File tree

5 files changed

+45
-71
lines changed

5 files changed

+45
-71
lines changed

packages/firestore/src/model/proto_values.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ const ISO_TIMESTAMP_REG_EXP = new RegExp(
3232
/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.(\d+))?Z$/
3333
);
3434

35-
// Denotes the possible representations for timestamps in the Value type.
36-
type ProtoTimestampValue =
37-
| string
38-
| { seconds?: string | number; nanos?: number };
39-
4035
/** Extracts the backend's type order for the provided value. */
4136
export function typeOrder(value: api.Value): TypeOrder {
4237
if ('nullValue' in value) {
@@ -235,10 +230,7 @@ function compareNumbers(left: api.Value, right: api.Value): number {
235230
return numericComparator(leftNumber, rightNumber);
236231
}
237232

238-
function compareTimestamps(
239-
left: ProtoTimestampValue,
240-
right: ProtoTimestampValue
241-
): number {
233+
function compareTimestamps(left: api.Timestamp, right: api.Timestamp): number {
242234
if (typeof left === 'string' && typeof right === 'string') {
243235
// Use string ordering for ISO 8601 timestamps, but strip the timezone
244236
// suffix to ensure proper ordering for timestamps of different precision.
@@ -376,7 +368,7 @@ function canonifyByteString(byteString: string | Uint8Array): string {
376368
return normalizeByteString(byteString).toBase64();
377369
}
378370

379-
function canonifyTimestamp(timestamp: ProtoTimestampValue): string {
371+
function canonifyTimestamp(timestamp: api.Timestamp): string {
380372
const normalizedTimestamp = normalizeTimestamp(timestamp);
381373
return `time(${normalizedTimestamp.seconds},${normalizedTimestamp.nanos})`;
382374
}
@@ -478,7 +470,7 @@ function estimateArrayByteSize(arrayValue: api.ArrayValue): number {
478470
* nanos" representation.
479471
*/
480472
export function normalizeTimestamp(
481-
date: ProtoTimestampValue
473+
date: api.Timestamp
482474
): { seconds: number; nanos: number } {
483475
assert(!!date, 'Cannot normalize null or undefined timestamp.');
484476

packages/firestore/src/protos/firestore_proto_api.d.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export declare type PromiseRequestService = any;
2626
export interface ApiClientObjectMap<T> {
2727
[k: string]: T;
2828
}
29+
export declare type Timestamp =
30+
| string
31+
| { seconds?: string | number; nanos?: number };
2932

3033
export declare type CompositeFilterOp = 'OPERATOR_UNSPECIFIED' | 'AND';
3134
export interface ICompositeFilterOpEnum {
@@ -180,7 +183,7 @@ export declare namespace firestoreV1ApiClientInterfaces {
180183
name?: string;
181184
fields?: ApiClientObjectMap<Value>;
182185
createTime?: string;
183-
updateTime?: string;
186+
updateTime?: Timestamp;
184187
}
185188
interface DocumentChange {
186189
document?: Document;
@@ -190,7 +193,7 @@ export declare namespace firestoreV1ApiClientInterfaces {
190193
interface DocumentDelete {
191194
document?: string;
192195
removedTargetIds?: number[];
193-
readTime?: string;
196+
readTime?: Timestamp;
194197
}
195198
interface DocumentMask {
196199
fieldPaths?: string[];
@@ -290,7 +293,7 @@ export declare namespace firestoreV1ApiClientInterfaces {
290293
}
291294
interface Precondition {
292295
exists?: boolean;
293-
updateTime?: string;
296+
updateTime?: Timestamp;
294297
}
295298
interface Projection {
296299
fields?: FieldReference[];
@@ -333,12 +336,12 @@ export declare namespace firestoreV1ApiClientInterfaces {
333336
startAt?: Cursor;
334337
endAt?: Cursor;
335338
offset?: number;
336-
limit?: number;
339+
limit?: number | { value: number };
337340
}
338341
interface Target {
339342
query?: QueryTarget;
340343
documents?: DocumentsTarget;
341-
resumeToken?: string;
344+
resumeToken?: string | Uint8Array;
342345
readTime?: string;
343346
targetId?: number;
344347
once?: boolean;
@@ -347,8 +350,8 @@ export declare namespace firestoreV1ApiClientInterfaces {
347350
targetChangeType?: TargetChangeTargetChangeType;
348351
targetIds?: number[];
349352
cause?: Status;
350-
resumeToken?: string;
351-
readTime?: string;
353+
resumeToken?: string | Uint8Array;
354+
readTime?: Timestamp;
352355
}
353356
interface TransactionOptions {
354357
readOnly?: ReadOnly;
@@ -363,7 +366,7 @@ export declare namespace firestoreV1ApiClientInterfaces {
363366
booleanValue?: boolean;
364367
integerValue?: string | number;
365368
doubleValue?: string | number;
366-
timestampValue?: string | { seconds?: string | number; nanos?: number };
369+
timestampValue?: Timestamp;
367370
stringValue?: string;
368371
bytesValue?: string | Uint8Array;
369372
referenceValue?: string;
@@ -382,17 +385,17 @@ export declare namespace firestoreV1ApiClientInterfaces {
382385
interface WriteRequest {
383386
streamId?: string;
384387
writes?: Write[];
385-
streamToken?: string;
388+
streamToken?: string | Uint8Array;
386389
labels?: ApiClientObjectMap<string>;
387390
}
388391
interface WriteResponse {
389392
streamId?: string;
390393
streamToken?: string;
391394
writeResults?: WriteResult[];
392-
commitTime?: string;
395+
commitTime?: Timestamp;
393396
}
394397
interface WriteResult {
395-
updateTime?: string;
398+
updateTime?: Timestamp;
396399
transformResults?: Value[];
397400
}
398401
}

packages/firestore/src/remote/serializer.ts

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import { assert, fail } from '../util/assert';
5454
import { Code, FirestoreError } from '../util/error';
5555
import * as obj from '../util/obj';
5656
import { ByteString } from '../util/byte_string';
57-
import * as typeUtils from '../util/types';
57+
import { isNegativeZero, isNullOrUndefined } from '../util/types';
5858

5959
import {
6060
ArrayRemoveTransformOperation,
@@ -99,14 +99,7 @@ const OPERATORS = (() => {
9999
})();
100100

101101
function assertPresent(value: unknown, description: string): asserts value {
102-
assert(!typeUtils.isNullOrUndefined(value), description + ' is missing');
103-
}
104-
105-
// This is a supplement to the generated proto interfaces, which fail to account
106-
// for the fact that a timestamp may be encoded as either a string OR this.
107-
interface TimestampProto {
108-
seconds?: string | number;
109-
nanos?: number;
102+
assert(!isNullOrUndefined(value), description + ' is missing');
110103
}
111104

112105
export interface SerializerOptions {
@@ -148,46 +141,33 @@ export class JsonProtoSerializer {
148141
* our generated proto interfaces say Int32Value must be. But GRPC actually
149142
* expects a { value: <number> } struct.
150143
*/
151-
private toInt32Value(val: number | null): number | null {
152-
if (this.options.useProto3Json || typeUtils.isNullOrUndefined(val)) {
144+
private toInt32Proto(val: number | null): number | { value: number } | null {
145+
if (this.options.useProto3Json || isNullOrUndefined(val)) {
153146
return val;
154147
} else {
155-
// ProtobufJS requires that we wrap Int32Values.
156-
// Use any because we need to match generated Proto types.
157-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
158-
return { value: val } as any;
148+
return { value: val };
159149
}
160150
}
161151

162152
/**
163153
* Returns a number (or null) from a google.protobuf.Int32Value proto.
164-
* DO NOT USE THIS FOR ANYTHING ELSE.
165-
* This method cheats. It's typed as accepting "number" because that's what
166-
* our generated proto interfaces say Int32Value must be, but it actually
167-
* accepts { value: number } to match our serialization in toInt32Value().
168154
*/
169-
private fromInt32Value(val: number | undefined): number | null {
155+
private fromInt32Proto(
156+
val: number | { value: number } | undefined
157+
): number | null {
170158
let result;
171159
if (typeof val === 'object') {
172-
// Use any because we need to match generated Proto types.
173-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
174-
result = (val as any).value;
160+
result = val.value;
175161
} else {
176-
// We accept raw numbers (without the {value: ... } wrapper) for
177-
// compatibility with legacy persisted data.
178162
result = val;
179163
}
180-
return typeUtils.isNullOrUndefined(result) ? null : result;
164+
return isNullOrUndefined(result) ? null : result;
181165
}
182166

183167
/**
184168
* Returns a value for a Date that's appropriate to put into a proto.
185-
* DO NOT USE THIS FOR ANYTHING ELSE.
186-
* This method cheats. It's typed as returning "string" because that's what
187-
* our generated proto interfaces say dates must be. But it's easier and safer
188-
* to actually return a Timestamp proto.
189169
*/
190-
private toTimestamp(timestamp: Timestamp): string {
170+
private toTimestamp(timestamp: Timestamp): api.Timestamp {
191171
if (this.options.useProto3Json) {
192172
// Serialize to ISO-8601 date format, but with full nano resolution.
193173
// Since JS Date has only millis, let's only use it for the seconds and
@@ -208,25 +188,21 @@ export class JsonProtoSerializer {
208188
}
209189
}
210190

211-
private fromTimestamp(date: string | TimestampProto): Timestamp {
191+
private fromTimestamp(date: api.Timestamp): Timestamp {
212192
const timestamp = normalizeTimestamp(date);
213193
return new Timestamp(timestamp.seconds, timestamp.nanos);
214194
}
215195

216196
/**
217197
* Returns a value for bytes that's appropriate to put in a proto.
218-
* DO NOT USE THIS FOR ANYTHING ELSE.
219-
* This method cheats. It's typed as returning "string" because that's what
220-
* our generated proto interfaces say bytes must be. But it should return
221-
* an Uint8Array in Node.
222198
*
223199
* Visible for testing.
224200
*/
225-
toBytes(bytes: Blob | ByteString): string {
201+
toBytes(bytes: Blob | ByteString): string | Uint8Array {
226202
if (this.options.useProto3Json) {
227203
return bytes.toBase64();
228204
} else {
229-
return (bytes.toUint8Array() as unknown) as string;
205+
return bytes.toUint8Array();
230206
}
231207
}
232208

@@ -249,11 +225,11 @@ export class JsonProtoSerializer {
249225
}
250226
}
251227

252-
toVersion(version: SnapshotVersion): string {
228+
toVersion(version: SnapshotVersion): api.Timestamp {
253229
return this.toTimestamp(version.toTimestamp());
254230
}
255231

256-
fromVersion(version: string): SnapshotVersion {
232+
fromVersion(version: api.Timestamp): SnapshotVersion {
257233
assert(!!version, "Trying to deserialize version that isn't set");
258234
return SnapshotVersion.fromTimestamp(this.fromTimestamp(version));
259235
}
@@ -374,7 +350,7 @@ export class JsonProtoSerializer {
374350
return { doubleValue: 'Infinity' } as {};
375351
} else if (doubleValue === -Infinity) {
376352
return { doubleValue: '-Infinity' } as {};
377-
} else if (typeUtils.isNegativeZero(doubleValue)) {
353+
} else if (isNegativeZero(doubleValue)) {
378354
return { doubleValue: '-0' } as {};
379355
}
380356
}
@@ -846,7 +822,7 @@ export class JsonProtoSerializer {
846822

847823
private fromWriteResult(
848824
proto: api.WriteResult,
849-
commitTime: string
825+
commitTime: api.Timestamp
850826
): MutationResult {
851827
// NOTE: Deletes don't have an updateTime.
852828
let version = proto.updateTime
@@ -873,7 +849,7 @@ export class JsonProtoSerializer {
873849

874850
fromWriteResults(
875851
protos: api.WriteResult[] | undefined,
876-
commitTime?: string
852+
commitTime?: api.Timestamp
877853
): MutationResult[] {
878854
if (protos && protos.length > 0) {
879855
assert(
@@ -1000,7 +976,7 @@ export class JsonProtoSerializer {
1000976
result.structuredQuery!.orderBy = orderBy;
1001977
}
1002978

1003-
const limit = this.toInt32Value(target.limit);
979+
const limit = this.toInt32Proto(target.limit);
1004980
if (limit !== null) {
1005981
result.structuredQuery!.limit = limit;
1006982
}
@@ -1046,7 +1022,7 @@ export class JsonProtoSerializer {
10461022

10471023
let limit: number | null = null;
10481024
if (query.limit) {
1049-
limit = this.fromInt32Value(query.limit);
1025+
limit = this.fromInt32Proto(query.limit);
10501026
}
10511027

10521028
let startAt: Bound | null = null;

packages/firestore/src/util/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ export interface StringMap {
2323
/**
2424
* Returns whether a variable is either undefined or null.
2525
*/
26-
export function isNullOrUndefined(value: unknown): boolean {
26+
export function isNullOrUndefined(value: unknown): value is null | undefined {
2727
return value === null || value === undefined;
2828
}
2929

3030
/** Returns whether the value represents -0. */
31-
export function isNegativeZero(value: number) : boolean {
31+
export function isNegativeZero(value: number): boolean {
3232
// Detect if the value is -0.0. Based on polyfill from
3333
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
3434
return value === -0 && 1 / value === 1 / -0;

packages/firestore/test/unit/specs/spec_test_runner.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,10 @@ class MockConnection implements Connection {
196196
return this.watchOpen.promise;
197197
}
198198

199-
ackWrite(commitTime?: string, mutationResults?: api.WriteResult[]): void {
199+
ackWrite(
200+
commitTime?: api.Timestamp,
201+
mutationResults?: api.WriteResult[]
202+
): void {
200203
this.writeStream!.callOnMessage({
201204
// Convert to base64 string so it can later be parsed into ByteString.
202205
streamToken: PlatformSupport.getPlatform().btoa(
@@ -1151,7 +1154,7 @@ abstract class TestRunner {
11511154
expect(actualTarget.query).to.deep.equal(expectedTarget.query);
11521155
expect(actualTarget.targetId).to.equal(expectedTarget.targetId);
11531156
expect(actualTarget.readTime).to.equal(expectedTarget.readTime);
1154-
expect(actualTarget.resumeToken || '').to.equal(expectedTarget.resumeToken || '');
1157+
expect(actualTarget.resumeToken).to.equal(expectedTarget.resumeToken);
11551158
delete actualTargets[targetId];
11561159
});
11571160
expect(obj.size(actualTargets)).to.equal(

0 commit comments

Comments
 (0)